Материал Capstone FTS: order execution from model to React and Node.js
0%

Capstone FTS: order execution from model to React and Node.js

Capstone FTS: order execution from model to React and Node.js

Capstone connects everything from the course into one end-to-end project: subject object, utility with rule and property, morphism, team admission theorem, HTTP service, form, and TypeScript generation for CI. One model, and five interfaces that use it. Below — the team and expected result at each stage, with no omissions.

1. Model

категория «Исполнение заказа»

  объект Заказ
    номер является строкой
    «готов к отгрузке» является состоянием «Готов к отгрузке»

  объект Покупка
    сумма является деньгами
    «постоянный клиент» является признаком

  утилита «Рассчитать скидку»
    принимает Покупка
    возвращает деньги
    начинает с 0

    правило «Большой заказ»
      если сумма не меньше 10000
      и сумма не больше 100000
      то добавить 10 процентов от поля сумма

    правило «Очень большой заказ»
      если сумма больше 100000
      то добавить 10000

    свойство «Скидка ограничена»
      результат не больше 10000

    пример «Заказ на двадцать тысяч»
      дано сумма равна 20000
      дано «постоянный клиент» равен нет
      ожидается результат равен 2000

    пример «Заказ ровно на потолок скидки»
      дано сумма равна 100000
      дано «постоянный клиент» равен нет
      ожидается результат равен 10000

  морфизм «Готовый заказ можно отгрузить»
    если «Готов к отгрузке»
    то «Отгрузить заказ разрешено»

  теорема «Заказ ЗК-7781 можно отгрузить»
    дано Заказ имеет «готов к отгрузке» равное да
    в данных заказы найти где номер равен «ЗК-7781»
    по морфизму «Готовый заказ можно отгрузить»
    следовательно «Отгрузить заказ разрешено»

Object Заказ carries the readiness fact, object Покупка is an input for calculating the discount. The utility calculates money, the morphism describes the allowed transition “ready → can ship”, the theorem applies the morphism to a specific order number. None of this writes to the database or calls the courier service — the model only calculates and checks access.

The discount ceiling is recorded as a number (результат не больше 10000), not a percentage of the field (результат не больше 20 процентов от поля сумма), and both rules explicitly name their boundaries by sum. This is done not for beauty: the property is a postcondition that is checked over the entire input domain, not just where the rules triggered. A percentage of the field changes sign together with the field, so with a negative sum the limit goes into the negative, the initial zero no longer satisfies it, and the utility crashes with FTS_UTILITY_PROPERTY on input that the rules didn’t even touch. The second rule covers sums above the ceiling, and the second example shows that the limit is exactly reachable — otherwise the property would not check anything. Walkthrough of both traps is in the chapter on antipatterns; the ftsmap coverage map from the language repository catches them before prod does.

2. Architecture

order.fts is compiled once into canonical JSON — a regular FtsDocument, without cycles and without hidden state. From this document, five independent branches diverge, and none reads the source text .fts again: React takes the form structure, the utility interpreter calculates a discount, the verifier builds a certificate for the guard, CI runs examples and checks generation, MCP tools deliver the same document to the agent. If a rule changes, only the left node of the graph changes — the rest recalculate from it, rather than being fixed individually.

3. Step by Step

3.1 Model Check

npx fts check order.fts
{ "valid": true, "document": { "category": "Исполнение заказа", "...": "..." }, "diagnostics": [] }

The compiler checks that the “Ready for shipment” state is typed, the utility fields exist in the Покупка object, and the morphism condition refers to a real fact. Next — the subject test:

npx fts test order.fts --pretty
{
  "valid": true,
  "total": 2,
  "passed": 2,
  "failed": 0,
  "results": [
    { "utility": "Рассчитать скидку", "example": "Заказ на двадцать тысяч", "passed": true, "expected": 2000, "actual": 2000 },
    { "utility": "Рассчитать скидку", "example": "Заказ ровно на потолок скидки", "passed": true, "expected": 10000, "actual": 10000 }
  ]
}

If the “Big Order” rule is ever changed to 12 percent, this step will fall first — before the HTTP service and before the React form.

3.2 Shipment Authorization: Theorem and Proof

Shipment permission — not a field in the database, but a result of prove on a data snapshot. Snapshot — a regular JSON, which the application service compiles from its tables before making a decision on the shipment:

