chore: use native OpenSpec validation
This commit is contained in:
@@ -15,8 +15,7 @@ Proposal, Delta Specs, Design, Tasks, changes и archives хранятся в `o
|
||||
|
||||
## Workflow и verification
|
||||
|
||||
Поддерживаемая CLI version задана в `openspec/.cli-version`; новые changes
|
||||
используют project-local schema `wealth-ledger-sdd`. Артефакты создаются по
|
||||
Новые changes используют project-local schema `wealth-ledger-sdd`. Артефакты создаются по
|
||||
одному после approval Change Owner. Lifecycle:
|
||||
[`../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),
|
||||
а не дублируется в schema instructions.
|
||||
|
||||
Из workspace canonical pre-archive gate:
|
||||
Для structural validation из `specs`:
|
||||
|
||||
```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 validate <change> --type change --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.
|
||||
|
||||
@@ -10,20 +10,12 @@ Design должен оценить `frontend`, `backend`, `specs`, `docs` в Del
|
||||
|
||||
## Проверка
|
||||
|
||||
Из workspace под Node 22 выполните:
|
||||
Для native validation из этого репозитория:
|
||||
|
||||
```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 validate <change> --type change --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).
|
||||
|
||||
@@ -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`.
|
||||
@@ -17,10 +17,8 @@ artifacts:
|
||||
оставь capabilities пустыми и установи `skip_specs: true`.
|
||||
- Оставь технологии, зависимости, архитектуру, API, database/code/file
|
||||
structure для Design, а implementation steps и verification — для Tasks.
|
||||
- После записи запусти
|
||||
`bash specs/scripts/verify-openspec-planning.sh <change>`. Исправляй
|
||||
artifact только при `artifact-validation`; при `invocation` исправь вызов,
|
||||
при `environment` — prerequisite. Structural success не является approval.
|
||||
- После записи используй native OpenSpec instructions и validation с
|
||||
`--store wealth-ledger-specs`. Structural success не является approval.
|
||||
|
||||
Required Shape:
|
||||
- **Why**: проблема или возможность и причина заняться ею сейчас.
|
||||
@@ -57,9 +55,8 @@ artifacts:
|
||||
`## RENAMED Requirements`; не оставляй пустые operation sections.
|
||||
- Каждый `### Requirement:` содержит SHALL/MUST и хотя бы один
|
||||
`#### Scenario:` с `- **WHEN**` и `- **THEN**`.
|
||||
- После explicit Proposal approval и записи запусти planning verifier.
|
||||
Исправляй artifact только при `artifact-validation`; `invocation` и
|
||||
`environment` не разрешают менять Delta Specs.
|
||||
- После explicit Proposal approval и записи используй native OpenSpec
|
||||
validation с `--store wealth-ledger-specs`.
|
||||
|
||||
Delta Operations:
|
||||
- `ADDED`: новый Requirement.
|
||||
@@ -99,9 +96,8 @@ artifacts:
|
||||
будущие results, commits или Completion Evidence.
|
||||
- Разреши до сохранения любой вопрос, который меняет Specs, approach или
|
||||
Tasks. `Open Questions` оставляет только genuinely deferrable unknowns.
|
||||
- После explicit Delta Specs approval и записи запусти planning verifier.
|
||||
Исправляй Design только при `artifact-validation`; `invocation` и
|
||||
`environment` требуют исправить вызов или prerequisite.
|
||||
- После explicit Delta Specs approval и записи используй native OpenSpec
|
||||
validation с `--store wealth-ledger-specs`.
|
||||
|
||||
Required Shape:
|
||||
- **Context**: current state и constraints подхода.
|
||||
@@ -162,9 +158,8 @@ artifacts:
|
||||
- Новое behavior возвращает change в Specs; новое architecture — в Design;
|
||||
multi-owner или vague task исправляется в Tasks. Не добавляй commits,
|
||||
archive path или post-archive Completion Evidence.
|
||||
- После записи запусти planning verifier. Исправляй Tasks только при
|
||||
`artifact-validation`; `invocation` и `environment` не разрешают менять
|
||||
checklist.
|
||||
- После записи используй native OpenSpec validation с
|
||||
`--store wealth-ledger-specs`.
|
||||
|
||||
Testing:
|
||||
Автоматические tests обязательны для security/privacy, денежных расчётов,
|
||||
@@ -187,8 +182,8 @@ apply:
|
||||
instruction: |
|
||||
Critical Rules:
|
||||
- Apply допустим только после explicit approvals Proposal, Delta Specs,
|
||||
Design и Tasks. Проверь exact OpenSpec version, canonical store
|
||||
`wealth-ledger-specs` и planning verifier до implementation.
|
||||
Design и Tasks. Используй canonical store `wealth-ledger-specs` для всех
|
||||
OpenSpec commands, читающих или изменяющих change.
|
||||
- Прочитай каждый path из `contextFiles`, возвращённых native OpenSpec apply
|
||||
instructions, затем applicable repository-local `AGENTS.md` affected areas.
|
||||
Не вычисляй planning paths и не создавай второй context map.
|
||||
@@ -202,8 +197,5 @@ apply:
|
||||
нормативного смысла или table alignment. Requirement wording, Scenario,
|
||||
architecture и planned Verification являются semantic changes и требуют
|
||||
соответствующего reapproval.
|
||||
- `artifact-validation` разрешает исправить указанную planning form;
|
||||
`invocation` требует исправить вызов; `environment` — prerequisite.
|
||||
Последние два класса не разрешают менять planning artifact.
|
||||
- Structural validation не заменяет semantic verification и independent
|
||||
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 получает контролируемое решение`
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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"
|
||||
@@ -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."
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user