shipwright-cli 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/code-reviewer.md +2 -0
- package/.claude/agents/devops-engineer.md +2 -0
- package/.claude/agents/doc-fleet-agent.md +2 -0
- package/.claude/agents/pipeline-agent.md +2 -0
- package/.claude/agents/shell-script-specialist.md +2 -0
- package/.claude/agents/test-specialist.md +2 -0
- package/.claude/hooks/agent-crash-capture.sh +32 -0
- package/.claude/hooks/post-tool-use.sh +3 -2
- package/.claude/hooks/pre-tool-use.sh +35 -3
- package/README.md +22 -8
- package/claude-code/hooks/config-change.sh +18 -0
- package/claude-code/hooks/instructions-reloaded.sh +7 -0
- package/claude-code/hooks/worktree-create.sh +25 -0
- package/claude-code/hooks/worktree-remove.sh +20 -0
- package/config/code-constitution.json +130 -0
- package/config/defaults.json +25 -2
- package/config/policy.json +1 -1
- package/dashboard/middleware/auth.ts +134 -0
- package/dashboard/middleware/constants.ts +21 -0
- package/dashboard/public/index.html +8 -6
- package/dashboard/public/styles.css +176 -97
- package/dashboard/routes/auth.ts +38 -0
- package/dashboard/server.ts +117 -25
- package/dashboard/services/config.ts +26 -0
- package/dashboard/services/db.ts +118 -0
- package/dashboard/src/canvas/pixel-agent.ts +298 -0
- package/dashboard/src/canvas/pixel-sprites.ts +440 -0
- package/dashboard/src/canvas/shipyard-effects.ts +367 -0
- package/dashboard/src/canvas/shipyard-scene.ts +616 -0
- package/dashboard/src/canvas/submarine-layout.ts +267 -0
- package/dashboard/src/components/header.ts +8 -7
- package/dashboard/src/core/api.ts +5 -0
- package/dashboard/src/core/router.ts +1 -0
- package/dashboard/src/design/submarine-theme.ts +253 -0
- package/dashboard/src/main.ts +2 -0
- package/dashboard/src/types/api.ts +12 -1
- package/dashboard/src/views/activity.ts +2 -1
- package/dashboard/src/views/metrics.ts +69 -1
- package/dashboard/src/views/shipyard.ts +39 -0
- package/dashboard/types/index.ts +166 -0
- package/docs/plans/2026-02-28-compound-audit-and-shipyard-design.md +186 -0
- package/docs/plans/2026-02-28-skipper-shipwright-implementation-plan.md +1182 -0
- package/docs/plans/2026-02-28-skipper-shipwright-integration-design.md +531 -0
- package/docs/plans/2026-03-01-ai-powered-skill-injection-design.md +298 -0
- package/docs/plans/2026-03-01-ai-powered-skill-injection-plan.md +1109 -0
- package/docs/plans/2026-03-01-capabilities-cleanup-plan.md +658 -0
- package/docs/plans/2026-03-01-clean-architecture-plan.md +924 -0
- package/docs/plans/2026-03-01-compound-audit-cascade-design.md +191 -0
- package/docs/plans/2026-03-01-compound-audit-cascade-plan.md +921 -0
- package/docs/plans/2026-03-01-deep-integration-plan.md +851 -0
- package/docs/plans/2026-03-01-pipeline-audit-trail-design.md +145 -0
- package/docs/plans/2026-03-01-pipeline-audit-trail-plan.md +770 -0
- package/docs/plans/2026-03-01-refined-depths-brand-design.md +382 -0
- package/docs/plans/2026-03-01-refined-depths-implementation.md +599 -0
- package/docs/plans/2026-03-01-skipper-kernel-integration-design.md +203 -0
- package/docs/plans/2026-03-01-unified-platform-design.md +272 -0
- package/docs/plans/2026-03-07-claude-code-feature-integration-design.md +189 -0
- package/docs/plans/2026-03-07-claude-code-feature-integration-plan.md +1165 -0
- package/docs/research/BACKLOG_QUICK_REFERENCE.md +352 -0
- package/docs/research/CUTTING_EDGE_RESEARCH_2026.md +546 -0
- package/docs/research/RESEARCH_INDEX.md +439 -0
- package/docs/research/RESEARCH_SOURCES.md +440 -0
- package/docs/research/RESEARCH_SUMMARY.txt +275 -0
- package/docs/superpowers/specs/2026-03-10-pipeline-quality-revolution-design.md +341 -0
- package/package.json +2 -2
- package/scripts/lib/adaptive-model.sh +427 -0
- package/scripts/lib/adaptive-timeout.sh +316 -0
- package/scripts/lib/audit-trail.sh +309 -0
- package/scripts/lib/auto-recovery.sh +471 -0
- package/scripts/lib/bandit-selector.sh +431 -0
- package/scripts/lib/bootstrap.sh +104 -2
- package/scripts/lib/causal-graph.sh +455 -0
- package/scripts/lib/compat.sh +126 -0
- package/scripts/lib/compound-audit.sh +337 -0
- package/scripts/lib/constitutional.sh +454 -0
- package/scripts/lib/context-budget.sh +359 -0
- package/scripts/lib/convergence.sh +594 -0
- package/scripts/lib/cost-optimizer.sh +634 -0
- package/scripts/lib/daemon-adaptive.sh +14 -2
- package/scripts/lib/daemon-dispatch.sh +106 -17
- package/scripts/lib/daemon-failure.sh +34 -4
- package/scripts/lib/daemon-patrol.sh +25 -4
- package/scripts/lib/daemon-poll-github.sh +361 -0
- package/scripts/lib/daemon-poll-health.sh +299 -0
- package/scripts/lib/daemon-poll.sh +27 -611
- package/scripts/lib/daemon-state.sh +119 -66
- package/scripts/lib/daemon-triage.sh +10 -0
- package/scripts/lib/dod-scorecard.sh +442 -0
- package/scripts/lib/error-actionability.sh +300 -0
- package/scripts/lib/formal-spec.sh +461 -0
- package/scripts/lib/helpers.sh +180 -5
- package/scripts/lib/intent-analysis.sh +409 -0
- package/scripts/lib/loop-convergence.sh +350 -0
- package/scripts/lib/loop-iteration.sh +682 -0
- package/scripts/lib/loop-progress.sh +48 -0
- package/scripts/lib/loop-restart.sh +185 -0
- package/scripts/lib/memory-effectiveness.sh +506 -0
- package/scripts/lib/mutation-executor.sh +352 -0
- package/scripts/lib/outcome-feedback.sh +521 -0
- package/scripts/lib/pipeline-cli.sh +336 -0
- package/scripts/lib/pipeline-commands.sh +1216 -0
- package/scripts/lib/pipeline-detection.sh +101 -3
- package/scripts/lib/pipeline-execution.sh +897 -0
- package/scripts/lib/pipeline-github.sh +28 -3
- package/scripts/lib/pipeline-intelligence-compound.sh +431 -0
- package/scripts/lib/pipeline-intelligence-scoring.sh +407 -0
- package/scripts/lib/pipeline-intelligence-skip.sh +181 -0
- package/scripts/lib/pipeline-intelligence.sh +104 -1138
- package/scripts/lib/pipeline-quality-bash-compat.sh +182 -0
- package/scripts/lib/pipeline-quality-checks.sh +17 -711
- package/scripts/lib/pipeline-quality-gates.sh +563 -0
- package/scripts/lib/pipeline-stages-build.sh +730 -0
- package/scripts/lib/pipeline-stages-delivery.sh +965 -0
- package/scripts/lib/pipeline-stages-intake.sh +1133 -0
- package/scripts/lib/pipeline-stages-monitor.sh +407 -0
- package/scripts/lib/pipeline-stages-review.sh +1022 -0
- package/scripts/lib/pipeline-stages.sh +161 -2901
- package/scripts/lib/pipeline-state.sh +36 -5
- package/scripts/lib/pipeline-util.sh +487 -0
- package/scripts/lib/policy-learner.sh +438 -0
- package/scripts/lib/process-reward.sh +493 -0
- package/scripts/lib/project-detect.sh +649 -0
- package/scripts/lib/quality-profile.sh +334 -0
- package/scripts/lib/recruit-commands.sh +885 -0
- package/scripts/lib/recruit-learning.sh +739 -0
- package/scripts/lib/recruit-roles.sh +648 -0
- package/scripts/lib/reward-aggregator.sh +458 -0
- package/scripts/lib/rl-optimizer.sh +362 -0
- package/scripts/lib/root-cause.sh +427 -0
- package/scripts/lib/scope-enforcement.sh +445 -0
- package/scripts/lib/session-restart.sh +493 -0
- package/scripts/lib/skill-memory.sh +300 -0
- package/scripts/lib/skill-registry.sh +775 -0
- package/scripts/lib/spec-driven.sh +476 -0
- package/scripts/lib/test-helpers.sh +18 -7
- package/scripts/lib/test-holdout.sh +429 -0
- package/scripts/lib/test-optimizer.sh +511 -0
- package/scripts/shipwright-file-suggest.sh +45 -0
- package/scripts/skills/adversarial-quality.md +61 -0
- package/scripts/skills/api-design.md +44 -0
- package/scripts/skills/architecture-design.md +50 -0
- package/scripts/skills/brainstorming.md +43 -0
- package/scripts/skills/data-pipeline.md +44 -0
- package/scripts/skills/deploy-safety.md +64 -0
- package/scripts/skills/documentation.md +38 -0
- package/scripts/skills/frontend-design.md +45 -0
- package/scripts/skills/generated/.gitkeep +0 -0
- package/scripts/skills/generated/_refinements/.gitkeep +0 -0
- package/scripts/skills/generated/_refinements/adversarial-quality.patch.md +3 -0
- package/scripts/skills/generated/_refinements/architecture-design.patch.md +3 -0
- package/scripts/skills/generated/_refinements/brainstorming.patch.md +3 -0
- package/scripts/skills/generated/cli-version-management.md +29 -0
- package/scripts/skills/generated/collection-system-validation.md +99 -0
- package/scripts/skills/generated/large-scale-c-refactoring-coordination.md +97 -0
- package/scripts/skills/generated/pattern-matching-similarity-scoring.md +195 -0
- package/scripts/skills/generated/test-parallelization-detection.md +65 -0
- package/scripts/skills/observability.md +79 -0
- package/scripts/skills/performance.md +48 -0
- package/scripts/skills/pr-quality.md +49 -0
- package/scripts/skills/product-thinking.md +43 -0
- package/scripts/skills/security-audit.md +49 -0
- package/scripts/skills/systematic-debugging.md +40 -0
- package/scripts/skills/testing-strategy.md +47 -0
- package/scripts/skills/two-stage-review.md +52 -0
- package/scripts/skills/validation-thoroughness.md +55 -0
- package/scripts/sw +9 -3
- package/scripts/sw-activity.sh +9 -8
- package/scripts/sw-adaptive.sh +8 -7
- package/scripts/sw-adversarial.sh +2 -1
- package/scripts/sw-architecture-enforcer.sh +3 -1
- package/scripts/sw-auth.sh +12 -2
- package/scripts/sw-autonomous.sh +5 -1
- package/scripts/sw-changelog.sh +4 -1
- package/scripts/sw-checkpoint.sh +2 -1
- package/scripts/sw-ci.sh +15 -6
- package/scripts/sw-cleanup.sh +4 -26
- package/scripts/sw-code-review.sh +45 -20
- package/scripts/sw-connect.sh +2 -1
- package/scripts/sw-context.sh +2 -1
- package/scripts/sw-cost.sh +107 -5
- package/scripts/sw-daemon.sh +71 -11
- package/scripts/sw-dashboard.sh +3 -1
- package/scripts/sw-db.sh +71 -20
- package/scripts/sw-decide.sh +8 -2
- package/scripts/sw-decompose.sh +360 -17
- package/scripts/sw-deps.sh +4 -1
- package/scripts/sw-developer-simulation.sh +4 -1
- package/scripts/sw-discovery.sh +378 -5
- package/scripts/sw-doc-fleet.sh +4 -1
- package/scripts/sw-docs-agent.sh +3 -1
- package/scripts/sw-docs.sh +2 -1
- package/scripts/sw-doctor.sh +453 -2
- package/scripts/sw-dora.sh +4 -1
- package/scripts/sw-durable.sh +12 -7
- package/scripts/sw-e2e-orchestrator.sh +17 -16
- package/scripts/sw-eventbus.sh +13 -4
- package/scripts/sw-evidence.sh +364 -12
- package/scripts/sw-feedback.sh +550 -9
- package/scripts/sw-fix.sh +20 -1
- package/scripts/sw-fleet-discover.sh +6 -2
- package/scripts/sw-fleet-viz.sh +9 -4
- package/scripts/sw-fleet.sh +5 -1
- package/scripts/sw-github-app.sh +18 -4
- package/scripts/sw-github-checks.sh +3 -2
- package/scripts/sw-github-deploy.sh +3 -2
- package/scripts/sw-github-graphql.sh +18 -7
- package/scripts/sw-guild.sh +5 -1
- package/scripts/sw-heartbeat.sh +5 -30
- package/scripts/sw-hello.sh +67 -0
- package/scripts/sw-hygiene.sh +10 -3
- package/scripts/sw-incident.sh +273 -5
- package/scripts/sw-init.sh +18 -2
- package/scripts/sw-instrument.sh +10 -2
- package/scripts/sw-intelligence.sh +44 -7
- package/scripts/sw-jira.sh +5 -1
- package/scripts/sw-launchd.sh +2 -1
- package/scripts/sw-linear.sh +4 -1
- package/scripts/sw-logs.sh +4 -1
- package/scripts/sw-loop.sh +436 -1076
- package/scripts/sw-memory.sh +357 -3
- package/scripts/sw-mission-control.sh +6 -1
- package/scripts/sw-model-router.sh +483 -27
- package/scripts/sw-otel.sh +15 -4
- package/scripts/sw-oversight.sh +14 -5
- package/scripts/sw-patrol-meta.sh +334 -0
- package/scripts/sw-pipeline-composer.sh +7 -1
- package/scripts/sw-pipeline-vitals.sh +12 -6
- package/scripts/sw-pipeline.sh +54 -2653
- package/scripts/sw-pm.sh +16 -8
- package/scripts/sw-pr-lifecycle.sh +2 -1
- package/scripts/sw-predictive.sh +17 -5
- package/scripts/sw-prep.sh +185 -2
- package/scripts/sw-ps.sh +5 -25
- package/scripts/sw-public-dashboard.sh +17 -4
- package/scripts/sw-quality.sh +14 -6
- package/scripts/sw-reaper.sh +8 -25
- package/scripts/sw-recruit.sh +156 -2303
- package/scripts/sw-regression.sh +19 -12
- package/scripts/sw-release-manager.sh +3 -1
- package/scripts/sw-release.sh +4 -1
- package/scripts/sw-remote.sh +3 -1
- package/scripts/sw-replay.sh +7 -1
- package/scripts/sw-retro.sh +158 -1
- package/scripts/sw-review-rerun.sh +3 -1
- package/scripts/sw-scale.sh +14 -5
- package/scripts/sw-security-audit.sh +6 -1
- package/scripts/sw-self-optimize.sh +173 -6
- package/scripts/sw-session.sh +9 -3
- package/scripts/sw-setup.sh +3 -1
- package/scripts/sw-stall-detector.sh +406 -0
- package/scripts/sw-standup.sh +15 -7
- package/scripts/sw-status.sh +3 -1
- package/scripts/sw-strategic.sh +14 -6
- package/scripts/sw-stream.sh +13 -4
- package/scripts/sw-swarm.sh +20 -7
- package/scripts/sw-team-stages.sh +13 -6
- package/scripts/sw-templates.sh +7 -31
- package/scripts/sw-testgen.sh +17 -6
- package/scripts/sw-tmux-pipeline.sh +4 -1
- package/scripts/sw-tmux-role-color.sh +2 -0
- package/scripts/sw-tmux-status.sh +1 -1
- package/scripts/sw-tmux.sh +37 -1
- package/scripts/sw-trace.sh +3 -1
- package/scripts/sw-tracker-github.sh +3 -0
- package/scripts/sw-tracker-jira.sh +3 -0
- package/scripts/sw-tracker-linear.sh +3 -0
- package/scripts/sw-tracker.sh +3 -1
- package/scripts/sw-triage.sh +3 -2
- package/scripts/sw-upgrade.sh +3 -1
- package/scripts/sw-ux.sh +5 -2
- package/scripts/sw-webhook.sh +5 -2
- package/scripts/sw-widgets.sh +9 -4
- package/scripts/sw-worktree.sh +15 -3
- package/scripts/test-skill-injection.sh +1233 -0
- package/templates/pipelines/autonomous.json +27 -3
- package/templates/pipelines/cost-aware.json +34 -8
- package/templates/pipelines/deployed.json +12 -0
- package/templates/pipelines/enterprise.json +12 -0
- package/templates/pipelines/fast.json +6 -0
- package/templates/pipelines/full.json +27 -3
- package/templates/pipelines/hotfix.json +6 -0
- package/templates/pipelines/standard.json +12 -0
- package/templates/pipelines/tdd.json +12 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Module guard - prevent double-sourcing
|
|
3
|
+
[[ -n "${_SPEC_DRIVEN_LOADED:-}" ]] && return 0
|
|
4
|
+
_SPEC_DRIVEN_LOADED=1
|
|
5
|
+
|
|
6
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
7
|
+
# ║ shipwright spec-driven — Specification-Driven Development System ║
|
|
8
|
+
# ║ Issue text → structured JSON spec → agent builds from spec ║
|
|
9
|
+
# ║ Diff spec vs implementation at review stage for misalignment detection ║
|
|
10
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
11
|
+
|
|
12
|
+
# shellcheck disable=SC2034
|
|
13
|
+
VERSION="3.3.0"
|
|
14
|
+
|
|
15
|
+
# ─── Output Helpers ──────────────────────────────────────────────────────────
|
|
16
|
+
[[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
|
|
17
|
+
[[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
|
|
18
|
+
[[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
|
|
19
|
+
[[ "$(type -t error 2>/dev/null)" == "function" ]] || error() { echo -e "\033[38;2;248;113;113m\033[1m✗\033[0m $*" >&2; }
|
|
20
|
+
if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
|
|
21
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# ─── Configuration ───────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
SPEC_DIR="${SPEC_DIR:-.claude/specs}"
|
|
27
|
+
SPEC_SCHEMA="${SPEC_SCHEMA:-}" # Path to specification.json schema (auto-detected)
|
|
28
|
+
|
|
29
|
+
# ─── Schema Location ────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
_find_spec_schema() {
|
|
32
|
+
if [[ -n "$SPEC_SCHEMA" && -f "$SPEC_SCHEMA" ]]; then
|
|
33
|
+
echo "$SPEC_SCHEMA"
|
|
34
|
+
return 0
|
|
35
|
+
fi
|
|
36
|
+
# Check relative to this script (Shipwright repo)
|
|
37
|
+
local script_dir
|
|
38
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
39
|
+
local repo_dir
|
|
40
|
+
repo_dir="$(cd "$script_dir/../.." && pwd)"
|
|
41
|
+
local candidates=(
|
|
42
|
+
"${repo_dir}/schemas/specification.json"
|
|
43
|
+
"./schemas/specification.json"
|
|
44
|
+
".claude/schemas/specification.json"
|
|
45
|
+
)
|
|
46
|
+
local c
|
|
47
|
+
for c in "${candidates[@]}"; do
|
|
48
|
+
if [[ -f "$c" ]]; then
|
|
49
|
+
echo "$c"
|
|
50
|
+
return 0
|
|
51
|
+
fi
|
|
52
|
+
done
|
|
53
|
+
return 1
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# ─── Spec Generation ────────────────────────────────────────────────────────
|
|
57
|
+
# Generate a structured spec from issue text or goal description.
|
|
58
|
+
# This produces a template that Claude (or the pipeline) fills in.
|
|
59
|
+
|
|
60
|
+
spec_generate() {
|
|
61
|
+
local title="${1:-}"
|
|
62
|
+
local body="${2:-}"
|
|
63
|
+
local issue_number="${3:-}"
|
|
64
|
+
local output_file="${4:-}"
|
|
65
|
+
local language="${5:-}"
|
|
66
|
+
|
|
67
|
+
if [[ -z "$title" ]]; then
|
|
68
|
+
error "spec_generate requires a title"
|
|
69
|
+
return 1
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
mkdir -p "$SPEC_DIR"
|
|
73
|
+
|
|
74
|
+
# Generate spec filename from title if not provided
|
|
75
|
+
if [[ -z "$output_file" ]]; then
|
|
76
|
+
local slug
|
|
77
|
+
slug=$(echo "$title" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd 'a-z0-9-' | head -c 60)
|
|
78
|
+
output_file="${SPEC_DIR}/${slug}.json"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# Estimate complexity from body length and keywords
|
|
82
|
+
local complexity="moderate"
|
|
83
|
+
local body_len=${#body}
|
|
84
|
+
if [[ "$body_len" -lt 100 ]]; then
|
|
85
|
+
complexity="simple"
|
|
86
|
+
elif [[ "$body_len" -lt 300 ]]; then
|
|
87
|
+
complexity="moderate"
|
|
88
|
+
elif [[ "$body_len" -lt 800 ]]; then
|
|
89
|
+
complexity="complex"
|
|
90
|
+
else
|
|
91
|
+
complexity="very_complex"
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# Extract potential goals from body (lines starting with - or *)
|
|
95
|
+
local goals_json="[]"
|
|
96
|
+
if [[ -n "$body" ]]; then
|
|
97
|
+
local extracted_goals
|
|
98
|
+
extracted_goals=$(echo "$body" | grep -E '^\s*[-*]' | sed 's/^[ \t]*[-*][ \t]*//' | head -10 || true)
|
|
99
|
+
if [[ -n "$extracted_goals" ]]; then
|
|
100
|
+
goals_json="["
|
|
101
|
+
local first=true
|
|
102
|
+
while IFS= read -r goal; do
|
|
103
|
+
[[ -z "$goal" ]] && continue
|
|
104
|
+
# Escape quotes for JSON
|
|
105
|
+
goal=$(echo "$goal" | sed 's/"/\\"/g')
|
|
106
|
+
if $first; then
|
|
107
|
+
goals_json="${goals_json}\"${goal}\""
|
|
108
|
+
first=false
|
|
109
|
+
else
|
|
110
|
+
goals_json="${goals_json},\"${goal}\""
|
|
111
|
+
fi
|
|
112
|
+
done <<< "$extracted_goals"
|
|
113
|
+
goals_json="${goals_json}]"
|
|
114
|
+
fi
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# If no goals extracted, use title as the goal
|
|
118
|
+
if [[ "$goals_json" == "[]" ]]; then
|
|
119
|
+
local escaped_title
|
|
120
|
+
escaped_title=$(echo "$title" | sed 's/"/\\"/g')
|
|
121
|
+
goals_json="[\"${escaped_title}\"]"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# Build source block
|
|
125
|
+
local source_json="{\"type\":\"manual\"}"
|
|
126
|
+
if [[ -n "$issue_number" ]]; then
|
|
127
|
+
source_json="{\"type\":\"github_issue\",\"issue_number\":${issue_number}}"
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# Write spec (use local fallback if mktemp unavailable in sandbox)
|
|
131
|
+
local tmp_file
|
|
132
|
+
tmp_file=$(mktemp 2>/dev/null || echo "${output_file}.raw")
|
|
133
|
+
cat > "$tmp_file" <<SPECEOF
|
|
134
|
+
{
|
|
135
|
+
"version": "1.0",
|
|
136
|
+
"title": $(printf '%s' "$title" | jq -Rs .),
|
|
137
|
+
"source": ${source_json},
|
|
138
|
+
"goals": ${goals_json},
|
|
139
|
+
"constraints": [],
|
|
140
|
+
"acceptance_criteria": [
|
|
141
|
+
{
|
|
142
|
+
"criterion": "All existing tests continue to pass",
|
|
143
|
+
"testable": true,
|
|
144
|
+
"verification_method": "unit_test"
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"edge_cases": [],
|
|
148
|
+
"security_requirements": [],
|
|
149
|
+
"performance_requirements": {},
|
|
150
|
+
"affected_files": [],
|
|
151
|
+
"dependencies": [],
|
|
152
|
+
"metadata": {
|
|
153
|
+
"created_at": "$(now_iso)",
|
|
154
|
+
"complexity": "${complexity}",
|
|
155
|
+
"language": "${language:-unknown}"
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
SPECEOF
|
|
159
|
+
|
|
160
|
+
# Validate and pretty-print with jq
|
|
161
|
+
if command -v jq >/dev/null 2>&1; then
|
|
162
|
+
if jq '.' "$tmp_file" > "${output_file}.pp" 2>/dev/null; then
|
|
163
|
+
mv "${output_file}.pp" "$output_file"
|
|
164
|
+
else
|
|
165
|
+
error "Generated spec is invalid JSON"
|
|
166
|
+
mv "$tmp_file" "$output_file"
|
|
167
|
+
fi
|
|
168
|
+
else
|
|
169
|
+
mv "$tmp_file" "$output_file"
|
|
170
|
+
fi
|
|
171
|
+
rm -f "$tmp_file" "${output_file}.pp" "${output_file}.raw" 2>/dev/null || true
|
|
172
|
+
|
|
173
|
+
success "Spec generated: ${output_file}" >&2
|
|
174
|
+
|
|
175
|
+
if type emit_event >/dev/null 2>&1; then
|
|
176
|
+
emit_event "spec_generated" \
|
|
177
|
+
"file=${output_file}" \
|
|
178
|
+
"complexity=${complexity}" \
|
|
179
|
+
"issue=${issue_number:-none}"
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
echo "$output_file"
|
|
183
|
+
return 0
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# ─── Spec Validation ────────────────────────────────────────────────────────
|
|
187
|
+
# Validate a spec file against the JSON schema (lightweight jq-based check).
|
|
188
|
+
|
|
189
|
+
spec_validate() {
|
|
190
|
+
local spec_file="${1:-}"
|
|
191
|
+
|
|
192
|
+
if [[ ! -f "$spec_file" ]]; then
|
|
193
|
+
error "Spec file not found: ${spec_file}"
|
|
194
|
+
return 1
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
198
|
+
warn "jq not available — skipping spec validation"
|
|
199
|
+
return 0
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# Check required fields
|
|
203
|
+
local errors=""
|
|
204
|
+
|
|
205
|
+
local version
|
|
206
|
+
version=$(jq -r '.version // empty' "$spec_file" 2>/dev/null)
|
|
207
|
+
if [[ "$version" != "1.0" ]]; then
|
|
208
|
+
errors="${errors}Missing or invalid version (expected '1.0')\n"
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
local title
|
|
212
|
+
title=$(jq -r '.title // empty' "$spec_file" 2>/dev/null)
|
|
213
|
+
if [[ -z "$title" ]]; then
|
|
214
|
+
errors="${errors}Missing required field: title\n"
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
local goals_count
|
|
218
|
+
goals_count=$(jq '.goals | length' "$spec_file" 2>/dev/null || echo "0")
|
|
219
|
+
if [[ "$goals_count" -eq 0 ]]; then
|
|
220
|
+
errors="${errors}Missing required field: goals (must have at least 1)\n"
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
local criteria_count
|
|
224
|
+
criteria_count=$(jq '.acceptance_criteria | length' "$spec_file" 2>/dev/null || echo "0")
|
|
225
|
+
if [[ "$criteria_count" -eq 0 ]]; then
|
|
226
|
+
errors="${errors}Missing required field: acceptance_criteria (must have at least 1)\n"
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
# Check acceptance_criteria structure
|
|
230
|
+
local invalid_criteria
|
|
231
|
+
invalid_criteria=$(jq '[.acceptance_criteria[]? | select(.criterion == null or .testable == null)] | length' "$spec_file" 2>/dev/null || echo "0")
|
|
232
|
+
if [[ "$invalid_criteria" -gt 0 ]]; then
|
|
233
|
+
errors="${errors}${invalid_criteria} acceptance criteria missing 'criterion' or 'testable' field\n"
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
if [[ -n "$errors" ]]; then
|
|
237
|
+
error "Spec validation failed for ${spec_file}:"
|
|
238
|
+
echo -e "$errors" | while IFS= read -r line; do
|
|
239
|
+
[[ -n "$line" ]] && echo " - $line"
|
|
240
|
+
done
|
|
241
|
+
return 1
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
success "Spec valid: ${spec_file}"
|
|
245
|
+
return 0
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# ─── Spec Load / Save ───────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
spec_load() {
|
|
251
|
+
local spec_file="${1:-}"
|
|
252
|
+
if [[ ! -f "$spec_file" ]]; then
|
|
253
|
+
error "Spec file not found: ${spec_file}"
|
|
254
|
+
return 1
|
|
255
|
+
fi
|
|
256
|
+
cat "$spec_file"
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
spec_save() {
|
|
260
|
+
local spec_file="${1:-}"
|
|
261
|
+
local spec_json="${2:-}"
|
|
262
|
+
|
|
263
|
+
if [[ -z "$spec_file" || -z "$spec_json" ]]; then
|
|
264
|
+
error "spec_save requires file path and JSON content"
|
|
265
|
+
return 1
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
mkdir -p "$(dirname "$spec_file")"
|
|
269
|
+
|
|
270
|
+
# Atomic write via temp file
|
|
271
|
+
local tmp_file
|
|
272
|
+
tmp_file=$(mktemp 2>/dev/null || echo "${spec_file}.tmp")
|
|
273
|
+
echo "$spec_json" | jq '.' > "$tmp_file" 2>/dev/null || {
|
|
274
|
+
error "Invalid JSON for spec save"
|
|
275
|
+
rm -f "$tmp_file"
|
|
276
|
+
return 1
|
|
277
|
+
}
|
|
278
|
+
mv "$tmp_file" "$spec_file"
|
|
279
|
+
|
|
280
|
+
# Update metadata timestamp
|
|
281
|
+
local updated
|
|
282
|
+
updated=$(jq --arg ts "$(now_iso)" '.metadata.updated_at = $ts' "$spec_file" 2>/dev/null)
|
|
283
|
+
if [[ -n "$updated" ]]; then
|
|
284
|
+
echo "$updated" > "$spec_file"
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
if type emit_event >/dev/null 2>&1; then
|
|
288
|
+
emit_event "spec_updated" "file=${spec_file}"
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
return 0
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# ─── Spec Diff ───────────────────────────────────────────────────────────────
|
|
295
|
+
# Compare spec against implementation. Returns JSON report of misalignments.
|
|
296
|
+
# This is used at the review stage to catch "spec says X, code does Y".
|
|
297
|
+
|
|
298
|
+
spec_diff() {
|
|
299
|
+
local spec_file="${1:-}"
|
|
300
|
+
local project_dir="${2:-.}"
|
|
301
|
+
|
|
302
|
+
if [[ ! -f "$spec_file" ]]; then
|
|
303
|
+
error "Spec file not found: ${spec_file}"
|
|
304
|
+
return 1
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
308
|
+
warn "jq not available — skipping spec diff"
|
|
309
|
+
return 0
|
|
310
|
+
fi
|
|
311
|
+
|
|
312
|
+
local report_file="${SPEC_DIR}/compliance-report.json"
|
|
313
|
+
mkdir -p "$SPEC_DIR"
|
|
314
|
+
|
|
315
|
+
# Extract spec data
|
|
316
|
+
local title goals_count criteria_count edge_count security_count
|
|
317
|
+
title=$(jq -r '.title' "$spec_file" 2>/dev/null)
|
|
318
|
+
goals_count=$(jq '.goals | length' "$spec_file" 2>/dev/null || echo "0")
|
|
319
|
+
criteria_count=$(jq '.acceptance_criteria | length' "$spec_file" 2>/dev/null || echo "0")
|
|
320
|
+
edge_count=$(jq '.edge_cases | length' "$spec_file" 2>/dev/null || echo "0")
|
|
321
|
+
security_count=$(jq '.security_requirements | length' "$spec_file" 2>/dev/null || echo "0")
|
|
322
|
+
|
|
323
|
+
# Check which affected files were actually modified
|
|
324
|
+
local affected_files
|
|
325
|
+
affected_files=$(jq -r '.affected_files[]?' "$spec_file" 2>/dev/null || true)
|
|
326
|
+
local files_modified=0
|
|
327
|
+
local files_missing=0
|
|
328
|
+
local missing_files=""
|
|
329
|
+
|
|
330
|
+
if [[ -n "$affected_files" ]]; then
|
|
331
|
+
while IFS= read -r af; do
|
|
332
|
+
[[ -z "$af" ]] && continue
|
|
333
|
+
if git -C "$project_dir" diff --name-only HEAD 2>/dev/null | grep -q "$af"; then
|
|
334
|
+
files_modified=$((files_modified + 1))
|
|
335
|
+
elif [[ -f "${project_dir}/${af}" ]]; then
|
|
336
|
+
# File exists but wasn't modified
|
|
337
|
+
files_missing=$((files_missing + 1))
|
|
338
|
+
if [[ -n "$missing_files" ]]; then
|
|
339
|
+
missing_files="${missing_files},\"${af}\""
|
|
340
|
+
else
|
|
341
|
+
missing_files="\"${af}\""
|
|
342
|
+
fi
|
|
343
|
+
fi
|
|
344
|
+
done <<< "$affected_files"
|
|
345
|
+
fi
|
|
346
|
+
|
|
347
|
+
# Check testable acceptance criteria have corresponding tests
|
|
348
|
+
local testable_criteria untested_criteria
|
|
349
|
+
testable_criteria=$(jq '[.acceptance_criteria[]? | select(.testable == true)] | length' "$spec_file" 2>/dev/null || echo "0")
|
|
350
|
+
untested_criteria=0 # Would need test discovery to check this properly
|
|
351
|
+
|
|
352
|
+
# Build compliance report
|
|
353
|
+
cat > "$report_file" <<EOF
|
|
354
|
+
{
|
|
355
|
+
"spec_file": "${spec_file}",
|
|
356
|
+
"checked_at": "$(now_iso)",
|
|
357
|
+
"title": $(printf '%s' "$title" | jq -Rs .),
|
|
358
|
+
"coverage": {
|
|
359
|
+
"goals_defined": ${goals_count},
|
|
360
|
+
"acceptance_criteria": ${criteria_count},
|
|
361
|
+
"testable_criteria": ${testable_criteria},
|
|
362
|
+
"edge_cases_defined": ${edge_count},
|
|
363
|
+
"security_requirements": ${security_count}
|
|
364
|
+
},
|
|
365
|
+
"file_coverage": {
|
|
366
|
+
"expected_files": $(jq '.affected_files | length' "$spec_file" 2>/dev/null || echo "0"),
|
|
367
|
+
"files_modified": ${files_modified},
|
|
368
|
+
"files_not_modified": ${files_missing},
|
|
369
|
+
"unmodified_files": [${missing_files}]
|
|
370
|
+
},
|
|
371
|
+
"warnings": [],
|
|
372
|
+
"verdict": "$(if [[ "$files_missing" -gt 0 ]]; then echo "review_needed"; else echo "compliant"; fi)"
|
|
373
|
+
}
|
|
374
|
+
EOF
|
|
375
|
+
|
|
376
|
+
local verdict
|
|
377
|
+
verdict=$(jq -r '.verdict' "$report_file" 2>/dev/null || echo "unknown")
|
|
378
|
+
|
|
379
|
+
if [[ "$verdict" == "compliant" ]]; then
|
|
380
|
+
success "Spec compliance: ${title} — all checks passed"
|
|
381
|
+
else
|
|
382
|
+
warn "Spec compliance: ${title} — review needed (${files_missing} expected files not modified)"
|
|
383
|
+
fi
|
|
384
|
+
|
|
385
|
+
if type emit_event >/dev/null 2>&1; then
|
|
386
|
+
emit_event "spec_compliance_checked" \
|
|
387
|
+
"spec=${spec_file}" \
|
|
388
|
+
"verdict=${verdict}" \
|
|
389
|
+
"criteria=${criteria_count}"
|
|
390
|
+
fi
|
|
391
|
+
|
|
392
|
+
echo "$report_file"
|
|
393
|
+
return 0
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
# ─── Spec List ───────────────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
spec_list() {
|
|
399
|
+
if [[ ! -d "$SPEC_DIR" ]]; then
|
|
400
|
+
info "No specs directory found"
|
|
401
|
+
return 0
|
|
402
|
+
fi
|
|
403
|
+
|
|
404
|
+
local specs
|
|
405
|
+
specs=$(find "$SPEC_DIR" -name "*.json" -not -name "compliance-report.json" 2>/dev/null | sort)
|
|
406
|
+
|
|
407
|
+
if [[ -z "$specs" ]]; then
|
|
408
|
+
info "No specs found in ${SPEC_DIR}"
|
|
409
|
+
return 0
|
|
410
|
+
fi
|
|
411
|
+
|
|
412
|
+
echo "Specifications:"
|
|
413
|
+
while IFS= read -r spec; do
|
|
414
|
+
local title complexity
|
|
415
|
+
title=$(jq -r '.title // "untitled"' "$spec" 2>/dev/null)
|
|
416
|
+
complexity=$(jq -r '.metadata.complexity // "unknown"' "$spec" 2>/dev/null)
|
|
417
|
+
local goals_count
|
|
418
|
+
goals_count=$(jq '.goals | length' "$spec" 2>/dev/null || echo "0")
|
|
419
|
+
printf " %-50s [%s] %d goals\n" "$title" "$complexity" "$goals_count"
|
|
420
|
+
done <<< "$specs"
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
# ─── Spec for Pipeline Prompt ────────────────────────────────────────────────
|
|
424
|
+
# Format spec as markdown for injection into agent prompts.
|
|
425
|
+
|
|
426
|
+
spec_to_prompt() {
|
|
427
|
+
local spec_file="${1:-}"
|
|
428
|
+
|
|
429
|
+
if [[ ! -f "$spec_file" ]]; then
|
|
430
|
+
return 1
|
|
431
|
+
fi
|
|
432
|
+
|
|
433
|
+
local title goals constraints criteria edge_cases security
|
|
434
|
+
|
|
435
|
+
title=$(jq -r '.title' "$spec_file" 2>/dev/null)
|
|
436
|
+
echo "## Specification: ${title}"
|
|
437
|
+
echo ""
|
|
438
|
+
|
|
439
|
+
echo "### Goals"
|
|
440
|
+
jq -r '.goals[]?' "$spec_file" 2>/dev/null | while IFS= read -r g; do
|
|
441
|
+
echo "- ${g}"
|
|
442
|
+
done
|
|
443
|
+
echo ""
|
|
444
|
+
|
|
445
|
+
local constraints_count
|
|
446
|
+
constraints_count=$(jq '.constraints | length' "$spec_file" 2>/dev/null || echo "0")
|
|
447
|
+
if [[ "$constraints_count" -gt 0 ]]; then
|
|
448
|
+
echo "### Constraints"
|
|
449
|
+
jq -r '.constraints[]?' "$spec_file" 2>/dev/null | while IFS= read -r c; do
|
|
450
|
+
echo "- ${c}"
|
|
451
|
+
done
|
|
452
|
+
echo ""
|
|
453
|
+
fi
|
|
454
|
+
|
|
455
|
+
echo "### Acceptance Criteria"
|
|
456
|
+
jq -r '.acceptance_criteria[]? | "- [\(if .testable then "testable" else "manual" end)] \(.criterion)"' "$spec_file" 2>/dev/null
|
|
457
|
+
echo ""
|
|
458
|
+
|
|
459
|
+
local edge_count
|
|
460
|
+
edge_count=$(jq '.edge_cases | length' "$spec_file" 2>/dev/null || echo "0")
|
|
461
|
+
if [[ "$edge_count" -gt 0 ]]; then
|
|
462
|
+
echo "### Edge Cases"
|
|
463
|
+
jq -r '.edge_cases[]? | "- **\(.scenario)**: \(.expected_behavior)"' "$spec_file" 2>/dev/null
|
|
464
|
+
echo ""
|
|
465
|
+
fi
|
|
466
|
+
|
|
467
|
+
local sec_count
|
|
468
|
+
sec_count=$(jq '.security_requirements | length' "$spec_file" 2>/dev/null || echo "0")
|
|
469
|
+
if [[ "$sec_count" -gt 0 ]]; then
|
|
470
|
+
echo "### Security Requirements"
|
|
471
|
+
jq -r '.security_requirements[]?' "$spec_file" 2>/dev/null | while IFS= read -r s; do
|
|
472
|
+
echo "- ${s}"
|
|
473
|
+
done
|
|
474
|
+
echo ""
|
|
475
|
+
fi
|
|
476
|
+
}
|
|
@@ -22,6 +22,7 @@ CYAN='\033[38;2;0;212;255m'
|
|
|
22
22
|
GREEN='\033[38;2;74;222;128m'
|
|
23
23
|
RED='\033[38;2;248;113;113m'
|
|
24
24
|
YELLOW='\033[38;2;250;204;21m'
|
|
25
|
+
PURPLE='\033[38;2;168;85;247m'
|
|
25
26
|
DIM='\033[2m'
|
|
26
27
|
BOLD='\033[1m'
|
|
27
28
|
RESET='\033[0m'
|
|
@@ -31,7 +32,16 @@ PASS=0
|
|
|
31
32
|
FAIL=0
|
|
32
33
|
TOTAL=0
|
|
33
34
|
FAILURES=()
|
|
34
|
-
|
|
35
|
+
|
|
36
|
+
# ─── Auto-initialize TEST_TEMP_DIR ──────────────────────────────────────────
|
|
37
|
+
# Many test files use TEST_TEMP_DIR in their setup_env() without calling
|
|
38
|
+
# setup_test_env(). Auto-create a temp dir so $TEST_TEMP_DIR is never empty.
|
|
39
|
+
# Save originals now so cleanup_test_env() can always restore them.
|
|
40
|
+
ORIG_HOME="${HOME}"
|
|
41
|
+
ORIG_PATH="${PATH}"
|
|
42
|
+
TEST_TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/sw-test-auto.XXXXXX")
|
|
43
|
+
mkdir -p "$TEST_TEMP_DIR/home/.shipwright"
|
|
44
|
+
mkdir -p "$TEST_TEMP_DIR/bin"
|
|
35
45
|
|
|
36
46
|
# ─── Assertions ──────────────────────────────────────────────────────────────
|
|
37
47
|
|
|
@@ -67,7 +77,7 @@ assert_contains() {
|
|
|
67
77
|
local desc="$1"
|
|
68
78
|
local haystack="$2"
|
|
69
79
|
local needle="$3"
|
|
70
|
-
if
|
|
80
|
+
if grep -qF -- "$needle" <<< "$haystack" 2>/dev/null; then
|
|
71
81
|
assert_pass "$desc"
|
|
72
82
|
else
|
|
73
83
|
assert_fail "$desc" "output missing: $needle"
|
|
@@ -78,7 +88,7 @@ assert_contains_regex() {
|
|
|
78
88
|
local desc="$1"
|
|
79
89
|
local haystack="$2"
|
|
80
90
|
local pattern="$3"
|
|
81
|
-
if
|
|
91
|
+
if grep -qE -- "$pattern" <<< "$haystack" 2>/dev/null; then
|
|
82
92
|
assert_pass "$desc"
|
|
83
93
|
else
|
|
84
94
|
assert_fail "$desc" "output missing pattern: $pattern"
|
|
@@ -145,14 +155,15 @@ assert_file_not_exists() {
|
|
|
145
155
|
|
|
146
156
|
setup_test_env() {
|
|
147
157
|
local test_name="${1:-sw-test}"
|
|
158
|
+
# Clean up auto-created temp dir and create a named one
|
|
159
|
+
[[ -n "$TEST_TEMP_DIR" && -d "$TEST_TEMP_DIR" ]] && rm -rf "$TEST_TEMP_DIR"
|
|
148
160
|
TEST_TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/${test_name}.XXXXXX")
|
|
149
161
|
mkdir -p "$TEST_TEMP_DIR/home/.shipwright"
|
|
150
162
|
mkdir -p "$TEST_TEMP_DIR/bin"
|
|
151
163
|
mkdir -p "$TEST_TEMP_DIR/project"
|
|
152
164
|
mkdir -p "$TEST_TEMP_DIR/logs"
|
|
153
165
|
|
|
154
|
-
ORIG_HOME
|
|
155
|
-
ORIG_PATH="${PATH}"
|
|
166
|
+
# ORIG_HOME/ORIG_PATH already saved at source time
|
|
156
167
|
export HOME="$TEST_TEMP_DIR/home"
|
|
157
168
|
export PATH="$TEST_TEMP_DIR/bin:$PATH"
|
|
158
169
|
export NO_GITHUB=true
|
|
@@ -167,8 +178,8 @@ cleanup_test_env() {
|
|
|
167
178
|
if [[ -n "$TEST_TEMP_DIR" && -d "$TEST_TEMP_DIR" ]]; then
|
|
168
179
|
rm -rf "$TEST_TEMP_DIR"
|
|
169
180
|
fi
|
|
170
|
-
[[ -n "${ORIG_HOME:-}" ]] && export HOME="$ORIG_HOME"
|
|
171
|
-
[[ -n "${ORIG_PATH:-}" ]] && export PATH="$ORIG_PATH"
|
|
181
|
+
[[ -n "${ORIG_HOME:-}" ]] && export HOME="$ORIG_HOME" || true
|
|
182
|
+
[[ -n "${ORIG_PATH:-}" ]] && export PATH="$ORIG_PATH" || true
|
|
172
183
|
}
|
|
173
184
|
|
|
174
185
|
# ─── Mock Helpers ────────────────────────────────────────────────────────────
|