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

Сквозной проект: исполнение заказа от модели до сервиса и формы

Сквозной проект: исполнение заказа от модели до сервиса и формы

Код в этой главе записан в прежней поверхности языка — со словами категория, объект, утилита. Сегодняшний компилятор её слова читает, но программой такой файл не считает: файл, где есть только утилиты, flang check отклоняет. Разбор задачи в главе верен; синтаксис переносится по таблице из главы «Старые модели».

Капстоун соединяет всё, что было в курсе, в одном сквозном проекте: предметный объект, утилиту с правилом и свойством, морфизм, теорему допуска команды, HTTP-сервис, форму и генерацию TypeScript для CI. Модель одна, а поверхностей, которые её используют, — пять. Ниже — команда и ожидаемый результат на каждом шаге, без пропусков.

1. Модель

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

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

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

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

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

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

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

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

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

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

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

Объект Заказ несёт факт готовности, объект Покупка — вход для расчёта скидки. Утилита считает деньги, морфизм описывает разрешённый переход «готов → можно отгружать», теорема применяет морфизм к конкретному номеру заказа. Ничего из этого не пишет в базу и не вызывает курьерскую службу — модель только считает и проверяет допуск.

Потолок скидки записан числом (результат не больше 10000), а не процентом от поля (результат не больше 20 процентов от поля сумма), и оба правила явно называют свои границы по сумме. Так сделано не для красоты: свойство — постусловие, которое проверяется на всей области входа, а не только там, где сработали правила. Процент от поля меняет знак вместе с полем, поэтому при отрицательной сумме предел уходит в минус, начальный ноль перестаёт ему удовлетворять, и утилита падает с FTS_UTILITY_PROPERTY на входе, которого правила даже не касались. Второе правило закрывает суммы выше потолка, а второй пример показывает, что предел достижим ровно — иначе свойство не проверяло бы ничего. Разбор обеих ловушек — в главе об антипаттернах; карта покрытия ftsmap из пакета языка ловит их до того, как это сделает прод.

2. Архитектура

order.fts компилируется один раз в canonical JSON — обычный FtsDocument, без циклов и без скрытого состояния. От этого документа расходятся пять независимых веток, и ни одна не читает исходный текст .fts повторно: React берёт структуру формы, интерпретатор утилит считает скидку, verifier строит сертификат для guard, CI гоняет examples и сверяет генерацию, MCP-инструменты отдают тот же документ агенту. Если правило меняется, меняется только левый узел графа — остальные пересчитываются от него, а не правятся по отдельности.

3. Шаг за шагом

3.1 Проверка модели

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

Компилятор проверяет, что состояние «Готов к отгрузке» типизировано, поля утилиты существуют в объекте Покупка, а условие морфизма ссылается на реальный факт. Дальше — предметный тест:

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 }
  ]
}

Если правило «Большой заказ» когда-нибудь поменяют на 12 процентов, этот шаг упадёт первым — раньше HTTP-сервиса и раньше React-формы.

3.2 Допуск отгрузки: теорема и доказательство

Разрешение на отгрузку — не поле в базе, а результат prove на снимке данных. Снимок — обычный JSON, который application service собирает из своих таблиц перед тем, как принять решение об отгрузке:

