chore: use native OpenSpec validation
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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`.
|
||||||
@@ -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 получает контролируемое решение`
|
|
||||||
@@ -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