{
  "заказы": [
    {
      "номер": "ЗК-7781",
      "клиент": "ООО Маяк",
      "оплачен": true,
      "склад подтвердил": true,
      "готов к отгрузке": true
    }
  ]
}
npx fts prove order.fts --context order-shipment.context.json --pretty
{
  "proof": "Исполнение заказа.Заказ.Type — заказы[номер=\"ЗК-7781\"].готов к отгрузке (Заказ ЗК-7781 можно отгрузить)",
  "categorical": { "category": "Исполнение заказа", "domain": "Заказ", "codomain": "Type" },
  "path": ["заказы", { "номер": "ЗК-7781" }, "готов к отгрузке"]
}

examples/fts/shipment-guard/guard.mjs wraps the same in a reusable function for application service:

node examples/fts/shipment-guard/guard.mjs ЗК-7781

yields "allowed": true and exit code 0. The same number, but with a screenshot where the warehouse did not confirm receipt:

node examples/fts/shipment-guard/guard.mjs ЗК-7781 --blocked

With the same order number, but a screenshot where "склад подтвердил": false and "готов к отгрузке": false, the answer changes to:

{
  "allowed": false,
  "reason": "witness does not match context at заказы[номер=\"ЗК-7781\"].готов к отгрузке: expected true, got false",
  "diagnostics": [{ "code": "FTS_WITNESS_MISMATCH", "severity": "error", "path": "$.proposition" }]
}

The model, theorem text, and guard code did not change — only the data snapshot changed, and the solution changed along with it. Exit code 1 makes this difference usable for a shell script and for CI, not just for a person reading JSON. The real call shipping.createShipment(orderId) in the application service must happen strictly after allowed === true — the effect lives outside FTS, not inside the guard.

3.3 HTTP Service

node examples/fts/discount-api/server.mjs

At startup, the service runs testUtilities(document): if the model example does not match, the process does not start at all. After startup:

curl -s localhost:8788/discount -d '{"сумма":20000,"постоянный клиент":true}'
# {"discount": 3000}

curl -s localhost:8788/contract
# {"utility":"Рассчитать скидку","input":[{"name":"сумма","type":"Деньги"},
#  {"name":"постоянный клиент","type":"Признак"}],"output":"Деньги"}

/contract does not hardcode the list of fields — it takes them from document.structures. Added a field to the model — the contract updated itself. A request with an input type that does not match the model (a string instead of a number) returns 400 with diagnostic FTS_UTILITY_INPUT_TYPE, not 500: the client sees the reason for the rejection in the language of the domain, not a stack trace.

3.4 Form

Form schema — a pure function of the model, without network access:

node examples/fts/form-schema/schema.mjs Покупка
{
  "id": "Продажи.Покупка",
  "title": "Покупка",
  "fields": [
    { "name": "сумма", "type": "Деньги", "control": "money", "required": true },
    { "name": "постоянный клиент", "type": "Признак", "control": "checkbox", "required": true }
  ]
}

examples/fts/react-form/DiscountForm.jsx consumes exactly this schema: draws a field under each fields element, and the pre-calculation of the discount is done by the same executeUtility directly in the browser — the user sees the discount amount without a request to the server. At the same time, the server still recalculates it when sending the form: the browser is not trusted, but the user is not made to wait. The pre-calculation is done by the same interpreter as on the server, so any diagnostics — from FTS_UTILITY_INPUT_TYPE to a triggered property — are visible in the browser before the form sends the request, but the final decision still belongs to /discount: the frontend accelerates feedback, not replacing the check. The property “Discount is limited” on a correct model never triggers — the ceiling is held by the rules, and the property remains a safeguard in case they are changed.

3.5 TypeScript Generation

node examples/fts/typescript-codegen/generate.mjs
# fts.utilities.ts, fts.utilities.test.ts — записано (примеры 3/3)

node examples/fts/typescript-codegen/generate.mjs --check
# fts.utilities.ts, fts.utilities.test.ts — совпадает с моделью (примеры 3/3)

The second call writes nothing — it only compares. If someone manually modifies the generated .ts, --check will return a list of outdated files and the return code 1. A detailed walkthrough is in the chapter on generation and CI.

3.6 CI

node --test "examples/fts/**/*.test.mjs"
ℹ tests 16
ℹ pass 16
ℹ fail 0

Ready workflow examples/fts/ci/fts-check.yml performs five steps in order: npm ci, fts check, fts test --pretty, generation check through --check, integration tests node --test. The first to fail is the cheapest step — model syntax, the last — the most expensive, integrations. It is the same order as in steps 3.1–3.5: model → acceptance → service/form → generation.

