#!/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##*/} " 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 "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