bigpowers 2.73.7 → 2.73.9

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/.pi/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.73.7",
3
+ "version": "2.73.9",
4
4
  "description": "75 skills — 73 agent skills synthesizing 17 years of software engineering discipline into a prescriptive methodology for solo developers",
5
5
  "keywords": [
6
6
  "pi-package"
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [2.73.9](https://github.com/danielvm-git/bigpowers/compare/v2.73.8...v2.73.9) (2026-07-07)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **gate-trace:** split trace-matrix.py under 300-line cap ([ece8389](https://github.com/danielvm-git/bigpowers/commit/ece8389c815614d116d91b57dda9c67f6133caaf))
7
+
8
+ ## [2.73.8](https://github.com/danielvm-git/bigpowers/compare/v2.73.7...v2.73.8) (2026-07-07)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **audit-code:** split audit-compliance.sh into lib modules under 300-line cap ([#63](https://github.com/danielvm-git/bigpowers/issues/63)) ([7d5ea6e](https://github.com/danielvm-git/bigpowers/commit/7d5ea6e0f0c992f26b493b3843ede886770eed6f))
14
+
1
15
  ## [2.73.7](https://github.com/danielvm-git/bigpowers/compare/v2.73.6...v2.73.7) (2026-07-07)
2
16
 
3
17
 
package/CONVENTIONS.md CHANGED
@@ -291,13 +291,12 @@ window. The following files are documented exceptions:
291
291
 
292
292
  | File | Lines | Rationale | Review date |
293
293
  |------|-------|-----------|-------------|
294
- | `scripts/lib/trace-matrix.py` | 523 | Traceability matrix engine: tag scanning, oracle-confidence scoring, and JSON/markdown emission share tightly-coupled dataclasses and a single pass over the story index. Splitting would fragment the matrix model across modules. Split candidate for a future plan-refactor (BUG-2026-07-04-filesize-gate-python-blindspot); documented meanwhile. Also exceeds the 500-line cap. | 2026-10-01 |
295
- | `scripts/lib/trace-stories.py` | 301 | Story-tag discovery + file-to-story mapping; the phases share regex state and a common file walk. One line over cap; monitor for split alongside trace-matrix.py. | 2026-10-01 |
296
294
  | `scripts/audit-compliance.sh` | 300 | Gherkin harness with embedded judge logic and waiver subsystem; splitting the judge loop from the reporting layer would require fragile state-passing. At the 300-line cap; monitor. | 2026-10-01 |
297
295
 
298
296
  **Note:** `scripts/trace-stories.sh` was refactored from a 613-line monolith into a
299
- 69-line orchestrator delegating to the `scripts/lib/*.py` engine above — it is no
300
- longer an exception. The prior exception row (and its waiver) referenced a file
301
- that no longer exists; see BUG-2026-07-04-filesize-gate-python-blindspot.
297
+ 69-line orchestrator delegating to `scripts/lib/trace-stories.py`. `scripts/lib/trace-matrix.py`
298
+ was split into `simple_yaml.py` + `trace_renderer.py` (252 lines); see
299
+ BUG-2026-07-06-gate-trace-matrix-oversized. Prior exception rows for trace-matrix.py
300
+ and trace-stories.py are removed — both are now under the 300-line cap.
302
301
 
303
302
  Any new exception requires an entry in this table with rationale and a review date.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.73.7",
3
+ "version": "2.73.9",
4
4
  "description": "73 agent skills synthesizing 17 years of software engineering discipline into a prescriptive methodology for solo developers",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,6 +1,11 @@
1
1
  #!/usr/bin/env bash
2
2
  # audit-compliance.sh — Agentic Gherkin Compliance Harness (LLM-Judge Upgrade)
3
- show_help() {
3
+
4
+ _LIB="$(dirname "${BASH_SOURCE[0]}")/lib"
5
+ source "$_LIB/audit-compliance-runner.sh"
6
+ source "$_LIB/audit-compliance-report.sh"
7
+
8
+ audit_show_help() {
4
9
  cat <<EOF
5
10
  Usage: bash scripts/audit-compliance.sh [feature-file|directory] [options]
6
11
 
@@ -15,320 +20,45 @@ If a directory is provided, all .feature files in that directory will be process
15
20
  EOF
16
21
  }
17
22
 
18
- # --- Arguments Parsing ---
19
23
  DRY_RUN=false
20
24
  SCENARIO_FILTER=""
21
25
  JUDGE="binary"
22
26
  MODEL=""
23
27
  INPUTS=()
28
+ TOTAL_GLOBAL_PASS=0
29
+ TOTAL_GLOBAL_FAIL=0
30
+ TOTAL_GLOBAL_WAIVED=0
31
+ TOTAL_GLOBAL_EXPIRED=0
32
+ SCORE=0
24
33
 
25
34
  while [[ $# -gt 0 ]]; do
26
35
  case "$1" in
27
- --help)
28
- show_help
29
- exit 0
30
- ;;
31
- --dry-run)
32
- DRY_RUN=true
33
- shift
34
- ;;
35
- --scenario)
36
- SCENARIO_FILTER="$2"
37
- shift 2
38
- ;;
39
- --judge)
40
- JUDGE="$2"
41
- shift 2
42
- ;;
43
- --model)
44
- MODEL="$2"
45
- shift 2
46
- ;;
47
- -*)
48
- echo "Unknown option: $1"
49
- show_help
50
- exit 1
51
- ;;
52
- *)
53
- INPUTS+=("$1")
54
- shift
55
- ;;
36
+ --help) audit_show_help; exit 0 ;;
37
+ --dry-run) DRY_RUN=true; shift ;;
38
+ --scenario) SCENARIO_FILTER="$2"; shift 2 ;;
39
+ --judge) JUDGE="$2"; shift 2 ;;
40
+ --model) MODEL="$2"; shift 2 ;;
41
+ -*) echo "Unknown option: $1"; audit_show_help; exit 1 ;;
42
+ *) INPUTS+=("$1"); shift ;;
56
43
  esac
57
44
  done
58
45
 
