docs(specs): archive local verification remediation

This commit is contained in:
2026-08-23 21:21:22 +03:00
parent 51344d4988
commit 72444dbbb1
11 changed files with 441 additions and 4 deletions
+1
View File
@@ -10,6 +10,7 @@
openspec validate <change> --type change --store wealth-ledger-specs openspec validate <change> --type change --store wealth-ledger-specs
openspec instructions apply --change <change> --json --store wealth-ledger-specs openspec instructions apply --change <change> --json --store wealth-ledger-specs
bash scripts/verify-openspec-authority.sh bash scripts/verify-openspec-authority.sh
bash scripts/verify-runtime-openapi.sh
``` ```
## Границы ## Границы
@@ -0,0 +1,3 @@
schema: spec-driven
created: 2026-08-23
goal: Закрыть WL-P1-05 и WL-P2-06, WL-P2-09, WL-P2-11; явно отложить WL-P2-05 и WL-P2-13 до стабильного Prisma migration gate.
@@ -0,0 +1,5 @@
# make-local-verification-agent-safe
Сделать конкретные локальные backend и runtime OpenAPI проверки
детерминированными и безопасными для агента; не маскировать заблокированный
Prisma e2e migration gate общим verdict.
@@ -0,0 +1,103 @@
## Context
Корень workspace — coordination directory, а не package-manager workspace и не
Git-репозиторий. `frontend`, `backend`, `specs` и `docs` — независимые Git
roots. `specs/scripts/verify-openspec-authority.sh` уже показывает правильное
место для versioned cross-repository tooling: script определяет workspace
относительно себя, а не доверяет текущей директории.
Frontend закрепляет Node 22 и имеет offline `npm run verify`. Для backend нет
эквивалентных runtime policy и отдельных quality gates: `lint` запускает ESLint
с `--fix`, base tsconfig включает тесты, а build tsconfig исключает их.
Существующие e2e tests создают in-memory Nest application, но их script сначала
запускает Prisma на фиксированной `/tmp` DB и сейчас падает до Jest. Swagger
доступен по `/api/docs-json` при `NODE_ENV=development`;
`frontend/scripts/openapi:sync.mjs` — единственный намеренный writer committed
snapshot и generated types.
## Goals / Non-Goals
**Цели:**
- Восстановить исполнимые backend gates для lint и TypeScript приложения и
тестов с понятной диагностикой failures.
- Доказать отдельной read-only командой, что полный runtime OpenAPI document
совпадает с committed frontend snapshot.
- Сделать Node/npm bootstrap и конкретные verification commands discoverable
для агентов.
- Сохранить pre-existing working-tree state пользователя; не создавать
versioned writes во время проверок.
**Не входит в scope:**
- CI/CD, remote verification, Docker, новый task runner, package-manager
workspace или общий workspace verifier.
- Изоляция backend e2e DB, обновление Prisma packages и восстановление
migration gate: WL-P2-05 и зависимый WL-P2-13 отложены из-за нестабильности
Prisma 7.9.1 на Node 22.
- JSON report format, cached results, plug-in architecture, arbitrary diff
bases, сохранённые hashes либо fixture/unit-test harness для process scripts.
- Изменение OpenAPI sync writer, generated frontend types, пользовательских API
statuses, T-Bank integration, application features, перенос business glossary
`CONTEXT.md` или удаление существующего OpenSpec authority fixture test.
## Decisions
### Backend check и fix — разные операции
Backend предоставляет `lint:check` без `--fix`, `lint:fix` с явным write intent
и сохраняет `lint` как совместимый alias для `lint:check`. Он предоставляет
отдельные команды `typecheck:app` и `typecheck:test` на основе выделенных
tsconfig files. Эти команды не создают repository artifacts; build и e2e не
объявляются частью нового общего verdict.
### Runtime OpenAPI сверяется отдельной read-only командой
`specs/scripts/verify-runtime-openapi.sh` определяет workspace из собственного
пути, поднимает backend с детерминированным development environment, ждёт local
readiness, получает `/api/docs-json` и всегда завершает child process. Команда
рекурсивно упорядочивает JSON object keys, сохраняя array order, записывает
canonical runtime и snapshot documents во временную директорию, сравнивает их
SHA-256 и выводит unified diff при различии.
Она никогда не вызывает `openapi:sync`, не записывает frontend snapshot и не
обновляет generated types. Это узкий контрактный gate, а не orchestration
interface для всех репозиториев.
### Bootstrap и документация задают один contract
Оба приложения требуют Node 22 и `npm ci`. Scoped `AGENTS.md` перечисляют
только реально существующие non-mutating verification commands и явно отделяют
писатели `lint:fix` и `openapi:sync`. Durable development documentation
показывает prerequisites и границы отдельной runtime-contract команды.
### Prisma findings явно отложены, а не маскируются обходом
`prisma migrate deploy` 7.9.1 воспроизводимо возвращает пустой schema-engine
error как на фиксированной, так и на уникальной временной SQLite DB; включение
engine diagnostics меняет результат. На 2026-08-23 опубликованная latest
stable version также 7.9.1. Не вводится второй migration mechanism и не
обновляются зависимости без подтверждённого stable candidate. Реестр фиксирует
дату, причину и trigger review: следующая stable Prisma должна трижды применить
existing migrations к чистой DB на Node 22.
## Risks / Trade-offs
- [Runtime backend не стартует или child process остаётся после ошибки] →
command создаёт cleanup trap до запуска backend и сохраняет фактический exit
status.
- [Canonicalization скрывает различие контрактов] → нормализуется только
порядок object keys; arrays и каждое JSON field остаются частью SHA-256 input.
- [Prisma schema engine остаётся недоступным на supported runtime] → WL-P2-05
и WL-P2-13 остаются `deferred` до опубликованной stable версии, прошедшей
оговорённый reproduction gate.
- [Ignored artifacts путаются с versioned writes] → отдельные команды не
считают `build` или e2e своим успехом и документируют свои временные ресурсы.
## Migration Plan
Не нужны migration production data или deployment sequence. Разработчики
запускают `npm ci` под Node 22 в каждом application repository, используют
`npm run lint:fix` только осознанно и запускают runtime OpenAPI check отдельно
из workspace. Rollback — обычный revert independently versioned changes; он не
меняет committed API artifacts или persistent user data.
@@ -0,0 +1,42 @@
## Why
В workspace четыре независимых Git-репозитория, но нет единого
воспроизводимого verdict локальной проверки. Backend `lint` изменяет исходные
файлы, backend e2e останавливается на настройке Prisma до Jest, для тестового
TypeScript нет исполнимого gate, а committed frontend OpenAPI snapshot не
сверяется с runtime-документом. Из-за этого агент может получить зелёный либо
неполный результат, не узнав об изменении исходников или дрейфе
межрепозиторного контракта.
## What Changes
- Разделить backend lint-проверку и явный auto-fix, добавить отдельные
TypeScript gates для приложения и тестов.
- Добавить сверку runtime OpenAPI с committed snapshot: она поднимает
детерминированный локальный backend, canonicalize оба полных документа во
временном хранилище, сравнивает SHA-256 и показывает diff при дрейфе, не
вызывая `openapi:sync`.
- Унифицировать local bootstrap frontend и backend на Node 22 и `npm ci`,
документировать конкретные gates и обновить scoped agent instructions.
- Обновить audit remediation registry: этот change станет единственным active
remediation для WL-P1-05, WL-P2-06, WL-P2-09 и WL-P2-11; WL-P2-05 и
WL-P2-13 будут явно отложены до стабильного Prisma migration gate.
## Capabilities
### Modified Capabilities
- `project-foundation`: Выравнивает требования к runtime/bootstrap приложений и
локальные agent verification instructions frontend и backend.
- `api-contract-generation`: Требует явной локальной runtime сверки между
backend OpenAPI и committed frontend snapshot.
## Impact
Затронуты `backend`, `frontend`, `specs` и `docs`. Изменятся backend package
scripts, TypeScript-конфигурации, local runtime metadata и документация;
generated frontend snapshot и offline-поведение `npm run verify` не изменятся.
Небольшая read-only runtime-contract команда в `specs/scripts` не имеет
manifest, сохранённых hashes, кэша, plug-in system или fixture test harness.
Не вводятся изменения пользовательского HTTP-контракта, persisted production
data, внешний сетевой доступ или T-Bank credentials.
@@ -0,0 +1,38 @@
## MODIFIED Requirements
### Requirement: Frontend не зависит от OpenAPI во время работы
Работающий frontend MUST использовать ранее сгенерированные transport types и MUST NOT требовать доступность Swagger или OpenAPI endpoint во время запуска, выполнения пользовательского сценария или локальной проверки актуальности generated artifact. Доступ к backend OpenAPI endpoint разрешён только команде синхронизации snapshot и отдельной read-only runtime-contract command.
#### Scenario: OpenAPI endpoint недоступен после сборки frontend
- **WHEN** frontend уже собран, а endpoint OpenAPI временно недоступен
- **THEN** frontend продолжает использовать сгенерированный код для запросов к API
#### Scenario: Локальная проверка выполняется без backend
- **WHEN** разработчик запускает локальный frontend quality gate
- **THEN** проверка актуальности generated artifact использует сохранённый snapshot и не обращается к OpenAPI endpoint
#### Scenario: Разработчик проверяет runtime-контракт
- **WHEN** разработчик запускает отдельную runtime-contract command
- **THEN** command может поднять изолированный local backend только для сравнения
его полного runtime OpenAPI document с committed frontend snapshot и не
обновляет ни один из артефактов
### Requirement: Отдельная команда обнаруживает runtime drift OpenAPI
Runtime-contract command MUST получать полный OpenAPI document от изолированного
local backend по `/api/docs-json`, рекурсивно canonicalize порядок JSON object
keys, сохраняя каждое field и array order, и сравнивать его SHA-256 с canonical
committed frontend snapshot. При любом content mismatch она MUST завершаться
ошибкой и показывать diff. Verification MUST NOT вызывать snapshot
synchronization command или записывать committed OpenAPI либо generated type
artifacts.
#### Scenario: Runtime success status отличается от committed snapshot
- **WHEN** backend runtime OpenAPI описывает response status, отличный от
committed frontend snapshot
- **THEN** runtime-contract command завершается ошибкой с diff и оставляет
snapshot и generated types без изменений
#### Scenario: Runtime OpenAPI совпадает с committed snapshot
- **WHEN** каждое field canonical runtime document совпадает с canonical
committed frontend snapshot
- **THEN** runtime-contract stage завершается успешно без изменения frontend files
@@ -0,0 +1,27 @@
## MODIFIED Requirements
### Requirement: Репозитории содержат локальные инструкции для AI-агентов
Система SHALL содержать обзорный `AGENTS.md` в корне рабочей области и отдельный `AGENTS.md` в репозиториях frontend, backend, specs и docs. Инструкции MUST описывать назначение области, локальные команды проверки, границы ответственности и связь с SDD-артефактами. Каждая инструкция MUST объявлять `wealth-ledger-specs` и `specs/openspec` как canonical OpenSpec authority, требовать явный `--store wealth-ledger-specs` для поддерживаемых OpenSpec операций и запрещать versioned framework-specific planning artifacts вне canonical store. Локальные инструкции MUST направлять агента к durable remediation registry, когда задача связана с findings технического аудита. Backend instructions MUST различать non-mutating verification commands и явные auto-fix commands.
#### Scenario: Агент начинает работу в отдельном репозитории
- **WHEN** агент открывает любой из репозиториев frontend, backend, specs или docs
- **THEN** он может определить локальные правила работы, canonical OpenSpec authority и источник связанных спецификаций из `AGENTS.md` этого репозитория
#### Scenario: Агент получает audit-derived задачу из отдельного репозитория
- **WHEN** задача ссылается на finding технического аудита
- **THEN** локальный `AGENTS.md` указывает на remediation registry и canonical OpenSpec workflow с явным store selection до начала изменения кода
#### Scenario: Агент выбирает backend quality gate
- **WHEN** агент читает backend instructions перед локальной проверкой
- **THEN** он запускает документированные non-mutating lint/typecheck commands и не получает auto-fix как неявный quality gate
### Requirement: Локальный план MVP не является источником спецификации
Система SHALL содержать корневой локальный `MVP.md` с границами MVP, принятыми решениями, порядком поставок и критериями готовности. Требования к функциональности MUST храниться в OpenSpec, а устойчивая документация MUST храниться в репозитории docs. Frontend и backend MUST требовать Node 22 и документировать lockfile-safe bootstrap через `npm ci`.
#### Scenario: Решение уточняется во время SDD-изменения
- **WHEN** изменение уточняет наблюдаемое поведение системы
- **THEN** оно фиксируется в OpenSpec и соответствующей документации, а `MVP.md` остается компактной картой поставки
#### Scenario: Агент bootstrap-ит application repository
- **WHEN** агент начинает локальную проверку frontend или backend на чистой машине
- **THEN** он может определить Node 22 и выполнить `npm ci` без угадывания runtime policy
@@ -0,0 +1,58 @@
## 1. Зафиксировать активный и отложенный audit scope
- [x] 1.1 Обновить `docs/development/audit-remediation.md`: пометить только
WL-P1-05, WL-P2-06, WL-P2-09 и WL-P2-11 как `active` с
`make-local-verification-agent-safe`; записать WL-P2-05 и WL-P2-13 как
`deferred` с датой, наблюдаемой причиной Prisma 7.9.1 и trigger review;
не помечать finding как `done`.
- [x] 1.2 Обновить durable development documentation и scoped `AGENTS.md`,
чтобы они называли только реально существующие non-mutating gates, Node 22 и
`npm ci`, а writers `lint:fix` и `openapi:sync` были явными.
## 2. Восстановить non-mutating local gates backend
- [x] 2.1 Добавить backend Node 22 runtime metadata и lockfile-safe bootstrap
guidance; разделить `lint:check`, `lint:fix` и совместимый non-mutating alias
`lint` в `backend/package.json`.
- [x] 2.2 Добавить отдельные backend TypeScript configurations и scripts для
приложения и тестов; application compilation исключает test sources, test
compilation включает test surface, оба выполняются без repository artifacts.
- [x] 2.3 Исправить существующий неполный `TbankClient` test double или
заменить его typed test adapter, чтобы test TypeScript gate проходил без
ослабления production client contract.
- [x] 2.4 Документировать, что текущие `test:e2e` и общий workspace verdict не
являются частью этого change, пока WL-P2-05/P2-13 отложены; не добавлять
обходной Prisma runner или ложный `npm run verify`.
## 3. Проверить runtime API contract без writers
- [x] 3.1 Реализовать минимальный runtime OpenAPI comparison helper, который
запускает backend с deterministic development test environment, ждёт local
readiness, получает `/api/docs-json`, canonicalizes object keys во временных
files, сравнивает SHA-256 полного document, выводит diff при mismatch и всегда
завершает child process.
- [x] 3.2 Подтвердить, что helper никогда не вызывает
`frontend/scripts/openapi-sync.mjs`, не обращается к external service, не
требует T-Bank credentials и не записывает committed snapshot/generated
types; запустить его для текущего contract и устранить каждый content mismatch
через согласованный contract workflow, а не скрытым переписыванием.
## 4. Completion evidence и audit tracking
- [x] 4.1 Запустить `openspec validate make-local-verification-agent-safe
--type change --strict --store wealth-ledger-specs`, `openspec validate
--specs --strict --store wealth-ledger-specs`, authority verifier, отдельный
runtime OpenAPI check и relevant per-repository checks; зафиксировать точные
commands и observed results.
- [x] 4.2 Выполнить independent read-only review diff, OpenSpec deltas,
temporary-resource behavior, runtime-contract comparison и точности
документации; устранить каждую confirmed issue и подтвердить, что этот task
list не содержит post-archive bookkeeping, commit или registry-completion
checkbox.
- [x] 4.3 Выполнить applicable pre-archive `opsx-verify` procedure и устранить
каждый blocking result. Эта задача завершается до archive. Только после
завершения каждого checkbox change может быть archived и synced; только после
этого remediation registry вне task list получает actual archive path,
commits, terminal evidence, четыре статуса `done` и ровно одну следующую
рекомендуемую задачу. WL-P2-05 и WL-P2-13 остаются `deferred` до своего
отдельного change.
+28 -2
View File
@@ -29,16 +29,42 @@ Frontend SHALL хранить в репозитории версионируем
- **THEN** команда завершается с ошибкой и существующие snapshot и сгенерированные transport types не изменяются - **THEN** команда завершается с ошибкой и существующие snapshot и сгенерированные transport types не изменяются
### Requirement: Frontend не зависит от OpenAPI во время работы ### Requirement: Frontend не зависит от OpenAPI во время работы
Работающий frontend MUST использовать ранее сгенерированные transport types и MUST NOT требовать доступность Swagger или OpenAPI endpoint во время запуска, выполнения пользовательского сценария или локальной проверки актуальности generated artifact. Доступ к backend OpenAPI endpoint разрешён только команде синхронизации snapshot. Работающий frontend MUST использовать ранее сгенерированные transport types и MUST NOT требовать доступность Swagger или OpenAPI endpoint во время запуска, выполнения пользовательского сценария или локальной проверки актуальности generated artifact. Доступ к backend OpenAPI endpoint разрешён только команде синхронизации snapshot и отдельной read-only runtime-contract command.
#### Scenario: OpenAPI endpoint недоступен после сборки frontend #### Scenario: OpenAPI endpoint недоступен после сборки frontend
- **WHEN** frontend уже собран, а endpoint OpenAPI временно недоступен - **WHEN** frontend уже собран, а endpoint OpenAPI временно недоступен
- **THEN** frontend продолжает использовать сгенерированный код для запросов к API - **THEN** frontend продолжает использовать сгенерированный код для запросов к API
#### Scenario: Локальная проверка выполняется без backend #### Scenario: Локальная проверка выполняется без backend
- **WHEN** разработчик запускает локальный quality gate без доступного backend - **WHEN** разработчик запускает локальный frontend quality gate
- **THEN** проверка актуальности generated artifact использует сохранённый snapshot и не обращается к OpenAPI endpoint - **THEN** проверка актуальности generated artifact использует сохранённый snapshot и не обращается к OpenAPI endpoint
#### Scenario: Разработчик проверяет runtime-контракт
- **WHEN** разработчик запускает отдельную runtime-contract command
- **THEN** command может поднять изолированный local backend только для сравнения
его полного runtime OpenAPI document с committed frontend snapshot и не
обновляет ни один из артефактов
### Requirement: Отдельная команда обнаруживает runtime drift OpenAPI
Runtime-contract command MUST получать полный OpenAPI document от изолированного
local backend по `/api/docs-json`, рекурсивно canonicalize порядок JSON object
keys, сохраняя каждое field и array order, и сравнивать его SHA-256 с canonical
committed frontend snapshot. При любом content mismatch она MUST завершаться
ошибкой и показывать diff. Verification MUST NOT вызывать snapshot
synchronization command или записывать committed OpenAPI либо generated type
artifacts.
#### Scenario: Runtime success status отличается от committed snapshot
- **WHEN** backend runtime OpenAPI описывает response status, отличный от
committed frontend snapshot
- **THEN** runtime-contract command завершается ошибкой с diff и оставляет
snapshot и generated types без изменений
#### Scenario: Runtime OpenAPI совпадает с committed snapshot
- **WHEN** каждое field canonical runtime document совпадает с canonical
committed frontend snapshot
- **THEN** runtime-contract stage завершается успешно без изменения frontend files
### Requirement: Frontend обнаруживает дрейф сгенерированного контракта ### Requirement: Frontend обнаруживает дрейф сгенерированного контракта
Frontend SHALL проверять, что committed generated transport types соответствуют versioned OpenAPI snapshot, и MUST завершать quality gate с ошибкой при расхождении. Frontend SHALL проверять, что committed generated transport types соответствуют versioned OpenAPI snapshot, и MUST завершать quality gate с ошибкой при расхождении.
+10 -2
View File
@@ -7,7 +7,7 @@
## Requirements ## Requirements
### Requirement: Репозитории содержат локальные инструкции для AI-агентов ### Requirement: Репозитории содержат локальные инструкции для AI-агентов
Система SHALL содержать обзорный `AGENTS.md` в корне рабочей области и отдельный `AGENTS.md` в репозиториях frontend, backend, specs и docs. Инструкции MUST описывать назначение области, локальные команды проверки, границы ответственности и связь с SDD-артефактами. Каждая инструкция MUST объявлять `wealth-ledger-specs` и `specs/openspec` как canonical OpenSpec authority, требовать явный `--store wealth-ledger-specs` для поддерживаемых OpenSpec операций и запрещать versioned framework-specific planning artifacts вне canonical store. Локальные инструкции MUST направлять агента к durable remediation registry, когда задача связана с findings технического аудита. Система SHALL содержать обзорный `AGENTS.md` в корне рабочей области и отдельный `AGENTS.md` в репозиториях frontend, backend, specs и docs. Инструкции MUST описывать назначение области, локальные команды проверки, границы ответственности и связь с SDD-артефактами. Каждая инструкция MUST объявлять `wealth-ledger-specs` и `specs/openspec` как canonical OpenSpec authority, требовать явный `--store wealth-ledger-specs` для поддерживаемых OpenSpec операций и запрещать versioned framework-specific planning artifacts вне canonical store. Локальные инструкции MUST направлять агента к durable remediation registry, когда задача связана с findings технического аудита. Backend instructions MUST различать non-mutating verification commands и явные auto-fix commands.
#### Scenario: Агент начинает работу в отдельном репозитории #### Scenario: Агент начинает работу в отдельном репозитории
- **WHEN** агент открывает любой из репозиториев frontend, backend, specs или docs - **WHEN** агент открывает любой из репозиториев frontend, backend, specs или docs
@@ -17,13 +17,21 @@
- **WHEN** задача ссылается на finding технического аудита - **WHEN** задача ссылается на finding технического аудита
- **THEN** локальный `AGENTS.md` указывает на remediation registry и canonical OpenSpec workflow с явным store selection до начала изменения кода - **THEN** локальный `AGENTS.md` указывает на remediation registry и canonical OpenSpec workflow с явным store selection до начала изменения кода
#### Scenario: Агент выбирает backend quality gate
- **WHEN** агент читает backend instructions перед локальной проверкой
- **THEN** он запускает документированные non-mutating lint/typecheck commands и не получает auto-fix как неявный quality gate
### Requirement: Локальный план MVP не является источником спецификации ### Requirement: Локальный план MVP не является источником спецификации
Система SHALL содержать корневой локальный `MVP.md` с границами MVP, принятыми решениями, порядком поставок и критериями готовности. Требования к функциональности MUST храниться в OpenSpec, а устойчивая документация MUST храниться в репозитории docs. Система SHALL содержать корневой локальный `MVP.md` с границами MVP, принятыми решениями, порядком поставок и критериями готовности. Требования к функциональности MUST храниться в OpenSpec, а устойчивая документация MUST храниться в репозитории docs. Frontend и backend MUST требовать Node 22 и документировать lockfile-safe bootstrap через `npm ci`.
#### Scenario: Решение уточняется во время SDD-изменения #### Scenario: Решение уточняется во время SDD-изменения
- **WHEN** изменение уточняет наблюдаемое поведение системы - **WHEN** изменение уточняет наблюдаемое поведение системы
- **THEN** оно фиксируется в OpenSpec и соответствующей документации, а `MVP.md` остается компактной картой поставки - **THEN** оно фиксируется в OpenSpec и соответствующей документации, а `MVP.md` остается компактной картой поставки
#### Scenario: Агент bootstrap-ит application repository
- **WHEN** агент начинает локальную проверку frontend или backend на чистой машине
- **THEN** он может определить Node 22 и выполнить `npm ci` без угадывания runtime policy
### Requirement: Frontend предоставляет минимальный каркас приложения ### Requirement: Frontend предоставляет минимальный каркас приложения
Frontend SHALL предоставлять русскоязычный адаптивный каркас приложения с маршрутами `/login`, `/register`, `/accounts`, `/accounts/:accountId` и `/settings`, светлой финансовой темой и навигацией для счетов и настроек. Каркас MUST содержать состояния загрузки, ошибки и пустых данных без реализации доменных сценариев. Frontend SHALL предоставлять русскоязычный адаптивный каркас приложения с маршрутами `/login`, `/register`, `/accounts`, `/accounts/:accountId` и `/settings`, светлой финансовой темой и навигацией для счетов и настроек. Каркас MUST содержать состояния загрузки, ошибки и пустых данных без реализации доменных сценариев.
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
workspace_dir="$(cd -- "$script_dir/../.." && pwd)"
backend_dir="$workspace_dir/backend"
snapshot_path="$workspace_dir/frontend/src/shared/api/spec/openapi.json"
if [[ ! -f "$snapshot_path" ]]; then
echo "Missing committed OpenAPI snapshot: $snapshot_path" >&2
exit 1
fi
node_major="$(node -p "process.versions.node.split('.')[0]")"
if [[ "$node_major" != "22" ]]; then
echo "Node 22 is required; found $(node --version)." >&2
exit 1
fi
temporary_dir="$(mktemp -d "${TMPDIR:-/tmp}/wealth-ledger-runtime-openapi.XXXXXX")"
backend_pid=""
cleanup() {
if [[ -n "$backend_pid" ]] && kill -0 "$backend_pid" 2>/dev/null; then
kill "$backend_pid" 2>/dev/null || true
wait "$backend_pid" 2>/dev/null || true
fi
rm -rf "$temporary_dir"
}
trap cleanup EXIT INT TERM
port="$(node -e "const server = require('node:net').createServer(); server.listen(0, '127.0.0.1', () => { console.log(server.address().port); server.close(); });")"
runtime_path="$temporary_dir/runtime.json"
canonical_runtime_path="$temporary_dir/runtime.canonical.json"
canonical_snapshot_path="$temporary_dir/snapshot.canonical.json"
backend_log="$temporary_dir/backend.log"
compile_log="$temporary_dir/compile.log"
if ! (
cd "$backend_dir"
./node_modules/.bin/tsc --project tsconfig.build.json --outDir "$temporary_dir/dist"
) >"$compile_log" 2>&1; then
echo "Temporary backend compilation failed:" >&2
sed -n '1,160p' "$compile_log" >&2
exit 1
fi
ln -s "$backend_dir/node_modules" "$temporary_dir/dist/node_modules"
(
cd "$backend_dir"
NODE_ENV=development \
PORT="$port" \
DATABASE_URL="file:$temporary_dir/runtime.db" \
FRONTEND_ORIGIN="http://localhost:5173" \
JWT_SECRET="runtime-openapi-check-secret-at-least-32" \
TOKEN_ENCRYPTION_KEY="MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=" \
TBANK_GRPC_ENDPOINT="127.0.0.1:1" \
TBANK_RATE_LIMIT_PER_SECOND=5 \
LOG_LEVEL=error \
node "$temporary_dir/dist/src/main.js"
) >"$backend_log" 2>&1 &
backend_pid="$!"
for _ in $(seq 1 60); do
if node -e "fetch(process.argv[1]).then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))" "http://127.0.0.1:$port/api/docs-json"; then
break
fi
if ! kill -0 "$backend_pid" 2>/dev/null; then
echo "Backend stopped before exposing /api/docs-json:" >&2
sed -n '1,160p' "$backend_log" >&2
exit 1
fi
sleep 0.25
done
if ! node -e 'fetch(process.argv[1]).then(async (response) => { if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); require("node:fs").writeFileSync(process.argv[2], await response.text()); }).catch((error) => { console.error(`OpenAPI fetch failed: ${error.message}`); process.exit(1); })' "http://127.0.0.1:$port/api/docs-json" "$runtime_path"; then
echo "Backend did not expose /api/docs-json within 15 seconds:" >&2
sed -n '1,160p' "$backend_log" >&2
exit 1
fi
if node - "$runtime_path" "$snapshot_path" "$canonical_runtime_path" "$canonical_snapshot_path" <<'NODE'
const { createHash } = require('node:crypto');
const { readFileSync, writeFileSync } = require('node:fs');
function canonicalize(value) {
if (Array.isArray(value)) return value.map(canonicalize);
if (value && typeof value === 'object') {
return Object.fromEntries(
Object.keys(value)
.sort()
.map((key) => [key, canonicalize(value[key])]),
);
}
return value;
}
const [runtimePath, snapshotPath, canonicalRuntimePath, canonicalSnapshotPath] =
process.argv.slice(2);
const documents = [runtimePath, snapshotPath].map((path) =>
canonicalize(JSON.parse(readFileSync(path, 'utf8'))),
);
const serialized = documents.map((document) => `${JSON.stringify(document, null, 2)}\n`);
writeFileSync(canonicalRuntimePath, serialized[0]);
writeFileSync(canonicalSnapshotPath, serialized[1]);
const hashes = serialized.map((value) =>
createHash('sha256').update(value).digest('hex'),
);
console.log(`runtime SHA-256: ${hashes[0]}`);
console.log(`snapshot SHA-256: ${hashes[1]}`);
process.exitCode = hashes[0] === hashes[1] ? 0 : 1;
NODE
then
comparison_status=0
else
comparison_status=$?
fi
if [[ "$comparison_status" -ne 0 ]]; then
echo "Runtime OpenAPI differs from the committed frontend snapshot:" >&2
diff -u "$canonical_snapshot_path" "$canonical_runtime_path" || true
exit 1
fi
echo "Runtime OpenAPI matches the committed frontend snapshot."