shipwright-cli 2.3.1 → 2.4.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.
Files changed (102) hide show
  1. package/README.md +82 -20
  2. package/config/policy.json +160 -2
  3. package/config/policy.schema.json +162 -1
  4. package/package.json +14 -2
  5. package/scripts/sw +1 -1
  6. package/scripts/sw-activity.sh +1 -1
  7. package/scripts/sw-adaptive.sh +1 -1
  8. package/scripts/sw-adversarial.sh +1 -1
  9. package/scripts/sw-architecture-enforcer.sh +1 -1
  10. package/scripts/sw-auth.sh +1 -1
  11. package/scripts/sw-autonomous.sh +1 -1
  12. package/scripts/sw-changelog.sh +1 -1
  13. package/scripts/sw-checkpoint.sh +1 -1
  14. package/scripts/sw-ci.sh +1 -1
  15. package/scripts/sw-cleanup.sh +1 -1
  16. package/scripts/sw-code-review.sh +1 -1
  17. package/scripts/sw-connect.sh +1 -1
  18. package/scripts/sw-context.sh +1 -1
  19. package/scripts/sw-cost.sh +1 -1
  20. package/scripts/sw-daemon.sh +1 -1
  21. package/scripts/sw-dashboard.sh +1 -1
  22. package/scripts/sw-db.sh +1 -1
  23. package/scripts/sw-decompose.sh +1 -1
  24. package/scripts/sw-deps.sh +1 -1
  25. package/scripts/sw-developer-simulation.sh +1 -1
  26. package/scripts/sw-discovery.sh +1 -1
  27. package/scripts/sw-doc-fleet.sh +1 -1
  28. package/scripts/sw-docs-agent.sh +1 -1
  29. package/scripts/sw-docs.sh +1 -1
  30. package/scripts/sw-doctor.sh +1 -1
  31. package/scripts/sw-dora.sh +1 -1
  32. package/scripts/sw-durable.sh +1 -1
  33. package/scripts/sw-e2e-orchestrator.sh +1 -1
  34. package/scripts/sw-eventbus.sh +1 -1
  35. package/scripts/sw-evidence.sh +664 -0
  36. package/scripts/sw-feedback.sh +1 -1
  37. package/scripts/sw-fix.sh +1 -1
  38. package/scripts/sw-fleet-discover.sh +1 -1
  39. package/scripts/sw-fleet-viz.sh +1 -1
  40. package/scripts/sw-fleet.sh +1 -1
  41. package/scripts/sw-github-app.sh +1 -1
  42. package/scripts/sw-github-checks.sh +1 -1
  43. package/scripts/sw-github-deploy.sh +1 -1
  44. package/scripts/sw-github-graphql.sh +1 -1
  45. package/scripts/sw-guild.sh +1 -1
  46. package/scripts/sw-heartbeat.sh +1 -1
  47. package/scripts/sw-hygiene.sh +1 -1
  48. package/scripts/sw-incident.sh +244 -1
  49. package/scripts/sw-init.sh +1 -1
  50. package/scripts/sw-instrument.sh +1 -1
  51. package/scripts/sw-intelligence.sh +1 -1
  52. package/scripts/sw-jira.sh +1 -1
  53. package/scripts/sw-launchd.sh +1 -1
  54. package/scripts/sw-linear.sh +1 -1
  55. package/scripts/sw-logs.sh +1 -1
  56. package/scripts/sw-loop.sh +1 -1
  57. package/scripts/sw-memory.sh +1 -1
  58. package/scripts/sw-mission-control.sh +1 -1
  59. package/scripts/sw-model-router.sh +1 -1
  60. package/scripts/sw-otel.sh +1 -1
  61. package/scripts/sw-oversight.sh +1 -1
  62. package/scripts/sw-pipeline-composer.sh +1 -1
  63. package/scripts/sw-pipeline-vitals.sh +1 -1
  64. package/scripts/sw-pipeline.sh +1 -1
  65. package/scripts/sw-pm.sh +1 -1
  66. package/scripts/sw-pr-lifecycle.sh +177 -5
  67. package/scripts/sw-predictive.sh +1 -1
  68. package/scripts/sw-prep.sh +1 -1
  69. package/scripts/sw-ps.sh +1 -1
  70. package/scripts/sw-public-dashboard.sh +1 -1
  71. package/scripts/sw-quality.sh +1 -1
  72. package/scripts/sw-reaper.sh +1 -1
  73. package/scripts/sw-regression.sh +1 -1
  74. package/scripts/sw-release-manager.sh +1 -1
  75. package/scripts/sw-release.sh +1 -1
  76. package/scripts/sw-remote.sh +1 -1
  77. package/scripts/sw-replay.sh +1 -1
  78. package/scripts/sw-retro.sh +1 -1
  79. package/scripts/sw-review-rerun.sh +220 -0
  80. package/scripts/sw-scale.sh +1 -1
  81. package/scripts/sw-security-audit.sh +1 -1
  82. package/scripts/sw-self-optimize.sh +1 -1
  83. package/scripts/sw-session.sh +1 -1
  84. package/scripts/sw-setup.sh +1 -1
  85. package/scripts/sw-standup.sh +1 -1
  86. package/scripts/sw-status.sh +1 -1
  87. package/scripts/sw-strategic.sh +1 -1
  88. package/scripts/sw-stream.sh +1 -1
  89. package/scripts/sw-swarm.sh +1 -1
  90. package/scripts/sw-team-stages.sh +1 -1
  91. package/scripts/sw-templates.sh +1 -1
  92. package/scripts/sw-testgen.sh +1 -1
  93. package/scripts/sw-tmux-pipeline.sh +1 -1
  94. package/scripts/sw-tmux.sh +1 -1
  95. package/scripts/sw-trace.sh +1 -1
  96. package/scripts/sw-tracker.sh +1 -1
  97. package/scripts/sw-triage.sh +1 -1
  98. package/scripts/sw-upgrade.sh +1 -1
  99. package/scripts/sw-ux.sh +1 -1
  100. package/scripts/sw-webhook.sh +1 -1
  101. package/scripts/sw-widgets.sh +1 -1
  102. package/scripts/sw-worktree.sh +1 -1