59
46
  if [[ ${#INPUTS[@]} -eq 0 ]]; then
60
47
  echo "Error: No feature file or directory specified."
61
- show_help
48
+ audit_show_help
62
49
  exit 1
63
50
  fi
64
51
 
65
- # --- Global Stats ---
66
- TOTAL_GLOBAL_PASS=0
67
- TOTAL_GLOBAL_FAIL=0
68
- TOTAL_GLOBAL_WAIVED=0
69
- TOTAL_GLOBAL_EXPIRED=0
70
-
71
- # --- Waiver Subsystem ---
72
- # Sourced from scripts/lib/waiver-utils.sh — provides has_waiver().
73
- WAIVER_FILE="specs/verifications/waivers.yaml"
74
- true && source "$(dirname "${BASH_SOURCE[0]}")/lib/waiver-utils.sh"
75
-
76
- # --- Judging Logic ---
77
-
78
- judge_with_gemini() {
79
- local step="$1"
80
- local feature_name="$2"
81
- local scenario_name="$3"
82
- local evidence="$4"
83
- local report_file="$5"
84
-
85
- echo " [JUDGE] Sending evidence to Gemini CLI..."
86
-
87
- local prompt="You are the Master Test Architect judging a compliance audit.
88
- Benchmark Feature: $feature_name
89
- Scenario: $scenario_name
90
- Compliance Step: $step
91
-
92
- Evidence gathered from the codebase:
93
- ---
94
- $evidence
95
- ---
96
-
97
- Based on the benchmark principles, does this evidence demonstrate compliance?
98
- Respond strictly in the following format:
99
- VERDICT: [PASS/FAIL]
100
- RATIONALE: [One sentence explanation]"
101
-
102
- local model_output
103
- local gemini_cmd="gemini --approval-mode plan"
104
- if [[ -n "$MODEL" ]]; then
105
- gemini_cmd="$gemini_cmd -m $MODEL"
106
- fi
107
-
108
- model_output=$($gemini_cmd -p "$prompt" 2>&1)
109
- local exit_code=$?
110
-
111
- if [[ $exit_code -eq 0 ]]; then
112
- local verdict
113
- verdict=$(echo "$model_output" | grep "VERDICT:" | cut -d' ' -f2)
114
- local rationale
115
- rationale=$(echo "$model_output" | grep "RATIONALE:" | cut -d' ' -f2-)
116
-
117
- if [[ "$verdict" == "PASS" ]]; then
118
- echo " Result: PASS"
119
- echo "- [x] $step (PASS) - $rationale" >> "$report_file"
120
- return 0
121
- else
122
- echo " Result: FAIL"
123
- echo "- [ ] $step (FAIL) - $rationale" >> "$report_file"
124
- return 1
125
- fi
126
- else
127
- echo " Result: ERROR (Gemini CLI failed)"
128
- echo "- [ ] $step (ERROR) - Gemini CLI exit code $exit_code. Output: $model_output" >> "$report_file"
129
- return 1
130
- fi
131
- }
132
-
133
-
134
-
135
- # --- Core Execution ---
136
-
137
- process_step() {
138
- local step="$1"
139
- local feature_name="$2"
140
- local scenario_name="$3"
141
- local report_file="$4"
142
- echo " [STEP] $step"
143
-
144
- if [[ "$DRY_RUN" == "true" ]]; then
145
- return 0
146
- fi
147
-
148
- local sanitized_step
149
- sanitized_step=$(echo "$step" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
150
- local step_script="specs/verifications/steps/${sanitized_step}.sh"
151
-
152
- if [[ -f "$step_script" ]]; then
153
- echo " [EXEC] Gathering evidence: $step_script"
154
- local evidence
155
- evidence=$(bash "$step_script" 2>&1)
156
- local exit_code=$?
157
-
158
- if [[ "$JUDGE" == "gemini" ]]; then
159
- judge_with_gemini "$step" "$feature_name" "$scenario_name" "$evidence" "$report_file"
160
- # Gemini path: rely on judge return code (no waiver check for LLM path)
161
- else
162
- # Binary path: check waivers before counting as FAIL
163
- if [[ $exit_code -eq 0 ]]; then
164
- echo " Result: PASS"
165
- echo "- [x] $step (PASS)" >> "$report_file"
166
- return 0
167
- fi
168
-
169
- # Step failed — check if it has a valid waiver
170
- has_waiver "$sanitized_step"
171
- local waiver_code=$?
172
-
173
- if [[ $waiver_code -eq 0 ]]; then
174
- # Valid waiver — exclude from denominator
175
- echo " Result: WAIVED (documented exception)"
176
- echo "- [~] $step (WAIVED)" >> "$report_file"
177
- ((TOTAL_GLOBAL_WAIVED++))
178
- return 3 # Special code: waived (not PASS, not FAIL)
179
- elif [[ $waiver_code -eq 2 ]]; then
180
- # Expired waiver — re-enter denominator as FAIL
181
- echo " Result: FAIL (EXPIRED WAIVER)"
182
- echo "- [ ] $step (FAIL) - EXPIRED WAIVER: review_date has passed" >> "$report_file"
183
- ((TOTAL_GLOBAL_EXPIRED++))
184
- ((TOTAL_GLOBAL_FAIL++))
185
- return 1
186
- else
187
- # No waiver — genuine FAIL
188
- echo " Result: FAIL"
189
- echo "- [ ] $step (FAIL) - $evidence" >> "$report_file"
190
- ((TOTAL_GLOBAL_FAIL++))
191
- return 1
192
- fi
193
- fi
194
- else
195
- echo " Result: FAIL (Missing evidence: $step_script)"
196
- echo "- [ ] $step (FAIL) - No verification script found at $step_script" >> "$report_file"
197
- ((TOTAL_GLOBAL_FAIL++))
198
- return 1
199
- fi
200
- }
201
-
202
- run_audit_file() {
203
- local FEATURE_FILE="$1"
204
- echo "------------------------------------------------------------"
205
- echo "FEATURE: $FEATURE_FILE"
206
-
207
- local REPORT_FILE="specs/verifications/reports/audit-$(basename "$FEATURE_FILE" .feature)-$(date +%Y%m%d-%H%M%S).md"
208
- mkdir -p specs/verifications/reports
209
-
210
- echo "# Audit Report: $FEATURE_FILE" > "$REPORT_FILE"
211
- echo "Date: $(date)" >> "$REPORT_FILE"
212
- echo "Mode: Autonomous Verification (Judge: $JUDGE)" >> "$REPORT_FILE"
213
- echo "" >> "$REPORT_FILE"
214
-
215
- local TOTAL_PASS=0
216
- local TOTAL_FAIL=0
217
- local CURRENT_FEATURE=""
218
- local CURRENT_SCENARIO=""
219
- local IN_SCENARIO=false
220
-
221
- while IFS= read -r line; do
222
- line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
223
-
224
- if [[ "$line" =~ ^Feature: ]]; then
225
- CURRENT_FEATURE="${line#Feature: }"
226
- echo "FEATURE: $CURRENT_FEATURE"
227
- echo "## Feature: $CURRENT_FEATURE" >> "$REPORT_FILE"
228
- elif [[ "$line" =~ ^Scenario: ]]; then
229
- CURRENT_SCENARIO="${line#Scenario: }"
230
- if [[ -n "$SCENARIO_FILTER" && "$CURRENT_SCENARIO" != "$SCENARIO_FILTER" ]]; then
231
- IN_SCENARIO=false
232
- continue
233
- fi
234
- IN_SCENARIO=true
235
- echo " SCENARIO: $CURRENT_SCENARIO"
236
- echo "### Scenario: $CURRENT_SCENARIO" >> "$REPORT_FILE"
237
- elif [[ "$line" =~ ^(Given|When|Then|And|But)\ ]]; then
238
- if [[ "$IN_SCENARIO" == "true" ]]; then
239
- local step_result
240
- process_step "$line" "$CURRENT_FEATURE" "$CURRENT_SCENARIO" "$REPORT_FILE"
241
- step_result=$?
242
- if [[ $step_result -eq 3 ]]; then
243
- # Waived — excluded from both pass and fail counts
244
- :
245
- elif [[ $step_result -eq 0 ]]; then
246
- ((TOTAL_PASS++))
247
- ((TOTAL_GLOBAL_PASS++))
248
- else
249
- ((TOTAL_FAIL++))
250
- fi
251
- fi
252
- fi
253
- done < "$FEATURE_FILE"
254
-
255
- echo ""
256
- echo "File Summary: PASS: $TOTAL_PASS, FAIL: $TOTAL_FAIL"
257
- echo "Report saved to: $REPORT_FILE"
258
- }
259
-
260
- # --- Main Execution ---
261
52
  for input in "${INPUTS[@]}"; do
262
53
  if [[ -d "$input" ]]; then
263
54
  for f in "$input"/*.feature; do
264
- if [[ -f "$f" ]]; then
265
- run_audit_file "$f"
266
- fi
55
+ [[ -f "$f" ]] && audit_run_file "$f"
267
56
  done
268
57
  elif [[ -f "$input" ]]; then
269
- run_audit_file "$input"
58
+ audit_run_file "$input"
270
59
  else
271
60
  echo "Warning: Input not found: $input"
272
61
  fi
273
62
  done
274
63
 
275
- echo "============================================================"
276
- echo "Global Audit Summary:"
277
- TOTAL_GLOBAL_ALL=$((TOTAL_GLOBAL_PASS + TOTAL_GLOBAL_FAIL))
278
- echo " TOTAL PASS: $TOTAL_GLOBAL_PASS"
279
- echo " TOTAL FAIL: $TOTAL_GLOBAL_FAIL"
280
- echo " TOTAL WAIVED: $TOTAL_GLOBAL_WAIVED (excluded from denominator)"
281
- if [[ $TOTAL_GLOBAL_EXPIRED -gt 0 ]]; then
282
- echo " EXPIRED WAIVERS: $TOTAL_GLOBAL_EXPIRED (counted as FAIL)"
283
- fi
284
- if [[ $TOTAL_GLOBAL_ALL -gt 0 ]]; then
285
- SCORE=$(awk "BEGIN { printf \"%d\", $TOTAL_GLOBAL_PASS * 100 / $TOTAL_GLOBAL_ALL }")
286
- else
287
- SCORE=0
288
- fi
289
-
290
- # ── OKF verification-report (e45s02) ────────────────────────────────────
291
- OKF_DIR="specs/verifications/reports"
292
- mkdir -p "$OKF_DIR"
293
- OKF_FILE="$OKF_DIR/audit-$(date +%Y-%m-%d).okf.md"
294
- TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
295
- GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
296
- GATE=$(if [[ $SCORE -ge 94 ]]; then echo "pass"; else echo "fail"; fi)
297
-
298
- cat > "$OKF_FILE" << OKF_EOF
299
- ---
300
- okf_kind: verification-report
301
- okf_version: "0.1"
302
- score: ${SCORE}
303
- gate_status: "${GATE}"
304
- threshold: 94
305
- total_pass: ${TOTAL_GLOBAL_PASS}
306
- total_fail: ${TOTAL_GLOBAL_FAIL}
307
- total_waived: ${TOTAL_GLOBAL_WAIVED}
308
- generated_by: scripts/audit-compliance.sh
309
- generated_at: "${TIMESTAMP}"
310
- git_commit: "${GIT_COMMIT}"
311
- ---
312
-
313
- # Compliance Audit — $(date +%Y-%m-%d)
314
-
315
- **Score:** ${SCORE}% (${TOTAL_GLOBAL_PASS}/${TOTAL_GLOBAL_ALL} passed, ${TOTAL_GLOBAL_WAIVED} waived)
316
- **Gate:** ${GATE} | **Threshold:** 94%
317
-
318
- See \`specs/verifications/reports/audit-*.md\` for detailed step-by-step reports.
319
- OKF_EOF
320
-
321
- echo "OKF report: $OKF_FILE"
322
-
323
- if [[ $TOTAL_GLOBAL_WAIVED -gt 0 ]]; then
324
- echo " SCORE: ${SCORE}% (of ${TOTAL_GLOBAL_ALL} unwaived checks, threshold 94%)"
325
- else
326
- echo " SCORE: ${SCORE}% (threshold 94%)"
327
- fi
328
- if [[ $SCORE -ge 94 ]]; then
329
- echo " GATE: PASS"
330
- exit 0
331
- else
332
- echo " GATE: FAIL (below 94%)"
333
- exit 1
334
- fi
64
+ audit_print_summary_and_exit
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ # story: e45s02
3
+ # Gemini LLM judge for audit-compliance.sh
4
+
5
+ if [ -n "${AUDIT_COMPLIANCE_JUDGE_LOADED:-}" ]; then return 0; fi
6
+ AUDIT_COMPLIANCE_JUDGE_LOADED=1
7
+
8
+ audit_judge_with_gemini() {
9
+ local step="$1" feature_name="$2" scenario_name="$3" evidence="$4" report_file="$5"
10
+ local prompt model_output gemini_cmd exit_code verdict rationale
11
+
12
+ echo " [JUDGE] Sending evidence to Gemini CLI..."
13
+
14
+ prompt="You are the Master Test Architect judging a compliance audit.
15
+ Benchmark Feature: $feature_name
16
+ Scenario: $scenario_name
17
+ Compliance Step: $step
18
+
19
+ Evidence gathered from the codebase:
20
+ ---
21
+ $evidence
22
+ ---
23
+
24
+ Based on the benchmark principles, does this evidence demonstrate compliance?
25
+ Respond strictly in the following format:
26
+ VERDICT: [PASS/FAIL]
27
+ RATIONALE: [One sentence explanation]"
28
+
29
+ gemini_cmd="gemini --approval-mode plan"
30
+ [[ -n "$MODEL" ]] && gemini_cmd="$gemini_cmd -m $MODEL"
31
+
32
+ model_output=$($gemini_cmd -p "$prompt" 2>&1)
33
+ exit_code=$?
34
+
35
+ if [[ $exit_code -eq 0 ]]; then
36
+ verdict=$(echo "$model_output" | grep "VERDICT:" | cut -d' ' -f2)
37
+ rationale=$(echo "$model_output" | grep "RATIONALE:" | cut -d' ' -f2-)
38
+ if [[ "$verdict" == "PASS" ]]; then
39
+ echo " Result: PASS"
40
+ echo "- [x] $step (PASS) - $rationale" >> "$report_file"
41
+ return 0
42
+ fi
43
+ echo " Result: FAIL"
44
+ echo "- [ ] $step (FAIL) - $rationale" >> "$report_file"
45
+ return 1
46
+ fi
47
+
48
+ echo " Result: ERROR (Gemini CLI failed)"
49
+ echo "- [ ] $step (ERROR) - Gemini CLI exit code $exit_code. Output: $model_output" >> "$report_file"
50
+ return 1
51
+ }
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ # story: e45s02
3
+ # OKF report and gate verdict for audit-compliance.sh
4
+
5
+ if [ -n "${AUDIT_COMPLIANCE_REPORT_LOADED:-}" ]; then return 0; fi
6
+ AUDIT_COMPLIANCE_REPORT_LOADED=1
7
+
8
+ audit_write_okf_report() {
9
+ local OKF_DIR OKF_FILE TIMESTAMP GIT_COMMIT GATE TOTAL_GLOBAL_ALL
10
+
11
+ TOTAL_GLOBAL_ALL=$((TOTAL_GLOBAL_PASS + TOTAL_GLOBAL_FAIL))
12
+ if [[ $TOTAL_GLOBAL_ALL -gt 0 ]]; then
13
+ SCORE=$(awk "BEGIN { printf \"%d\", $TOTAL_GLOBAL_PASS * 100 / $TOTAL_GLOBAL_ALL }")
14
+ else
15
+ SCORE=0
16
+ fi
17
+
18
+ OKF_DIR="specs/verifications/reports"
19
+ mkdir -p "$OKF_DIR"
20
+ OKF_FILE="$OKF_DIR/audit-$(date +%Y-%m-%d).okf.md"
21
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
22
+ GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
23
+ GATE=$(if [[ $SCORE -ge 94 ]]; then echo "pass"; else echo "fail"; fi)
24
+
25
+ cat > "$OKF_FILE" << OKF_EOF
26
+ ---
27
+ okf_kind: verification-report
28
+ okf_version: "0.1"
29
+ score: ${SCORE}
30
+ gate_status: "${GATE}"
31
+ threshold: 94
32
+ total_pass: ${TOTAL_GLOBAL_PASS}
33
+ total_fail: ${TOTAL_GLOBAL_FAIL}
34
+ total_waived: ${TOTAL_GLOBAL_WAIVED}
35
+ generated_by: scripts/audit-compliance.sh
36
+ generated_at: "${TIMESTAMP}"
37
+ git_commit: "${GIT_COMMIT}"
38
+ ---
39
+
40
+ # Compliance Audit — $(date +%Y-%m-%d)
41
+
42
+ **Score:** ${SCORE}% (${TOTAL_GLOBAL_PASS}/${TOTAL_GLOBAL_ALL} passed, ${TOTAL_GLOBAL_WAIVED} waived)
43
+ **Gate:** ${GATE} | **Threshold:** 94%
44
+
45
+ See \`specs/verifications/reports/audit-*.md\` for detailed step-by-step reports.
46
+ OKF_EOF
47
+
48
+ echo "OKF report: $OKF_FILE"
49
+ }
50
+
51
+ audit_print_summary_and_exit() {
52
+ local TOTAL_GLOBAL_ALL=$((TOTAL_GLOBAL_PASS + TOTAL_GLOBAL_FAIL))
53
+
54
+ echo "============================================================"
55
+ echo "Global Audit Summary:"
56
+ echo " TOTAL PASS: $TOTAL_GLOBAL_PASS"
57
+ echo " TOTAL FAIL: $TOTAL_GLOBAL_FAIL"
58
+ echo " TOTAL WAIVED: $TOTAL_GLOBAL_WAIVED (excluded from denominator)"
59
+ [[ $TOTAL_GLOBAL_EXPIRED -gt 0 ]] && echo " EXPIRED WAIVERS: $TOTAL_GLOBAL_EXPIRED (counted as FAIL)"
60
+
61
+ audit_write_okf_report
62
+
63
+ if [[ $TOTAL_GLOBAL_WAIVED -gt 0 ]]; then
64
+ echo " SCORE: ${SCORE}% (of ${TOTAL_GLOBAL_ALL} unwaived checks, threshold 94%)"
65
+ else
66
+ echo " SCORE: ${SCORE}% (threshold 94%)"
67
+ fi
68
+
69
+ if [[ $SCORE -ge 94 ]]; then
70
+ echo " GATE: PASS"
71
+ exit 0
72
+ fi
73
+ echo " GATE: FAIL (below 94%)"
74
+ exit 1
75
+ }
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env bash
2
+ # story: e45s02
3
+ # Feature-file runner for audit-compliance.sh
4
+
5
+ if [ -n "${AUDIT_COMPLIANCE_RUNNER_LOADED:-}" ]; then return 0; fi
6
+ AUDIT_COMPLIANCE_RUNNER_LOADED=1
7
+
8
+ source "$(dirname "${BASH_SOURCE[0]}")/audit-compliance-judge.sh"
9
+ WAIVER_FILE="specs/verifications/waivers.yaml"
10
+ true && source "$(dirname "${BASH_SOURCE[0]}")/waiver-utils.sh"
11
+
12
+ audit_process_step() {
13
+ local step="$1" feature_name="$2" scenario_name="$3" report_file="$4"
14
+ local sanitized_step step_script evidence exit_code waiver_code
15
+
16
+ echo " [STEP] $step"
17
+ [[ "$DRY_RUN" == "true" ]] && return 0
18
+
19
+ sanitized_step=$(echo "$step" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//')
20
+ step_script="specs/verifications/steps/${sanitized_step}.sh"
21
+
22
+ if [[ ! -f "$step_script" ]]; then
23
+ echo " Result: FAIL (Missing evidence: $step_script)"
24
+ echo "- [ ] $step (FAIL) - No verification script found at $step_script" >> "$report_file"
25
+ ((TOTAL_GLOBAL_FAIL++))
26
+ return 1
27
+ fi
28
+
29
+ echo " [EXEC] Gathering evidence: $step_script"
30
+ evidence=$(bash "$step_script" 2>&1)
31
+ exit_code=$?
32
+
33
+ if [[ "$JUDGE" == "gemini" ]]; then
34
+ audit_judge_with_gemini "$step" "$feature_name" "$scenario_name" "$evidence" "$report_file"
35
+ return $?
36
+ fi
37
+
38
+ if [[ $exit_code -eq 0 ]]; then
39
+ echo " Result: PASS"
40
+ echo "- [x] $step (PASS)" >> "$report_file"
41
+ return 0
42
+ fi
43
+
44
+ has_waiver "$sanitized_step"
45
+ waiver_code=$?
46
+
47
+ if [[ $waiver_code -eq 0 ]]; then
48
+ echo " Result: WAIVED (documented exception)"
49
+ echo "- [~] $step (WAIVED)" >> "$report_file"
50
+ ((TOTAL_GLOBAL_WAIVED++))
51
+ return 3
52
+ fi
53
+ if [[ $waiver_code -eq 2 ]]; then
54
+ echo " Result: FAIL (EXPIRED WAIVER)"
55
+ echo "- [ ] $step (FAIL) - EXPIRED WAIVER: review_date has passed" >> "$report_file"
56
+ ((TOTAL_GLOBAL_EXPIRED++))
57
+ ((TOTAL_GLOBAL_FAIL++))
58
+ return 1
59
+ fi
60
+
61
+ echo " Result: FAIL"
62
+ echo "- [ ] $step (FAIL) - $evidence" >> "$report_file"
63
+ ((TOTAL_GLOBAL_FAIL++))
64
+ return 1
65
+ }
66
+
67
+ audit_run_file() {
68
+ local FEATURE_FILE="$1" REPORT_FILE line TOTAL_PASS TOTAL_FAIL step_result
69
+ local CURRENT_FEATURE="" CURRENT_SCENARIO="" IN_SCENARIO=false
70
+
71
+ echo "------------------------------------------------------------"
72
+ echo "FEATURE: $FEATURE_FILE"
73
+
74
+ REPORT_FILE="specs/verifications/reports/audit-$(basename "$FEATURE_FILE" .feature)-$(date +%Y%m%d-%H%M%S).md"
75
+ mkdir -p specs/verifications/reports
76
+
77
+ {
78
+ echo "# Audit Report: $FEATURE_FILE"
79
+ echo "Date: $(date)"
80
+ echo "Mode: Autonomous Verification (Judge: $JUDGE)"
81
+ echo ""
82
+ } > "$REPORT_FILE"
83
+
84
+ TOTAL_PASS=0
85
+ TOTAL_FAIL=0
86
+
87
+ while IFS= read -r line; do
88
+ line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
89
+
90
+ if [[ "$line" =~ ^Feature: ]]; then
91
+ CURRENT_FEATURE="${line#Feature: }"
92
+ echo "FEATURE: $CURRENT_FEATURE"
93
+ echo "## Feature: $CURRENT_FEATURE" >> "$REPORT_FILE"
94
+ elif [[ "$line" =~ ^Scenario: ]]; then
95
+ CURRENT_SCENARIO="${line#Scenario: }"
96
+ if [[ -n "$SCENARIO_FILTER" && "$CURRENT_SCENARIO" != "$SCENARIO_FILTER" ]]; then
97
+ IN_SCENARIO=false
98
+ continue
99
+ fi
100
+ IN_SCENARIO=true
101
+ echo " SCENARIO: $CURRENT_SCENARIO"
102
+ echo "### Scenario: $CURRENT_SCENARIO" >> "$REPORT_FILE"
103
+ elif [[ "$line" =~ ^(Given|When|Then|And|But)\ ]]; then
104
+ [[ "$IN_SCENARIO" != "true" ]] && continue
105
+ audit_process_step "$line" "$CURRENT_FEATURE" "$CURRENT_SCENARIO" "$REPORT_FILE"
106
+ step_result=$?
107
+ if [[ $step_result -eq 3 ]]; then
108
+ :
109
+ elif [[ $step_result -eq 0 ]]; then
110
+ ((TOTAL_PASS++))
111
+ ((TOTAL_GLOBAL_PASS++))
112
+ else
113
+ ((TOTAL_FAIL++))
114
+ fi
115
+ fi
116
+ done < "$FEATURE_FILE"
117
+
118
+ echo ""
119
+ echo "File Summary: PASS: $TOTAL_PASS, FAIL: $TOTAL_FAIL"
120
+ echo "Report saved to: $REPORT_FILE"
121
+ }
@@ -0,0 +1,130 @@
1
+ # story: e38s01
2
+ # simple_yaml.py — Custom lightweight YAML parser extracted from trace-matrix.py.
3
+ import re
4
+
5
+ def parse_simple_yaml(text: str) -> dict:
6
+ """Parse flat and one-level-nested YAML including lists of objects."""
7
+ root: dict = {}
8
+ # Each stack entry: (indent, container, parent_dict, key_in_parent)
9
+ stack: list = [(0, root, None, None)]
10
+ skip_until_indent = None # skip block scalar continuation lines (| and >)
11
+ for i, raw in enumerate(text.splitlines()):
12
+ stripped = raw.strip()
13
+ if not stripped or stripped.startswith("#"):
14
+ # Reset block-scalar skip on blank lines
15
+ continue
16
+ indent = len(raw) - len(raw.lstrip())
17
+
18
+ # Skip block scalar continuation (| and > in YAML)
19
+ if skip_until_indent is not None:
20
+ if indent > skip_until_indent:
21
+ continue
22
+ else:
23
+ skip_until_indent = None
24
+
25
+ # Pop stack until we're at the right nesting level
26
+ # Don't pop list containers at same indent — sibling items stay in same list
27
+ while len(stack) > 1:
28
+ if indent < stack[-1][0]:
29
+ stack.pop()
30
+ elif indent == stack[-1][0]:
31
+ # Same indent: pop dicts (moving to sibling key), keep lists (sibling items)
32
+ if isinstance(stack[-1][1], list) and stripped.startswith("- "):
33
+ break # stay in same list for next item
34
+ elif isinstance(stack[-1][1], dict) and stripped.startswith("- "):
35
+ # dict at same indent with upcoming list item — convert to list
36
+ entry_container = stack[-1][1]
37
+ entry_parent = stack[-1][2]
38
+ entry_key = stack[-1][3]
39
+ if entry_parent is not None and entry_key is not None:
40
+ if not isinstance(entry_parent.get(entry_key), list):
41
+ entry_parent[entry_key] = []
42
+ stack[-1] = (stack[-1][0], entry_parent[entry_key], entry_parent, entry_key)
43
+ break
44
+ stack.pop()
45
+ else:
46
+ stack.pop() # other same-indent transitions
47
+ else:
48
+ break # indent > stack indent — staying inside
49
+
50
+ container = stack[-1][1]
51
+ parent_dict = stack[-1][2]
52
+ key_in_parent = stack[-1][3]
53
+
54
+ # List item
55
+ if stripped.startswith("- "):
56
+ inner = stripped[2:]
57
+ # Convert dict to list if needed (preserve original indent from dict push)
58
+ if isinstance(container, dict) and parent_dict is not None and key_in_parent is not None:
59
+ if not isinstance(parent_dict.get(key_in_parent), list):
60
+ orig_indent = stack[-1][0]
61
+ parent_dict[key_in_parent] = []
62
+ container = parent_dict[key_in_parent]
63
+ stack[-1] = (orig_indent, container, parent_dict, key_in_parent)
64
+
65
+ if ":" in inner:
66
+ k, _, v = inner.partition(":")
67
+ k = k.strip()
68
+ v = v.strip()
69
+ # Detect block scalars in list items too
70
+ if v in ("|", ">", "|-", ">-", "|+", ">+"):
71
+ item = {k: v}
72
+ container.append(item)
73
+ skip_until_indent = indent
74
+ continue
75
+ item = {k: _yaml_scalar(v) if v else None}
76
+ container.append(item)
77
+ if v == "":
78
+ stack.append((indent, item, container, k))
79
+ else:
80
+ container.append(_yaml_scalar(inner))
81
+ continue
82
+
83
+ # Key: value
84
+ if ":" not in stripped:
85
+ continue
86
+ key, _, val = stripped.partition(":")
87
+ key = key.strip()
88
+ val = val.strip()
89
+
90
+ # Detect block scalars (|, >, |- etc.)
91
+ if val in ("|", ">", "|-", ">-", "|+", ">+"):
92
+ if isinstance(container, list) and container:
93
+ container[-1][key] = val # store the marker
94
+ elif isinstance(container, dict):
95
+ container[key] = val
96
+ skip_until_indent = indent
97
+ continue
98
+
99
+ if isinstance(container, list) and container:
100
+ # Inside a list item dict
101
+ container[-1][key] = _yaml_scalar(val) if val else None
102
+ if val == "":
103
+ nxt = {}
104
+ container[-1][key] = nxt
105
+ stack.append((indent, nxt, container[-1], key))
106
+ elif isinstance(container, dict):
107
+ if val == "":
108
+ # Store as dict first; will convert to list if next line is "-"
109
+ nxt = {}
110
+ container[key] = nxt
111
+ stack.append((indent, nxt, container, key))
112
+ else:
113
+ container[key] = _yaml_scalar(val)
114
+ return root
115
+
116
+
117
+ def _yaml_scalar(val: str):
118
+ """Convert a YAML scalar string to Python type."""
119
+ val = val.strip('"').strip("'")
120
+ if val in ("true", "false"):
121
+ return val == "true"
122
+ if val in ("null", "~", ""):
123
+ return None
124
+ try:
125
+ return int(val)
126
+ except ValueError:
127
+ try:
128
+ return float(val)
129
+ except ValueError:
130
+ return val
@@ -12,6 +12,12 @@ import json, os, re, sys, time
12
12
  from pathlib import Path
13
13
  from datetime import datetime, timezone
14
14
 
15
+ # Add parent or current directory to sys.path so we can import local helper modules
16
+ sys.path.insert(0, str(Path(__file__).parent))
17
+
18
+ from simple_yaml import parse_simple_yaml
19
+ from trace_renderer import emit_matrix_json, emit_trace_md, emit_okf_bundle
20
+
15
21
  ROOT = Path(sys.argv[1])
16
22
  MATRIX_JSON = Path(sys.argv[2])
17
23
  TRACE_MD = Path(sys.argv[3])
@@ -19,136 +25,6 @@ OKF_DIR = Path(sys.argv[4])
19
25
  STRICT = int(sys.argv[5])
20
26
  MODE = sys.argv[6]
21
27
 
22
- # -----------------------------------------------------------------------
23
- # 1. Parse release-plan.yaml → story inventory
24
- # -----------------------------------------------------------------------
25
- def parse_simple_yaml(text: str) -> dict:
26
- """Parse flat and one-level-nested YAML including lists of objects."""
27
- root: dict = {}
28
- # Each stack entry: (indent, container, parent_dict, key_in_parent)
29
- stack: list = [(0, root, None, None)]
30
- skip_until_indent = None # skip block scalar continuation lines (| and >)
31
- for i, raw in enumerate(text.splitlines()):
32
- stripped = raw.strip()
33
- if not stripped or stripped.startswith("#"):
34
- # Reset block-scalar skip on blank lines
35
- continue
36
- indent = len(raw) - len(raw.lstrip())
37
-
38
- # Skip block scalar continuation (| and > in YAML)
39
- if skip_until_indent is not None:
40
- if indent > skip_until_indent:
41
- continue
42
- else:
43
- skip_until_indent = None
44
-
45
- # Pop stack until we're at the right nesting level
46
- # Don't pop list containers at same indent — sibling items stay in same list
47
- while len(stack) > 1:
48
- if indent < stack[-1][0]:
49
- stack.pop()
50
- elif indent == stack[-1][0]:
51
- # Same indent: pop dicts (moving to sibling key), keep lists (sibling items)
52
- if isinstance(stack[-1][1], list) and stripped.startswith("- "):
53
- break # stay in same list for next item
54
- elif isinstance(stack[-1][1], dict) and stripped.startswith("- "):
55
- # dict at same indent with upcoming list item — convert to list
56
- entry_container = stack[-1][1]
57
- entry_parent = stack[-1][2]
58
- entry_key = stack[-1][3]
59
- if entry_parent is not None and entry_key is not None:
60
- if not isinstance(entry_parent.get(entry_key), list):
61
- entry_parent[entry_key] = []
62
- stack[-1] = (stack[-1][0], entry_parent[entry_key], entry_parent, entry_key)
63
- break
64
- stack.pop()
65
- else:
66
- stack.pop() # other same-indent transitions
67
- else:
68
- break # indent > stack indent — staying inside
69
-
70
- container = stack[-1][1]
71
- parent_dict = stack[-1][2]
72
- key_in_parent = stack[-1][3]
73
-
74
- # List item
75
- if stripped.startswith("- "):
76
- inner = stripped[2:]
77
- # Convert dict to list if needed (preserve original indent from dict push)
78
- if isinstance(container, dict) and parent_dict is not None and key_in_parent is not None:
79
- if not isinstance(parent_dict.get(key_in_parent), list):
80
- orig_indent = stack[-1][0]
81
- parent_dict[key_in_parent] = []
82
- container = parent_dict[key_in_parent]
83
- stack[-1] = (orig_indent, container, parent_dict, key_in_parent)
84
-
85
- if ":" in inner:
86
- k, _, v = inner.partition(":")
87
- k = k.strip()
88
- v = v.strip()
89
- # Detect block scalars in list items too
90
- if v in ("|", ">", "|-", ">-", "|+", ">+"):
91
- item = {k: v}
92
- container.append(item)
93
- skip_until_indent = indent
94
- continue
95
- item = {k: _yaml_scalar(v) if v else None}
96
- container.append(item)
97
- if v == "":
98
- stack.append((indent, item, container, k))
99
- else:
100
- container.append(_yaml_scalar(inner))
101
- continue
102
-
103
- # Key: value
104
- if ":" not in stripped:
105
- continue
106
- key, _, val = stripped.partition(":")
107
- key = key.strip()
108
- val = val.strip()
109
-
110
- # Detect block scalars (|, >, |- etc.)
111
- if val in ("|", ">", "|-", ">-", "|+", ">+"):
112
- if isinstance(container, list) and container:
113
- container[-1][key] = val # store the marker
114
- elif isinstance(container, dict):
115
- container[key] = val
116
- skip_until_indent = indent
117
- continue
118
-
119
- if isinstance(container, list) and container:
120
- # Inside a list item dict
121
- container[-1][key] = _yaml_scalar(val) if val else None
122
- if val == "":
123
- nxt = {}
124
- container[-1][key] = nxt
125
- stack.append((indent, nxt, container[-1], key))
126
- elif isinstance(container, dict):
127
- if val == "":
128
- # Store as dict first; will convert to list if next line is "-"
129
- nxt = {}
130
- container[key] = nxt
131
- stack.append((indent, nxt, container, key))
132
- else:
133
- container[key] = _yaml_scalar(val)
134
- return root
135
-
136
-
137
- def _yaml_scalar(val: str):
138
- """Convert a YAML scalar string to Python type."""
139
- val = val.strip('"').strip("'")
140
- if val in ("true", "false"):
141
- return val == "true"
142
- if val in ("null", "~", ""):
143
- return None
144
- try:
145
- return int(val)
146
- except ValueError:
147
- try:
148
- return float(val)
149
- except ValueError:
150
- return val
151
-
152
28
  # Parse release plan for epics/stories
153
29
  release_plan_path = ROOT / "specs" / "release-plan.yaml"
154
30
  release_text = release_plan_path.read_text(encoding="utf-8")
@@ -248,10 +124,6 @@ for line in result.stdout.splitlines():
248
124
  # -----------------------------------------------------------------------
249
125
  # 4. Resolve each story through oracle tiers
250
126
  # -----------------------------------------------------------------------
251
- # Tier 1: explicit story tag (already captured above)
252
- # Tier 2: file-name heuristic match
253
- # Tier 3: epic capsule task reference
254
-
255
127
  # Collect all file basenames for heuristic matching
256
128
  all_files = []
257
129
  for root_dir, dirs, files in os.walk(str(ROOT)):
@@ -353,153 +225,10 @@ for sid in sorted(tagged_sids):
353
225
  if sid in stories and dev_status.get(sid) == "done":
354
226
  stale_tags.append(sid)
355
227
 
356
- # -----------------------------------------------------------------------
357
- # 5. Emit matrix JSON
358
- # -----------------------------------------------------------------------
359
- matrix = {
360
- "generated_at": datetime.now(timezone.utc).isoformat(),
361
- "matrix_version": "1.0",
362
- "stories": matrix_stories,
363
- "summary": {
364
- "total_stories": len(stories),
365
- "tagged_stories": len(tagged_sids & set(stories.keys())),
366
- "dark_stories": dark_stories,
367
- "dark_count": len(dark_stories),
368
- "orphan_tags": orphan_tags,
369
- "orphan_count": len(orphan_tags),
370
- "stale_tags": stale_tags,
371
- "stale_count": len(stale_tags),
372
- "oracle_stats": {
373
- "high": sum(1 for s in matrix_stories for l in s["links"] if l["confidence"] == "high"),
374
- "medium": sum(1 for s in matrix_stories for l in s["links"] if l["confidence"] == "medium"),
375
- "low": sum(1 for s in matrix_stories for l in s["links"] if l["confidence"] == "low")
376
- }
377
- }
378
- }
379
-
380
- MATRIX_JSON.parent.mkdir(parents=True, exist_ok=True)
381
- MATRIX_JSON.write_text(json.dumps(matrix, indent=2), encoding="utf-8")
382
-
383
- # -----------------------------------------------------------------------
384
- # 6. Emit TRACEABILITY_LATEST.md (human-readable report)
385
- # -----------------------------------------------------------------------
386
- lines = []
387
- lines.append("# Traceability Matrix")
388
- lines.append("")
389
- lines.append(f"**Generated:** {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}")
390
- lines.append(f"**Total stories:** {len(stories)}")
391
- lines.append(f"**Tagged stories:** {len(tagged_sids & set(stories.keys()))}")
392
- lines.append(f"**Dark stories:** {len(dark_stories)}")
393
- lines.append(f"**Orphan tags:** {len(orphan_tags)}")
394
- lines.append(f"**Stale tags:** {len(stale_tags)}")
395
- lines.append("")
396
- lines.append("## Oracle Stats")
397
- lines.append("")
398
- lines.append(f"- **High** (explicit tag): {matrix['summary']['oracle_stats']['high']}")
399
- lines.append(f"- **Medium** (file heuristic): {matrix['summary']['oracle_stats']['medium']}")
400
- lines.append(f"- **Low** (task reference): {matrix['summary']['oracle_stats']['low']}")
401
- lines.append("")
402
-
403
- # Story coverage table
404
- lines.append("## Story Coverage")
405
- lines.append("")
406
- lines.append("| Story | Title | Epic | BCP | WSJF | Status | Links |")
407
- lines.append("|-------|-------|------|-----|------|--------|-------|")
408
- for s in matrix_stories:
409
- lines.append(f"| {s['id']} | {s['title'][:60]} | {s['epic_id']} | {s['bcp']} | {s['wsjf']} | {s['status']} | {s['link_count']} |")
410
- lines.append("")
411
-
412
- # Findings
413
- if dark_stories:
414
- lines.append("## Dark Stories (no code links)")
415
- lines.append("")
416
- for ds in dark_stories:
417
- sinfo = stories.get(ds, {})
418
- lines.append(f"- **{ds}**: {sinfo.get('title', 'Unknown')} (status: {dev_status.get(ds, '?')})")
419
- lines.append("")
420
-
421
- if orphan_tags:
422
- lines.append("## Orphan Tags (tag in code, no matching story)")
423
- lines.append("")
424
- for ot in orphan_tags:
425
- lines.append(f"- `{ot}`")
426
- lines.append("")
427
-
428
- if stale_tags:
429
- lines.append("## Stale Tags (story done, tag still in code)")
430
- lines.append("")
431
- for st in stale_tags:
432
- lines.append(f"- `{st}`")
433
- lines.append("")
434
-
435
- TRACE_MD.parent.mkdir(parents=True, exist_ok=True)
436
- TRACE_MD.write_text("\n".join(lines), encoding="utf-8")
437
-
438
- # -----------------------------------------------------------------------
439
- # 7. Emit OKF bundle (specs/codebase-wiki/)
440
- # -----------------------------------------------------------------------
441
- OKF_DIR.mkdir(parents=True, exist_ok=True)
442
-
443
- # Generate index.md
444
- idx_lines = []
445
- idx_lines.append("---")
446
- idx_lines.append("type: Index")
447
- idx_lines.append(f"generated_at: {datetime.now(timezone.utc).isoformat()}")
448
- idx_lines.append(f"total_concepts: {len(stories)}")
449
- idx_lines.append("---")
450
- idx_lines.append("")
451
- idx_lines.append("# Codebase Wiki — Story Traceability")
452
- idx_lines.append("")
453
- idx_lines.append("Auto-generated OKF bundle from trace-stories.sh.")
454
- idx_lines.append("")
455
- idx_lines.append("| Story | Title | Confidence | Links |")
456
- idx_lines.append("|-------|-------|------------|-------|")
457
- for s in matrix_stories:
458
- confs = {l["confidence"] for l in s["links"]}
459
- max_conf = "high" if "high" in confs else ("medium" if "medium" in confs else ("low" if confs else "none"))
460
- idx_lines.append(f"| [{s['id']}](./{s['id']}.md) | {s['title'][:60]} | {max_conf} | {s['link_count']} |")
461
- idx_lines.append("")
462
- (OKF_DIR / "index.md").write_text("\n".join(idx_lines), encoding="utf-8")
463
-
464
- # Generate one concept per story
465
- for s in matrix_stories:
466
- concept = []
467
- concept.append("---")
468
- concept.append(f"type: Story")
469
- concept.append(f"id: {s['id']}")
470
- concept.append(f"epic: {s['epic_id']}")
471
- concept.append(f"bcps: {s['bcp']}")
472
- concept.append(f"wsjf: {s['wsjf']}")
473
- concept.append(f"implementation_status: {s['status']}")
474
- max_confs = set()
475
- for l in s["links"]:
476
- max_confs.add(l["confidence"])
477
- conf = "high" if "high" in max_confs else ("medium" if "medium" in max_confs else ("low" if max_confs else "none"))
478
- concept.append(f"coverage_status: {'covered' if s['link_count'] > 0 else 'dark'}")
479
- concept.append(f"confidence: {conf}")
480
- concept.append("links:")
481
- for l in s["links"]:
482
- concept.append(f" - file: {l['file']}")
483
- concept.append(f" line: {l['line']}")
484
- concept.append(f" confidence: {l['confidence']}")
485
- concept.append(f" method: {l['method']}")
486
- concept.append("---")
487
- concept.append("")
488
- concept.append(f"# {s['id']}: {s['title']}")
489
- concept.append("")
490
- concept.append(f"**Epic:** {s['epic_id']} — {s.get('epic_title', 'Unknown')}")
491
- concept.append(f"**Status:** {s['status']}")
492
- concept.append(f"**BCP:** {s['bcp']} | **WSJF:** {s['wsjf']}")
493
- concept.append("")
494
- if s["links"]:
495
- concept.append("## Implemented In")
496
- concept.append("")
497
- for l in s["links"]:
498
- concept.append(f"- `{l['file']}` (line {l['line']}, confidence: {l['confidence']}, method: {l['method']})")
499
- else:
500
- concept.append("*No code links found — this is a dark story.*")
501
- concept.append("")
502
- (OKF_DIR / f"{s['id']}.md").write_text("\n".join(concept), encoding="utf-8")
228
+ # Emit outputs via trace_renderer
229
+ matrix = emit_matrix_json(MATRIX_JSON, stories, tagged_sids, dark_stories, orphan_tags, stale_tags, matrix_stories)
230
+ emit_trace_md(TRACE_MD, stories, tagged_sids, dark_stories, orphan_tags, stale_tags, matrix_stories, matrix, dev_status)
231
+ emit_okf_bundle(OKF_DIR, stories, matrix_stories)
503
232
 
504
233
  # -----------------------------------------------------------------------
505
234
  # 8. Strict mode — exit non-zero on P0 uncovered
@@ -0,0 +1,147 @@
1
+ # story: e38s01
2
+ # trace_renderer.py — Output renderer for traceability matrix data.
3
+ import json
4
+ from pathlib import Path
5
+ from datetime import datetime, timezone
6
+
7
+ def emit_matrix_json(matrix_json_path: Path, stories: dict, tagged_sids: set, dark_stories: list, orphan_tags: list, stale_tags: list, matrix_stories: list) -> dict:
8
+ matrix = {
9
+ "generated_at": datetime.now(timezone.utc).isoformat(),
10
+ "matrix_version": "1.0",
11
+ "stories": matrix_stories,
12
+ "summary": {
13
+ "total_stories": len(stories),
14
+ "tagged_stories": len(tagged_sids & set(stories.keys())),
15
+ "dark_stories": dark_stories,
16
+ "dark_count": len(dark_stories),
17
+ "orphan_tags": orphan_tags,
18
+ "orphan_count": len(orphan_tags),
19
+ "stale_tags": stale_tags,
20
+ "stale_count": len(stale_tags),
21
+ "oracle_stats": {
22
+ "high": sum(1 for s in matrix_stories for l in s["links"] if l["confidence"] == "high"),
23
+ "medium": sum(1 for s in matrix_stories for l in s["links"] if l["confidence"] == "medium"),
24
+ "low": sum(1 for s in matrix_stories for l in s["links"] if l["confidence"] == "low")
25
+ }
26
+ }
27
+ }
28
+ matrix_json_path.parent.mkdir(parents=True, exist_ok=True)
29
+ matrix_json_path.write_text(json.dumps(matrix, indent=2), encoding="utf-8")
30
+ return matrix
31
+
32
+ def emit_trace_md(trace_md_path: Path, stories: dict, tagged_sids: set, dark_stories: list, orphan_tags: list, stale_tags: list, matrix_stories: list, matrix: dict, dev_status: dict) -> None:
33
+ lines = []
34
+ lines.append("# Traceability Matrix")
35
+ lines.append("")
36
+ lines.append(f"**Generated:** {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}")
37
+ lines.append(f"**Total stories:** {len(stories)}")
38
+ lines.append(f"**Tagged stories:** {len(tagged_sids & set(stories.keys()))}")
39
+ lines.append(f"**Dark stories:** {len(dark_stories)}")
40
+ lines.append(f"**Orphan tags:** {len(orphan_tags)}")
41
+ lines.append(f"**Stale tags:** {len(stale_tags)}")
42
+ lines.append("")
43
+ lines.append("## Oracle Stats")
44
+ lines.append("")
45
+ lines.append(f"- **High** (explicit tag): {matrix['summary']['oracle_stats']['high']}")
46
+ lines.append(f"- **Medium** (file heuristic): {matrix['summary']['oracle_stats']['medium']}")
47
+ lines.append(f"- **Low** (task reference): {matrix['summary']['oracle_stats']['low']}")
48
+ lines.append("")
49
+
50
+ # Story coverage table
51
+ lines.append("## Story Coverage")
52
+ lines.append("")
53
+ lines.append("| Story | Title | Epic | BCP | WSJF | Status | Links |")
54
+ lines.append("|-------|-------|------|-----|------|--------|-------|")
55
+ for s in matrix_stories:
56
+ lines.append(f"| {s['id']} | {s['title'][:60]} | {s['epic_id']} | {s['bcp']} | {s['wsjf']} | {s['status']} | {s['link_count']} |")
57
+ lines.append("")
58
+
59
+ # Findings
60
+ if dark_stories:
61
+ lines.append("## Dark Stories (no code links)")
62
+ lines.append("")
63
+ for ds in dark_stories:
64
+ sinfo = stories.get(ds, {})
65
+ lines.append(f"- **{ds}**: {sinfo.get('title', 'Unknown')} (status: {dev_status.get(ds, '?')})")
66
+ lines.append("")
67
+
68
+ if orphan_tags:
69
+ lines.append("## Orphan Tags (tag in code, no matching story)")
70
+ lines.append("")
71
+ for ot in orphan_tags:
72
+ lines.append(f"- `{ot}`")
73
+ lines.append("")
74
+
75
+ if stale_tags:
76
+ lines.append("## Stale Tags (story done, tag still in code)")
77
+ lines.append("")
78
+ for st in stale_tags:
79
+ lines.append(f"- `{st}`")
80
+ lines.append("")
81
+
82
+ trace_md_path.parent.mkdir(parents=True, exist_ok=True)
83
+ trace_md_path.write_text("\n".join(lines), encoding="utf-8")
84
+
85
+ def emit_okf_bundle(okf_dir: Path, stories: dict, matrix_stories: list) -> None:
86
+ okf_dir.mkdir(parents=True, exist_ok=True)
87
+
88
+ # Generate index.md
89
+ idx_lines = []
90
+ idx_lines.append("---")
91
+ idx_lines.append("type: Index")
92
+ idx_lines.append(f"generated_at: {datetime.now(timezone.utc).isoformat()}")
93
+ idx_lines.append(f"total_concepts: {len(stories)}")
94
+ idx_lines.append("---")
95
+ idx_lines.append("")
96
+ idx_lines.append("# Codebase Wiki — Story Traceability")
97
+ idx_lines.append("")
98
+ idx_lines.append("Auto-generated OKF bundle from trace-stories.sh.")
99
+ idx_lines.append("")
100
+ idx_lines.append("| Story | Title | Confidence | Links |")
101
+ idx_lines.append("|-------|-------|------------|-------|")
102
+ for s in matrix_stories:
103
+ confs = {l["confidence"] for l in s["links"]}
104
+ max_conf = "high" if "high" in confs else ("medium" if "medium" in confs else ("low" if confs else "none"))
105
+ idx_lines.append(f"| [{s['id']}](./{s['id']}.md) | {s['title'][:60]} | {max_conf} | {s['link_count']} |")
106
+ idx_lines.append("")
107
+ (okf_dir / "index.md").write_text("\n".join(idx_lines), encoding="utf-8")
108
+
109
+ # Generate one concept per story
110
+ for s in matrix_stories:
111
+ concept = []
112
+ concept.append("---")
113
+ concept.append(f"type: Story")
114
+ concept.append(f"id: {s['id']}")
115
+ concept.append(f"epic: {s['epic_id']}")
116
+ concept.append(f"bcps: {s['bcp']}")
117
+ concept.append(f"wsjf: {s['wsjf']}")
118
+ concept.append(f"implementation_status: {s['status']}")
119
+ max_confs = set()
120
+ for l in s["links"]:
121
+ max_confs.add(l["confidence"])
122
+ conf = "high" if "high" in max_confs else ("medium" if "medium" in max_confs else ("low" if max_confs else "none"))
123
+ concept.append(f"coverage_status: {'covered' if s['link_count'] > 0 else 'dark'}")
124
+ concept.append(f"confidence: {conf}")
125
+ concept.append("links:")
126
+ for l in s["links"]:
127
+ concept.append(f" - file: {l['file']}")
128
+ concept.append(f" line: {l['line']}")
129
+ concept.append(f" confidence: {l['confidence']}")
130
+ concept.append(f" method: {l['method']}")
131
+ concept.append("---")
132
+ concept.append("")
133
+ concept.append(f"# {s['id']}: {s['title']}")
134
+ concept.append("")
135
+ concept.append(f"**Epic:** {s['epic_id']} — {s.get('epic_title', 'Unknown')}")
136
+ concept.append(f"**Status:** {s['status']}")
137
+ concept.append(f"**BCP:** {s['bcp']} | **WSJF:** {s['wsjf']}")
138
+ concept.append("")
139
+ if s["links"]:
140
+ concept.append("## Implemented In")
141
+ concept.append("")
142
+ for l in s["links"]:
143
+ concept.append(f"- `{l['file']}` (line {l['line']}, confidence: {l['confidence']}, method: {l['method']})")
144
+ else:
145
+ concept.append("*No code links found — this is a dark story.*")
146
+ concept.append("")
147
+ (okf_dir / f"{s['id']}.md").write_text("\n".join(concept), encoding="utf-8")
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env bash
2
+ # test-trace-matrix-size.sh — recurrence guard for BUG-2026-07-06-gate-trace-matrix-oversized
3
+ #
4
+ # Asserts trace-matrix.py and its extracted modules stay under the 300-line cap.
5
+ #
6
+ # Usage: bash scripts/test-trace-matrix-size.sh
7
+
8
+ set -euo pipefail
9
+ source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
10
+ resolve_repo_root
11
+
12
+ RED='\033[0;31m'
13
+ GREEN='\033[0;32m'
14
+ NC='\033[0m'
15
+
16
+ CAP=300
17
+ FAILURES=0
18
+
19
+ pass() { echo -e "${GREEN}PASS${NC} $1"; }
20
+ fail() { echo -e "${RED}FAIL${NC} $1"; FAILURES=$((FAILURES + 1)); }
21
+
22
+ check_lines() {
23
+ local file="$1"
24
+ local lines
25
+ lines=$(wc -l < "$REPO_ROOT/$file" | tr -d ' ')
26
+ if [[ "$lines" -le "$CAP" ]]; then
27
+ pass "$file: $lines lines (≤ $CAP)"
28
+ else
29
+ fail "$file: $lines lines (exceeds $CAP)"
30
+ fi
31
+ }
32
+
33
+ for f in \
34
+ scripts/lib/trace-matrix.py \
35
+ scripts/lib/simple_yaml.py \
36
+ scripts/lib/trace_renderer.py
37
+ do
38
+ check_lines "$f"
39
+ done
40
+
41
+ if [[ "$FAILURES" -gt 0 ]]; then
42
+ echo "test-trace-matrix-size.sh: $FAILURES failure(s)" >&2
43
+ exit 1
44
+ fi
45
+
46
+ exit 0