chore: use native OpenSpec validation

This commit is contained in:
2026-08-26 23:14:13 +03:00
parent 6f47589211
commit 2faf4eb8f3
17 changed files with 209 additions and 1429 deletions
+2 -11
View File
@@ -15,8 +15,7 @@ Proposal, Delta Specs, Design, Tasks, changes и archives хранятся в `o
## Workflow и verification ## Workflow и verification
Поддерживаемая CLI version задана в `openspec/.cli-version`; новые changes Новые changes используют project-local schema `wealth-ledger-sdd`. Артефакты создаются по
используют project-local schema `wealth-ledger-sdd`. Артефакты создаются по
одному после approval Change Owner. Lifecycle: одному после approval Change Owner. Lifecycle:
[`../docs/development/openspec-authority-and-lifecycle.md`](../docs/development/openspec-authority-and-lifecycle.md). [`../docs/development/openspec-authority-and-lifecycle.md`](../docs/development/openspec-authority-and-lifecycle.md).
@@ -26,20 +25,12 @@ affected areas. Подробный token-efficient routing загружаетс
из [`../docs/development/agent-token-efficiency.md`](../docs/development/agent-token-efficiency.md), из [`../docs/development/agent-token-efficiency.md`](../docs/development/agent-token-efficiency.md),
а не дублируется в schema instructions. а не дублируется в schema instructions.
Из workspace canonical pre-archive gate: Для structural validation из `specs`:
```bash ```bash
bash specs/scripts/verify-workspace-change.sh <change>
```
Для scoped diagnostics из `specs`:
```bash
bash scripts/verify-openspec-version.sh
openspec schema validate wealth-ledger-sdd --verbose openspec schema validate wealth-ledger-sdd --verbose
openspec validate <change> --type change --strict --store wealth-ledger-specs openspec validate <change> --type change --strict --store wealth-ledger-specs
openspec validate --specs --strict --store wealth-ledger-specs openspec validate --specs --strict --store wealth-ledger-specs
bash scripts/verify-openspec-authority.sh
``` ```
Structural validation не заменяет semantic verification и independent read-only review. Tasks отмечаются только после выполненной работы; archive не допускает unchecked tasks или verification override. Structural validation не заменяет semantic verification и independent read-only review. Tasks отмечаются только после выполненной работы; archive не допускает unchecked tasks или verification override.
+1 -9
View File
@@ -10,20 +10,12 @@ Design должен оценить `frontend`, `backend`, `specs`, `docs` в Del
## Проверка ## Проверка
Из workspace под Node 22 выполните: Для native validation из этого репозитория:
```bash ```bash
bash specs/scripts/verify-workspace-change.sh <change>
```
Для диагностики OpenSpec из этого репозитория:
```bash
bash scripts/verify-openspec-version.sh
openspec schema validate wealth-ledger-sdd --verbose openspec schema validate wealth-ledger-sdd --verbose
openspec validate <change> --type change --strict --store wealth-ledger-specs openspec validate <change> --type change --strict --store wealth-ledger-specs
openspec validate --specs --strict --store wealth-ledger-specs openspec validate --specs --strict --store wealth-ledger-specs
bash scripts/verify-openspec-authority.sh
``` ```
Structural validation не заменяет semantic verification и independent read-only review перед archive. Audit-derived работа маршрутизируется через [live remediation registry](../docs/development/audit-remediation.md). Structural validation не заменяет semantic verification и independent read-only review перед archive. Audit-derived работа маршрутизируется через [live remediation registry](../docs/development/audit-remediation.md).
@@ -0,0 +1,10 @@
schema: wealth-ledger-sdd
created: 2026-08-26
goal: Simplify verification infrastructure by removing redundant project-owned
wrappers while preserving native and risk-based checks.
skip_specs: true
affected_areas:
- frontend
- backend
- specs
- docs
@@ -0,0 +1,128 @@
## Context
Текущий workflow накладывает самописные planning, authority, version и workspace
verifiers поверх native OpenSpec, а repository gates включают runtime preflight,
hygiene-скрипты, meta-tests конфигурации и exact bundle baseline. Workspace
script повторно запускает уже завершённые local gates. Runtime OpenAPI shell
script вручную компилирует и поднимает приложение, хотя проверяемый риск —
согласованность публичного contract — естественно принадлежит integration test.
## Goals / Non-Goals
**Goals:**
- Оставить короткие local gates, где каждая проверка владеет самостоятельным
инженерным инвариантом.
- Использовать native OpenSpec validation для структуры SDD и стандартные
project tools для source, dependency и build quality.
- Сохранить проверку runtime OpenAPI contract через backend integration boundary.
- Исключить повторный запуск full repository gates через workspace dispatcher.
**Non-Goals:**
- Не менять application behavior, API document или persisted data.
- Не вводить CI, монорепозиторий, workspace package manager, новую dependency
или новый threshold качества.
- Не переписывать historical archives и Completion Evidence, которые описывают
фактические прежние команды.
## Decisions
### Native tools own structural and repository validation
Удаляются project-owned OpenSpec planning, authority и version scripts, их
fixtures и references. Native OpenSpec commands с explicit canonical store
становятся единственным structural validation; lifecycle approval, semantic
verification, completed tasks и independent review остаются человеческими
archive conditions, а не получают новую автоматическую обёртку.
Каждый affected repository сохраняет один local `verify` entrypoint, но он
состоит только из непосредственно полезных инструментов: TypeScript,
Biome/ESLint, Vitest/Jest, Knip, dependency-cruiser и build. `engines` и
`.nvmrc` остаются декларацией Node 22; отдельная runtime-проверка не входит в
pipeline. Repository hygiene scripts удаляются: version-control policy остаётся
в `.gitignore`, а scoped diff review обнаруживает material workspace artifacts.
### Contract verification moves to a Jest integration test
`verify-runtime-openapi.sh` удаляется. Backend получает focused Jest contract
test, который строит configured Nest application на той же boundary, извлекает
runtime OpenAPI document и сопоставляет его с committed frontend snapshot после
детерминированной canonicalization. Такой test запускается только как required
contract gate change, затрагивающего обе стороны API contract; он не добавляется
к каждому несвязанному backend verification run.
Frontend сохраняет проверку generated types against committed snapshot. Это
разделяет две разные гарантии: frontend snapshot согласован с generated client,
а snapshot согласован с backend runtime contract.
### Keep bounded bundle and dependency checks; remove meta-verification
Dependency-cruiser продолжает проверять actual source graph. Его fixture tests
удаляются, потому что тестируют configuration tool, а не source boundary.
Bundle check сохраняет фиксированный maximum initial payload, но удаляет exact
per-route baseline, baseline update command и тесты собственного measurement
script. Fixed ceiling защищает понятный user-facing limit; exact historical
sizes создают churn без самостоятельного performance contract.
### Completion is explicit rather than dispatched
`verify-workspace-change.sh` удаляется. Tasks каждого change перечисляют
affected repository gates, а contract gate указывается только если Design
делает его required. Перед завершением gates выполняются по одному разу в
affected areas; semantic comparison с approved artifacts и independent review
фиксируются отдельно. Documentation, AGENTS, schema instructions and agent
entrypoints описывают эту прямую последовательность без shell dispatcher.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `affected` | Упрощённый local gate без runtime/hygiene wrappers, meta-tests dependency configuration и exact lazy-route baseline; сохранены quality tools и initial payload ceiling. |
| backend | `affected` | Упрощённый local gate и focused Jest OpenAPI contract integration boundary вместо workspace shell script. |
| specs | `affected` | Native OpenSpec lifecycle и explicit per-area completion заменяют project-owned verifiers and dispatcher. |
| docs | `affected` | As-built workflow, repository entrypoints and verification strategy отражают итоговые direct gates. |
### Contract Gate Decision
| Gate | Status | Reason |
| --- | --- | --- |
| runtime-openapi | `N/A` | Change переносит способ проверки, но не изменяет frontend и backend стороны публичного API contract; новый Jest test сохраняет contract gate для будущих API changes. |
## Solution Assessment
| Option | Fit | Maintenance and migration cost | Decision |
| --- | --- | --- | --- |
| Retain project-owned wrappers and fixtures | Preserves current checks but repeats native and local work. | High ongoing cost and duplicated failure surface. | Rejected. |
| Native OpenSpec plus direct repository tools | Covers structural validation and source-quality invariants with maintained tools already installed. | Low; documentation and task routing change once. | Chosen. |
| New workflow runner or CI framework | Could centralize orchestration, but adds a platform not required by the stated risk. | High adoption and operating cost. | Rejected. |
The existing Jest/Nest testing stack is the mature compatible mechanism for the
runtime contract assertion, so no custom process runner or dependency is added.
## Risks / Trade-offs
- Removing every-run hygiene scans means an explicitly force-added ignored
artifact is caught by repository review rather than the full verification
pipeline; `.gitignore` remains the normal prevention boundary.
- The OpenAPI test intentionally remains an opt-in contract gate, avoiding an
integration startup for changes that cannot alter the API.
- Fixed bundle ceiling detects material initial-load regression but no longer
blocks harmless byte-level movement among lazy routes.
## Migration Plan
1. Replace project-owned wrappers and their references with direct native or
repository-owned checks.
2. Move the runtime OpenAPI assertion to the backend Jest integration boundary
and remove the shell implementation.
3. Simplify package entrypoints while retaining each selected base tool.
4. Update as-built guidance and agent routing, then verify each affected area
once without the removed dispatcher.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `N/A` |
| Reason | Tooling-only change has no data, auth, security behavior, public API or other difficult-to-reverse runtime decision; reverting affected repository commits restores the previous local workflow. |
@@ -0,0 +1,35 @@
## Why
Проверки change и репозиториев дополнительно проходят через самописные
обёртки, мета-проверки и точные baseline-сравнения. Они дублируют возможности
поддерживаемых инструментов, повторяют tool calls и усложняют обязательный
workflow без защиты самостоятельного продуктового риска.
## What Changes
- Упростить verification infrastructure, оставив только проверки, которые
непосредственно подтверждают наблюдаемые инженерные инварианты.
- Установить единый итоговый подход: TypeScript, Biome/ESLint, Vitest/Jest,
Knip, dependency-cruiser, build и native OpenSpec validation — как основные
автоматические доказательства; contract и semantic review остаются
соразмерными риску change.
- Исключить самописные verification и meta-verification механизмы, когда они
лишь оркестрируют, повторяют или тестируют уже применяемые инструменты.
- Не ослаблять проверку типов, стиля, риск-ориентированных тестов,
архитектурных границ, сборки, публичного API contract или проверяемых
repository hygiene-инвариантов.
- Не менять application behavior, API или данные и не вводить CI, новые
зависимости либо новые quality thresholds.
## Capabilities
### New Capabilities
### Modified Capabilities
## Impact
Затронуты локальные verification workflows frontend, backend, specs и docs,
OpenSpec agent entrypoints и as-built process documentation. Разработчики и
агенты получат более короткий, прозрачный набор команд; существующие базовые
quality и contract checks сохранятся в итоговой стратегии.
@@ -0,0 +1,23 @@
## 1. Заменить избыточное владение verification
- [x] 1.1 [specs] Удалить project-owned OpenSpec scripts для planning, authority, version и workspace verification вместе с fixtures; обновить schema, store routing и agent entrypoints для native OpenSpec validation и явного завершения по областям.
- **Covers:** `N/A` — Proposal не содержит поведения на уровне Specs; задача реализует утверждённое в Design решение о владении tooling.
- **Verification:** Выполнить native strict validation change и main specs из canonical store.
- [x] 1.2 [backend] Заменить shell verifier runtime OpenAPI на focused Jest integration contract test с явным entrypoint; упростить backend verification pipeline и удалить runtime/hygiene wrappers.
- **Covers:** `N/A` — Публичный API не меняется; задача переносит существующую verification boundary по утверждённому Design.
- **Verification:** Выполнить focused Jest contract test и один раз `npm run verify` в `backend`.
- [x] 1.3 [frontend] Упростить frontend verification pipeline, сохранив OpenAPI generated-type, TypeScript, Biome, Vitest, Knip, dependency-cruiser, build и initial-payload checks, но удалив избыточные wrappers, meta-tests и exact lazy-route baseline behavior.
- **Covers:** `N/A` — Наблюдаемое frontend behavior не меняется; это утверждённое упрощение local gate.
- **Verification:** Выполнить один раз `npm run verify` в `frontend`.
## 2. Согласовать поставленный workflow и completion evidence
- [x] 2.1 [docs] Обновить as-built verification strategy, repository workflow documentation и local entrypoints для прямых native и repository gates; сохранить historical evidence как исторический факт.
- **Covers:** `N/A` — Documentation отражает поставленный tooling workflow и не определяет product behavior.
- **Verification:** Выполнить один раз `npm run verify` в `docs` и проверить scoped local links.
- [x] 2.2 [specs] Обновить canonical SDD guidance и change lifecycle references так, чтобы archive требовал native validation, approved artifacts, semantic verification и independent review без удалённого dispatcher.
- **Covers:** `N/A` — Задача согласует versioned process authority с утверждённым Design; Delta Requirement отсутствует.
- **Verification:** Выполнить native strict change validation и strict main-spec validation из `specs`.
- [ ] 2.3 [workspace] До archive по одному разу выполнить gates affected repositories, новый focused OpenAPI contract test, semantic verification against approved artifacts и independent read-only review.
- **Covers:** `N/A` — Workspace владеет только cross-repository completion orchestration, заданной утверждённым Design.
- **Verification:** Зафиксировать один full gate на affected repository, contract test, semantic verification и review outcome без `verify-workspace-change.sh`.
+10 -18
View File
@@ -17,10 +17,8 @@ artifacts:
оставь capabilities пустыми и установи `skip_specs: true`. оставь capabilities пустыми и установи `skip_specs: true`.
- Оставь технологии, зависимости, архитектуру, API, database/code/file - Оставь технологии, зависимости, архитектуру, API, database/code/file
structure для Design, а implementation steps и verification — для Tasks. structure для Design, а implementation steps и verification — для Tasks.
- После записи запусти - После записи используй native OpenSpec instructions и validation с
`bash specs/scripts/verify-openspec-planning.sh <change>`. Исправляй `--store wealth-ledger-specs`. Structural success не является approval.
artifact только при `artifact-validation`; при `invocation` исправь вызов,
при `environment` — prerequisite. Structural success не является approval.
Required Shape: Required Shape:
- **Why**: проблема или возможность и причина заняться ею сейчас. - **Why**: проблема или возможность и причина заняться ею сейчас.
@@ -57,9 +55,8 @@ artifacts:
`## RENAMED Requirements`; не оставляй пустые operation sections. `## RENAMED Requirements`; не оставляй пустые operation sections.
- Каждый `### Requirement:` содержит SHALL/MUST и хотя бы один - Каждый `### Requirement:` содержит SHALL/MUST и хотя бы один
`#### Scenario:` с `- **WHEN**` и `- **THEN**`. `#### Scenario:` с `- **WHEN**` и `- **THEN**`.
- После explicit Proposal approval и записи запусти planning verifier. - После explicit Proposal approval и записи используй native OpenSpec
Исправляй artifact только при `artifact-validation`; `invocation` и validation с `--store wealth-ledger-specs`.
`environment` не разрешают менять Delta Specs.
Delta Operations: Delta Operations:
- `ADDED`: новый Requirement. - `ADDED`: новый Requirement.
@@ -99,9 +96,8 @@ artifacts:
будущие results, commits или Completion Evidence. будущие results, commits или Completion Evidence.
- Разреши до сохранения любой вопрос, который меняет Specs, approach или - Разреши до сохранения любой вопрос, который меняет Specs, approach или
Tasks. `Open Questions` оставляет только genuinely deferrable unknowns. Tasks. `Open Questions` оставляет только genuinely deferrable unknowns.
- После explicit Delta Specs approval и записи запусти planning verifier. - После explicit Delta Specs approval и записи используй native OpenSpec
Исправляй Design только при `artifact-validation`; `invocation` и validation с `--store wealth-ledger-specs`.
`environment` требуют исправить вызов или prerequisite.
Required Shape: Required Shape:
- **Context**: current state и constraints подхода. - **Context**: current state и constraints подхода.
@@ -162,9 +158,8 @@ artifacts:
- Новое behavior возвращает change в Specs; новое architecture — в Design; - Новое behavior возвращает change в Specs; новое architecture — в Design;
multi-owner или vague task исправляется в Tasks. Не добавляй commits, multi-owner или vague task исправляется в Tasks. Не добавляй commits,
archive path или post-archive Completion Evidence. archive path или post-archive Completion Evidence.
- После записи запусти planning verifier. Исправляй Tasks только при - После записи используй native OpenSpec validation с
`artifact-validation`; `invocation` и `environment` не разрешают менять `--store wealth-ledger-specs`.
checklist.
Testing: Testing:
Автоматические tests обязательны для security/privacy, денежных расчётов, Автоматические tests обязательны для security/privacy, денежных расчётов,
@@ -187,8 +182,8 @@ apply:
instruction: | instruction: |
Critical Rules: Critical Rules:
- Apply допустим только после explicit approvals Proposal, Delta Specs, - Apply допустим только после explicit approvals Proposal, Delta Specs,
Design и Tasks. Проверь exact OpenSpec version, canonical store Design и Tasks. Используй canonical store `wealth-ledger-specs` для всех
`wealth-ledger-specs` и planning verifier до implementation. OpenSpec commands, читающих или изменяющих change.
- Прочитай каждый path из `contextFiles`, возвращённых native OpenSpec apply - Прочитай каждый path из `contextFiles`, возвращённых native OpenSpec apply
instructions, затем applicable repository-local `AGENTS.md` affected areas. instructions, затем applicable repository-local `AGENTS.md` affected areas.
Не вычисляй planning paths и не создавай второй context map. Не вычисляй planning paths и не создавай второй context map.
@@ -202,8 +197,5 @@ apply:
нормативного смысла или table alignment. Requirement wording, Scenario, нормативного смысла или table alignment. Requirement wording, Scenario,
architecture и planned Verification являются semantic changes и требуют architecture и planned Verification являются semantic changes и требуют
соответствующего reapproval. соответствующего reapproval.
- `artifact-validation` разрешает исправить указанную planning form;
`invocation` требует исправить вызов; `environment` — prerequisite.
Последние два класса не разрешают менять planning artifact.
- Structural validation не заменяет semantic verification и independent - Structural validation не заменяет semantic verification и independent
read-only review. read-only review.
@@ -1,19 +0,0 @@
## Why
Controlled fixture validates operation-neutral Delta Spec behavior.
## What Changes
- Exercise every supported delta operation in one controlled artifact.
## Capabilities
### New Capabilities
### Modified Capabilities
- `workspace-artifact-hygiene`: controlled operation grammar fixture.
## Impact
Test fixture only; application behavior is not delivered.
@@ -1,38 +0,0 @@
## ADDED Requirements
### Requirement: Controlled fixture requirement
The fixture SHALL provide one complete added requirement.
#### Scenario: Added operation is complete
- **WHEN** the controlled delta is validated
- **THEN** the added requirement has observable behavior
## MODIFIED Requirements
### Requirement: Рабочая frontend configuration не является versioned input
Frontend SHALL поставлять versioned `.env.example`, достаточный для создания
локального `.env`, но MUST NOT хранить рабочий `.env` или другой локальный
`.env*` файл в source control. Ignore policy MUST исключать локальные frontend
configuration files, сохраняя явно разрешённый example template.
#### Scenario: Участник создаёт local frontend configuration
- **WHEN** участник копирует `.env.example` в `.env` для запуска frontend
- **THEN** локальный файл доступен runtime, но не появляется среди tracked
файлов Git
## REMOVED Requirements
### Requirement: Runtime SQLite-данные не являются versioned artifact
**Reason**: Controlled fixture exercises the removal grammar.
**Migration**: Existing behavior remains authoritative outside this fixture.
## RENAMED Requirements
- FROM: `### Requirement: Dependency advisory получает проверяемое решение`
- TO: `### Requirement: Dependency advisory получает контролируемое решение`
-125
View File
@@ -1,125 +0,0 @@
#!/usr/bin/env bash
wl_area_index() {
case "$1" in
frontend) printf '0' ;;
backend) printf '1' ;;
specs) printf '2' ;;
docs) printf '3' ;;
*) return 1 ;;
esac
}
wl_trim() {
local value=$1
value=${value#"${value%%[![:space:]]*}"}
value=${value%"${value##*[![:space:]]}"}
printf '%s' "$value"
}
wl_parse_delivery_map() {
local metadata_file=$1
local design_file=$2
local raw_area raw_status raw_reason area status reason gate index
wl_areas=(frontend backend specs docs)
wl_affected_flags=(0 0 0 0)
wl_map_statuses=('' '' '' '')
wl_map_reasons=('' '' '' '')
wl_contract_status=''
wl_contract_reason=''
while IFS= read -r area; do
[[ -n "$area" ]] || continue
if ! index=$(wl_area_index "$area"); then
printf 'Unknown affected area in metadata: %s\n' "$area" >&2
return 1
fi
if [[ "${wl_affected_flags[$index]}" -eq 1 ]]; then
printf 'Duplicate affected area in metadata: %s\n' "$area" >&2
return 1
fi
wl_affected_flags[$index]=1
done < <(
awk '
$0 == "affected_areas:" { in_areas = 1; next }
in_areas && /^ - / { sub(/^ - /, ""); print; next }
in_areas { exit }
' "$metadata_file"
)
if [[ "${wl_affected_flags[2]}" -ne 1 ]]; then
printf 'Delivery Map violation: specs must be affected for every versioned change.\n' >&2
return 1
fi
while IFS='|' read -r _ raw_area raw_status raw_reason _; do
area=$(wl_trim "$raw_area")
case "$area" in
frontend|backend|specs|docs) ;;
*) continue ;;
esac
index=$(wl_area_index "$area")
if [[ -n "${wl_map_statuses[$index]}" ]]; then
printf 'Delivery Map contains duplicate row for %s.\n' "$area" >&2
return 1
fi
status=$(wl_trim "$raw_status")
status=${status//\`/}
reason=$(wl_trim "$raw_reason")
wl_map_statuses[$index]=$status
wl_map_reasons[$index]=$reason
done < "$design_file"
while IFS='|' read -r _ raw_area raw_status raw_reason _; do
gate=$(wl_trim "$raw_area")
[[ "$gate" == 'runtime-openapi' ]] || continue
if [[ -n "$wl_contract_status" ]]; then
printf 'Contract Gate Decision contains duplicate runtime-openapi row.\n' >&2
return 1
fi
wl_contract_status=$(wl_trim "$raw_status")
wl_contract_status=${wl_contract_status//\`/}
wl_contract_reason=$(wl_trim "$raw_reason")
done < "$design_file"
for index in 0 1 2 3; do
area=${wl_areas[$index]}
status=${wl_map_statuses[$index]}
reason=${wl_map_reasons[$index]}
if [[ -z "$status" ]]; then
printf 'Delivery Map is missing area: %s\n' "$area" >&2
return 1
fi
if [[ "${wl_affected_flags[$index]}" -eq 1 ]]; then
if [[ "$status" != 'affected' ]]; then
printf 'Delivery Map mismatch: %s is listed as affected in metadata but has status %s.\n' "$area" "$status" >&2
return 1
fi
elif [[ "$status" != 'N/A' ]]; then
printf 'Delivery Map mismatch: %s is absent from affected_areas but has status %s.\n' "$area" "$status" >&2
return 1
fi
if [[ -z "$reason" || "$reason" == '-' || "$reason" == '—' || "$reason" == *'<!--'* ]]; then
printf 'Delivery Map requires a concrete outcome or N/A reason for %s.\n' "$area" >&2
return 1
fi
done
if [[ -z "$wl_contract_status" ]]; then
printf 'Contract Gate Decision is missing runtime-openapi.\n' >&2
return 1
fi
if [[ "$wl_contract_status" != 'required' && "$wl_contract_status" != 'N/A' ]]; then
printf 'Invalid runtime-openapi decision: %s. Expected required or N/A.\n' "$wl_contract_status" >&2
return 1
fi
if [[ -z "$wl_contract_reason" || "$wl_contract_reason" == '-' || "$wl_contract_reason" == '—' || "$wl_contract_reason" == *'<!--'* ]]; then
printf 'Contract Gate Decision requires a concrete runtime-openapi reason.\n' >&2
return 1
fi
if [[ "$wl_contract_status" == 'required' && ( "${wl_affected_flags[0]}" -ne 1 || "${wl_affected_flags[1]}" -ne 1 ) ]]; then
printf 'runtime-openapi cannot be required unless frontend and backend are affected.\n' >&2
return 1
fi
}
-231
View File
@@ -1,231 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
workspace=${WEALTH_LEDGER_WORKSPACE:-$(cd "$script_dir/../.." && pwd -P)}
failures=0
bash "$script_dir/verify-openspec-version.sh" "$workspace/specs/openspec/.cli-version"
expected_version=$(tr -d '[:space:]' < "$workspace/specs/openspec/.cli-version")
fail() {
printf 'FAIL: %s\n' "$1" >&2
failures=$((failures + 1))
}
require_file_contains() {
local file=$1
local text=$2
if [[ ! -f "$file" ]]; then
fail "missing required file: ${file#$workspace/}"
elif ! grep -Fq -- "$text" "$file"; then
fail "${file#$workspace/} must contain: $text"
fi
}
require_schema_edge() {
local file=$1
local artifact=$2
local dependency=$3
if [[ ! -f "$file" ]]; then
fail "missing required file: ${file#$workspace/}"
return
fi
if ! awk -v artifact="$artifact" -v dependency="$dependency" '
$0 == " - id: " artifact { in_artifact = 1; in_requires = 0; next }
in_artifact && /^ - id:/ { in_artifact = 0; in_requires = 0 }
in_artifact && $0 == " requires:" { in_requires = 1; next }
in_artifact && in_requires && $0 == " - " dependency { found = 1 }
END { exit(found ? 0 : 1) }
' "$file"; then
fail "${file#$workspace/} must require $artifact <- $dependency"
fi
}
store_file="$workspace/specs/.openspec-store/store.yaml"
require_file_contains "$store_file" 'id: wealth-ledger-specs'
canonical_root="$workspace/specs/openspec"
if [[ ! -d "$canonical_root" ]]; then
fail "missing canonical OpenSpec root: specs/openspec"
fi
config_file="$canonical_root/config.yaml"
require_file_contains "$config_file" 'schema: wealth-ledger-sdd'
schema_file="$canonical_root/schemas/wealth-ledger-sdd/schema.yaml"
require_file_contains "$schema_file" 'name: wealth-ledger-sdd'
require_file_contains "$schema_file" 'version: 3'
require_schema_edge "$schema_file" 'specs' 'proposal'
require_schema_edge "$schema_file" 'design' 'specs'
require_schema_edge "$schema_file" 'tasks' 'design'
require_file_contains "$schema_file" 'requires: [ tasks ]'
if [[ -d "$workspace/specs" ]]; then
if ! schema_validation=$(cd "$workspace/specs" && openspec schema validate wealth-ledger-sdd 2>&1); then
fail "wealth-ledger-sdd schema validation failed: $schema_validation"
fi
if ! schema_source=$(cd "$workspace/specs" && openspec schema which wealth-ledger-sdd --json 2>&1); then
fail "wealth-ledger-sdd schema resolution failed: $schema_source"
elif ! grep -Fq '"source": "project"' <<<"$schema_source"; then
fail "wealth-ledger-sdd must resolve from the project-local schema"
fi
fi
openspec_roots=()
while IFS= read -r root; do
openspec_roots+=("$root")
done < <(
find "$workspace" \
\( -path "$workspace/references" -o -path "$workspace/.git" -o -path '*/node_modules' -o -path '*/dist' -o -path '*/coverage' \) -prune -o \
-type d -name openspec -print | sort
)
if [[ ${#openspec_roots[@]} -ne 1 || ${openspec_roots[0]:-} != "$canonical_root" ]]; then
fail "expected exactly one project-owned OpenSpec root at specs/openspec"
fi
agent_files=(AGENTS.md frontend/AGENTS.md backend/AGENTS.md specs/AGENTS.md docs/AGENTS.md)
for relative_path in "${agent_files[@]}"; do
file="$workspace/$relative_path"
require_file_contains "$file" 'wealth-ledger-specs'
require_file_contains "$file" '--store wealth-ledger-specs'
require_file_contains "$file" 'specs/openspec'
done
entrypoints=(
.opencode/commands/opsx-apply.md
.opencode/commands/opsx-archive.md
.opencode/commands/opsx-continue.md
.opencode/commands/opsx-explore.md
.opencode/commands/opsx-new.md
.opencode/commands/opsx-propose.md
.opencode/commands/opsx-sync.md
.opencode/commands/opsx-verify.md
.opencode/skills/openspec-apply-change/SKILL.md
.opencode/skills/openspec-archive-change/SKILL.md
.opencode/skills/openspec-continue-change/SKILL.md
.opencode/skills/openspec-explore/SKILL.md
.opencode/skills/openspec-new-change/SKILL.md
.opencode/skills/openspec-propose/SKILL.md
.opencode/skills/openspec-sync-specs/SKILL.md
.opencode/skills/openspec-verify-change/SKILL.md
)
for relative_path in "${entrypoints[@]}"; do
file="$workspace/$relative_path"
require_file_contains "$file" '--store wealth-ledger-specs'
require_file_contains "$file" 'bash specs/scripts/verify-openspec-version.sh'
if [[ -f "$file" ]] && grep -Eqi 'nearest local openspec|global default(store)?|defaultstore|without a store|store: pointer|--store <id>|--store "<id>"|when applicable' "$file"; then
fail "$relative_path permits conditional OpenSpec store selection"
fi
if [[ -f "$file" ]]; then
while IFS= read -r line; do
if [[ $line =~ (^|[^[:alnum:]_])openspec[[:space:]]+(new[[:space:]]+change[[:space:]]+|status[[:space:]]+|instructions[[:space:]]+|list[[:space:]]+|show[[:space:]]+|validate[[:space:]]+|archive[[:space:]]+|doctor[[:space:]]+|context[[:space:]]+|schemas[[:space:]]+|view[[:space:]]+) ]] && [[ $line != *'--store wealth-ledger-specs'* ]]; then
fail "$relative_path contains an unscoped OpenSpec invocation: $line"
fi
done < "$file"
fi
done
skill_entrypoints=(
.opencode/skills/openspec-apply-change/SKILL.md
.opencode/skills/openspec-archive-change/SKILL.md
.opencode/skills/openspec-continue-change/SKILL.md
.opencode/skills/openspec-explore/SKILL.md
.opencode/skills/openspec-new-change/SKILL.md
.opencode/skills/openspec-propose/SKILL.md
.opencode/skills/openspec-sync-specs/SKILL.md
.opencode/skills/openspec-verify-change/SKILL.md
)
for relative_path in "${skill_entrypoints[@]}"; do
require_file_contains "$workspace/$relative_path" "generatedBy: \"$expected_version\""
done
one_artifact_entrypoints=(
.opencode/commands/opsx-continue.md
.opencode/commands/opsx-explore.md
.opencode/commands/opsx-propose.md
.opencode/skills/openspec-continue-change/SKILL.md
.opencode/skills/openspec-explore/SKILL.md
.opencode/skills/openspec-propose/SKILL.md
)
for relative_path in "${one_artifact_entrypoints[@]}"; do
file="$workspace/$relative_path"
require_file_contains "$file" 'Create at most ONE planning artifact per invocation'
require_file_contains "$file" 'STOP for explicit Change Owner approval'
if [[ -f "$file" ]] && grep -Eqi 'generate all artifacts|create every artifact|until every requested artifact|continue until every requested artifact' "$file"; then
fail "$relative_path permits multi-artifact generation without owner checkpoints"
fi
done
planning_entrypoints=(
.opencode/commands/opsx-apply.md
.opencode/commands/opsx-archive.md
.opencode/commands/opsx-continue.md
.opencode/commands/opsx-explore.md
.opencode/commands/opsx-propose.md
.opencode/commands/opsx-verify.md
.opencode/skills/openspec-apply-change/SKILL.md
.opencode/skills/openspec-archive-change/SKILL.md
.opencode/skills/openspec-continue-change/SKILL.md
.opencode/skills/openspec-explore/SKILL.md
.opencode/skills/openspec-propose/SKILL.md
.opencode/skills/openspec-verify-change/SKILL.md
)
for relative_path in "${planning_entrypoints[@]}"; do
require_file_contains "$workspace/$relative_path" 'bash specs/scripts/verify-openspec-planning.sh'
done
approval_blocker_entrypoints=(
.opencode/commands/opsx-apply.md
.opencode/commands/opsx-archive.md
.opencode/skills/openspec-apply-change/SKILL.md
.opencode/skills/openspec-archive-change/SKILL.md
)
for relative_path in "${approval_blocker_entrypoints[@]}"; do
file="$workspace/$relative_path"
require_file_contains "$file" 'explicit Change Owner approval'
require_file_contains "$file" 'invalidate dependent approvals'
done
archive_entrypoints=(
.opencode/commands/opsx-archive.md
.opencode/skills/openspec-archive-change/SKILL.md
)
for relative_path in "${archive_entrypoints[@]}"; do
file="$workspace/$relative_path"
require_file_contains "$file" 'affected delivery area'
require_file_contains "$file" 'semantic verification'
require_file_contains "$file" 'independent read-only review'
require_file_contains "$file" 'Do not ask for or accept a confirmation override.'
if [[ -f "$file" ]] && grep -Eqi 'archive complete \(with warnings\)|don.t block archive on warnings|archived with [0-9]+ incomplete' "$file"; then
fail "$relative_path permits archive with incomplete work"
fi
done
verification_entrypoints=(
.opencode/commands/opsx-verify.md
.opencode/skills/openspec-verify-change/SKILL.md
)
for relative_path in "${verification_entrypoints[@]}"; do
file="$workspace/$relative_path"
require_file_contains "$file" 'affected delivery area'
require_file_contains "$file" 'workspace verifier'
require_file_contains "$file" 'independent read-only review'
done
for relative_path in docs/superpowers/plans superpowers/plans .superpowers; do
if [[ -e "$workspace/$relative_path" ]]; then
fail "prohibited framework planning location: $relative_path"
fi
done
if [[ $failures -gt 0 ]]; then
exit 1
fi
printf 'OpenSpec authority verified: wealth-ledger-specs at specs/openspec\n'
-209
View File
@@ -1,209 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
verifier="$script_dir/verify-openspec-authority.sh"
contract_version=$(tr -d '[:space:]' < "$script_dir/../openspec/.cli-version")
if [[ ! -f "$verifier" ]]; then
echo "expected verifier at $verifier" >&2
exit 1
fi
fixture=$(mktemp -d)
trap 'rm -rf "$fixture"' EXIT
write_file() {
local relative_path=$1
local content=$2
mkdir -p "$fixture/$(dirname "$relative_path")"
printf '%s\n' "$content" > "$fixture/$relative_path"
}
write_file "specs/.openspec-store/store.yaml" $'version: 1\nid: wealth-ledger-specs'
mkdir -p "$fixture/specs/openspec"
write_file "specs/openspec/.cli-version" "$contract_version"
write_file "specs/openspec/config.yaml" 'schema: wealth-ledger-sdd'
mkdir -p "$fixture/specs/openspec/schemas"
cp -R "$script_dir/../openspec/schemas/wealth-ledger-sdd" "$fixture/specs/openspec/schemas/wealth-ledger-sdd"
for path in AGENTS.md frontend/AGENTS.md backend/AGENTS.md specs/AGENTS.md docs/AGENTS.md; do
write_file "$path" $'OpenSpec store: wealth-ledger-specs\nUse --store wealth-ledger-specs\nSDD artifacts only in specs/openspec\nreferences/** is read-only.'
done
entrypoints=(
.opencode/commands/opsx-apply.md
.opencode/commands/opsx-archive.md
.opencode/commands/opsx-continue.md
.opencode/commands/opsx-explore.md
.opencode/commands/opsx-new.md
.opencode/commands/opsx-propose.md
.opencode/commands/opsx-sync.md
.opencode/commands/opsx-verify.md
.opencode/skills/openspec-apply-change/SKILL.md
.opencode/skills/openspec-archive-change/SKILL.md
.opencode/skills/openspec-continue-change/SKILL.md
.opencode/skills/openspec-explore/SKILL.md
.opencode/skills/openspec-new-change/SKILL.md
.opencode/skills/openspec-propose/SKILL.md
.opencode/skills/openspec-sync-specs/SKILL.md
.opencode/skills/openspec-verify-change/SKILL.md
)
planning_line='bash specs/scripts/verify-openspec-planning.sh "<change>"'
for path in "${entrypoints[@]}"; do
content=$'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs'
if [[ "$path" == .opencode/skills/* ]]; then
content="generatedBy: \"$contract_version\""$'\n'"$content"
fi
if [[ "$path" == *continue* || "$path" == *explore* || "$path" == *propose* ]]; then
content="$content"$'\nCreate at most ONE planning artifact per invocation.\nSTOP for explicit Change Owner approval.'
fi
if [[ "$path" == *apply* || "$path" == *archive* ]]; then
content="$content"$'\nRequire explicit Change Owner approval and invalidate dependent approvals after upstream edits.'
fi
if [[ "$path" != *new* && "$path" != *sync* ]]; then
content="$content"$'\n'"$planning_line"
fi
write_file "$path" "$content"
done
archive_gate='bash specs/scripts/verify-openspec-version.sh
openspec status --store wealth-ledger-specs
Require affected delivery area gates, semantic verification and independent read-only review.
Require explicit Change Owner approval and invalidate dependent approvals after upstream edits.
Do not ask for or accept a confirmation override.'
archive_gate="$archive_gate"$'\n'"$planning_line"
write_file ".opencode/commands/opsx-archive.md" "$archive_gate"
write_file ".opencode/skills/openspec-archive-change/SKILL.md" "generatedBy: \"$contract_version\""$'\n'"$archive_gate"
verification_gate='bash specs/scripts/verify-openspec-version.sh
openspec status --store wealth-ledger-specs
Check every affected delivery area with the workspace verifier and require independent read-only review.'
verification_gate="$verification_gate"$'\n'"$planning_line"
write_file ".opencode/commands/opsx-verify.md" "$verification_gate"
write_file ".opencode/skills/openspec-verify-change/SKILL.md" "generatedBy: \"$contract_version\""$'\n'"$verification_gate"
apply_gate='bash specs/scripts/verify-openspec-version.sh
openspec status --store wealth-ledger-specs
Require explicit Change Owner approval and invalidate dependent approvals after upstream edits.'
apply_gate="$apply_gate"$'\n'"$planning_line"
run_verifier() {
WEALTH_LEDGER_WORKSPACE="$fixture" bash "$verifier"
}
run_verifier
perl -0pi -e 's/^bash specs\/scripts\/verify-openspec-planning\.sh .*\n//m' "$fixture/.opencode/commands/opsx-propose.md"
if run_verifier; then
echo "expected missing planning gate in an artifact entrypoint to fail" >&2
exit 1
fi
printf '%s\n' "$planning_line" >> "$fixture/.opencode/commands/opsx-propose.md"
write_file "specs/openspec/.cli-version" '9.9.9'
if run_verifier; then
echo "expected wrong OpenSpec version contract to fail" >&2
exit 1
fi
write_file "specs/openspec/.cli-version" "$contract_version"
write_file "specs/openspec/config.yaml" 'schema: spec-driven'
if run_verifier; then
echo "expected non-project default schema to fail" >&2
exit 1
fi
write_file "specs/openspec/config.yaml" 'schema: wealth-ledger-sdd'
perl -0pi -e 's/^version: 3$/version: 2/m' "$fixture/specs/openspec/schemas/wealth-ledger-sdd/schema.yaml"
if run_verifier; then
echo "expected obsolete project schema version to fail" >&2
exit 1
fi
rm -rf "$fixture/specs/openspec/schemas/wealth-ledger-sdd"
cp -R "$script_dir/../openspec/schemas/wealth-ledger-sdd" "$fixture/specs/openspec/schemas/wealth-ledger-sdd"
write_file "specs/openspec/schemas/wealth-ledger-sdd/schema.yaml" $'name: wealth-ledger-sdd\nversion: 1'
if run_verifier; then
echo "expected incomplete project schema graph to fail" >&2
exit 1
fi
rm -rf "$fixture/specs/openspec/schemas/wealth-ledger-sdd"
cp -R "$script_dir/../openspec/schemas/wealth-ledger-sdd" "$fixture/specs/openspec/schemas/wealth-ledger-sdd"
mkdir -p "$fixture/frontend/openspec"
if run_verifier; then
echo "expected additional OpenSpec root to fail" >&2
exit 1
fi
rmdir "$fixture/frontend/openspec"
write_file ".opencode/commands/opsx-apply.md" $'bash specs/scripts/verify-openspec-version.sh\nWithout a store, commands act on the nearest local openspec root.'
if run_verifier; then
echo "expected implicit store selection to fail" >&2
exit 1
fi
write_file ".opencode/commands/opsx-apply.md" "$apply_gate"
write_file ".opencode/commands/opsx-apply.md" $'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs\nFor another store, append --store "<id>" when applicable.'
if run_verifier; then
echo "expected conditional store selection to fail" >&2
exit 1
fi
write_file ".opencode/commands/opsx-apply.md" "$apply_gate"
write_file ".opencode/commands/opsx-apply.md" $'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs\nRun `openspec status --change "example" --json` before continuing.'
if run_verifier; then
echo "expected unscoped OpenSpec invocation to fail" >&2
exit 1
fi
write_file ".opencode/commands/opsx-apply.md" "$apply_gate"
write_file ".opencode/commands/opsx-apply.md" $'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs\nUse artifact graph (openspec status --change "example" --json) before continuing.'
if run_verifier; then
echo "expected parenthesized unscoped OpenSpec invocation to fail" >&2
exit 1
fi
write_file ".opencode/commands/opsx-apply.md" "$apply_gate"
write_file ".opencode/commands/opsx-propose.md" $'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs\nCreate every artifact in the required set.\nSTOP for explicit Change Owner approval.'
if run_verifier; then
echo "expected multi-artifact proposal guidance to fail" >&2
exit 1
fi
write_file ".opencode/commands/opsx-propose.md" $'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs\nCreate at most ONE planning artifact per invocation.\nSTOP for explicit Change Owner approval.\n'"$planning_line"
write_file ".opencode/commands/opsx-archive.md" $'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs\nDon\x27t block archive on warnings - just inform and confirm.'
if run_verifier; then
echo "expected archive override guidance to fail" >&2
exit 1
fi
write_file ".opencode/commands/opsx-archive.md" "$archive_gate"
write_file ".opencode/commands/opsx-archive.md" $'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs\nDo not ask for or accept a confirmation override.'
if run_verifier; then
echo "expected archive without required verification to fail" >&2
exit 1
fi
write_file ".opencode/commands/opsx-archive.md" "$archive_gate"
write_file ".opencode/commands/opsx-verify.md" $'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs'
if run_verifier; then
echo "expected semantic verifier without affected-area review to fail" >&2
exit 1
fi
write_file ".opencode/commands/opsx-verify.md" "$verification_gate"
mkdir -p "$fixture/docs/superpowers/plans"
if run_verifier; then
echo "expected prohibited planning location to fail" >&2
exit 1
fi
rmdir "$fixture/docs/superpowers/plans"
rmdir "$fixture/docs/superpowers"
mkdir -p "$fixture/references/example/superpowers/plans"
run_verifier
echo "verify-openspec-authority tests passed"
-303
View File
@@ -1,303 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
invocation_fail() {
printf 'Planning invocation failed: %s\n' "$1" >&2
exit 2
}
environment_fail() {
printf 'Planning environment failed: %s\n' "$1" >&2
exit 3
}
artifact_fail() {
printf 'Planning artifact-validation failed: %s\n' "$1" >&2
exit 1
}
if [[ $# -ne 1 ]]; then
invocation_fail "usage: ${0##*/} <change-name>"
fi
change=$1
if [[ ! "$change" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
invocation_fail "invalid OpenSpec change name: $change"
fi
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
workspace=${WEALTH_LEDGER_WORKSPACE:-$(cd "$script_dir/../.." && pwd -P)}
specs_root="$workspace/specs"
openspec_root="$specs_root/openspec"
change_dir="$openspec_root/changes/$change"
metadata_file="$change_dir/.openspec.yaml"
proposal_file="$change_dir/proposal.md"
design_file="$change_dir/design.md"
tasks_file="$change_dir/tasks.md"
# shellcheck source=lib/delivery-map.sh
source "$script_dir/lib/delivery-map.sh"
fail() {
artifact_fail "$1"
}
require_file() {
[[ -f "$1" ]] || fail "missing ${1#$workspace/}"
}
require_heading() {
grep -Fqx -- "$2" "$1" || fail "${1#$workspace/} must contain heading: $2"
}
if ! version_output=$(bash "$script_dir/verify-openspec-version.sh" "$openspec_root/.cli-version" 2>&1); then
environment_fail "$version_output"
fi
[[ -d "$change_dir" ]] || invocation_fail "change $change is absent from canonical store wealth-ledger-specs"
require_file "$metadata_file"
require_file "$proposal_file"
if ! grep -Fqx 'schema: wealth-ledger-sdd' "$metadata_file"; then
fail '.openspec.yaml must select schema wealth-ledger-sdd'
fi
if ! grep -Eq '^goal: .+' "$metadata_file"; then
fail '.openspec.yaml must contain a non-empty goal'
fi
if ! status_output=$(cd "$specs_root" && openspec status --change "$change" --json --store wealth-ledger-specs 2>&1); then
invocation_fail "change $change is not resolvable through canonical store wealth-ledger-specs: $status_output"
fi
for heading in '## Why' '## What Changes' '## Capabilities' '### New Capabilities' '### Modified Capabilities' '## Impact'; do
require_heading "$proposal_file" "$heading"
done
if ! temporary=$(mktemp -d); then
environment_fail 'cannot create temporary planning-verification directory'
fi
trap 'rm -rf "$temporary"' EXIT
declared_capabilities="$temporary/declared-capabilities.tsv"
actual_capabilities="$temporary/actual-capabilities.txt"
delta_requirements="$temporary/delta-requirements.tsv"
covered_requirements="$temporary/covered-requirements.tsv"
: > "$declared_capabilities"
: > "$actual_capabilities"
: > "$delta_requirements"
: > "$covered_requirements"
awk '
/^### New Capabilities$/ { kind = "new"; next }
/^### Modified Capabilities$/ { kind = "modified"; next }
/^## / || /^### / { kind = "" }
kind != "" && /^- `[^`]+`:/ {
line = $0
sub(/^- `/, "", line)
sub(/`:.*/, "", line)
print kind "\t" line
}
' "$proposal_file" > "$declared_capabilities"
while IFS=$'\t' read -r kind capability; do
[[ -n "$capability" ]] || continue
if [[ "$capability" == /* || "$capability" == *'..'* || ! "$capability" =~ ^[a-z0-9]+([/-][a-z0-9]+)*$ ]]; then
fail "invalid capability path in Proposal: $capability"
fi
if [[ "$kind" == 'modified' && ! -f "$openspec_root/specs/$capability/spec.md" ]]; then
fail "modified capability does not exist in canonical specs: $capability"
fi
done < "$declared_capabilities"
skip_specs=0
if grep -Fqx 'skip_specs: true' "$metadata_file"; then
skip_specs=1
if [[ -s "$declared_capabilities" ]]; then
fail 'skip_specs: true cannot be combined with declared capabilities'
fi
fi
has_specs=0
if [[ -d "$change_dir/specs" ]] && find "$change_dir/specs" -type f -name spec.md -print -quit | grep -q .; then
[[ $skip_specs -eq 0 ]] || fail 'skip_specs: true cannot be combined with delta spec files'
has_specs=1
while IFS= read -r file; do
capability=${file#"$change_dir/specs/"}
capability=${capability%/spec.md}
printf '%s\n' "$capability" >> "$actual_capabilities"
awk -v capability="$capability" '/^### Requirement: / { sub(/^### Requirement: /, ""); print capability "\t" $0 }' "$file" >> "$delta_requirements"
done < <(find "$change_dir/specs" -type f -name spec.md | sort)
while IFS= read -r capability; do
if ! awk -F '\t' -v capability="$capability" '$2 == capability { found = 1 } END { exit(found ? 0 : 1) }' "$declared_capabilities"; then
fail "delta capability is absent from Proposal: $capability"
fi
done < "$actual_capabilities"
while IFS=$'\t' read -r _ capability; do
[[ -n "$capability" ]] || continue
grep -Fqx "$capability" "$actual_capabilities" || fail "Proposal capability has no delta spec: $capability"
done < "$declared_capabilities"
if ! (cd "$specs_root" && openspec validate "$change" --type change --strict --store wealth-ledger-specs >/dev/null); then
fail 'strict Delta Specs validation failed'
fi
elif [[ ! -s "$declared_capabilities" && $skip_specs -eq 0 ]]; then
fail 'Proposal must declare capabilities or .openspec.yaml must set skip_specs: true'
fi
has_design=0
if [[ -f "$design_file" ]]; then
has_design=1
[[ $has_specs -eq 1 || $skip_specs -eq 1 ]] || fail 'Design exists before Delta Specs'
for heading in '## Context' '## Goals / Non-Goals' '## Decisions' '## Delivery Map' '### Contract Gate Decision' '## Risks / Trade-offs' '## Migration Plan' '### Rollback Contract'; do
require_heading "$design_file" "$heading"
done
delivery_map_error="$temporary/delivery-map-error.txt"
if ! wl_parse_delivery_map "$metadata_file" "$design_file" 2>"$delivery_map_error"; then
artifact_fail "$(<"$delivery_map_error")"
fi
rollback_status_count=$(awk -F'|' '$2 ~ /^[[:space:]]*Status[[:space:]]*$/ { count++ } END { print count + 0 }' "$design_file")
[[ "$rollback_status_count" -eq 1 ]] || fail 'Rollback Contract must contain exactly one Status row'
rollback_status=$(awk -F'|' '$2 ~ /^[[:space:]]*Status[[:space:]]*$/ { value=$3; gsub(/^[[:space:]`]+|[[:space:]`]+$/, "", value); print value }' "$design_file")
if [[ "$rollback_status" == 'N/A' ]]; then
rollback_reason=$(awk -F'|' '$2 ~ /^[[:space:]]*Reason[[:space:]]*$/ { value=$3; gsub(/^[[:space:]]+|[[:space:]]+$/, "", value); print value }' "$design_file")
[[ -n "$rollback_reason" && "$rollback_reason" != '-' && "$rollback_reason" != '—' && "$rollback_reason" != *'<!--'* ]] || fail 'Rollback Contract N/A requires a concrete Reason row'
elif [[ "$rollback_status" == 'required' ]]; then
for field in Trigger Actions 'Data safety' Verification; do
count=$(awk -F'|' -v field="$field" '$2 ~ "^[[:space:]]*" field "[[:space:]]*$" { count++ } END { print count + 0 }' "$design_file")
[[ "$count" -eq 1 ]] || fail "Rollback Contract required status needs exactly one $field row"
done
else
fail "Rollback Contract Status must be required or N/A, got: $rollback_status"
fi
fi
has_tasks=0
if [[ -f "$tasks_file" ]]; then
has_tasks=1
[[ $has_design -eq 1 ]] || fail 'Tasks exists before Design'
task_number=''
task_area=''
covers_count=0
verification_count=0
covers_is_na=0
task_count=0
area_task_counts=(0 0 0 0)
area_gate_counts=(0 0 0 0)
finalize_task() {
[[ -n "$task_number" ]] || return 0
[[ "$covers_count" -eq 1 ]] || fail "task $task_number must contain exactly one Covers line"
[[ "$verification_count" -eq 1 ]] || fail "task $task_number must contain exactly one Verification line"
if [[ "$task_area" == 'workspace' && "$covers_is_na" -ne 1 ]]; then
fail "workspace task $task_number must use a concrete N/A coverage rationale"
fi
}
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" == '- [ ] '* || "$line" == '- [x] '* || "$line" == '- [X] '* ]]; then
finalize_task
if [[ ! "$line" =~ ^-\ \[[\ xX]\]\ ([0-9]+\.[0-9]+)\ \[(frontend|backend|specs|docs|workspace)\]\ .+ ]]; then
fail "invalid task checkbox shape: $line"
fi
task_number=${BASH_REMATCH[1]}
task_area=${BASH_REMATCH[2]}
covers_count=0
verification_count=0
covers_is_na=0
task_count=$((task_count + 1))
if [[ "$task_area" != 'workspace' ]]; then
index=$(wl_area_index "$task_area")
if [[ "${wl_map_statuses[$index]}" != 'affected' ]]; then
fail "task $task_number uses $task_area, but Delivery Map marks it ${wl_map_statuses[$index]}"
fi
area_task_counts[$index]=$((area_task_counts[$index] + 1))
fi
continue
fi
if [[ "$line" == ' - **Covers:** '* ]]; then
[[ -n "$task_number" ]] || fail 'Covers metadata appears before a task'
covers_count=$((covers_count + 1))
payload=${line#' - **Covers:** '}
if [[ "$payload" == \`N/A\`\ —\ * ]]; then
reason=${payload#\`N/A\`\ —\ }
[[ -n "$reason" && "$reason" != '-' && "$reason" != '—' ]] || fail "task $task_number has empty N/A rationale"
[[ "$reason" != *'<!--'* && "$reason" != *'-->'* ]] || fail "task $task_number has unresolved N/A rationale placeholder"
covers_is_na=1
else
IFS=';' read -r -a references <<< "$payload"
for reference in "${references[@]}"; do
reference=$(wl_trim "$reference")
if ! grep -Eq '^`[^`]+` — `[^`]+`$' <<< "$reference"; then
fail "task $task_number has malformed requirement reference: $reference"
fi
clean=${reference//\`/}
capability=${clean%% — *}
requirement=${clean#* — }
key="$capability"$'\t'"$requirement"
if ! grep -Fqx "$key" "$delta_requirements"; then
fail "task $task_number has unknown requirement reference: $capability — $requirement"
fi
printf '%s\n' "$key" >> "$covered_requirements"
done
fi
continue
fi
if [[ "$line" == ' - **Verification:** '* ]]; then
[[ -n "$task_number" ]] || fail 'Verification metadata appears before a task'
verification_count=$((verification_count + 1))
verification=${line#' - **Verification:** '}
[[ -n "$verification" ]] || fail "task $task_number has empty Verification"
if [[ "$task_area" != 'workspace' ]]; then
index=$(wl_area_index "$task_area")
case "$task_area" in
frontend|backend|docs)
[[ "$verification" == *'npm run verify'* ]] && area_gate_counts[$index]=$((area_gate_counts[$index] + 1))
;;
specs)
if [[ "$verification" == *'strict change validation'* || "$verification" == *'openspec validate'* ]]; then
area_gate_counts[$index]=$((area_gate_counts[$index] + 1))
fi
;;
esac
fi
fi
done < "$tasks_file"
finalize_task
[[ "$task_count" -gt 0 ]] || fail 'Tasks contains no tracked checkboxes'
for index in 0 1 2 3; do
if [[ "${wl_affected_flags[$index]}" -eq 1 ]]; then
[[ "${area_task_counts[$index]}" -gt 0 ]] || fail "affected area ${wl_areas[$index]} has no owned task"
[[ "${area_gate_counts[$index]}" -gt 0 ]] || fail "affected area ${wl_areas[$index]} has no canonical gate in Verification"
fi
done
while IFS= read -r key; do
[[ -n "$key" ]] || continue
if ! grep -Fqx "$key" "$covered_requirements"; then
capability=${key%%$'\t'*}
requirement=${key#*$'\t'}
fail "uncovered Requirement: $capability — $requirement"
fi
done < "$delta_requirements"
requirement_count=$(wc -l < "$delta_requirements" | tr -d ' ')
printf 'Delta requirement coverage verified: %s/%s\n' "$requirement_count" "$requirement_count"
fi
if [[ $has_tasks -eq 1 ]]; then
printf 'Planning stage verified: tasks\n'
printf 'Full planning structure verified; semantic approval remains human.\n'
elif [[ $has_design -eq 1 ]]; then
printf 'Planning stage verified: design\n'
printf 'Full change validation pending: tasks\n'
elif [[ $has_specs -eq 1 ]]; then
printf 'Planning stage verified: specs\n'
printf 'Full change validation pending: design, tasks\n'
else
printf 'Planning stage verified: proposal\n'
printf 'Full change validation pending: specs, design, tasks\n'
fi
-237
View File
@@ -1,237 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
workspace=$(cd "$script_dir/../.." && pwd -P)
verifier="$script_dir/verify-openspec-planning.sh"
changes_root="$workspace/specs/openspec/changes"
source_change="$changes_root/harden-openspec-sdd-quality"
if [[ ! -d "$source_change" ]]; then
source_change="$changes_root/archive/2026-08-26-harden-openspec-sdd-quality"
fi
prefix="planning-fixture-$$"
created_changes=('')
cleanup() {
local change
for change in "${created_changes[@]}"; do
[[ -n "$change" ]] || continue
rm -rf "$changes_root/$change"
done
}
trap cleanup EXIT
if [[ ! -x "$verifier" ]]; then
printf 'expected executable planning verifier at %s\n' "$verifier" >&2
exit 1
fi
make_change() {
local suffix=$1
fixture_change="$prefix-$suffix"
cp -R "$source_change" "$changes_root/$fixture_change"
created_changes+=("$fixture_change")
}
run_verifier() {
bash "$verifier" "$1"
}
expect_failure() {
local change=$1
local expected=$2
local expected_status=${3:-1}
local output
local status
set +e
output=$(run_verifier "$change" 2>&1)
status=$?
set -e
if [[ $status -eq 0 ]]; then
printf 'expected planning verification to fail for %s\n' "$change" >&2
exit 1
fi
if [[ $status -ne $expected_status ]]; then
printf 'expected exit %s for %s, got %s:\n%s\n' "$expected_status" "$change" "$status" "$output" >&2
exit 1
fi
if [[ "$output" != *'Planning artifact-validation failed:'* ]]; then
printf 'expected artifact-validation category for %s, got:\n%s\n' "$change" "$output" >&2
exit 1
fi
if [[ "$output" != *"$expected"* ]]; then
printf 'expected failure containing %q, got:\n%s\n' "$expected" "$output" >&2
exit 1
fi
}
make_change proposal
proposal_change=$fixture_change
rm -rf "$changes_root/$proposal_change/specs"
rm -f "$changes_root/$proposal_change/design.md" "$changes_root/$proposal_change/tasks.md"
proposal_output=$(run_verifier "$proposal_change")
[[ "$proposal_output" == *'Planning stage verified: proposal'* ]]
[[ "$proposal_output" == *'Full change validation pending: specs, design, tasks'* ]]
set +e
invocation_output=$(bash "$verifier" 'INVALID CHANGE' 2>&1)
invocation_status=$?
set -e
if [[ $invocation_status -ne 2 || "$invocation_output" != *'Planning invocation failed:'* ]]; then
printf 'expected invalid-name invocation failure, got exit %s:\n%s\n' "$invocation_status" "$invocation_output" >&2
exit 1
fi
set +e
missing_change_output=$(bash "$verifier" "$prefix-missing" 2>&1)
missing_change_status=$?
set -e
if [[ $missing_change_status -ne 2 || "$missing_change_output" != *'Planning invocation failed:'* ]]; then
printf 'expected missing-change invocation failure, got exit %s:\n%s\n' "$missing_change_status" "$missing_change_output" >&2
exit 1
fi
set +e
environment_output=$(PATH=/usr/bin:/bin bash "$verifier" "$proposal_change" 2>&1)
environment_status=$?
set -e
if [[ $environment_status -ne 3 || "$environment_output" != *'Planning environment failed:'* ]]; then
printf 'expected environment failure, got exit %s:\n%s\n' "$environment_status" "$environment_output" >&2
exit 1
fi
make_change skip-conflict
skip_conflict=$fixture_change
rm -rf "$changes_root/$skip_conflict/specs"
rm -f "$changes_root/$skip_conflict/design.md" "$changes_root/$skip_conflict/tasks.md"
printf 'skip_specs: true\n' >> "$changes_root/$skip_conflict/.openspec.yaml"
expect_failure "$skip_conflict" 'skip_specs: true cannot be combined with declared capabilities'
make_change specs
specs_change=$fixture_change
rm -f "$changes_root/$specs_change/design.md" "$changes_root/$specs_change/tasks.md"
specs_output=$(run_verifier "$specs_change")
[[ "$specs_output" == *'Planning stage verified: specs'* ]]
[[ "$specs_output" == *'Full change validation pending: design, tasks'* ]]
make_change capability-mismatch
capability_mismatch=$fixture_change
mv "$changes_root/$capability_mismatch/specs/audit-remediation-workflow" "$changes_root/$capability_mismatch/specs/not-declared"
expect_failure "$capability_mismatch" 'delta capability is absent from Proposal: not-declared'
make_change design
design_change=$fixture_change
rm -f "$changes_root/$design_change/tasks.md"
design_output=$(run_verifier "$design_change")
[[ "$design_output" == *'Planning stage verified: design'* ]]
[[ "$design_output" == *'Full change validation pending: tasks'* ]]
make_change required-rollback
required_rollback=$fixture_change
rm -f "$changes_root/$required_rollback/tasks.md"
perl -0pi -e 's/\| Status \| `N\/A` \|/| Status | `required` |/; s/(\| Verification \|)/| Trigger | Deployment fails the local gate. |\n| Actions | Restore the previous local state. |\n| Data safety | No application data changes. |\n$1/' "$changes_root/$required_rollback/design.md"
required_output=$(run_verifier "$required_rollback")
[[ "$required_output" == *'Planning stage verified: design'* ]]
perl -0pi -e 's/^\| Data safety \|.*\n//m' "$changes_root/$required_rollback/design.md"
expect_failure "$required_rollback" 'Rollback Contract required status needs exactly one Data safety row'
make_change tasks
tasks_change=$fixture_change
tasks_output=$(run_verifier "$tasks_change")
[[ "$tasks_output" == *'Planning stage verified: tasks'* ]]
[[ "$tasks_output" == *'Delta requirement coverage verified: 9/9'* ]]
[[ "$tasks_output" == *'Full planning structure verified; semantic approval remains human.'* ]]
for artifact in proposal specs design tasks; do
instructions=$(cd "$workspace/specs" && openspec instructions "$artifact" --change "$tasks_change" --store wealth-ledger-specs --json)
node -e '
const result = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
if (!result.instruction.trimStart().startsWith("Critical Rules:")) process.exit(1);
' <<< "$instructions"
done
specs_instructions=$(cd "$workspace/specs" && openspec instructions specs --change "$tasks_change" --store wealth-ledger-specs --json)
node -e '
const result = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
const activeOperation = /^## (ADDED|MODIFIED|REMOVED|RENAMED) Requirements$/m;
if (activeOperation.test(result.template)) process.exit(1);
for (const token of ["Purpose", "ADDED", "MODIFIED", "REMOVED", "RENAMED"]) {
if (!result.template.includes(token)) process.exit(1);
}
' <<< "$specs_instructions"
operation_change="$prefix-operation-neutral"
cp -R "$script_dir/fixtures/operation-neutral-change" "$changes_root/$operation_change"
created_changes+=("$operation_change")
operation_output=$(run_verifier "$operation_change")
if [[ "$operation_output" != *'Planning stage verified: specs'* ]]; then
printf 'expected controlled operation fixture to verify, got:\n%s\n' "$operation_output" >&2
exit 1
fi
for operation in ADDED MODIFIED REMOVED RENAMED; do
if [[ $(grep -Fxc "## $operation Requirements" "$changes_root/$operation_change/specs/workspace-artifact-hygiene/spec.md") -ne 1 ]]; then
printf 'expected exactly one controlled %s operation section\n' "$operation" >&2
exit 1
fi
done
# A completed fixture receives OpenSpec's terminal archive message instead of
# the schema's Apply instruction, so leave one task pending for this contract.
perl -0pi -e 's/^- \[[xX]\]/- [ ]/m' "$changes_root/$tasks_change/tasks.md"
apply_instructions=$(cd "$workspace/specs" && openspec instructions apply --change "$tasks_change" --store wealth-ledger-specs --json)
node -e '
const result = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
for (const token of ["contextFiles", "AGENTS.md", "planned Verification", "Feedback routing", "Механическая правка", "Requirement wording"]) {
if (!result.instruction.includes(token)) process.exit(1);
}
' <<< "$apply_instructions"
design_instructions=$(cd "$workspace/specs" && openspec instructions design --change "$tasks_change" --store wealth-ledger-specs --json)
node -e '
const result = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
if (!result.instruction.includes("инвалидируй downstream approvals")) process.exit(1);
' <<< "$design_instructions"
make_change invalid-area
invalid_area=$fixture_change
perl -0pi -e 's/\[specs\]/[frontend]/' "$changes_root/$invalid_area/tasks.md"
expect_failure "$invalid_area" 'task 1.1 uses frontend, but Delivery Map marks it N/A'
make_change unknown-reference
unknown_reference=$fixture_change
perl -0pi -e 's/`Planning artifact проверяется до запроса approval`/`Несуществующее требование`/' "$changes_root/$unknown_reference/tasks.md"
expect_failure "$unknown_reference" 'unknown requirement reference'
make_change uncovered-requirement
uncovered_requirement=$fixture_change
sed -i '' '/`audit-remediation-workflow` — `Каждое audit claim получает доказательный disposition`/s/`audit-remediation-workflow` — `Каждое audit claim получает доказательный disposition`/`audit-remediation-workflow` — `Реестр remediation отделён от снимка аудита`/' "$changes_root/$uncovered_requirement/tasks.md"
expect_failure "$uncovered_requirement" 'uncovered Requirement: audit-remediation-workflow — Каждое audit claim получает доказательный disposition'
make_change missing-metadata
missing_metadata=$fixture_change
perl -0pi -e 's/^ - \*\*Verification:\*\*.*\n//m' "$changes_root/$missing_metadata/tasks.md"
expect_failure "$missing_metadata" 'task 1.1 must contain exactly one Verification line'
make_change unresolved-na-placeholder
unresolved_na_placeholder=$fixture_change
perl -0pi -e 's/^ - \*\*Covers:\*\*.*$/ - **Covers:** `N\/A` — <!-- unresolved reason -->/m' "$changes_root/$unresolved_na_placeholder/tasks.md"
expect_failure "$unresolved_na_placeholder" 'task 1.1 has unresolved N/A rationale placeholder'
make_change empty-na
empty_na=$fixture_change
perl -0pi -e 's/^ - \*\*Covers:\*\*.*$/ - **Covers:** `N\/A` — /m' "$changes_root/$empty_na/tasks.md"
expect_failure "$empty_na" 'task 1.1 has empty N/A rationale'
make_change separator-na
separator_na=$fixture_change
perl -0pi -e 's/^ - \*\*Covers:\*\*.*$/ - **Covers:** `N\/A` — —/m' "$changes_root/$separator_na/tasks.md"
expect_failure "$separator_na" 'task 1.1 has empty N/A rationale'
make_change malformed-rollback
malformed_rollback=$fixture_change
sed -i '' '/| Status | `N\/A` |/d' "$changes_root/$malformed_rollback/design.md"
expect_failure "$malformed_rollback" 'Rollback Contract must contain exactly one Status row'
printf 'verify-openspec-planning tests passed\n'
-29
View File
@@ -1,29 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
contract_file=${1:-"$script_dir/../openspec/.cli-version"}
if [[ ! -f "$contract_file" ]]; then
printf 'OpenSpec version contract is missing: %s\n' "$contract_file" >&2
exit 1
fi
expected_version=$(tr -d '[:space:]' < "$contract_file")
if [[ -z "$expected_version" ]]; then
printf 'OpenSpec version contract is empty: %s\n' "$contract_file" >&2
exit 1
fi
if ! command -v openspec >/dev/null 2>&1; then
printf 'OpenSpec CLI %s is required, but openspec is not available in PATH.\n' "$expected_version" >&2
exit 1
fi
actual_version=$(openspec --version 2>/dev/null | tr -d '\r\n')
if [[ "$actual_version" != "$expected_version" ]]; then
printf 'Unsupported OpenSpec CLI version: expected %s, actual %s.\n' "$expected_version" "${actual_version:-unknown}" >&2
exit 1
fi
printf 'OpenSpec CLI version verified: %s\n' "$actual_version"
-126
View File
@@ -1,126 +0,0 @@
#!/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."
-74
View File
@@ -1,74 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 1 ]]; then
printf 'Usage: %s <change-name>\n' "${0##*/}" >&2
exit 2
fi
change=$1
if [[ ! "$change" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
printf 'Invalid OpenSpec change name: %s\n' "$change" >&2
exit 2
fi
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
workspace=$(cd "$script_dir/../.." && pwd -P)
change_dir="$workspace/specs/openspec/changes/$change"
metadata_file="$change_dir/.openspec.yaml"
design_file="$change_dir/design.md"
for required_file in "$metadata_file" "$design_file"; do
if [[ ! -f "$required_file" ]]; then
printf 'Missing canonical change input: %s\n' "${required_file#$workspace/}" >&2
exit 1
fi
done
# shellcheck source=lib/delivery-map.sh
source "$script_dir/lib/delivery-map.sh"
wl_parse_delivery_map "$metadata_file" "$design_file"
areas=("${wl_areas[@]}")
affected_flags=("${wl_affected_flags[@]}")
map_statuses=("${wl_map_statuses[@]}")
contract_status=$wl_contract_status
printf 'Delivery Map verified:'
for index in 0 1 2 3; do
printf ' %s=%s' "${areas[$index]}" "${map_statuses[$index]}"
done
printf '\n'
printf 'Contract Gate Decision verified: runtime-openapi=%s\n' "$contract_status"
run_gate() {
local label=$1
shift
printf '\n==> %s\n' "$label"
"$@"
}
run_gate 'OpenSpec CLI contract' bash "$workspace/specs/scripts/verify-openspec-version.sh"
run_gate 'OpenSpec planning structure' bash "$workspace/specs/scripts/verify-openspec-planning.sh" "$change"
run_gate 'OpenSpec project schema' bash -c 'cd "$1" && openspec schema validate wealth-ledger-sdd' _ "$workspace/specs"
run_gate 'OpenSpec change validation' bash -c 'cd "$1" && openspec validate "$2" --type change --strict --store wealth-ledger-specs' _ "$workspace/specs" "$change"
run_gate 'OpenSpec main specs' bash -c 'cd "$1" && openspec validate --specs --strict --store wealth-ledger-specs' _ "$workspace/specs"
run_gate 'OpenSpec authority' bash "$workspace/specs/scripts/verify-openspec-authority.sh"
if [[ "${affected_flags[0]}" -eq 1 ]]; then
run_gate 'Frontend canonical verification' npm --prefix "$workspace/frontend" run verify
fi
if [[ "${affected_flags[1]}" -eq 1 ]]; then
run_gate 'Backend canonical verification' npm --prefix "$workspace/backend" run verify
fi
if [[ "${affected_flags[3]}" -eq 1 ]]; then
run_gate 'Docs canonical verification' npm --prefix "$workspace/docs" run verify
fi
if [[ "$contract_status" == "required" ]]; then
run_gate 'Runtime OpenAPI contract' bash "$workspace/specs/scripts/verify-runtime-openapi.sh"
fi
printf '\nWorkspace verification passed for %s.\n' "$change"