{
  "заказы": [
    {
      "номер": "ЗК-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/spec/shipment-guard/guard.mjs оборачивает то же самое в переиспользуемую функцию для application service:

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

даёт "allowed": true и exit code 0. Тот же номер, но со снимком, где склад не подтвердил приёмку:

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

С тем же номером заказа, но снимком, где "склад подтвердил": false и "готов к отгрузке": false, ответ меняется на:

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

Модель, текст теоремы и код guard не менялись — изменился только снимок данных, и решение изменилось вместе с ним. Exit code 1 делает эту разницу пригодной для shell-скрипта и для CI, а не только для человека, читающего JSON. Реальный вызов shipping.createShipment(orderId) в application service должен идти строго после allowed === true — эффект живёт вне FTS, а не внутри guard.

3.3 HTTP-сервис

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

Сервис на старте прогоняет testUtilities(document): если пример модели не сходится, процесс не поднимается вообще. После старта:

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

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

/contract не хардкодит список полей — он берёт их из document.structures. Добавили поле в модель — контракт обновился сам. Запрос с типом входа не по модели (строка вместо числа) возвращает 400 с диагностикой FTS_UTILITY_INPUT_TYPE, а не 500: клиент видит причину отказа на языке предметной области, а не стектрейс.

3.4 Форма

Схема формы — чистая функция от модели, без обращения к сети:

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

examples/spec/react-form/DiscountForm.jsx потребляет ровно эту схему: рисует поле под каждый элемент fields, а предпросчёт скидки выполняет тем же executeUtility прямо в браузере — пользователь видит сумму скидки без запроса на сервер. При этом сервер всё равно считает заново при отправке формы: браузеру не доверяют, но и не заставляют ждать его. Предпросчёт идёт тем же интерпретатором, что и на сервере, поэтому любая диагностика — от FTS_UTILITY_INPUT_TYPE до сработавшего свойства — видна в браузере раньше, чем форма отправит запрос, но финальное решение всё равно остаётся за /discount: фронтенд ускоряет обратную связь, а не заменяет проверку. Само свойство «Скидка ограничена» на исправной модели не срабатывает никогда — потолок держат правила, а свойство остаётся страховкой на случай их правки.

3.5 Генерация TypeScript

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

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

Второй вызов ничего не пишет — только сравнивает. Если кто-то поправит сгенерированный .ts руками, --check вернёт список отставших файлов и код возврата 1. Подробный разбор — в главе про generation и CI.

3.6 CI

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

Готовый workflow examples/spec/ci/fts-check.yml выполняет пять шагов по порядку: npm ci, fts check, fts test --pretty, сверку генерации через --check, интеграционные тесты node --test. Первым падает самый дешёвый шаг — синтаксис модели, последним — самый дорогой, интеграции. Это тот же порядок, что и в шагах 3.1–3.5: модель → допуск → сервис/форма → генерация.

4. Definition of done

  • fts check успешен;
  • все examples проходят;
  • property защищает верхний предел скидки, задан числом (а не процентом от поля, который меняет знак вместе с ним) и достигается хотя бы одним примером ровно — иначе предел не проверяет ничего;
  • React читает структуру через browser entrypoint;
  • Node загружает модель один раз при старте, а не на каждый запрос;
  • command guard требует allowed: true до вызова эффекта;
  • внешний shipping effect не находится внутри FTS;
  • CI проверяет generated drift режимом --check;
  • README явно перечисляет assumptions морфизма — какие переходы считаются разрешёнными и почему.

5. Типичные ошибки капстоуна

  • Хардкодить список полей формы вместо чтения document.structures — тогда React и модель расходятся при первом же добавленном поле.
  • Вызывать shipping.createShipment без проверки decision.allowed — command guard теряет смысл, если его результат не читают.
  • Коммитить сгенерированный .ts без шага --check в CI — FTS_UTILITY_PROPERTY или изменённое правило останутся незамеченными до продакшена.
  • Передавать в HTTP-сервис вход без валидации типов — FTS_UTILITY_INPUT_TYPE должен вернуться клиенту как 400, а не всплыть как 500.
  • Подставлять пользовательский ввод в текст теоремы без проверки формата — shipmentTheorem в guard.mjs нарочно отклоняет номер заказа, не подходящий под /^[\p{L}\p{N}-]{1,32}$/u, прежде чем он попадёт в исходник .fts.

6. Задание для ролей

Junior добавляет поле и его отображение в схеме формы, проверяя, что required вычисляется автоматически из иногда является. Middle строит Node-эндпоинт и обработку диагностик FTS_UTILITY_* как 400, а не 500. Senior проектирует снимок данных для prove/verify и транзакционную границу вокруг command guard — где заканчивается ответственность FTS и начинается optimistic lock. Lead проверяет, что термины модели совпадают с языком предметного эксперта, и версионирует canonical JSON. Агент может предложить новый пример, но обязан пройти fts check, fts test и fts verify прежде, чем предложение попадёт в pull request.

Практика в песочнице

Критерий зрелости

Проект готов не тогда, когда .fts выглядит красиво, а когда удалено реальное дублирование: одна модель управляет вычислением и тестом, а адаптеры используют canonical JSON. При этом эффекты, UX и инфраструктура остаются в своих слоях — FTS не подменяет ни репозиторий, ни шину событий, ни React.

Вернитесь к каталогу из 232 прикладных кейсов, выберите ближайший к своей работе и повторите капстоун на собственном bounded context: один объект, одна утилита с примерами, один морфизм допуска команды и один адаптер (HTTP, форма или CLI), который использует canonical JSON, не затаскивая побочные эффекты внутрь спецификации.

Куда дальше: то, что начинается за границей спецификации

Курс кончается там же, где кончается FTS, — на границе, которую он проводит сознательно. Циклов, рекурсии, списков и вычислений над строками в языке нет, и это не недоделка: именно из-за них утилита FTS гарантированно завершается, а значит её можно исполнить в проверке допуска, в CI и внутри агента, не боясь зависания.

Но у границы есть вторая сторона, и однажды вы в неё упрётесь. Написать на FTS парсер собственного формата, обход дерева или сборку отчёта нельзя — там нужны ровно те средства, которых язык не даёт. В самом репозитории это ограничение записано прямым текстом:

в ядре FTS строка является типом поля, но не значением, над которым можно вычислять

Из-за этой строки ядро FTS нельзя было написать на самом FTS — парсер это вычисления над строками. Ради неё и появился flang: полный язык, снимающий ровно это ограничение. Отношение двух языков строгое, а не родственное:

  • FTS — тотальное подмножество flang. Любая существующая модель .fts — валидная программа языка, и вся она попадает в тотальный класс — то подмножество, где завершение доказано компилятором, а не обещано автором. Проверено сверкой двух движков на 19 593 входах, ноль расхождений.
  • Ядро FTS теперь написано на самом flang и работает нативно: четыре файла, 300 функций, все тотальные, побайтовое совпадение канонического JSON с ядром на TypeScript — и то же ядро, напечатанное в C, собирается обычным cc и даёт тот же документ без Node вообще.
  • Любая ваша модель печатается в восемь языков — C, C#, Elixir, Go, Java, JavaScript, Python, Rust — одной командой flang emit, с требованием совпадать с интерпретатором по значению и по тексту ошибки (модуль «Интеграция с любым языком»).

Практический вывод для того, кто дочитал капстоун: правило остаётся в .fts, а инструмент вокруг правила пишется на flang. Это ровно та граница, которую вы весь курс проводили между спецификацией и эффектами, только теперь у второй половины появился язык, у которого с первой доказанная совместимость, а не соглашение.

Если захотите разобраться, как устроен сам язык — почему он делит программы на два класса, как доказывается завершение и что произошло, когда компилятор языка переписали на нём самом, — это отдельный трек: «flang: язык, которому сутки». Начинать его после этого курса удобнее, чем до: половина решений там объясняется через FTS, и вы эту половину уже знаете.

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

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

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

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

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