4. Definition of done

  • fts check is successful;
  • all examples pass;
  • property protects the upper discount limit, set as a number (not a percentage of a field that changes sign with it) and achieved by at least one example exactly — otherwise the limit checks nothing;
  • React reads the structure through the browser entrypoint;
  • Node loads the model once at startup, not for each request;
  • command guard requires allowed: true before calling the effect;
  • external shipping effect is not inside FTS;
  • CI checks generated drift in mode --check;
  • README explicitly lists the morphism assumptions — which transitions are considered allowed and why.

5. Typical Capstone Mistakes

  • Hardcoding the form field list instead of reading document.structures — then React and the model diverge at the first added field.
  • Calling shipping.createShipment without checking decision.allowed — the command guard loses its meaning if its result is not read.
  • Committing the generated .ts without the --check step in CI — FTS_UTILITY_PROPERTY or a modified rule will remain unnoticed until production.
  • Passing input to an HTTP service without type validation — FTS_UTILITY_INPUT_TYPE should be returned to the client as 400, not bubble up as 500.
  • Plugging user input into a theorem text without format checking — shipmentTheorem in guard.mjs deliberately rejects an order number that does not match /^[\p{L}\p{N}-]{1,32}$/u, before it reaches the source .fts.

6. Task for Roles

Junior adds a field and its visualization in the form schema, checking that required is computed automatically from иногда является. Middle builds a Node endpoint and diagnostic handling FTS_UTILITY_* as 400, not 500. Senior designs a data snapshot for prove/verify and a transactional boundary around the command guard — where FTS responsibility ends and optimistic lock begins. Lead checks that model terms match the language of the domain expert and version canonical JSON. An agent may propose a new example, but must pass fts check, fts test, and fts verify before the proposal gets into a pull request.

Practice in the sandbox

Maturity Criterion

The project is ready not when .fts looks nice, but when real duplication is removed: one model controls the computation and test, while adapters use canonical JSON. At the same time, effects, UX, and infrastructure remain in their own layers — FTS does not replace the repository, the event bus, or React.

Return to the catalog of 232 applied cases, select the one closest to your work and repeat the capstone in your own bounded context: one object, one utility with examples, one team admission morphism and one adapter (HTTP, form or CLI), which uses canonical JSON, without dragging side effects inside the specification.

Where to go next: what begins beyond the specification boundary

The course ends where FTS ends — at the boundary it draws consciously. There are no loops, recursion, lists, or computations over strings in the language, and this is not an omission: precisely because of this, the FTS utility is guaranteed to terminate, which means it can be executed in the admission check, in CI, and inside the agent, without fear of hanging.

But the boundary has a second side, and one day you will bump into it. You cannot write a parser for your own format, tree traversal, or report generation in FTS — there you need exactly the means that the language does not provide. This limitation is recorded in plain text in the repository itself:

in the core of FTS a string is a field type, but not a value over which computations can be performed

Because of this string the FTS kernel could not be written in FTS itself — the parser is computation over strings. For this reason flang appeared: a full-fledged language that removes exactly this limitation. The relationship between the two languages is strict, not familial:

  • FTS — total subset of flang. Any existing model .fts — is a valid language program, and all of it falls into the total class — that subset where termination is proven by the compiler, not promised by the author. Checked by comparing two engines on 19,593 inputs, zero discrepancies.
  • FTS core is now written in flang itself and runs natively: four files, 300 functions, all total, byte-for-byte match of canonical JSON with the core in TypeScript — and the same core, printed in C, is built with a regular cc and gives the same document without Node at all.
  • Any of your model is printed in eight languages — C, C#, Elixir, Go, Java, JavaScript, Python, Rust — with one command flang emit, requiring matching the interpreter by value and by error text (module «Integration with any language»).

Practical takeaway for those who read through the capstone: the rule remains in .fts, and the tool around the rule is written in flang. This is exactly the boundary you have drawn throughout the course between the specification and effects, only now the second half has a language with proven compatibility with the first, not just an agreement.

If you want to understand how the language itself is structured — why it divides programs into two classes, how termination is proven, and what happened when the language compiler was rewritten in the language itself — this is a separate track: «flang: a language that takes a day». It is more convenient to start it after this course, rather than before: half of the solutions there are explained through FTS, and you already know this half.

Cases in the catalog on this topic

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов
Дальше