Исполняемые спецификации на flang Генерация и CI: код, который не расходится с правилом
0%

Генерация и CI: код, который не расходится с правилом

Генерация и 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"

Порядок не случаен — каждый шаг дешевле и специфичнее следующего:

  1. fts check — синтаксис и типы: опечатка в ключевом слове или ссылка на несуществующее поле падает за миллисекунды, не доходя до логики правил.
  2. fts test --pretty — предметные примеры: правило скомпилировалось, но даёт не то число. Это первое место, где падает содержательная ошибка бизнес-логики.
  3. generate.mjs --check — drift между моделью и закоммиченным TypeScript. Модель уже верна на этом шаге; ошибка означает, что генерацию забыли перезапустить после правки .fts.
  4. 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 сгенерированного файла.

Кейсы каталога по этой теме

Дальше: performance

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

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

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

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