feat: harden OpenSpec SDD workflow

This commit is contained in:
2026-08-26 00:42:35 +03:00
parent 039b72eed3
commit a8b2c4eed9
42 changed files with 4024 additions and 181 deletions
+125
View File
@@ -0,0 +1,125 @@
#!/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
}
+80
View File
@@ -0,0 +1,80 @@
#!/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)}
source_root="$workspace/specs/agent-projection/opencode"
projection_root="$workspace/.opencode"
for tree in commands skills; do
if [[ ! -d "$source_root/$tree" ]]; then
printf 'Missing canonical projection tree: %s\n' "${source_root#$workspace/}/$tree" >&2
exit 1
fi
done
mkdir -p "$projection_root"
stage=$(mktemp -d "$projection_root/.projection-stage.XXXXXX")
backup=$(mktemp -d "$projection_root/.projection-backup.XXXXXX")
commands_existed=0
skills_existed=0
cutover_started=0
cleanup_temporary() {
rm -rf "$stage" "$backup"
}
rollback() {
if [[ $cutover_started -eq 1 ]]; then
rm -rf "$projection_root/commands" "$projection_root/skills"
if [[ $commands_existed -eq 1 && -d "$backup/commands" ]]; then
mv "$backup/commands" "$projection_root/commands"
fi
if [[ $skills_existed -eq 1 && -d "$backup/skills" ]]; then
mv "$backup/skills" "$projection_root/skills"
fi
fi
cleanup_temporary
}
trap rollback EXIT
cp -R "$source_root/commands" "$stage/commands"
cp -R "$source_root/skills" "$stage/skills"
if [[ -d "$projection_root/commands" ]]; then
commands_existed=1
cp -R "$projection_root/commands" "$backup/commands"
fi
if [[ "${WEALTH_LEDGER_SYNC_FAIL_DURING_BACKUP:-0}" == '1' ]]; then
printf 'Injected projection backup failure; local projection was not modified.\n' >&2
exit 1
fi
if [[ -d "$projection_root/skills" ]]; then
skills_existed=1
cp -R "$projection_root/skills" "$backup/skills"
fi
cutover_started=1
rm -rf "$projection_root/commands" "$projection_root/skills"
if ! mv "$stage/commands" "$projection_root/commands"; then
printf 'Projection sync failed while installing commands; previous state will be restored.\n' >&2
exit 1
fi
if [[ "${WEALTH_LEDGER_SYNC_FAIL_AFTER_COMMANDS:-0}" == '1' ]]; then
printf 'Injected projection sync failure after commands; previous state will be restored.\n' >&2
exit 1
fi
if ! mv "$stage/skills" "$projection_root/skills"; then
printf 'Projection sync failed while installing skills; previous state will be restored.\n' >&2
exit 1
fi
if ! WEALTH_LEDGER_WORKSPACE="$workspace" bash "$script_dir/verify-agent-projection.sh"; then
printf 'Projection verification failed after cutover; previous state will be restored.\n' >&2
exit 1
fi
cutover_started=0
trap - EXIT
cleanup_temporary
printf 'Agent projection synchronized from specs authority.\n'
+72
View File
@@ -0,0 +1,72 @@
#!/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)}
source_root="$workspace/specs/agent-projection/opencode"
projection_root="$workspace/.opencode"
version_file="$workspace/specs/openspec/.cli-version"
failures=0
fail() {
printf 'FAIL: %s\n' "$1" >&2
failures=$((failures + 1))
}
if [[ ! -f "$version_file" ]]; then
fail 'missing OpenSpec version contract: specs/openspec/.cli-version'
expected_version=''
else
expected_version=$(tr -d '[:space:]' < "$version_file")
[[ -n "$expected_version" ]] || fail 'OpenSpec version contract is empty'
fi
for tree in commands skills; do
source_tree="$source_root/$tree"
projection_tree="$projection_root/$tree"
if [[ ! -d "$source_tree" ]]; then
fail "missing canonical projection tree: specs/agent-projection/opencode/$tree"
continue
fi
if [[ ! -d "$projection_tree" ]]; then
fail "missing projection directory: .opencode/$tree"
continue
fi
while IFS= read -r source_file; do
relative_path=${source_file#"$source_root/"}
projection_file="$projection_root/$relative_path"
if [[ ! -f "$projection_file" ]]; then
fail "missing projection file: $relative_path"
elif ! cmp -s "$source_file" "$projection_file"; then
fail "projection content drift: $relative_path"
fi
if [[ "$relative_path" == skills/*/SKILL.md ]]; then
marker_count=$(awk -v expected="generatedBy: \"$expected_version\"" '
{
line = $0
sub(/^[[:space:]]*/, "", line)
if (line == expected) count++
}
END { print count + 0 }
' "$source_file")
if [[ "$marker_count" -ne 1 ]]; then
fail "generated version marker mismatch in $relative_path: expected $expected_version"
fi
fi
done < <(find "$source_tree" -type f | sort)
while IFS= read -r projection_file; do
relative_path=${projection_file#"$projection_root/"}
if [[ ! -f "$source_root/$relative_path" ]]; then
fail "unexpected projection file: $relative_path"
fi
done < <(find "$projection_tree" -type f | sort)
done
if [[ $failures -gt 0 ]]; then
exit 1
fi
printf 'Agent projection verified against versioned authority: OpenSpec %s\n' "$expected_version"
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
verifier="$script_dir/verify-agent-projection.sh"
syncer="$script_dir/sync-agent-projection.sh"
contract_version=$(tr -d '[:space:]' < "$script_dir/../openspec/.cli-version")
if [[ ! -x "$verifier" ]]; then
printf 'expected executable projection verifier at %s\n' "$verifier" >&2
exit 1
fi
if [[ ! -x "$syncer" ]]; then
printf 'expected executable projection sync at %s\n' "$syncer" >&2
exit 1
fi
fixture=$(mktemp -d)
trap 'rm -rf "$fixture"' EXIT
mkdir -p "$fixture/specs/openspec" \
"$fixture/specs/agent-projection/opencode/commands" \
"$fixture/specs/agent-projection/opencode/skills/example" \
"$fixture/.opencode"
printf '%s\n' "$contract_version" > "$fixture/specs/openspec/.cli-version"
printf 'command authority\n' > "$fixture/specs/agent-projection/opencode/commands/example.md"
printf '%s\n' '---' 'name: example' 'metadata:' " generatedBy: \"$contract_version\"" '---' > "$fixture/specs/agent-projection/opencode/skills/example/SKILL.md"
printf '{"private":true}\n' > "$fixture/.opencode/package.json"
WEALTH_LEDGER_WORKSPACE="$fixture" bash "$syncer"
WEALTH_LEDGER_WORKSPACE="$fixture" bash "$verifier"
[[ -f "$fixture/.opencode/package.json" ]]
printf 'manual drift\n' >> "$fixture/.opencode/commands/example.md"
if output=$(WEALTH_LEDGER_WORKSPACE="$fixture" bash "$verifier" 2>&1); then
printf 'expected content drift to fail projection verification\n' >&2
exit 1
fi
[[ "$output" == *'projection content drift: commands/example.md'* ]]
WEALTH_LEDGER_WORKSPACE="$fixture" bash "$syncer"
rm "$fixture/.opencode/skills/example/SKILL.md"
if output=$(WEALTH_LEDGER_WORKSPACE="$fixture" bash "$verifier" 2>&1); then
printf 'expected missing projection file to fail verification\n' >&2
exit 1
fi
[[ "$output" == *'missing projection file: skills/example/SKILL.md'* ]]
WEALTH_LEDGER_WORKSPACE="$fixture" bash "$syncer"
printf 'unexpected\n' > "$fixture/.opencode/commands/unexpected.md"
if output=$(WEALTH_LEDGER_WORKSPACE="$fixture" bash "$verifier" 2>&1); then
printf 'expected extra projection file to fail verification\n' >&2
exit 1
fi
[[ "$output" == *'unexpected projection file: commands/unexpected.md'* ]]
WEALTH_LEDGER_WORKSPACE="$fixture" bash "$syncer"
perl -0pi -e 's/generatedBy: "1\.10\.0"/generatedBy: "9.9.9"/' "$fixture/specs/agent-projection/opencode/skills/example/SKILL.md"
if output=$(WEALTH_LEDGER_WORKSPACE="$fixture" bash "$verifier" 2>&1); then
printf 'expected incompatible canonical marker to fail verification\n' >&2
exit 1
fi
[[ "$output" == *"generated version marker mismatch in skills/example/SKILL.md: expected $contract_version"* ]]
perl -0pi -e "s/generatedBy: \"9\\.9\\.9\"/generatedBy: \"$contract_version\"/" "$fixture/specs/agent-projection/opencode/skills/example/SKILL.md"
printf 'old command\n' > "$fixture/.opencode/commands/example.md"
printf '%s\n' '---' 'name: old' 'metadata:' " generatedBy: \"$contract_version\"" '---' > "$fixture/.opencode/skills/example/SKILL.md"
cp -R "$fixture/.opencode/commands" "$fixture/expected-commands"
cp -R "$fixture/.opencode/skills" "$fixture/expected-skills"
if WEALTH_LEDGER_WORKSPACE="$fixture" WEALTH_LEDGER_SYNC_FAIL_DURING_BACKUP=1 bash "$syncer"; then
printf 'expected injected backup failure\n' >&2
exit 1
fi
diff -r "$fixture/expected-commands" "$fixture/.opencode/commands"
diff -r "$fixture/expected-skills" "$fixture/.opencode/skills"
if WEALTH_LEDGER_WORKSPACE="$fixture" WEALTH_LEDGER_SYNC_FAIL_AFTER_COMMANDS=1 bash "$syncer"; then
printf 'expected injected sync failure\n' >&2
exit 1
fi
diff -r "$fixture/expected-commands" "$fixture/.opencode/commands"
diff -r "$fixture/expected-skills" "$fixture/.opencode/skills"
rm -rf "$fixture/.opencode/commands" "$fixture/.opencode/skills"
WEALTH_LEDGER_WORKSPACE="$fixture" bash "$syncer"
WEALTH_LEDGER_WORKSPACE="$fixture" bash "$verifier"
printf 'verify-agent-projection tests passed\n'
+25 -1
View File
@@ -6,6 +6,7 @@ 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
@@ -57,11 +58,16 @@ 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: 2'
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 ! projection_validation=$(WEALTH_LEDGER_WORKSPACE="$workspace" bash "$script_dir/verify-agent-projection.sh" 2>&1); then
fail "agent projection verification failed: $projection_validation"
fi
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"
@@ -140,7 +146,7 @@ skill_entrypoints=(
.opencode/skills/openspec-verify-change/SKILL.md
)
for relative_path in "${skill_entrypoints[@]}"; do
require_file_contains "$workspace/$relative_path" 'generatedBy: "1.10.0"'
require_file_contains "$workspace/$relative_path" "generatedBy: \"$expected_version\""
done
one_artifact_entrypoints=(
@@ -160,6 +166,24 @@ for relative_path in "${one_artifact_entrypoints[@]}"; do
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
+48 -6
View File
@@ -3,6 +3,7 @@ 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
@@ -21,7 +22,7 @@ write_file() {
write_file "specs/.openspec-store/store.yaml" $'version: 1\nid: wealth-ledger-specs'
mkdir -p "$fixture/specs/openspec"
write_file "specs/openspec/.cli-version" '1.10.0'
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"
@@ -48,11 +49,12 @@ entrypoints=(
.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: "1.10.0"\n'"$content"
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.'
@@ -60,6 +62,9 @@ for path in "${entrypoints[@]}"; do
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
@@ -68,18 +73,25 @@ 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: "1.10.0"\n'"$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: "1.10.0"\n'"$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"
mkdir -p "$fixture/specs/agent-projection/opencode"
cp -R "$fixture/.opencode/commands" "$fixture/specs/agent-projection/opencode/commands"
cp -R "$fixture/.opencode/skills" "$fixture/specs/agent-projection/opencode/skills"
run_verifier() {
WEALTH_LEDGER_WORKSPACE="$fixture" bash "$verifier"
@@ -87,12 +99,34 @@ run_verifier() {
run_verifier
rm "$fixture/specs/agent-projection/opencode/commands/opsx-apply.md"
if run_verifier; then
echo "expected missing canonical projection source to fail" >&2
exit 1
fi
cp "$fixture/.opencode/commands/opsx-apply.md" "$fixture/specs/agent-projection/opencode/commands/opsx-apply.md"
for path in \
.opencode/commands/opsx-propose.md \
specs/agent-projection/opencode/commands/opsx-propose.md; do
perl -0pi -e 's/^bash specs\/scripts\/verify-openspec-planning\.sh .*\n//m' "$fixture/$path"
done
if run_verifier; then
echo "expected missing planning gate in an artifact entrypoint to fail" >&2
exit 1
fi
for path in \
.opencode/commands/opsx-propose.md \
specs/agent-projection/opencode/commands/opsx-propose.md; do
printf '%s\n' "$planning_line" >> "$fixture/$path"
done
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" '1.10.0'
write_file "specs/openspec/.cli-version" "$contract_version"
write_file "specs/openspec/config.yaml" 'schema: spec-driven'
if run_verifier; then
@@ -101,6 +135,14 @@ if run_verifier; then
fi
write_file "specs/openspec/config.yaml" 'schema: wealth-ledger-sdd'
perl -0pi -e 's/^version: 2$/version: 1/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
@@ -149,7 +191,7 @@ 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.'
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
+282
View File
@@ -0,0 +1,282 @@
#!/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=${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() {
printf 'Planning verification failed: %s\n' "$1" >&2
exit 1
}
require_file() {
[[ -f "$1" ]] || fail "missing ${1#$workspace/}"
}
require_heading() {
grep -Fqx -- "$2" "$1" || fail "${1#$workspace/} must contain heading: $2"
}
bash "$script_dir/verify-openspec-version.sh" "$openspec_root/.cli-version" >/dev/null
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 ! (cd "$specs_root" && openspec status --change "$change" --json --store wealth-ledger-specs >/dev/null); then
fail "change $change is not resolvable through canonical store wealth-ledger-specs"
fi
for heading in '## Why' '## What Changes' '## Capabilities' '### New Capabilities' '### Modified Capabilities' '## Impact'; do
require_heading "$proposal_file" "$heading"
done
temporary=$(mktemp -d)
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
wl_parse_delivery_map "$metadata_file" "$design_file" || exit 1
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"
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
+128
View File
@@ -0,0 +1,128 @@
#!/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"
source_change="$workspace/specs/openspec/changes/harden-openspec-sdd-quality"
changes_root="$workspace/specs/openspec/changes"
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 output
if output=$(run_verifier "$change" 2>&1); then
printf 'expected planning verification to fail for %s\n' "$change" >&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'* ]]
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 projection gate. |\n| Actions | Restore the previous projection. |\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.'* ]]
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 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'
+8 -127
View File
@@ -25,134 +25,14 @@ for required_file in "$metadata_file" "$design_file"; do
fi
done
areas=(frontend backend specs docs)
affected_flags=(0 0 0 0)
map_statuses=('' '' '' '')
map_reasons=('' '' '' '')
contract_status=''
contract_reason=''
# shellcheck source=lib/delivery-map.sh
source "$script_dir/lib/delivery-map.sh"
wl_parse_delivery_map "$metadata_file" "$design_file"
area_index() {
case "$1" in
frontend) printf '0' ;;
backend) printf '1' ;;
specs) printf '2' ;;
docs) printf '3' ;;
*) return 1 ;;
esac
}
while IFS= read -r area; do
[[ -n "$area" ]] || continue
if ! index=$(area_index "$area"); then
printf 'Unknown affected area in %s: %s\n' "${metadata_file#$workspace/}" "$area" >&2
exit 1
fi
if [[ "${affected_flags[$index]}" -eq 1 ]]; then
printf 'Duplicate affected area in %s: %s\n' "${metadata_file#$workspace/}" "$area" >&2
exit 1
fi
affected_flags[$index]=1
done < <(
awk '
$0 == "affected_areas:" { in_areas = 1; next }
in_areas && /^ - / { sub(/^ - /, ""); print; next }
in_areas { exit }
' "$metadata_file"
)
if [[ "${affected_flags[2]}" -ne 1 ]]; then
printf 'Delivery Map violation: specs must be affected for every versioned change.\n' >&2
exit 1
fi
trim() {
local value=$1
value=${value#"${value%%[![:space:]]*}"}
value=${value%"${value##*[![:space:]]}"}
printf '%s' "$value"
}
while IFS='|' read -r _ raw_area raw_status raw_reason _; do
area=$(trim "$raw_area")
case "$area" in
frontend|backend|specs|docs) ;;
*) continue ;;
esac
index=$(area_index "$area")
if [[ -n "${map_statuses[$index]}" ]]; then
printf 'Delivery Map contains duplicate row for %s.\n' "$area" >&2
exit 1
fi
status=$(trim "$raw_status")
status=${status//\`/}
reason=$(trim "$raw_reason")
map_statuses[$index]=$status
map_reasons[$index]=$reason
done < "$design_file"
while IFS='|' read -r _ raw_gate raw_status raw_reason _; do
gate=$(trim "$raw_gate")
[[ "$gate" == "runtime-openapi" ]] || continue
if [[ -n "$contract_status" ]]; then
printf 'Contract Gate Decision contains duplicate runtime-openapi row.\n' >&2
exit 1
fi
contract_status=$(trim "$raw_status")
contract_status=${contract_status//\`/}
contract_reason=$(trim "$raw_reason")
done < "$design_file"
for index in 0 1 2 3; do
area=${areas[$index]}
status=${map_statuses[$index]}
reason=${map_reasons[$index]}
if [[ -z "$status" ]]; then
printf 'Delivery Map is missing area: %s\n' "$area" >&2
exit 1
fi
if [[ "${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
exit 1
fi
if [[ -z "$reason" || "$reason" == "-" || "$reason" == "—" || "$reason" == *"<!--"* ]]; then
printf 'Delivery Map requires an architectural outcome for affected area %s.\n' "$area" >&2
exit 1
fi
else
if [[ "$status" != "N/A" ]]; then
printf 'Delivery Map mismatch: %s is absent from affected_areas but has status %s.\n' "$area" "$status" >&2
exit 1
fi
if [[ -z "$reason" || "$reason" == "-" || "$reason" == "—" || "$reason" == *"<!--"* ]]; then
printf 'Delivery Map requires a concrete N/A reason for %s.\n' "$area" >&2
exit 1
fi
fi
done
if [[ -z "$contract_status" ]]; then
printf 'Contract Gate Decision is missing runtime-openapi.\n' >&2
exit 1
fi
if [[ "$contract_status" != "required" && "$contract_status" != "N/A" ]]; then
printf 'Invalid runtime-openapi decision: %s. Expected required or N/A.\n' "$contract_status" >&2
exit 1
fi
if [[ -z "$contract_reason" || "$contract_reason" == "-" || "$contract_reason" == "—" || "$contract_reason" == *"<!--"* ]]; then
printf 'Contract Gate Decision requires a concrete runtime-openapi reason.\n' >&2
exit 1
fi
if [[ "$contract_status" == "required" ]]; then
if [[ "${affected_flags[0]}" -ne 1 || "${affected_flags[1]}" -ne 1 ]]; then
printf 'runtime-openapi cannot be required unless frontend and backend are affected.\n' >&2
exit 1
fi
fi
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
@@ -169,6 +49,7 @@ run_gate() {
}
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"