@@ -0,0 +1,664 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright evidence — Machine-Verifiable Proof for Agent Deliveries ║
4
+ # ║ Browser · API · Database · CLI · Webhook · Custom collectors ║
5
+ # ║ Capture · Verify · Manifest assertions · Artifact freshness ║
6
+ # ║ Part of the Code Factory pattern for deterministic merge evidence ║
7
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
8
+ set -euo pipefail
9
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
10
+
11
+ VERSION="2.4.0"
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
14
+
15
+ # shellcheck source=lib/compat.sh
16
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
17
+ # shellcheck source=lib/helpers.sh
18
+ [[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
19
+ [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
20
+ [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
21
+ [[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
22
+ [[ "$(type -t error 2>/dev/null)" == "function" ]] || error() { echo -e "\033[38;2;248;113;113m\033[1m✗\033[0m $*" >&2; }
23
+ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
24
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
25
+ now_epoch() { date +%s; }
26
+ fi
27
+ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
28
+ emit_event() {
29
+ local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
30
+ local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
31
+ while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
32
+ echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
33
+ }
34
+ fi
35
+
36
+ # Cross-platform timeout: macOS lacks GNU timeout
37
+ _run_with_timeout() {
38
+ local secs="$1"; shift
39
+ if command -v gtimeout &>/dev/null; then
40
+ gtimeout "$secs" "$@"
41
+ elif command -v timeout &>/dev/null; then
42
+ timeout "$secs" "$@"
43
+ else
44
+ # Fallback: run without timeout
45
+ "$@"
46
+ fi
47
+ }
48
+
49
+ EVIDENCE_DIR="${REPO_DIR}/.claude/evidence"
50
+ MANIFEST_FILE="${EVIDENCE_DIR}/manifest.json"
51
+ POLICY_FILE="${REPO_DIR}/config/policy.json"
52
+
53
+ ensure_evidence_dir() {
54
+ mkdir -p "$EVIDENCE_DIR"
55
+ }
56
+
57
+ # ─── Policy Accessors ────────────────────────────────────────────────────────
58
+
59
+ get_collectors() {
60
+ local type_filter="${1:-}"
61
+ if [[ -f "$POLICY_FILE" ]]; then
62
+ if [[ -n "$type_filter" ]]; then
63
+ jq -c ".evidence.collectors[]? | select(.type == \"${type_filter}\")" "$POLICY_FILE" 2>/dev/null
64
+ else
65
+ jq -c '.evidence.collectors[]?' "$POLICY_FILE" 2>/dev/null
66
+ fi
67
+ fi
68
+ }
69
+
70
+ get_max_age_minutes() {
71
+ if [[ -f "$POLICY_FILE" ]]; then
72
+ jq -r '.evidence.artifactMaxAgeMinutes // 30' "$POLICY_FILE" 2>/dev/null
73
+ else
74
+ echo "30"
75
+ fi
76
+ }
77
+
78
+ get_require_fresh() {
79
+ if [[ -f "$POLICY_FILE" ]]; then
80
+ jq -r '.evidence.requireFreshArtifacts // true' "$POLICY_FILE" 2>/dev/null
81
+ else
82
+ echo "true"
83
+ fi
84
+ }
85
+
86
+ # ═════════════════════════════════════════════════════════════════════════════
87
+ # TYPE-SPECIFIC COLLECTORS
88
+ # Each returns a JSON evidence record written to EVIDENCE_DIR/<name>.json
89
+ # ═════════════════════════════════════════════════════════════════════════════
90
+
91
+ # ─── Browser: HTTP page load against a URL path ──────────────────────────────
92
+
93
+ collect_browser() {
94
+ local name="$1"
95
+ local collector_json="$2"
96
+
97
+ local entrypoint base_url url
98
+ entrypoint=$(echo "$collector_json" | jq -r '.entrypoint // "/"')
99
+ base_url=$(echo "$collector_json" | jq -r '.baseUrl // "http://localhost:3000"')
100
+ url="${base_url}${entrypoint}"
101
+
102
+ info "[browser] ${name}: ${url}"
103
+
104
+ local http_status="0"
105
+ local response_size="0"
106
+ local response_body=""
107
+
108
+ if command -v curl &>/dev/null; then
109
+ local tmpfile="/tmp/sw-evidence-${name}.txt"
110
+ http_status=$(curl -s -o "$tmpfile" -w "%{http_code}" --max-time 30 "$url" 2>/dev/null || echo "0")
111
+ if [[ -f "$tmpfile" ]]; then
112
+ response_size=$(wc -c < "$tmpfile" 2>/dev/null || echo "0")
113
+ response_body=$(cat "$tmpfile" 2>/dev/null || echo "")
114
+ rm -f "$tmpfile"
115
+ fi
116
+ fi
117
+
118
+ local passed="false"
119
+ [[ "$http_status" -ge 200 && "$http_status" -lt 400 ]] && passed="true"
120
+
121
+ write_evidence_record "$name" "browser" "$passed" \
122
+ "$(jq -n --arg url "$url" --argjson status "$http_status" --argjson size "$response_size" \
123
+ '{url: $url, http_status: $status, response_size: $size}')"
124
+ }
125
+
126
+ # ─── API: REST/GraphQL endpoint verification ─────────────────────────────────
127
+
128
+ collect_api() {
129
+ local name="$1"
130
+ local collector_json="$2"
131
+
132
+ local url method expected_status headers_json body timeout
133
+ url=$(echo "$collector_json" | jq -r '.url // ""')
134
+ method=$(echo "$collector_json" | jq -r '.method // "GET"')
135
+ expected_status=$(echo "$collector_json" | jq -r '.expectedStatus // 200')
136
+ body=$(echo "$collector_json" | jq -r '.body // ""')
137
+ timeout=$(echo "$collector_json" | jq -r '.timeout // 30')
138
+
139
+ if [[ -z "$url" ]]; then
140
+ local base_url entrypoint
141
+ base_url=$(echo "$collector_json" | jq -r '.baseUrl // "http://localhost:3000"')
142
+ entrypoint=$(echo "$collector_json" | jq -r '.entrypoint // "/"')
143
+ url="${base_url}${entrypoint}"
144
+ fi
145
+
146
+ info "[api] ${name}: ${method} ${url}"
147
+
148
+ local http_status="0"
149
+ local response_size="0"
150
+ local response_body=""
151
+ local content_type=""
152
+
153
+ if command -v curl &>/dev/null; then
154
+ local tmpfile="/tmp/sw-evidence-${name}.txt"
155
+ local header_file="/tmp/sw-evidence-${name}-headers.txt"
156
+ local curl_args=(-s -o "$tmpfile" -D "$header_file" -w "%{http_code}" -X "$method" --max-time "$timeout")
157
+
158
+ # Add custom headers
159
+ local custom_headers
160
+ custom_headers=$(echo "$collector_json" | jq -r '.headers // {} | to_entries[] | "-H\n\(.key): \(.value)"' 2>/dev/null || true)
161
+ if [[ -n "$custom_headers" ]]; then
162
+ while IFS= read -r line; do
163
+ [[ "$line" == "-H" ]] && continue
164
+ curl_args+=(-H "$line")
165
+ done <<< "$custom_headers"
166
+ fi
167
+
168
+ # Add body for POST/PUT/PATCH
169
+ if [[ -n "$body" && "$method" != "GET" && "$method" != "HEAD" ]]; then
170
+ curl_args+=(-d "$body")
171
+ fi
172
+
173
+ http_status=$(curl "${curl_args[@]}" "$url" 2>/dev/null || echo "0")
174
+
175
+ if [[ -f "$tmpfile" ]]; then
176
+ response_size=$(wc -c < "$tmpfile" 2>/dev/null || echo "0")
177
+ response_body=$(cat "$tmpfile" 2>/dev/null || echo "")
178
+ rm -f "$tmpfile"
179
+ fi
180
+ if [[ -f "$header_file" ]]; then
181
+ content_type=$(grep -i "^content-type:" "$header_file" 2>/dev/null | head -1 | sed 's/^[^:]*: *//' | tr -d '\r' || echo "")
182
+ rm -f "$header_file"
183
+ fi
184
+ fi
185
+
186
+ local passed="false"
187
+ [[ "$http_status" -eq "$expected_status" ]] && passed="true"
188
+
189
+ # Check if response is valid JSON when content-type suggests it
190
+ local valid_json="false"
191
+ if echo "$response_body" | jq empty 2>/dev/null; then
192
+ valid_json="true"
193
+ fi
194
+
195
+ write_evidence_record "$name" "api" "$passed" \
196
+ "$(jq -n --arg url "$url" --arg method "$method" \
197
+ --argjson status "$http_status" --argjson expected "$expected_status" \
198
+ --argjson size "$response_size" --arg content_type "$content_type" \
199
+ --arg valid_json "$valid_json" \
200
+ '{url: $url, method: $method, http_status: $status, expected_status: $expected, response_size: $size, content_type: $content_type, valid_json: ($valid_json == "true")}')"
201
+ }
202
+
203
+ # ─── CLI: Execute a command and check exit code ──────────────────────────────
204
+
205
+ collect_cli() {
206
+ local name="$1"
207
+ local collector_json="$2"
208
+
209
+ local command_str expected_exit timeout
210
+ command_str=$(echo "$collector_json" | jq -r '.command // ""')
211
+ expected_exit=$(echo "$collector_json" | jq -r '.expectedExitCode // 0')
212
+ timeout=$(echo "$collector_json" | jq -r '.timeout // 60')
213
+
214
+ if [[ -z "$command_str" ]]; then
215
+ error "[cli] ${name}: no command specified"
216
+ write_evidence_record "$name" "cli" "false" '{"error": "no command specified"}'
217
+ return
218
+ fi
219
+
220
+ info "[cli] ${name}: ${command_str}"
221
+
222
+ local exit_code=0
223
+ local output=""
224
+ local start_time
225
+ start_time=$(date +%s)
226
+
227
+ output=$(cd "$REPO_DIR" && _run_with_timeout "$timeout" bash -c "$command_str" 2>&1) || exit_code=$?
228
+
229
+ local elapsed=$(( $(date +%s) - start_time ))
230
+
231
+ local passed="false"
232
+ [[ "$exit_code" -eq "$expected_exit" ]] && passed="true"
233
+
234
+ local valid_json="false"
235
+ if echo "$output" | jq empty 2>/dev/null; then
236
+ valid_json="true"
237
+ fi
238
+
239
+ local output_size=${#output}
240
+ # Truncate output for the evidence record (keep first 2000 chars)
241
+ local output_preview="${output:0:2000}"
242
+
243
+ write_evidence_record "$name" "cli" "$passed" \
244
+ "$(jq -n --arg cmd "$command_str" --argjson exit_code "$exit_code" \
245
+ --argjson expected "$expected_exit" --argjson elapsed "$elapsed" \
246
+ --argjson output_size "$output_size" --arg valid_json "$valid_json" \
247
+ --arg output_preview "$output_preview" \
248
+ '{command: $cmd, exit_code: $exit_code, expected_exit_code: $expected, elapsed_seconds: $elapsed, output_size: $output_size, valid_json: ($valid_json == "true"), output_preview: $output_preview}')"
249
+ }
250
+
251
+ # ─── Database: Schema/migration check via command ─────────────────────────────
252
+
253
+ collect_database() {
254
+ local name="$1"
255
+ local collector_json="$2"
256
+
257
+ local command_str expected_exit timeout
258
+ command_str=$(echo "$collector_json" | jq -r '.command // ""')
259
+ expected_exit=$(echo "$collector_json" | jq -r '.expectedExitCode // 0')
260
+ timeout=$(echo "$collector_json" | jq -r '.timeout // 30')
261
+
262
+ if [[ -z "$command_str" ]]; then
263
+ error "[database] ${name}: no command specified"
264
+ write_evidence_record "$name" "database" "false" '{"error": "no command specified"}'
265
+ return
266
+ fi
267
+
268
+ info "[database] ${name}: ${command_str}"
269
+
270
+ local exit_code=0
271
+ local output=""
272
+ output=$(cd "$REPO_DIR" && _run_with_timeout "$timeout" bash -c "$command_str" 2>&1) || exit_code=$?
273
+
274
+ local passed="false"
275
+ [[ "$exit_code" -eq "$expected_exit" ]] && passed="true"
276
+
277
+ local output_preview="${output:0:2000}"
278
+
279
+ write_evidence_record "$name" "database" "$passed" \
280
+ "$(jq -n --arg cmd "$command_str" --argjson exit_code "$exit_code" \
281
+ --argjson expected "$expected_exit" --arg output_preview "$output_preview" \
282
+ '{command: $cmd, exit_code: $exit_code, expected_exit_code: $expected, output_preview: $output_preview}')"
283
+ }
284
+
285
+ # ─── Webhook: Issue a callback and verify response ───────────────────────────
286
+
287
+ collect_webhook() {
288
+ local name="$1"
289
+ local collector_json="$2"
290
+
291
+ local url method expected_status body timeout
292
+ url=$(echo "$collector_json" | jq -r '.url // ""')
293
+ method=$(echo "$collector_json" | jq -r '.method // "POST"')
294
+ expected_status=$(echo "$collector_json" | jq -r '.expectedStatus // 200')
295
+ body=$(echo "$collector_json" | jq -r '.body // "{}"')
296
+ timeout=$(echo "$collector_json" | jq -r '.timeout // 15')
297
+
298
+ if [[ -z "$url" ]]; then
299
+ error "[webhook] ${name}: no URL specified"
300
+ write_evidence_record "$name" "webhook" "false" '{"error": "no URL specified"}'
301
+ return
302
+ fi
303
+
304
+ info "[webhook] ${name}: ${method} ${url}"
305
+
306
+ local http_status="0"
307
+ local response_body=""
308
+
309
+ if command -v curl &>/dev/null; then
310
+ local tmpfile="/tmp/sw-evidence-${name}.txt"
311
+ http_status=$(curl -s -o "$tmpfile" -w "%{http_code}" -X "$method" \
312
+ -H "Content-Type: application/json" -d "$body" \
313
+ --max-time "$timeout" "$url" 2>/dev/null || echo "0")
314
+ if [[ -f "$tmpfile" ]]; then
315
+ response_body=$(cat "$tmpfile" 2>/dev/null || echo "")
316
+ rm -f "$tmpfile"
317
+ fi
318
+ fi
319
+
320
+ local passed="false"
321
+ [[ "$http_status" -eq "$expected_status" ]] && passed="true"
322
+
323
+ write_evidence_record "$name" "webhook" "$passed" \
324
+ "$(jq -n --arg url "$url" --arg method "$method" \
325
+ --argjson status "$http_status" --argjson expected "$expected_status" \
326
+ '{url: $url, method: $method, http_status: $status, expected_status: $expected}')"
327
+ }
328
+
329
+ # ─── Custom: User-defined script execution ───────────────────────────────────
330
+
331
+ collect_custom() {
332
+ local name="$1"
333
+ local collector_json="$2"
334
+
335
+ local command_str expected_exit timeout
336
+ command_str=$(echo "$collector_json" | jq -r '.command // ""')
337
+ expected_exit=$(echo "$collector_json" | jq -r '.expectedExitCode // 0')
338
+ timeout=$(echo "$collector_json" | jq -r '.timeout // 60')
339
+
340
+ if [[ -z "$command_str" ]]; then
341
+ error "[custom] ${name}: no command specified"
342
+ write_evidence_record "$name" "custom" "false" '{"error": "no command specified"}'
343
+ return
344
+ fi
345
+
346
+ info "[custom] ${name}: ${command_str}"
347
+
348
+ local exit_code=0
349
+ local output=""
350
+ output=$(cd "$REPO_DIR" && _run_with_timeout "$timeout" bash -c "$command_str" 2>&1) || exit_code=$?
351
+
352
+ local passed="false"
353
+ [[ "$exit_code" -eq "$expected_exit" ]] && passed="true"
354
+
355
+ local output_preview="${output:0:2000}"
356
+
357
+ write_evidence_record "$name" "custom" "$passed" \
358
+ "$(jq -n --arg cmd "$command_str" --argjson exit_code "$exit_code" \
359
+ --argjson expected "$expected_exit" --arg output_preview "$output_preview" \
360
+ '{command: $cmd, exit_code: $exit_code, expected_exit_code: $expected, output_preview: $output_preview}')"
361
+ }
362
+
363
+ # ═════════════════════════════════════════════════════════════════════════════
364
+ # EVIDENCE RECORD WRITER
365
+ # ═════════════════════════════════════════════════════════════════════════════
366
+
367
+ write_evidence_record() {
368
+ local name="$1"
369
+ local type="$2"
370
+ local passed="$3"
371
+ local details="$4"
372
+
373
+ local evidence_file="${EVIDENCE_DIR}/${name}.json"
374
+ local captured_at
375
+ captured_at=$(now_iso)
376
+
377
+ jq -n --arg name "$name" --arg type "$type" --arg passed "$passed" \
378
+ --arg captured_at "$captured_at" --argjson captured_epoch "$(now_epoch)" \
379
+ --argjson details "$details" \
380
+ '{
381
+ name: $name,
382
+ type: $type,
383
+ passed: ($passed == "true"),
384
+ captured_at: $captured_at,
385
+ captured_epoch: $captured_epoch,
386
+ details: $details
387
+ }' > "$evidence_file"
388
+
389
+ if [[ "$passed" == "true" ]]; then
390
+ success "[${type}] ${name}: passed"
391
+ else
392
+ error "[${type}] ${name}: failed"
393
+ fi
394
+ }
395
+
396
+ # ═════════════════════════════════════════════════════════════════════════════
397
+ # COMMANDS
398
+ # ═════════════════════════════════════════════════════════════════════════════
399
+
400
+ cmd_capture() {
401
+ local type_filter="${1:-}"
402
+ ensure_evidence_dir
403
+
404
+ info "Capturing evidence${type_filter:+ (type: ${type_filter})}..."
405
+
406
+ local collectors
407
+ collectors=$(get_collectors "$type_filter")
408
+
409
+ if [[ -z "$collectors" ]]; then
410
+ warn "No evidence collectors defined in policy — nothing to capture"
411
+ return 0
412
+ fi
413
+
414
+ local total=0
415
+ local passed=0
416
+ local failed=0
417
+ local manifest_entries="[]"
418
+
419
+ while IFS= read -r collector; do
420
+ [[ -z "$collector" ]] && continue
421
+
422
+ local cname ctype
423
+ cname=$(echo "$collector" | jq -r '.name')
424
+ ctype=$(echo "$collector" | jq -r '.type')
425
+
426
+ case "$ctype" in
427
+ browser) collect_browser "$cname" "$collector" ;;
428
+ api) collect_api "$cname" "$collector" ;;
429
+ cli) collect_cli "$cname" "$collector" ;;
430
+ database) collect_database "$cname" "$collector" ;;
431
+ webhook) collect_webhook "$cname" "$collector" ;;
432
+ custom) collect_custom "$cname" "$collector" ;;
433
+ *) warn "Unknown collector type: ${ctype} (skipping ${cname})" ; continue ;;
434
+ esac
435
+
436
+ ((total++))
437
+
438
+ local evidence_file="${EVIDENCE_DIR}/${cname}.json"
439
+ local cpassed="false"
440
+ if [[ -f "$evidence_file" ]]; then
441
+ cpassed=$(jq -r '.passed' "$evidence_file" 2>/dev/null || echo "false")
442
+ fi
443
+
444
+ if [[ "$cpassed" == "true" ]]; then
445
+ ((passed++))
446
+ else
447
+ ((failed++))
448
+ fi
449
+
450
+ manifest_entries=$(echo "$manifest_entries" | jq \
451
+ --arg name "$cname" --arg type "$ctype" --arg file "$evidence_file" --arg passed "$cpassed" \
452
+ '. + [{"name": $name, "type": $type, "file": $file, "passed": ($passed == "true")}]')
453
+
454
+ done <<< "$collectors"
455
+
456
+ # Write manifest
457
+ jq -n --arg captured_at "$(now_iso)" --argjson captured_epoch "$(now_epoch)" \
458
+ --argjson total "$total" --argjson passed "$passed" --argjson failed "$failed" \
459
+ --argjson collectors "$manifest_entries" \
460
+ '{
461
+ captured_at: $captured_at,
462
+ captured_epoch: $captured_epoch,
463
+ collector_count: $total,
464
+ passed: $passed,
465
+ failed: $failed,
466
+ collectors: $collectors
467
+ }' > "$MANIFEST_FILE"
468
+
469
+ echo ""
470
+ if [[ "$failed" -eq 0 ]]; then
471
+ success "All ${total} collector(s) passed"
472
+ else
473
+ warn "${passed}/${total} passed, ${failed} failed"
474
+ fi
475
+
476
+ emit_event "evidence.captured" "total=${total}" "passed=${passed}" "failed=${failed}" "type=${type_filter:-all}"
477
+ }
478
+
479
+ cmd_verify() {
480
+ ensure_evidence_dir
481
+
482
+ if [[ ! -f "$MANIFEST_FILE" ]]; then
483
+ error "No evidence manifest found — run 'capture' first"
484
+ return 1
485
+ fi
486
+
487
+ info "Verifying evidence..."
488
+
489
+ local all_passed="true"
490
+ local checked=0
491
+ local failed=0
492
+
493
+ # Check freshness
494
+ local require_fresh
495
+ require_fresh=$(get_require_fresh)
496
+ local max_age_minutes
497
+ max_age_minutes=$(get_max_age_minutes)
498
+ local max_age_seconds=$((max_age_minutes * 60))
499
+
500
+ local captured_epoch
501
+ captured_epoch=$(jq -r '.captured_epoch' "$MANIFEST_FILE" 2>/dev/null || echo "0")
502
+ local current_epoch
503
+ current_epoch=$(now_epoch)
504
+ local age_seconds=$((current_epoch - captured_epoch))
505
+
506
+ if [[ "$require_fresh" == "true" && "$age_seconds" -gt "$max_age_seconds" ]]; then
507
+ error "Evidence is stale: captured ${age_seconds}s ago (max: ${max_age_seconds}s)"
508
+ all_passed="false"
509
+ ((failed++))
510
+ else
511
+ local age_minutes=$((age_seconds / 60))
512
+ info "Evidence age: ${age_minutes}m (max: ${max_age_minutes}m)"
513
+ fi
514
+
515
+ # Check all collectors in manifest
516
+ local collector_count
517
+ collector_count=$(jq -r '.collector_count' "$MANIFEST_FILE" 2>/dev/null || echo "0")
518
+
519
+ local collectors_json
520
+ collectors_json=$(jq -c '.collectors[]?' "$MANIFEST_FILE" 2>/dev/null)
521
+
522
+ while IFS= read -r entry; do
523
+ [[ -z "$entry" ]] && continue
524
+ ((checked++))
525
+
526
+ local cname ctype cpassed
527
+ cname=$(echo "$entry" | jq -r '.name')
528
+ ctype=$(echo "$entry" | jq -r '.type')
529
+ cpassed=$(echo "$entry" | jq -r '.passed')
530
+
531
+ if [[ "$cpassed" != "true" ]]; then
532
+ error "Collector '${cname}' (${ctype}) failed"
533
+ all_passed="false"
534
+ ((failed++))
535
+ else
536
+ success "Collector '${cname}' (${ctype}) passed"
537
+ fi
538
+ done <<< "$collectors_json"
539
+
540
+ echo ""
541
+ if [[ "$all_passed" == "true" ]]; then
542
+ success "All ${checked} evidence check(s) passed"
543
+ emit_event "evidence.verified" "total=${checked}" "result=pass"
544
+ return 0
545
+ else
546
+ error "${failed} of ${checked} evidence check(s) failed"
547
+ emit_event "evidence.verified" "total=${checked}" "result=fail" "failed=${failed}"
548
+ return 1
549
+ fi
550
+ }
551
+
552
+ cmd_pre_pr() {
553
+ local type_filter="${1:-}"
554
+ info "Running pre-PR evidence check..."
555
+ cmd_capture "$type_filter"
556
+ cmd_verify
557
+ }
558
+
559
+ cmd_status() {
560
+ ensure_evidence_dir
561
+
562
+ if [[ ! -f "$MANIFEST_FILE" ]]; then
563
+ warn "No evidence manifest found"
564
+ return 0
565
+ fi
566
+
567
+ local captured_at collector_count passed_count failed_count
568
+ captured_at=$(jq -r '.captured_at' "$MANIFEST_FILE" 2>/dev/null || echo "unknown")
569
+ collector_count=$(jq -r '.collector_count' "$MANIFEST_FILE" 2>/dev/null || echo "0")
570
+ passed_count=$(jq -r '.passed' "$MANIFEST_FILE" 2>/dev/null || echo "0")
571
+ failed_count=$(jq -r '.failed' "$MANIFEST_FILE" 2>/dev/null || echo "0")
572
+
573
+ echo "Evidence Status"
574
+ echo "━━━━━━━━━━━━━━━"
575
+ echo "Manifest: ${MANIFEST_FILE}"
576
+ echo "Captured at: ${captured_at}"
577
+ echo "Collectors: ${collector_count} (${passed_count} passed, ${failed_count} failed)"
578
+ echo ""
579
+
580
+ # Group by type
581
+ local types
582
+ types=$(jq -r '.collectors[].type' "$MANIFEST_FILE" 2>/dev/null | sort -u)
583
+
584
+ while IFS= read -r type; do
585
+ [[ -z "$type" ]] && continue
586
+ echo " ${type}:"
587
+ jq -r ".collectors[] | select(.type == \"${type}\") | \" \\(if .passed then \"✓\" else \"✗\" end) \\(.name)\"" "$MANIFEST_FILE" 2>/dev/null || true
588
+ done <<< "$types"
589
+ }
590
+
591
+ cmd_list_types() {
592
+ echo "Supported evidence types:"
593
+ echo ""
594
+ echo " browser HTTP page load — verifies UI renders correctly"
595
+ echo " api REST/GraphQL endpoint — verifies response status, body, content-type"
596
+ echo " database Schema/migration check — verifies DB integrity via command"
597
+ echo " cli Command execution — verifies exit code and output"
598
+ echo " webhook Callback verification — verifies webhook endpoint responds"
599
+ echo " custom User-defined script — any verification logic"
600
+ echo ""
601
+ echo "Configure collectors in config/policy.json under the 'evidence' section."
602
+ }
603
+
604
+ show_help() {
605
+ cat << 'EOF'
606
+ Usage: shipwright evidence <command> [args]
607
+
608
+ Commands:
609
+ capture [type] Capture evidence (optionally filter by type)
610
+ verify Verify evidence manifest and freshness
611
+ pre-pr [type] Capture + verify (run before PR creation)
612
+ status Show current evidence state grouped by type
613
+ types List supported evidence types
614
+
615
+ Evidence Types:
616
+ browser HTTP page load verification
617
+ api REST/GraphQL endpoint checks
618
+ database Schema/migration integrity
619
+ cli Command execution and exit code
620
+ webhook Callback endpoint verification
621
+ custom User-defined verification scripts
622
+
623
+ Evidence collectors are defined in config/policy.json under the
624
+ 'evidence.collectors' array. Each collector specifies a type,
625
+ target, and assertions.
626
+
627
+ Part of the Code Factory pattern for machine-verifiable proof.
628
+ EOF
629
+ }
630
+
631
+ main() {
632
+ local subcommand="${1:-help}"
633
+ shift || true
634
+
635
+ case "$subcommand" in
636
+ capture)
637
+ cmd_capture "$@"
638
+ ;;
639
+ verify)
640
+ cmd_verify "$@"
641
+ ;;
642
+ pre-pr)
643
+ cmd_pre_pr "$@"
644
+ ;;
645
+ status)
646
+ cmd_status
647
+ ;;
648
+ types)
649
+ cmd_list_types
650
+ ;;
651
+ help|--help|-h)
652
+ show_help
653
+ ;;
654
+ *)
655
+ error "Unknown subcommand: $subcommand"
656
+ show_help
657
+ return 1
658
+ ;;
659
+ esac
660
+ }
661
+
662
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
663
+ main "$@"
664
+ fi
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="2.3.1"
9
+ VERSION="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
package/scripts/sw-fix.sh CHANGED
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="2.3.1"
9
+ VERSION="2.4.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
12
12
  # ─── Cross-platform compatibility ──────────────────────────────────────────