Генерация и CI: код, который не расходится с правилом
Код в этой главе записан в прежней поверхности языка — со словами
категория,объект,утилита. Сегодняшний компилятор её слова читает, но программой такой файл не считает: файл, где есть только утилиты,flang checkотклоняет. Разбор задачи в главе верен; синтаксис переносится по таблице из главы «Старые модели».
Глава показывает, что именно генерирует fts generate, почему режим
--check ловит правку правила мимо модели и как выстроить порядок шагов в
CI так, чтобы дешёвая проверка падала раньше дорогой.
Минимальный пример
категория «Продажи»
объект Покупка
сумма является деньгами
«постоянный клиент» является признаком
утилита «Рассчитать скидку»
принимает Покупка
возвращает деньги
начинает с 0
правило «Большая покупка»
если сумма не меньше 10000
и сумма не больше 100000
то добавить 10 процентов от поля сумма
правило «Постоянный клиент»
если «постоянный клиент» равен да
и сумма больше 0
и сумма не больше 100000
то добавить 5 процентов от поля сумма
правило «Очень крупная покупка»
если сумма больше 100000
то добавить 15000
свойство «Скидка ограничена»
результат не больше 15000
пример «Обычная покупка»
дано сумма равна 5000
дано «постоянный клиент» равен нет
ожидается результат равен 0
пример «Постоянный клиент на пять тысяч»
дано сумма равна 5000
дано «постоянный клиент» равен да
ожидается результат равен 250
пример «Большая покупка постоянного клиента»
дано сумма равна 20000
дано «постоянный клиент» равен да
ожидается результат равен 3000
пример «Покупка на потолок скидки»
дано сумма равна 100000
дано «постоянный клиент» равен да
ожидается результат равен 15000
пример «Очень крупная покупка»
дано сумма равна 200000
дано «постоянный клиент» равен нет
ожидается результат равен 15000
Это модель static/spec/models/order-discount.fts, на которой построены
examples/spec/discount-api и examples/spec/typescript-codegen.
Что делает компилятор
fts generate сначала прогоняет assertValid(compile(source)), затем
generateTypeScript(document). Результат — не абстрактный код, а прямая
транслитерация правил модели. Для утилиты выше сгенерированный
fts.utilities.ts выглядит так:
// Generated by FTS. Do not edit by hand.
export interface FtsInput0 {
"сумма": number
"постоянный клиент": boolean
}
export const ftsUtilities = {
"Рассчитать скидку": (input: FtsInput0): number => {
let result: number = 0
if (input["сумма"] >= 10000 && input["сумма"] <= 100000) {
result += (10 / 100) * input["сумма"]
}
if (input["постоянный клиент"] === true && input["сумма"] > 0 && input["сумма"] <= 100000) {
result += (5 / 100) * input["сумма"]
}
if (input["сумма"] > 100000) {
result += 15000
}
if (!(result <= 15000)) throw new Error("Нарушено свойство «Скидка ограничена»")
return result
},
} as const
Каждое если/то становится if, каждый процент от поля — арифметикой,
каждое свойство — рантайм-проверкой с throw. Второй файл,
fts.utilities.test.ts, — обычный node:test с одним test(...) на каждый
пример: пять примеров в модели дают пять сгенерированных тестов.
Режим --check и как ловится drift
examples/spec/typescript-codegen/generate.mjs оборачивает генерацию в
writeGenerated. В режиме --check функция не пишет файлы, а сравнивает
уже сгенерированный код с содержимым на диске и собирает список «отставших»
файлов:
node examples/spec/typescript-codegen/generate.mjs # записать generated/
node examples/spec/typescript-codegen/generate.mjs --check # сверить с моделью
Если кто-то поправил result += 50 / 100 * ... прямо в .ts, а не в
.fts, --check увидит несовпадение и завершится с кодом 1:
Сгенерированный код отстал от модели: fts.utilities.ts
Запустите: node examples/spec/typescript-codegen/generate.mjs
generate.test.mjs проверяет это отдельным тестом: он записывает во
временную директорию сгенерированный код, затем перезаписывает
fts.utilities.ts строкой // правка мимо модели и убеждается, что
--check вернёт stale: ['fts.utilities.ts']. Второй тест защищает от
обратной ошибки — генерации из модели, которая сама не проходит собственные
примеры: если заменить ожидание 3000 на 4200, generate() бросает
не проходит свои примеры: 2/3 ещё до вызова generateTypeScript. Кодогенерация
не имеет смысла поверх модели, которая не сходится сама с собой.
Порядок шагов в CI и что падает первым
Готовый workflow лежит в examples/spec/ci/fts-check.yml (кладётся в
.github/workflows/fts-check.yml):
- run: npm ci
- run: npx fts check static/spec/models/order-discount.fts
- run: npx fts test static/spec/models/order-discount.fts --pretty
- run: node examples/spec/typescript-codegen/generate.mjs --check
- run: node --test "examples/spec/**/*.test.mjs"
Порядок не случаен — каждый шаг дешевле и специфичнее следующего:
fts check— синтаксис и типы: опечатка в ключевом слове или ссылка на несуществующее поле падает за миллисекунды, не доходя до логики правил.fts test --pretty— предметные примеры: правило скомпилировалось, но даёт не то число. Это первое место, где падает содержательная ошибка бизнес-логики.generate.mjs --check— drift между моделью и закоммиченным TypeScript. Модель уже верна на этом шаге; ошибка означает, что генерацию забыли перезапустить после правки.fts.node --test examples/spec/**/*.test.mjs— интеграции: HTTP-сервис, command guard, форма. Самый дорогой и самый дальний от модели слой падает последним.
Если поменять правило в .fts, не запуская fts generate, третий шаг
поймает расхождение раньше, чем код дойдёт до продакшена — именно это и
проверяет generate.test.mjs.
Работа агента
MCP-инструмент fts_generate устроен строже, чем CLI: у него нет параметра
--out, и он всегда возвращает { generation: generateTypeScript(...) }
как structured content, ничего не записывая на диск. Это осознанное
ограничение — агент может предложить генерацию для просмотра, но запись
файлов и коммит остаются отдельным контролируемым шагом, который выполняет
человек или pipeline, а не сам вызов инструмента.
Практика в песочнице
Измените процент в правиле «Большая покупка» и посмотрите, как меняется
сгенерированный ftsUtilities.
Типичные ошибки
FTS_NO_UTILITY_EXAMPLES— в утилите нет ни одного примера.fts generateиfts testтребуют хотя бы один пример: без него нечего запускать какnode:test, а.ts-реализация без проверки — не более чем недоказанное обещание.FTS_UTILITY_PROPERTY— сгенерированная функция и интерпретатор одинаково бросают ошибку, если результат нарушаетсвойство. В CI это будет падение сгенерированного теста, а не молчаливо неверное число.FTS_UTILITY_INPUT_TYPE— вход утилиты не совпадает с типом поля (например, строка вместо числа). Диагностика одна и та же что приfts run, что при вызовеftsUtilities[...]из сгенерированного кода: источник типов — модель, а не ручная TypeScript-сигнатура.
Чек-лист
- Модель
.ftsпроходитfts checkиfts testдо генерации. fts generate --outвызывается после каждой правки правила.- CI сверяет закоммиченный код через
--check, а не доверяет тому, что разработчик не забыл перегенерировать. - Порядок шагов в CI — от дешёвой проверки синтаксиса к дорогим интеграциям.
- Pull request с правкой
.ftsпоказывает маленький читаемый diff модели, а не только большой diff сгенерированного файла.