feat: harden OpenSpec SDD workflow
This commit is contained in:
@@ -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
|
||||
}
|
||||
Executable
+80
@@ -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'
|
||||
Executable
+72
@@ -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"
|
||||
Executable
+88
@@ -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'
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+282
@@ -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
|
||||
Executable
+128
@@ -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'
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user