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 +1 -1
- package/CHANGELOG.md +14 -0
- package/CONVENTIONS.md +4 -5
- package/package.json +1 -1
- package/scripts/audit-compliance.sh +22 -292
- package/scripts/lib/audit-compliance-judge.sh +51 -0
- package/scripts/lib/audit-compliance-report.sh +75 -0
- package/scripts/lib/audit-compliance-runner.sh +121 -0
- package/scripts/lib/simple_yaml.py +130 -0
- package/scripts/lib/trace-matrix.py +10 -281
- package/scripts/lib/trace_renderer.py +147 -0
- package/scripts/test-trace-matrix-size.sh +46 -0
package/.pi/package.json
CHANGED
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
|
|
300
|
-
|
|
301
|
-
|
|
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,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# audit-compliance.sh — Agentic Gherkin Compliance Harness (LLM-Judge Upgrade)
|
|
3
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
--
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
run_audit_file "$f"
|
|
266
|
-
fi
|
|
55
|
+
[[ -f "$f" ]] && audit_run_file "$f"
|
|
267
56
|
done
|
|
268
57
|
elif [[ -f "$input" ]]; then
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|