FTS toolchain: CLI, library, MCP and canonical JSON
This chapter is about tools around the language, not about syntax: how to install FTS, what commands to check the model with and what exactly is sent between processes, if it runs not only Node.js, but also React in the browser and an agent via MCP.
Minimal example
категория «Продажи»
объект Покупка
сумма является деньгами
«постоянный клиент» является признаком
утилита «Рассчитать скидку»
принимает Покупка
возвращает деньги
начинает с 0
правило «Большая покупка»
если сумма не меньше 10000
и сумма не больше 100000
то добавить 10 процентов от поля сумма
правило «Постоянный клиент»
если «постоянный клиент» равен да
и сумма больше 0
и сумма не больше 100000
то добавить 5 процентов от поля сумма
правило «Очень крупная покупка»
если сумма больше 100000
то добавить 15000
свойство «Скидка ограничена»
результат не больше 15000
пример «Обычная покупка»
дано сумма равна 5000
дано «постоянный клиент» равен нет
ожидается результат равен 0
пример «Постоянный клиент на пять тысяч»
дано сумма равна 5000
дано «постоянный клиент» равен да
ожидается результат равен 250
пример «Большая покупка постоянного клиента»
дано сумма равна 20000
дано «постоянный клиент» равен да
ожидается результат равен 3000
пример «Покупка на потолок скидки»
дано сумма равна 100000
дано «постоянный клиент» равен да
ожидается результат равен 15000
пример «Очень крупная покупка»
дано сумма равна 200000
дано «постоянный клиент» равен нет
ожидается результат равен 15000
Further in this chapter we will look at what this compiles into and with what commands this is checked.
What the compiler does
CLI, MCP and the library do not duplicate semantics — they call the same
compile/validate/testUtilities/generateTypeScript/prove/certify.
cli.ts and mcp.ts do not contain language logic: they read the source or
canonical JSON, call the core function and print the result. Therefore
the behavior of fts check model.fts in the terminal and fts_check in the agent matches
literally — this is one code, not two similar ones.
Installation
The repository is open - github.com/digitable-lol/flang. The package is not yet published in npm, so the CLI is built from source code:
git clone git@github.com:digitable-lol/flang.git
cd fts
npm ci
npm test
npm run build
After publication in the registry, the package and CLI will be installed in the usual way:
npm install @digitable-lol/fts
npx fts check model.fts
FTS requires Node.js 20 or newer. The package has no runtime dependencies, access
to the file system or network inside the core — the only I/O in CLI is reading
the model file and optional writing of the generated code through --out.
Sixth team in the same package: flang
It is worth knowing in advance, because this changes the perception of the
tool boundaries. In addition to fts, ftsc, ftsvm, ftspec and fts-mcp the
package declares a sixth command — flang, a full language into which FTS has
grown. And it accepts .fts directly, without conversion:
$ node flang/bin/flang.mjs check examples/utilities/discount.fts
{"valid":true,"module":"Продажи","functions":[{"name":"Рассчитать скидку","total":true}],
"types":["Покупка"],"diagnostics":[]}
This answer is read as follows. The FTS category has become a module of the language, the object
Покупка — a type, the utility — a function, and "total": true means that
the language compiler has proven the computation termination: not “tests passed”, but “this
function cannot loop indefinitely”. For the FTS utility, it could not be otherwise — loops and
recursion are completely absent in the specification language — and therefore the compatibility promise
is formulated stronger than “accepts a file”: any .fts model falls into the
total class flang entirely.
One practical caveat: for flang to read .fts, the FTS core must be
built — the bridge loads dist/src/index.js. Without npm run build you will get
an honest ERR_MODULE_NOT_FOUND, not a mysterious parsing error.
The practical sense at this stage of the course is small: counting the model is simpler
with the familiar fts run. The sense appears further, in the module
«Integration with any language», — and there it
is quite tangible.
Bridge between two worlds — module flang/src/compat.mjs, and it is designed so that
it repeats the semantics of the core literally, not by meaning: the order of rules,
short-circuiting of conditions inside a rule, the order of multipliers in percentages
((процент / 100) * поле, not the other way around — swapping changes the last bit
of the mantissa) and checking properties after all rules. A separate decision is worth
remembering, because it concerns the design of contracts in general: the code and the text of an
violated property are sent to the program as data, not as knowledge, hardcoded in
the engine. Otherwise, the coincidence of error codes between two implementations would depend on
the implementation and would be random.
One pipeline, three overlays
Library API fits a Node.js application and works the same in the browser (without Node certificate module):
import { compile, validate, testUtilities } from "@digitable-lol/fts"
const document = compile(source)
const report = validate(document)
if (!report.valid) throw new Error(JSON.stringify(report.diagnostics))
CLI fits CI and any language that can run a process. Full list of commands:
fts compile model.fts # source -> канонический JSON
fts check model.fts --pretty # компиляция + validate
fts test model.fts --pretty # выполнить примеры утилит
fts run model.fts --utility "Рассчитать скидку" --input input.json
fts generate model.fts --out generated # TypeScript + node:test
fts prove model.fts --context ctx.json # символьный вывод теоремы
fts certify model.fts --context ctx.json --pretty > proof.json
fts verify model.fts --context ctx.json --certificate proof.json
fts visualize model.fts --mode proof
fts pipeline model.fts --context ctx.json --mode all
fts mcp # поднять MCP-сервер по stdio
A successful team prints JSON to stdout; check and test on failure print
diagnostics to stderr and exit with a non-zero code.
MCP fits the agent. The server declares itself as fts (Formal Type Surface,
0.3.0) and publishes read-only tools: fts_compile, fts_check,
fts_test, fts_generate, fts_execute, fts_prove, fts_visualize,
fts_certify, fts_verify, fts_pipeline. The agent does not gain access to
the file system through these tools: source or already compiled
document, as well as context and certificate are passed as explicit JSON-
arguments of the call, not a file path.
Canonical JSON
All surfaces converge to a single FtsDocument. This is how the
fts compile of the minimal example for this chapter looks in reality (shortened — the full list
rules/examples is longer):
{
"category": "Продажи",
"structures": [
{
"name": "Покупка",
"fields": [
{ "name": "сумма", "type": "Деньги" },
{ "name": "постоянный клиент", "type": "Признак" }
]
}
],
"functors": [],
"proposition": null,
"ts_compat": {},
"utilities": [
{
"name": "Рассчитать скидку",
"input": "Покупка",
"output": "Деньги",
"initial": 0,
"rules": [
{
"name": "Большая покупка",
"when": [
{ "field": "сумма", "operator": "gte", "value": { "kind": "value", "value": 10000 } },
{ "field": "сумма", "operator": "lte", "value": { "kind": "value", "value": 100000 } }
],
"action": { "kind": "add", "value": { "kind": "percent", "percent": 10, "field": "сумма" } }
}
],
"properties": [
{
"name": "Скидка ограничена",
"operator": "lte",
"value": { "kind": "value", "value": 15000 }
}
],
"examples": [
{ "name": "Обычная покупка", "input": { "сумма": 5000, "постоянный клиент": false }, "expected": 0 }
]
}
]
}
This is the main integration contract. React is not required to understand Russian
keywords: it receives structures and builds a form. Python is not required
to embed a TypeScript parser: it reads JSON from stdout CLI. The agent does not receive
the right to read an arbitrary file: it passes the source as a JSON argument to MCP.
The functors field is an internal name of a section for морфизм; this is a name inherited
from the first version of the wire format fts/1, not a hint about functors in category
theory in user syntax. ts_compat is reserved for explicit TypeScript compatibility hints for fields — in the models of this course
it is usually empty.
JSON-first discipline
Do not parse pretty text and do not determine success by the presence of the word ok —
orient yourself by the exit code and the JSON field valid:
if fts test policy.fts > result.json; then
node publish-report.mjs result.json
else
echo "FTS policy failed" >&2
exit 1
fi
Practice in the sandbox
Read the JSON response check. Find the field valid in it and make sure that
diagnostics is an empty array.
Call the «Calculate Delivery» utility with weight 20 and distance 100. Calculate the expected number manually according to the rules and compare it with the result.
Open the generated TypeScript. Find in it the test corresponding to the
example “Regular customer for five thousand”, and compare the numbers with the original
.fts-model.
Common Fallacies
FTS_NO_UTILITY_EXAMPLES — fts test is triggered on a document where none
of the utilities have a single пример. The model may be syntactically valid
and even pass fts check, but test will refuse to confirm anything:
the diagnostics literally say «utilities do not contain examples». Fix — add at least
one пример with дано and ожидается.
FTS_UTILITY_INPUT_TYPE — fts run or executeUtility received
JSON input where the field value does not match the declared structure type.
If a sum is passed as a string "20000" instead of a number, the compiler will respond
поле «сумма» не соответствует типу «Деньги». This is a deliberate choice by the core:
the HTTP service from examples/fts/discount-api returns such an error to the client
as 400, not as 500, because the cause lies in the request data, not in
the server logic. Fix — cast types at the boundary before calling the utility.
Checklist
- I know which CLI command to use for verification, for utility tests, and for running one computation.
- I can explain the difference between a library API, a CLI process, and an MCP- tool in one phrase: one compiler, three interfaces.
- I find structures, rules, and examples in canonical JSON without reading
the source
.fts. - I check the CLI result by exit code and the
validfield, not by text.