chore: use native OpenSpec validation

This commit is contained in:
2026-08-26 23:14:13 +03:00
parent 6f47589211
commit 2faf4eb8f3
17 changed files with 209 additions and 1429 deletions
@@ -1,19 +0,0 @@
## Why
Controlled fixture validates operation-neutral Delta Spec behavior.
## What Changes
- Exercise every supported delta operation in one controlled artifact.
## Capabilities
### New Capabilities
### Modified Capabilities
- `workspace-artifact-hygiene`: controlled operation grammar fixture.
## Impact
Test fixture only; application behavior is not delivered.
@@ -1,38 +0,0 @@
## ADDED Requirements
### Requirement: Controlled fixture requirement
The fixture SHALL provide one complete added requirement.
#### Scenario: Added operation is complete
- **WHEN** the controlled delta is validated
- **THEN** the added requirement has observable behavior
## MODIFIED Requirements
### Requirement: Рабочая frontend configuration не является versioned input
Frontend SHALL поставлять versioned `.env.example`, достаточный для создания
локального `.env`, но MUST NOT хранить рабочий `.env` или другой локальный
`.env*` файл в source control. Ignore policy MUST исключать локальные frontend
configuration files, сохраняя явно разрешённый example template.
#### Scenario: Участник создаёт local frontend configuration
- **WHEN** участник копирует `.env.example` в `.env` для запуска frontend
- **THEN** локальный файл доступен runtime, но не появляется среди tracked
файлов Git
## REMOVED Requirements
### Requirement: Runtime SQLite-данные не являются versioned artifact
**Reason**: Controlled fixture exercises the removal grammar.
**Migration**: Existing behavior remains authoritative outside this fixture.
## RENAMED Requirements
- FROM: `### Requirement: Dependency advisory получает проверяемое решение`
- TO: `### Requirement: Dependency advisory получает контролируемое решение`
-125
View File
@@ -1,125 +0,0 @@
#!/usr/bin/env bash
wl_area_index() {
case "$1" in
frontend) printf '0' ;;
backend) printf '1' ;;
specs) printf '2' ;;
docs) printf '3' ;;
*) return 1 ;;
esac
}
wl_trim() {
local value=$1
value=${value#"${value%%[![:space:]]*}"}
value=${value%"${value##*[![:space:]]}"}
printf '%s' "$value"
}
wl_parse_delivery_map() {
local metadata_file=$1
local design_file=$2
local raw_area raw_status raw_reason area status reason gate index
wl_areas=(frontend backend specs docs)
wl_affected_flags=(0 0 0 0)
wl_map_statuses=('' '' '' '')
wl_map_reasons=('' '' '' '')
wl_contract_status=''
wl_contract_reason=''
while IFS= read -r area; do
[[ -n "$area" ]] || continue
if ! index=$(wl_area_index "$area"); then
printf 'Unknown affected area in metadata: %s\n' "$area" >&2
return 1
fi
if [[ "${wl_affected_flags[$index]}" -eq 1 ]]; then
printf 'Duplicate affected area in metadata: %s\n' "$area" >&2
return 1
fi
wl_affected_flags[$index]=1
done < <(
awk '
$0 == "affected_areas:" { in_areas = 1; next }
in_areas && /^ - / { sub(/^ - /, ""); print; next }
in_areas { exit }
' "$metadata_file"
)
if [[ "${wl_affected_flags[2]}" -ne 1 ]]; then
printf 'Delivery Map violation: specs must be affected for every versioned change.\n' >&2
return 1
fi
while IFS='|' read -r _ raw_area raw_status raw_reason _; do
area=$(wl_trim "$raw_area")
case "$area" in
frontend|backend|specs|docs) ;;
*) continue ;;
esac
index=$(wl_area_index "$area")
if [[ -n "${wl_map_statuses[$index]}" ]]; then
printf 'Delivery Map contains duplicate row for %s.\n' "$area" >&2
return 1
fi
status=$(wl_trim "$raw_status")
status=${status//\`/}
reason=$(wl_trim "$raw_reason")
wl_map_statuses[$index]=$status
wl_map_reasons[$index]=$reason
done < "$design_file"
while IFS='|' read -r _ raw_area raw_status raw_reason _; do
gate=$(wl_trim "$raw_area")
[[ "$gate" == 'runtime-openapi' ]] || continue
if [[ -n "$wl_contract_status" ]]; then
printf 'Contract Gate Decision contains duplicate runtime-openapi row.\n' >&2
return 1
fi
wl_contract_status=$(wl_trim "$raw_status")
wl_contract_status=${wl_contract_status//\`/}
wl_contract_reason=$(wl_trim "$raw_reason")
done < "$design_file"
for index in 0 1 2 3; do
area=${wl_areas[$index]}
status=${wl_map_statuses[$index]}
reason=${wl_map_reasons[$index]}
if [[ -z "$status" ]]; then
printf 'Delivery Map is missing area: %s\n' "$area" >&2
return 1
fi
if [[ "${wl_affected_flags[$index]}" -eq 1 ]]; then
if [[ "$status" != 'affected' ]]; then
printf 'Delivery Map mismatch: %s is listed as affected in metadata but has status %s.\n' "$area" "$status" >&2
return 1
fi
elif [[ "$status" != 'N/A' ]]; then
printf 'Delivery Map mismatch: %s is absent from affected_areas but has status %s.\n' "$area" "$status" >&2
return 1
fi
if [[ -z "$reason" || "$reason" == '-' || "$reason" == '—' || "$reason" == *'<!--'* ]]; then
printf 'Delivery Map requires a concrete outcome or N/A reason for %s.\n' "$area" >&2
return 1
fi
done
if [[ -z "$wl_contract_status" ]]; then
printf 'Contract Gate Decision is missing runtime-openapi.\n' >&2
return 1
fi
if [[ "$wl_contract_status" != 'required' && "$wl_contract_status" != 'N/A' ]]; then
printf 'Invalid runtime-openapi decision: %s. Expected required or N/A.\n' "$wl_contract_status" >&2
return 1
fi
if [[ -z "$wl_contract_reason" || "$wl_contract_reason" == '-' || "$wl_contract_reason" == '—' || "$wl_contract_reason" == *'<!--'* ]]; then
printf 'Contract Gate Decision requires a concrete runtime-openapi reason.\n' >&2
return 1
fi
if [[ "$wl_contract_status" == 'required' && ( "${wl_affected_flags[0]}" -ne 1 || "${wl_affected_flags[1]}" -ne 1 ) ]]; then
printf 'runtime-openapi cannot be required unless frontend and backend are affected.\n' >&2
return 1
fi
}
-231
View File
@@ -1,231 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
workspace=${WEALTH_LEDGER_WORKSPACE:-$(cd "$script_dir/../.." && pwd -P)}
failures=0
bash "$script_dir/verify-openspec-version.sh" "$workspace/specs/openspec/.cli-version"
expected_version=$(tr -d '[:space:]' < "$workspace/specs/openspec/.cli-version")
fail() {
printf 'FAIL: %s\n' "$1" >&2
failures=$((failures + 1))
}
require_file_contains() {
local file=$1
local text=$2
if [[ ! -f "$file" ]]; then
fail "missing required file: ${file#$workspace/}"
elif ! grep -Fq -- "$text" "$file"; then
fail "${file#$workspace/} must contain: $text"
fi
}
require_schema_edge() {
local file=$1
local artifact=$2
local dependency=$3
if [[ ! -f "$file" ]]; then
fail "missing required file: ${file#$workspace/}"
return
fi
if ! awk -v artifact="$artifact" -v dependency="$dependency" '
$0 == " - id: " artifact { in_artifact = 1; in_requires = 0; next }
in_artifact && /^ - id:/ { in_artifact = 0; in_requires = 0 }
in_artifact && $0 == " requires:" { in_requires = 1; next }
in_artifact && in_requires && $0 == " - " dependency { found = 1 }
END { exit(found ? 0 : 1) }
' "$file"; then
fail "${file#$workspace/} must require $artifact <- $dependency"
fi
}
store_file="$workspace/specs/.openspec-store/store.yaml"
require_file_contains "$store_file" 'id: wealth-ledger-specs'
canonical_root="$workspace/specs/openspec"
if [[ ! -d "$canonical_root" ]]; then
fail "missing canonical OpenSpec root: specs/openspec"
fi
config_file="$canonical_root/config.yaml"
require_file_contains "$config_file" 'schema: wealth-ledger-sdd'
schema_file="$canonical_root/schemas/wealth-ledger-sdd/schema.yaml"
require_file_contains "$schema_file" 'name: wealth-ledger-sdd'
require_file_contains "$schema_file" 'version: 3'
require_schema_edge "$schema_file" 'specs' 'proposal'
require_schema_edge "$schema_file" 'design' 'specs'
require_schema_edge "$schema_file" 'tasks' 'design'
require_file_contains "$schema_file" 'requires: [ tasks ]'
if [[ -d "$workspace/specs" ]]; then
if ! schema_validation=$(cd "$workspace/specs" && openspec schema validate wealth-ledger-sdd 2>&1); then
fail "wealth-ledger-sdd schema validation failed: $schema_validation"
fi
if ! schema_source=$(cd "$workspace/specs" && openspec schema which wealth-ledger-sdd --json 2>&1); then
fail "wealth-ledger-sdd schema resolution failed: $schema_source"
elif ! grep -Fq '"source": "project"' <<<"$schema_source"; then
fail "wealth-ledger-sdd must resolve from the project-local schema"
fi
fi
openspec_roots=()
while IFS= read -r root; do
openspec_roots+=("$root")
done < <(
find "$workspace" \
\( -path "$workspace/references" -o -path "$workspace/.git" -o -path '*/node_modules' -o -path '*/dist' -o -path '*/coverage' \) -prune -o \
-type d -name openspec -print | sort
)
if [[ ${#openspec_roots[@]} -ne 1 || ${openspec_roots[0]:-} != "$canonical_root" ]]; then
fail "expected exactly one project-owned OpenSpec root at specs/openspec"
fi
agent_files=(AGENTS.md frontend/AGENTS.md backend/AGENTS.md specs/AGENTS.md docs/AGENTS.md)
for relative_path in "${agent_files[@]}"; do
file="$workspace/$relative_path"
require_file_contains "$file" 'wealth-ledger-specs'
require_file_contains "$file" '--store wealth-ledger-specs'
require_file_contains "$file" 'specs/openspec'
done
entrypoints=(
.opencode/commands/opsx-apply.md
.opencode/commands/opsx-archive.md
.opencode/commands/opsx-continue.md
.opencode/commands/opsx-explore.md
.opencode/commands/opsx-new.md
.opencode/commands/opsx-propose.md
.opencode/commands/opsx-sync.md
.opencode/commands/opsx-verify.md
.opencode/skills/openspec-apply-change/SKILL.md
.opencode/skills/openspec-archive-change/SKILL.md
.opencode/skills/openspec-continue-change/SKILL.md
.opencode/skills/openspec-explore/SKILL.md
.opencode/skills/openspec-new-change/SKILL.md
.opencode/skills/openspec-propose/SKILL.md
.opencode/skills/openspec-sync-specs/SKILL.md
.opencode/skills/openspec-verify-change/SKILL.md
)
for relative_path in "${entrypoints[@]}"; do
file="$workspace/$relative_path"
require_file_contains "$file" '--store wealth-ledger-specs'
require_file_contains "$file" 'bash specs/scripts/verify-openspec-version.sh'
if [[ -f "$file" ]] && grep -Eqi 'nearest local openspec|global default(store)?|defaultstore|without a store|store: pointer|--store <id>|--store "<id>"|when applicable' "$file"; then
fail "$relative_path permits conditional OpenSpec store selection"
fi
if [[ -f "$file" ]]; then
while IFS= read -r line; do
if [[ $line =~ (^|[^[:alnum:]_])openspec[[:space:]]+(new[[:space:]]+change[[:space:]]+|status[[:space:]]+|instructions[[:space:]]+|list[[:space:]]+|show[[:space:]]+|validate[[:space:]]+|archive[[:space:]]+|doctor[[:space:]]+|context[[:space:]]+|schemas[[:space:]]+|view[[:space:]]+) ]] && [[ $line != *'--store wealth-ledger-specs'* ]]; then
fail "$relative_path contains an unscoped OpenSpec invocation: $line"
fi
done < "$file"
fi
done
skill_entrypoints=(
.opencode/skills/openspec-apply-change/SKILL.md
.opencode/skills/openspec-archive-change/SKILL.md
.opencode/skills/openspec-continue-change/SKILL.md
.opencode/skills/openspec-explore/SKILL.md
.opencode/skills/openspec-new-change/SKILL.md
.opencode/skills/openspec-propose/SKILL.md
.opencode/skills/openspec-sync-specs/SKILL.md
.opencode/skills/openspec-verify-change/SKILL.md
)
for relative_path in "${skill_entrypoints[@]}"; do
require_file_contains "$workspace/$relative_path" "generatedBy: \"$expected_version\""
done
one_artifact_entrypoints=(
.opencode/commands/opsx-continue.md
.opencode/commands/opsx-explore.md
.opencode/commands/opsx-propose.md
.opencode/skills/openspec-continue-change/SKILL.md
.opencode/skills/openspec-explore/SKILL.md
.opencode/skills/openspec-propose/SKILL.md
)
for relative_path in "${one_artifact_entrypoints[@]}"; do
file="$workspace/$relative_path"
require_file_contains "$file" 'Create at most ONE planning artifact per invocation'
require_file_contains "$file" 'STOP for explicit Change Owner approval'
if [[ -f "$file" ]] && grep -Eqi 'generate all artifacts|create every artifact|until every requested artifact|continue until every requested artifact' "$file"; then
fail "$relative_path permits multi-artifact generation without owner checkpoints"
fi
done
planning_entrypoints=(
.opencode/commands/opsx-apply.md
.opencode/commands/opsx-archive.md
.opencode/commands/opsx-continue.md
.opencode/commands/opsx-explore.md
.opencode/commands/opsx-propose.md
.opencode/commands/opsx-verify.md
.opencode/skills/openspec-apply-change/SKILL.md
.opencode/skills/openspec-archive-change/SKILL.md
.opencode/skills/openspec-continue-change/SKILL.md
.opencode/skills/openspec-explore/SKILL.md
.opencode/skills/openspec-propose/SKILL.md
.opencode/skills/openspec-verify-change/SKILL.md
)
for relative_path in "${planning_entrypoints[@]}"; do
require_file_contains "$workspace/$relative_path" 'bash specs/scripts/verify-openspec-planning.sh'
done
approval_blocker_entrypoints=(
.opencode/commands/opsx-apply.md
.opencode/commands/opsx-archive.md
.opencode/skills/openspec-apply-change/SKILL.md
.opencode/skills/openspec-archive-change/SKILL.md
)
for relative_path in "${approval_blocker_entrypoints[@]}"; do
file="$workspace/$relative_path"
require_file_contains "$file" 'explicit Change Owner approval'
require_file_contains "$file" 'invalidate dependent approvals'
done
archive_entrypoints=(
.opencode/commands/opsx-archive.md
.opencode/skills/openspec-archive-change/SKILL.md
)
for relative_path in "${archive_entrypoints[@]}"; do
file="$workspace/$relative_path"
require_file_contains "$file" 'affected delivery area'
require_file_contains "$file" 'semantic verification'
require_file_contains "$file" 'independent read-only review'
require_file_contains "$file" 'Do not ask for or accept a confirmation override.'
if [[ -f "$file" ]] && grep -Eqi 'archive complete \(with warnings\)|don.t block archive on warnings|archived with [0-9]+ incomplete' "$file"; then
fail "$relative_path permits archive with incomplete work"
fi
done
verification_entrypoints=(
.opencode/commands/opsx-verify.md
.opencode/skills/openspec-verify-change/SKILL.md
)
for relative_path in "${verification_entrypoints[@]}"; do
file="$workspace/$relative_path"
require_file_contains "$file" 'affected delivery area'
require_file_contains "$file" 'workspace verifier'
require_file_contains "$file" 'independent read-only review'
done
for relative_path in docs/superpowers/plans superpowers/plans .superpowers; do
if [[ -e "$workspace/$relative_path" ]]; then
fail "prohibited framework planning location: $relative_path"
fi
done
if [[ $failures -gt 0 ]]; then
exit 1
fi
printf 'OpenSpec authority verified: wealth-ledger-specs at specs/openspec\n'
-209
View File
@@ -1,209 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
verifier="$script_dir/verify-openspec-authority.sh"
contract_version=$(tr -d '[:space:]' < "$script_dir/../openspec/.cli-version")
if [[ ! -f "$verifier" ]]; then
echo "expected verifier at $verifier" >&2
exit 1
fi
fixture=$(mktemp -d)
trap 'rm -rf "$fixture"' EXIT
write_file() {
local relative_path=$1
local content=$2
mkdir -p "$fixture/$(dirname "$relative_path")"
printf '%s\n' "$content" > "$fixture/$relative_path"
}
write_file "specs/.openspec-store/store.yaml" $'version: 1\nid: wealth-ledger-specs'
mkdir -p "$fixture/specs/openspec"
write_file "specs/openspec/.cli-version" "$contract_version"
write_file "specs/openspec/config.yaml" 'schema: wealth-ledger-sdd'
mkdir -p "$fixture/specs/openspec/schemas"
cp -R "$script_dir/../openspec/schemas/wealth-ledger-sdd" "$fixture/specs/openspec/schemas/wealth-ledger-sdd"
for path in AGENTS.md frontend/AGENTS.md backend/AGENTS.md specs/AGENTS.md docs/AGENTS.md; do
write_file "$path" $'OpenSpec store: wealth-ledger-specs\nUse --store wealth-ledger-specs\nSDD artifacts only in specs/openspec\nreferences/** is read-only.'
done
entrypoints=(
.opencode/commands/opsx-apply.md
.opencode/commands/opsx-archive.md
.opencode/commands/opsx-continue.md
.opencode/commands/opsx-explore.md
.opencode/commands/opsx-new.md
.opencode/commands/opsx-propose.md
.opencode/commands/opsx-sync.md
.opencode/commands/opsx-verify.md
.opencode/skills/openspec-apply-change/SKILL.md
.opencode/skills/openspec-archive-change/SKILL.md
.opencode/skills/openspec-continue-change/SKILL.md
.opencode/skills/openspec-explore/SKILL.md
.opencode/skills/openspec-new-change/SKILL.md
.opencode/skills/openspec-propose/SKILL.md
.opencode/skills/openspec-sync-specs/SKILL.md
.opencode/skills/openspec-verify-change/SKILL.md
)
planning_line='bash specs/scripts/verify-openspec-planning.sh "<change>"'
for path in "${entrypoints[@]}"; do
content=$'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs'
if [[ "$path" == .opencode/skills/* ]]; then
content="generatedBy: \"$contract_version\""$'\n'"$content"
fi
if [[ "$path" == *continue* || "$path" == *explore* || "$path" == *propose* ]]; then
content="$content"$'\nCreate at most ONE planning artifact per invocation.\nSTOP for explicit Change Owner approval.'
fi
if [[ "$path" == *apply* || "$path" == *archive* ]]; then
content="$content"$'\nRequire explicit Change Owner approval and invalidate dependent approvals after upstream edits.'
fi
if [[ "$path" != *new* && "$path" != *sync* ]]; then
content="$content"$'\n'"$planning_line"
fi
write_file "$path" "$content"
done
archive_gate='bash specs/scripts/verify-openspec-version.sh
openspec status --store wealth-ledger-specs
Require affected delivery area gates, semantic verification and independent read-only review.
Require explicit Change Owner approval and invalidate dependent approvals after upstream edits.
Do not ask for or accept a confirmation override.'
archive_gate="$archive_gate"$'\n'"$planning_line"
write_file ".opencode/commands/opsx-archive.md" "$archive_gate"
write_file ".opencode/skills/openspec-archive-change/SKILL.md" "generatedBy: \"$contract_version\""$'\n'"$archive_gate"
verification_gate='bash specs/scripts/verify-openspec-version.sh
openspec status --store wealth-ledger-specs
Check every affected delivery area with the workspace verifier and require independent read-only review.'
verification_gate="$verification_gate"$'\n'"$planning_line"
write_file ".opencode/commands/opsx-verify.md" "$verification_gate"
write_file ".opencode/skills/openspec-verify-change/SKILL.md" "generatedBy: \"$contract_version\""$'\n'"$verification_gate"
apply_gate='bash specs/scripts/verify-openspec-version.sh
openspec status --store wealth-ledger-specs
Require explicit Change Owner approval and invalidate dependent approvals after upstream edits.'
apply_gate="$apply_gate"$'\n'"$planning_line"
run_verifier() {
WEALTH_LEDGER_WORKSPACE="$fixture" bash "$verifier"
}
run_verifier
perl -0pi -e 's/^bash specs\/scripts\/verify-openspec-planning\.sh .*\n//m' "$fixture/.opencode/commands/opsx-propose.md"
if run_verifier; then
echo "expected missing planning gate in an artifact entrypoint to fail" >&2
exit 1
fi
printf '%s\n' "$planning_line" >> "$fixture/.opencode/commands/opsx-propose.md"
write_file "specs/openspec/.cli-version" '9.9.9'
if run_verifier; then
echo "expected wrong OpenSpec version contract to fail" >&2
exit 1
fi
write_file "specs/openspec/.cli-version" "$contract_version"
write_file "specs/openspec/config.yaml" 'schema: spec-driven'
if run_verifier; then
echo "expected non-project default schema to fail" >&2
exit 1
fi
write_file "specs/openspec/config.yaml" 'schema: wealth-ledger-sdd'
perl -0pi -e 's/^version: 3$/version: 2/m' "$fixture/specs/openspec/schemas/wealth-ledger-sdd/schema.yaml"
if run_verifier; then
echo "expected obsolete project schema version to fail" >&2
exit 1
fi
rm -rf "$fixture/specs/openspec/schemas/wealth-ledger-sdd"
cp -R "$script_dir/../openspec/schemas/wealth-ledger-sdd" "$fixture/specs/openspec/schemas/wealth-ledger-sdd"
write_file "specs/openspec/schemas/wealth-ledger-sdd/schema.yaml" $'name: wealth-ledger-sdd\nversion: 1'
if run_verifier; then
echo "expected incomplete project schema graph to fail" >&2
exit 1
fi
rm -rf "$fixture/specs/openspec/schemas/wealth-ledger-sdd"
cp -R "$script_dir/../openspec/schemas/wealth-ledger-sdd" "$fixture/specs/openspec/schemas/wealth-ledger-sdd"
mkdir -p "$fixture/frontend/openspec"
if run_verifier; then
echo "expected additional OpenSpec root to fail" >&2
exit 1
fi
rmdir "$fixture/frontend/openspec"
write_file ".opencode/commands/opsx-apply.md" $'bash specs/scripts/verify-openspec-version.sh\nWithout a store, commands act on the nearest local openspec root.'
if run_verifier; then
echo "expected implicit store selection to fail" >&2
exit 1
fi
write_file ".opencode/commands/opsx-apply.md" "$apply_gate"
write_file ".opencode/commands/opsx-apply.md" $'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs\nFor another store, append --store "<id>" when applicable.'
if run_verifier; then
echo "expected conditional store selection to fail" >&2
exit 1
fi
write_file ".opencode/commands/opsx-apply.md" "$apply_gate"
write_file ".opencode/commands/opsx-apply.md" $'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs\nRun `openspec status --change "example" --json` before continuing.'
if run_verifier; then
echo "expected unscoped OpenSpec invocation to fail" >&2
exit 1
fi
write_file ".opencode/commands/opsx-apply.md" "$apply_gate"
write_file ".opencode/commands/opsx-apply.md" $'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs\nUse artifact graph (openspec status --change "example" --json) before continuing.'
if run_verifier; then
echo "expected parenthesized unscoped OpenSpec invocation to fail" >&2
exit 1
fi
write_file ".opencode/commands/opsx-apply.md" "$apply_gate"
write_file ".opencode/commands/opsx-propose.md" $'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs\nCreate every artifact in the required set.\nSTOP for explicit Change Owner approval.'
if run_verifier; then
echo "expected multi-artifact proposal guidance to fail" >&2
exit 1
fi
write_file ".opencode/commands/opsx-propose.md" $'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs\nCreate at most ONE planning artifact per invocation.\nSTOP for explicit Change Owner approval.\n'"$planning_line"
write_file ".opencode/commands/opsx-archive.md" $'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs\nDon\x27t block archive on warnings - just inform and confirm.'
if run_verifier; then
echo "expected archive override guidance to fail" >&2
exit 1
fi
write_file ".opencode/commands/opsx-archive.md" "$archive_gate"
write_file ".opencode/commands/opsx-archive.md" $'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs\nDo not ask for or accept a confirmation override.'
if run_verifier; then
echo "expected archive without required verification to fail" >&2
exit 1
fi
write_file ".opencode/commands/opsx-archive.md" "$archive_gate"
write_file ".opencode/commands/opsx-verify.md" $'bash specs/scripts/verify-openspec-version.sh\nopenspec status --store wealth-ledger-specs'
if run_verifier; then
echo "expected semantic verifier without affected-area review to fail" >&2
exit 1
fi
write_file ".opencode/commands/opsx-verify.md" "$verification_gate"
mkdir -p "$fixture/docs/superpowers/plans"
if run_verifier; then
echo "expected prohibited planning location to fail" >&2
exit 1
fi
rmdir "$fixture/docs/superpowers/plans"
rmdir "$fixture/docs/superpowers"
mkdir -p "$fixture/references/example/superpowers/plans"
run_verifier
echo "verify-openspec-authority tests passed"
-303
View File
@@ -1,303 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
invocation_fail() {
printf 'Planning invocation failed: %s\n' "$1" >&2
exit 2
}
environment_fail() {
printf 'Planning environment failed: %s\n' "$1" >&2
exit 3
}
artifact_fail() {
printf 'Planning artifact-validation failed: %s\n' "$1" >&2
exit 1
}
if [[ $# -ne 1 ]]; then
invocation_fail "usage: ${0##*/} <change-name>"
fi
change=$1
if [[ ! "$change" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
invocation_fail "invalid OpenSpec change name: $change"
fi
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
workspace=${WEALTH_LEDGER_WORKSPACE:-$(cd "$script_dir/../.." && pwd -P)}
specs_root="$workspace/specs"
openspec_root="$specs_root/openspec"
change_dir="$openspec_root/changes/$change"
metadata_file="$change_dir/.openspec.yaml"
proposal_file="$change_dir/proposal.md"
design_file="$change_dir/design.md"
tasks_file="$change_dir/tasks.md"
# shellcheck source=lib/delivery-map.sh
source "$script_dir/lib/delivery-map.sh"
fail() {
artifact_fail "$1"
}
require_file() {
[[ -f "$1" ]] || fail "missing ${1#$workspace/}"
}
require_heading() {
grep -Fqx -- "$2" "$1" || fail "${1#$workspace/} must contain heading: $2"
}
if ! version_output=$(bash "$script_dir/verify-openspec-version.sh" "$openspec_root/.cli-version" 2>&1); then
environment_fail "$version_output"
fi
[[ -d "$change_dir" ]] || invocation_fail "change $change is absent from canonical store wealth-ledger-specs"
require_file "$metadata_file"
require_file "$proposal_file"
if ! grep -Fqx 'schema: wealth-ledger-sdd' "$metadata_file"; then
fail '.openspec.yaml must select schema wealth-ledger-sdd'
fi
if ! grep -Eq '^goal: .+' "$metadata_file"; then
fail '.openspec.yaml must contain a non-empty goal'
fi
if ! status_output=$(cd "$specs_root" && openspec status --change "$change" --json --store wealth-ledger-specs 2>&1); then
invocation_fail "change $change is not resolvable through canonical store wealth-ledger-specs: $status_output"
fi
for heading in '## Why' '## What Changes' '## Capabilities' '### New Capabilities' '### Modified Capabilities' '## Impact'; do
require_heading "$proposal_file" "$heading"
done
if ! temporary=$(mktemp -d); then
environment_fail 'cannot create temporary planning-verification directory'
fi
trap 'rm -rf "$temporary"' EXIT
declared_capabilities="$temporary/declared-capabilities.tsv"
actual_capabilities="$temporary/actual-capabilities.txt"
delta_requirements="$temporary/delta-requirements.tsv"
covered_requirements="$temporary/covered-requirements.tsv"
: > "$declared_capabilities"
: > "$actual_capabilities"
: > "$delta_requirements"
: > "$covered_requirements"
awk '
/^### New Capabilities$/ { kind = "new"; next }
/^### Modified Capabilities$/ { kind = "modified"; next }
/^## / || /^### / { kind = "" }
kind != "" && /^- `[^`]+`:/ {
line = $0
sub(/^- `/, "", line)
sub(/`:.*/, "", line)
print kind "\t" line
}
' "$proposal_file" > "$declared_capabilities"
while IFS=$'\t' read -r kind capability; do
[[ -n "$capability" ]] || continue
if [[ "$capability" == /* || "$capability" == *'..'* || ! "$capability" =~ ^[a-z0-9]+([/-][a-z0-9]+)*$ ]]; then
fail "invalid capability path in Proposal: $capability"
fi
if [[ "$kind" == 'modified' && ! -f "$openspec_root/specs/$capability/spec.md" ]]; then
fail "modified capability does not exist in canonical specs: $capability"
fi
done < "$declared_capabilities"
skip_specs=0
if grep -Fqx 'skip_specs: true' "$metadata_file"; then
skip_specs=1
if [[ -s "$declared_capabilities" ]]; then
fail 'skip_specs: true cannot be combined with declared capabilities'
fi
fi
has_specs=0
if [[ -d "$change_dir/specs" ]] && find "$change_dir/specs" -type f -name spec.md -print -quit | grep -q .; then
[[ $skip_specs -eq 0 ]] || fail 'skip_specs: true cannot be combined with delta spec files'
has_specs=1
while IFS= read -r file; do
capability=${file#"$change_dir/specs/"}
capability=${capability%/spec.md}
printf '%s\n' "$capability" >> "$actual_capabilities"
awk -v capability="$capability" '/^### Requirement: / { sub(/^### Requirement: /, ""); print capability "\t" $0 }' "$file" >> "$delta_requirements"
done < <(find "$change_dir/specs" -type f -name spec.md | sort)
while IFS= read -r capability; do
if ! awk -F '\t' -v capability="$capability" '$2 == capability { found = 1 } END { exit(found ? 0 : 1) }' "$declared_capabilities"; then
fail "delta capability is absent from Proposal: $capability"
fi
done < "$actual_capabilities"
while IFS=$'\t' read -r _ capability; do
[[ -n "$capability" ]] || continue
grep -Fqx "$capability" "$actual_capabilities" || fail "Proposal capability has no delta spec: $capability"
done < "$declared_capabilities"
if ! (cd "$specs_root" && openspec validate "$change" --type change --strict --store wealth-ledger-specs >/dev/null); then
fail 'strict Delta Specs validation failed'
fi
elif [[ ! -s "$declared_capabilities" && $skip_specs -eq 0 ]]; then
fail 'Proposal must declare capabilities or .openspec.yaml must set skip_specs: true'
fi
has_design=0
if [[ -f "$design_file" ]]; then
has_design=1
[[ $has_specs -eq 1 || $skip_specs -eq 1 ]] || fail 'Design exists before Delta Specs'
for heading in '## Context' '## Goals / Non-Goals' '## Decisions' '## Delivery Map' '### Contract Gate Decision' '## Risks / Trade-offs' '## Migration Plan' '### Rollback Contract'; do
require_heading "$design_file" "$heading"
done
delivery_map_error="$temporary/delivery-map-error.txt"
if ! wl_parse_delivery_map "$metadata_file" "$design_file" 2>"$delivery_map_error"; then
artifact_fail "$(<"$delivery_map_error")"
fi
rollback_status_count=$(awk -F'|' '$2 ~ /^[[:space:]]*Status[[:space:]]*$/ { count++ } END { print count + 0 }' "$design_file")
[[ "$rollback_status_count" -eq 1 ]] || fail 'Rollback Contract must contain exactly one Status row'
rollback_status=$(awk -F'|' '$2 ~ /^[[:space:]]*Status[[:space:]]*$/ { value=$3; gsub(/^[[:space:]`]+|[[:space:]`]+$/, "", value); print value }' "$design_file")
if [[ "$rollback_status" == 'N/A' ]]; then
rollback_reason=$(awk -F'|' '$2 ~ /^[[:space:]]*Reason[[:space:]]*$/ { value=$3; gsub(/^[[:space:]]+|[[:space:]]+$/, "", value); print value }' "$design_file")
[[ -n "$rollback_reason" && "$rollback_reason" != '-' && "$rollback_reason" != '—' && "$rollback_reason" != *'<!--'* ]] || fail 'Rollback Contract N/A requires a concrete Reason row'
elif [[ "$rollback_status" == 'required' ]]; then
for field in Trigger Actions 'Data safety' Verification; do
count=$(awk -F'|' -v field="$field" '$2 ~ "^[[:space:]]*" field "[[:space:]]*$" { count++ } END { print count + 0 }' "$design_file")
[[ "$count" -eq 1 ]] || fail "Rollback Contract required status needs exactly one $field row"
done
else
fail "Rollback Contract Status must be required or N/A, got: $rollback_status"
fi
fi
has_tasks=0
if [[ -f "$tasks_file" ]]; then
has_tasks=1
[[ $has_design -eq 1 ]] || fail 'Tasks exists before Design'
task_number=''
task_area=''
covers_count=0
verification_count=0
covers_is_na=0
task_count=0
area_task_counts=(0 0 0 0)
area_gate_counts=(0 0 0 0)
finalize_task() {
[[ -n "$task_number" ]] || return 0
[[ "$covers_count" -eq 1 ]] || fail "task $task_number must contain exactly one Covers line"
[[ "$verification_count" -eq 1 ]] || fail "task $task_number must contain exactly one Verification line"
if [[ "$task_area" == 'workspace' && "$covers_is_na" -ne 1 ]]; then
fail "workspace task $task_number must use a concrete N/A coverage rationale"
fi
}
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" == '- [ ] '* || "$line" == '- [x] '* || "$line" == '- [X] '* ]]; then
finalize_task
if [[ ! "$line" =~ ^-\ \[[\ xX]\]\ ([0-9]+\.[0-9]+)\ \[(frontend|backend|specs|docs|workspace)\]\ .+ ]]; then
fail "invalid task checkbox shape: $line"
fi
task_number=${BASH_REMATCH[1]}
task_area=${BASH_REMATCH[2]}
covers_count=0
verification_count=0
covers_is_na=0
task_count=$((task_count + 1))
if [[ "$task_area" != 'workspace' ]]; then
index=$(wl_area_index "$task_area")
if [[ "${wl_map_statuses[$index]}" != 'affected' ]]; then
fail "task $task_number uses $task_area, but Delivery Map marks it ${wl_map_statuses[$index]}"
fi
area_task_counts[$index]=$((area_task_counts[$index] + 1))
fi
continue
fi
if [[ "$line" == ' - **Covers:** '* ]]; then
[[ -n "$task_number" ]] || fail 'Covers metadata appears before a task'
covers_count=$((covers_count + 1))
payload=${line#' - **Covers:** '}
if [[ "$payload" == \`N/A\`\ —\ * ]]; then
reason=${payload#\`N/A\`\ —\ }
[[ -n "$reason" && "$reason" != '-' && "$reason" != '—' ]] || fail "task $task_number has empty N/A rationale"
[[ "$reason" != *'<!--'* && "$reason" != *'-->'* ]] || fail "task $task_number has unresolved N/A rationale placeholder"
covers_is_na=1
else
IFS=';' read -r -a references <<< "$payload"
for reference in "${references[@]}"; do
reference=$(wl_trim "$reference")
if ! grep -Eq '^`[^`]+` — `[^`]+`$' <<< "$reference"; then
fail "task $task_number has malformed requirement reference: $reference"
fi
clean=${reference//\`/}
capability=${clean%% — *}
requirement=${clean#* — }
key="$capability"$'\t'"$requirement"
if ! grep -Fqx "$key" "$delta_requirements"; then
fail "task $task_number has unknown requirement reference: $capability — $requirement"
fi
printf '%s\n' "$key" >> "$covered_requirements"
done
fi
continue
fi
if [[ "$line" == ' - **Verification:** '* ]]; then
[[ -n "$task_number" ]] || fail 'Verification metadata appears before a task'
verification_count=$((verification_count + 1))
verification=${line#' - **Verification:** '}
[[ -n "$verification" ]] || fail "task $task_number has empty Verification"
if [[ "$task_area" != 'workspace' ]]; then
index=$(wl_area_index "$task_area")
case "$task_area" in
frontend|backend|docs)
[[ "$verification" == *'npm run verify'* ]] && area_gate_counts[$index]=$((area_gate_counts[$index] + 1))
;;
specs)
if [[ "$verification" == *'strict change validation'* || "$verification" == *'openspec validate'* ]]; then
area_gate_counts[$index]=$((area_gate_counts[$index] + 1))
fi
;;
esac
fi
fi
done < "$tasks_file"
finalize_task
[[ "$task_count" -gt 0 ]] || fail 'Tasks contains no tracked checkboxes'
for index in 0 1 2 3; do
if [[ "${wl_affected_flags[$index]}" -eq 1 ]]; then
[[ "${area_task_counts[$index]}" -gt 0 ]] || fail "affected area ${wl_areas[$index]} has no owned task"
[[ "${area_gate_counts[$index]}" -gt 0 ]] || fail "affected area ${wl_areas[$index]} has no canonical gate in Verification"
fi
done
while IFS= read -r key; do
[[ -n "$key" ]] || continue
if ! grep -Fqx "$key" "$covered_requirements"; then
capability=${key%%$'\t'*}
requirement=${key#*$'\t'}
fail "uncovered Requirement: $capability — $requirement"
fi
done < "$delta_requirements"
requirement_count=$(wc -l < "$delta_requirements" | tr -d ' ')
printf 'Delta requirement coverage verified: %s/%s\n' "$requirement_count" "$requirement_count"
fi
if [[ $has_tasks -eq 1 ]]; then
printf 'Planning stage verified: tasks\n'
printf 'Full planning structure verified; semantic approval remains human.\n'
elif [[ $has_design -eq 1 ]]; then
printf 'Planning stage verified: design\n'
printf 'Full change validation pending: tasks\n'
elif [[ $has_specs -eq 1 ]]; then
printf 'Planning stage verified: specs\n'
printf 'Full change validation pending: design, tasks\n'
else
printf 'Planning stage verified: proposal\n'
printf 'Full change validation pending: specs, design, tasks\n'
fi
-237
View File
@@ -1,237 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
workspace=$(cd "$script_dir/../.." && pwd -P)
verifier="$script_dir/verify-openspec-planning.sh"
changes_root="$workspace/specs/openspec/changes"
source_change="$changes_root/harden-openspec-sdd-quality"
if [[ ! -d "$source_change" ]]; then
source_change="$changes_root/archive/2026-08-26-harden-openspec-sdd-quality"
fi
prefix="planning-fixture-$$"
created_changes=('')
cleanup() {
local change
for change in "${created_changes[@]}"; do
[[ -n "$change" ]] || continue
rm -rf "$changes_root/$change"
done
}
trap cleanup EXIT
if [[ ! -x "$verifier" ]]; then
printf 'expected executable planning verifier at %s\n' "$verifier" >&2
exit 1
fi
make_change() {
local suffix=$1
fixture_change="$prefix-$suffix"
cp -R "$source_change" "$changes_root/$fixture_change"
created_changes+=("$fixture_change")
}
run_verifier() {
bash "$verifier" "$1"
}
expect_failure() {
local change=$1
local expected=$2
local expected_status=${3:-1}
local output
local status
set +e
output=$(run_verifier "$change" 2>&1)
status=$?
set -e
if [[ $status -eq 0 ]]; then
printf 'expected planning verification to fail for %s\n' "$change" >&2
exit 1
fi
if [[ $status -ne $expected_status ]]; then
printf 'expected exit %s for %s, got %s:\n%s\n' "$expected_status" "$change" "$status" "$output" >&2
exit 1
fi
if [[ "$output" != *'Planning artifact-validation failed:'* ]]; then
printf 'expected artifact-validation category for %s, got:\n%s\n' "$change" "$output" >&2
exit 1
fi
if [[ "$output" != *"$expected"* ]]; then
printf 'expected failure containing %q, got:\n%s\n' "$expected" "$output" >&2
exit 1
fi
}
make_change proposal
proposal_change=$fixture_change
rm -rf "$changes_root/$proposal_change/specs"
rm -f "$changes_root/$proposal_change/design.md" "$changes_root/$proposal_change/tasks.md"
proposal_output=$(run_verifier "$proposal_change")
[[ "$proposal_output" == *'Planning stage verified: proposal'* ]]
[[ "$proposal_output" == *'Full change validation pending: specs, design, tasks'* ]]
set +e
invocation_output=$(bash "$verifier" 'INVALID CHANGE' 2>&1)
invocation_status=$?
set -e
if [[ $invocation_status -ne 2 || "$invocation_output" != *'Planning invocation failed:'* ]]; then
printf 'expected invalid-name invocation failure, got exit %s:\n%s\n' "$invocation_status" "$invocation_output" >&2
exit 1
fi
set +e
missing_change_output=$(bash "$verifier" "$prefix-missing" 2>&1)
missing_change_status=$?
set -e
if [[ $missing_change_status -ne 2 || "$missing_change_output" != *'Planning invocation failed:'* ]]; then
printf 'expected missing-change invocation failure, got exit %s:\n%s\n' "$missing_change_status" "$missing_change_output" >&2
exit 1
fi
set +e
environment_output=$(PATH=/usr/bin:/bin bash "$verifier" "$proposal_change" 2>&1)
environment_status=$?
set -e
if [[ $environment_status -ne 3 || "$environment_output" != *'Planning environment failed:'* ]]; then
printf 'expected environment failure, got exit %s:\n%s\n' "$environment_status" "$environment_output" >&2
exit 1
fi
make_change skip-conflict
skip_conflict=$fixture_change
rm -rf "$changes_root/$skip_conflict/specs"
rm -f "$changes_root/$skip_conflict/design.md" "$changes_root/$skip_conflict/tasks.md"
printf 'skip_specs: true\n' >> "$changes_root/$skip_conflict/.openspec.yaml"
expect_failure "$skip_conflict" 'skip_specs: true cannot be combined with declared capabilities'
make_change specs
specs_change=$fixture_change
rm -f "$changes_root/$specs_change/design.md" "$changes_root/$specs_change/tasks.md"
specs_output=$(run_verifier "$specs_change")
[[ "$specs_output" == *'Planning stage verified: specs'* ]]
[[ "$specs_output" == *'Full change validation pending: design, tasks'* ]]
make_change capability-mismatch
capability_mismatch=$fixture_change
mv "$changes_root/$capability_mismatch/specs/audit-remediation-workflow" "$changes_root/$capability_mismatch/specs/not-declared"
expect_failure "$capability_mismatch" 'delta capability is absent from Proposal: not-declared'
make_change design
design_change=$fixture_change
rm -f "$changes_root/$design_change/tasks.md"
design_output=$(run_verifier "$design_change")
[[ "$design_output" == *'Planning stage verified: design'* ]]
[[ "$design_output" == *'Full change validation pending: tasks'* ]]
make_change required-rollback
required_rollback=$fixture_change
rm -f "$changes_root/$required_rollback/tasks.md"
perl -0pi -e 's/\| Status \| `N\/A` \|/| Status | `required` |/; s/(\| Verification \|)/| Trigger | Deployment fails the local gate. |\n| Actions | Restore the previous local state. |\n| Data safety | No application data changes. |\n$1/' "$changes_root/$required_rollback/design.md"
required_output=$(run_verifier "$required_rollback")
[[ "$required_output" == *'Planning stage verified: design'* ]]
perl -0pi -e 's/^\| Data safety \|.*\n//m' "$changes_root/$required_rollback/design.md"
expect_failure "$required_rollback" 'Rollback Contract required status needs exactly one Data safety row'
make_change tasks
tasks_change=$fixture_change
tasks_output=$(run_verifier "$tasks_change")
[[ "$tasks_output" == *'Planning stage verified: tasks'* ]]
[[ "$tasks_output" == *'Delta requirement coverage verified: 9/9'* ]]
[[ "$tasks_output" == *'Full planning structure verified; semantic approval remains human.'* ]]
for artifact in proposal specs design tasks; do
instructions=$(cd "$workspace/specs" && openspec instructions "$artifact" --change "$tasks_change" --store wealth-ledger-specs --json)
node -e '
const result = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
if (!result.instruction.trimStart().startsWith("Critical Rules:")) process.exit(1);
' <<< "$instructions"
done
specs_instructions=$(cd "$workspace/specs" && openspec instructions specs --change "$tasks_change" --store wealth-ledger-specs --json)
node -e '
const result = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
const activeOperation = /^## (ADDED|MODIFIED|REMOVED|RENAMED) Requirements$/m;
if (activeOperation.test(result.template)) process.exit(1);
for (const token of ["Purpose", "ADDED", "MODIFIED", "REMOVED", "RENAMED"]) {
if (!result.template.includes(token)) process.exit(1);
}
' <<< "$specs_instructions"
operation_change="$prefix-operation-neutral"
cp -R "$script_dir/fixtures/operation-neutral-change" "$changes_root/$operation_change"
created_changes+=("$operation_change")
operation_output=$(run_verifier "$operation_change")
if [[ "$operation_output" != *'Planning stage verified: specs'* ]]; then
printf 'expected controlled operation fixture to verify, got:\n%s\n' "$operation_output" >&2
exit 1
fi
for operation in ADDED MODIFIED REMOVED RENAMED; do
if [[ $(grep -Fxc "## $operation Requirements" "$changes_root/$operation_change/specs/workspace-artifact-hygiene/spec.md") -ne 1 ]]; then
printf 'expected exactly one controlled %s operation section\n' "$operation" >&2
exit 1
fi
done
# A completed fixture receives OpenSpec's terminal archive message instead of
# the schema's Apply instruction, so leave one task pending for this contract.
perl -0pi -e 's/^- \[[xX]\]/- [ ]/m' "$changes_root/$tasks_change/tasks.md"
apply_instructions=$(cd "$workspace/specs" && openspec instructions apply --change "$tasks_change" --store wealth-ledger-specs --json)
node -e '
const result = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
for (const token of ["contextFiles", "AGENTS.md", "planned Verification", "Feedback routing", "Механическая правка", "Requirement wording"]) {
if (!result.instruction.includes(token)) process.exit(1);
}
' <<< "$apply_instructions"
design_instructions=$(cd "$workspace/specs" && openspec instructions design --change "$tasks_change" --store wealth-ledger-specs --json)
node -e '
const result = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
if (!result.instruction.includes("инвалидируй downstream approvals")) process.exit(1);
' <<< "$design_instructions"
make_change invalid-area
invalid_area=$fixture_change
perl -0pi -e 's/\[specs\]/[frontend]/' "$changes_root/$invalid_area/tasks.md"
expect_failure "$invalid_area" 'task 1.1 uses frontend, but Delivery Map marks it N/A'
make_change unknown-reference
unknown_reference=$fixture_change
perl -0pi -e 's/`Planning artifact проверяется до запроса approval`/`Несуществующее требование`/' "$changes_root/$unknown_reference/tasks.md"
expect_failure "$unknown_reference" 'unknown requirement reference'
make_change uncovered-requirement
uncovered_requirement=$fixture_change
sed -i '' '/`audit-remediation-workflow` — `Каждое audit claim получает доказательный disposition`/s/`audit-remediation-workflow` — `Каждое audit claim получает доказательный disposition`/`audit-remediation-workflow` — `Реестр remediation отделён от снимка аудита`/' "$changes_root/$uncovered_requirement/tasks.md"
expect_failure "$uncovered_requirement" 'uncovered Requirement: audit-remediation-workflow — Каждое audit claim получает доказательный disposition'
make_change missing-metadata
missing_metadata=$fixture_change
perl -0pi -e 's/^ - \*\*Verification:\*\*.*\n//m' "$changes_root/$missing_metadata/tasks.md"
expect_failure "$missing_metadata" 'task 1.1 must contain exactly one Verification line'
make_change unresolved-na-placeholder
unresolved_na_placeholder=$fixture_change
perl -0pi -e 's/^ - \*\*Covers:\*\*.*$/ - **Covers:** `N\/A` — <!-- unresolved reason -->/m' "$changes_root/$unresolved_na_placeholder/tasks.md"
expect_failure "$unresolved_na_placeholder" 'task 1.1 has unresolved N/A rationale placeholder'
make_change empty-na
empty_na=$fixture_change
perl -0pi -e 's/^ - \*\*Covers:\*\*.*$/ - **Covers:** `N\/A` — /m' "$changes_root/$empty_na/tasks.md"
expect_failure "$empty_na" 'task 1.1 has empty N/A rationale'
make_change separator-na
separator_na=$fixture_change
perl -0pi -e 's/^ - \*\*Covers:\*\*.*$/ - **Covers:** `N\/A` — —/m' "$changes_root/$separator_na/tasks.md"
expect_failure "$separator_na" 'task 1.1 has empty N/A rationale'
make_change malformed-rollback
malformed_rollback=$fixture_change
sed -i '' '/| Status | `N\/A` |/d' "$changes_root/$malformed_rollback/design.md"
expect_failure "$malformed_rollback" 'Rollback Contract must contain exactly one Status row'
printf 'verify-openspec-planning tests passed\n'
-29
View File
@@ -1,29 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
contract_file=${1:-"$script_dir/../openspec/.cli-version"}
if [[ ! -f "$contract_file" ]]; then
printf 'OpenSpec version contract is missing: %s\n' "$contract_file" >&2
exit 1
fi
expected_version=$(tr -d '[:space:]' < "$contract_file")
if [[ -z "$expected_version" ]]; then
printf 'OpenSpec version contract is empty: %s\n' "$contract_file" >&2
exit 1
fi
if ! command -v openspec >/dev/null 2>&1; then
printf 'OpenSpec CLI %s is required, but openspec is not available in PATH.\n' "$expected_version" >&2
exit 1
fi
actual_version=$(openspec --version 2>/dev/null | tr -d '\r\n')
if [[ "$actual_version" != "$expected_version" ]]; then
printf 'Unsupported OpenSpec CLI version: expected %s, actual %s.\n' "$expected_version" "${actual_version:-unknown}" >&2
exit 1
fi
printf 'OpenSpec CLI version verified: %s\n' "$actual_version"
-126
View File
@@ -1,126 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
workspace_dir="$(cd -- "$script_dir/../.." && pwd)"
backend_dir="$workspace_dir/backend"
snapshot_path="$workspace_dir/frontend/src/shared/api/spec/openapi.json"
if [[ ! -f "$snapshot_path" ]]; then
echo "Missing committed OpenAPI snapshot: $snapshot_path" >&2
exit 1
fi
node_major="$(node -p "process.versions.node.split('.')[0]")"
if [[ "$node_major" != "22" ]]; then
echo "Node 22 is required; found $(node --version)." >&2
exit 1
fi
temporary_dir="$(mktemp -d "${TMPDIR:-/tmp}/wealth-ledger-runtime-openapi.XXXXXX")"
backend_pid=""
cleanup() {
if [[ -n "$backend_pid" ]] && kill -0 "$backend_pid" 2>/dev/null; then
kill "$backend_pid" 2>/dev/null || true
wait "$backend_pid" 2>/dev/null || true
fi
rm -rf "$temporary_dir"
}
trap cleanup EXIT INT TERM
port="$(node -e "const server = require('node:net').createServer(); server.listen(0, '127.0.0.1', () => { console.log(server.address().port); server.close(); });")"
runtime_path="$temporary_dir/runtime.json"
canonical_runtime_path="$temporary_dir/runtime.canonical.json"
canonical_snapshot_path="$temporary_dir/snapshot.canonical.json"
backend_log="$temporary_dir/backend.log"
compile_log="$temporary_dir/compile.log"
if ! (
cd "$backend_dir"
./node_modules/.bin/tsc --project tsconfig.build.json --outDir "$temporary_dir/dist"
) >"$compile_log" 2>&1; then
echo "Temporary backend compilation failed:" >&2
sed -n '1,160p' "$compile_log" >&2
exit 1
fi
ln -s "$backend_dir/node_modules" "$temporary_dir/dist/node_modules"
(
cd "$backend_dir"
NODE_ENV=development \
PORT="$port" \
DATABASE_URL="file:$temporary_dir/runtime.db" \
FRONTEND_ORIGIN="http://localhost:5173" \
JWT_SECRET="runtime-openapi-check-secret-at-least-32" \
TOKEN_ENCRYPTION_KEY="MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=" \
TBANK_GRPC_ENDPOINT="127.0.0.1:1" \
TBANK_RATE_LIMIT_PER_SECOND=5 \
LOG_LEVEL=error \
node "$temporary_dir/dist/src/main.js"
) >"$backend_log" 2>&1 &
backend_pid="$!"
for _ in $(seq 1 60); do
if node -e "fetch(process.argv[1]).then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))" "http://127.0.0.1:$port/api/docs-json"; then
break
fi
if ! kill -0 "$backend_pid" 2>/dev/null; then
echo "Backend stopped before exposing /api/docs-json:" >&2
sed -n '1,160p' "$backend_log" >&2
exit 1
fi
sleep 0.25
done
if ! node -e 'fetch(process.argv[1]).then(async (response) => { if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); require("node:fs").writeFileSync(process.argv[2], await response.text()); }).catch((error) => { console.error(`OpenAPI fetch failed: ${error.message}`); process.exit(1); })' "http://127.0.0.1:$port/api/docs-json" "$runtime_path"; then
echo "Backend did not expose /api/docs-json within 15 seconds:" >&2
sed -n '1,160p' "$backend_log" >&2
exit 1
fi
if node - "$runtime_path" "$snapshot_path" "$canonical_runtime_path" "$canonical_snapshot_path" <<'NODE'
const { createHash } = require('node:crypto');
const { readFileSync, writeFileSync } = require('node:fs');
function canonicalize(value) {
if (Array.isArray(value)) return value.map(canonicalize);
if (value && typeof value === 'object') {
return Object.fromEntries(
Object.keys(value)
.sort()
.map((key) => [key, canonicalize(value[key])]),
);
}
return value;
}
const [runtimePath, snapshotPath, canonicalRuntimePath, canonicalSnapshotPath] =
process.argv.slice(2);
const documents = [runtimePath, snapshotPath].map((path) =>
canonicalize(JSON.parse(readFileSync(path, 'utf8'))),
);
const serialized = documents.map((document) => `${JSON.stringify(document, null, 2)}\n`);
writeFileSync(canonicalRuntimePath, serialized[0]);
writeFileSync(canonicalSnapshotPath, serialized[1]);
const hashes = serialized.map((value) =>
createHash('sha256').update(value).digest('hex'),
);
console.log(`runtime SHA-256: ${hashes[0]}`);
console.log(`snapshot SHA-256: ${hashes[1]}`);
process.exitCode = hashes[0] === hashes[1] ? 0 : 1;
NODE
then
comparison_status=0
else
comparison_status=$?
fi
if [[ "$comparison_status" -ne 0 ]]; then
echo "Runtime OpenAPI differs from the committed frontend snapshot:" >&2
diff -u "$canonical_snapshot_path" "$canonical_runtime_path" || true
exit 1
fi
echo "Runtime OpenAPI matches the committed frontend snapshot."
-74
View File
@@ -1,74 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 1 ]]; then
printf 'Usage: %s <change-name>\n' "${0##*/}" >&2
exit 2
fi
change=$1
if [[ ! "$change" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
printf 'Invalid OpenSpec change name: %s\n' "$change" >&2
exit 2
fi
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
workspace=$(cd "$script_dir/../.." && pwd -P)
change_dir="$workspace/specs/openspec/changes/$change"
metadata_file="$change_dir/.openspec.yaml"
design_file="$change_dir/design.md"
for required_file in "$metadata_file" "$design_file"; do
if [[ ! -f "$required_file" ]]; then
printf 'Missing canonical change input: %s\n' "${required_file#$workspace/}" >&2
exit 1
fi
done
# shellcheck source=lib/delivery-map.sh
source "$script_dir/lib/delivery-map.sh"
wl_parse_delivery_map "$metadata_file" "$design_file"
areas=("${wl_areas[@]}")
affected_flags=("${wl_affected_flags[@]}")
map_statuses=("${wl_map_statuses[@]}")
contract_status=$wl_contract_status
printf 'Delivery Map verified:'
for index in 0 1 2 3; do
printf ' %s=%s' "${areas[$index]}" "${map_statuses[$index]}"
done
printf '\n'
printf 'Contract Gate Decision verified: runtime-openapi=%s\n' "$contract_status"
run_gate() {
local label=$1
shift
printf '\n==> %s\n' "$label"
"$@"
}
run_gate 'OpenSpec CLI contract' bash "$workspace/specs/scripts/verify-openspec-version.sh"
run_gate 'OpenSpec planning structure' bash "$workspace/specs/scripts/verify-openspec-planning.sh" "$change"
run_gate 'OpenSpec project schema' bash -c 'cd "$1" && openspec schema validate wealth-ledger-sdd' _ "$workspace/specs"
run_gate 'OpenSpec change validation' bash -c 'cd "$1" && openspec validate "$2" --type change --strict --store wealth-ledger-specs' _ "$workspace/specs" "$change"
run_gate 'OpenSpec main specs' bash -c 'cd "$1" && openspec validate --specs --strict --store wealth-ledger-specs' _ "$workspace/specs"
run_gate 'OpenSpec authority' bash "$workspace/specs/scripts/verify-openspec-authority.sh"
if [[ "${affected_flags[0]}" -eq 1 ]]; then
run_gate 'Frontend canonical verification' npm --prefix "$workspace/frontend" run verify
fi
if [[ "${affected_flags[1]}" -eq 1 ]]; then
run_gate 'Backend canonical verification' npm --prefix "$workspace/backend" run verify
fi
if [[ "${affected_flags[3]}" -eq 1 ]]; then
run_gate 'Docs canonical verification' npm --prefix "$workspace/docs" run verify
fi
if [[ "$contract_status" == "required" ]]; then
run_gate 'Runtime OpenAPI contract' bash "$workspace/specs/scripts/verify-runtime-openapi.sh"
fi
printf '\nWorkspace verification passed for %s.\n' "$change"