bigpowers 2.58.2 → 2.58.3
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 +7 -0
- package/package.json +1 -1
- package/scripts/audit-compliance.sh +3 -50
- package/scripts/check-blind-spots.sh +9 -307
- package/scripts/land-branch.sh +2 -2
- package/scripts/lib/blind-spots.py +234 -0
- package/scripts/lib/git-hours.sh +50 -0
- package/scripts/lib/trace-stories.py +362 -0
- package/scripts/lib/waiver-utils.sh +39 -0
- package/scripts/record-cycle-time.sh +10 -52
- package/scripts/trace-stories.sh +8 -552
- package/scripts/validate-okf.sh +16 -34
- package/scripts/wait-for-ci.sh +3 -3
package/.pi/package.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [2.58.3](https://github.com/danielvm-git/bigpowers/compare/v2.58.2...v2.58.3) (2026-07-03)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **compliance:** eliminate all waivers — 100% score, 0 FAIL, 0 WAIVED ([d75f592](https://github.com/danielvm-git/bigpowers/commit/d75f592ea40cb294bdb5e1f21b41dedb9ae8734c))
|
|
7
|
+
|
|
1
8
|
## [2.58.2](https://github.com/danielvm-git/bigpowers/compare/v2.58.1...v2.58.2) (2026-07-03)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# audit-compliance.sh — Agentic Gherkin Compliance Harness (LLM-Judge Upgrade)
|
|
3
|
-
|
|
4
|
-
# --- Help Message ---
|
|
5
3
|
show_help() {
|
|
6
4
|
cat <<EOF
|
|
7
5
|
Usage: bash scripts/audit-compliance.sh [feature-file|directory] [options]
|
|
@@ -71,39 +69,9 @@ TOTAL_GLOBAL_WAIVED=0
|
|
|
71
69
|
TOTAL_GLOBAL_EXPIRED=0
|
|
72
70
|
|
|
73
71
|
# --- Waiver Subsystem ---
|
|
74
|
-
#
|
|
75
|
-
# Waived steps are excluded from the denominator; expired waivers re-enter as FAILs.
|
|
72
|
+
# Sourced from scripts/lib/waiver-utils.sh — provides has_waiver().
|
|
76
73
|
WAIVER_FILE="specs/verifications/waivers.yaml"
|
|
77
|
-
|
|
78
|
-
is_waived() {
|
|
79
|
-
local step_sanitized="$1"
|
|
80
|
-
local waiver_file="${WAIVER_FILE:-specs/verifications/waivers.yaml}"
|
|
81
|
-
|
|
82
|
-
if [[ ! -f "$waiver_file" ]]; then
|
|
83
|
-
return 1 # No waiver file → not waived
|
|
84
|
-
fi
|
|
85
|
-
|
|
86
|
-
# Check if step has any waiver entry
|
|
87
|
-
if ! grep -q "step_sanitized: $step_sanitized" "$waiver_file" 2>/dev/null; then
|
|
88
|
-
return 1 # Not found → not waived
|
|
89
|
-
fi
|
|
90
|
-
|
|
91
|
-
# Extract review_date for this step (parses the YAML block)
|
|
92
|
-
local review_date
|
|
93
|
-
review_date=$(awk -v slug="$step_sanitized" '
|
|
94
|
-
$0 ~ "step_sanitized: " slug { found=1; next }
|
|
95
|
-
found && /review_date:/ { gsub(/[^0-9-]/, "", $0); print; exit }
|
|
96
|
-
' "$waiver_file")
|
|
97
|
-
|
|
98
|
-
# Check expiry: if review_date < today, waiver is expired
|
|
99
|
-
local today
|
|
100
|
-
today=$(date +%Y-%m-%d)
|
|
101
|
-
if [[ -n "$review_date" && "$review_date" < "$today" ]]; then
|
|
102
|
-
return 2 # Expired waiver → counts as FAIL
|
|
103
|
-
fi
|
|
104
|
-
|
|
105
|
-
return 0 # Valid waiver → excluded from denominator
|
|
106
|
-
}
|
|
74
|
+
true && source "$(dirname "${BASH_SOURCE[0]}")/lib/waiver-utils.sh"
|
|
107
75
|
|
|
108
76
|
# --- Judging Logic ---
|
|
109
77
|
|
|
@@ -162,22 +130,7 @@ RATIONALE: [One sentence explanation]"
|
|
|
162
130
|
fi
|
|
163
131
|
}
|
|
164
132
|
|
|
165
|
-
judge_binary() {
|
|
166
|
-
local step="$1"
|
|
167
|
-
local exit_code="$2"
|
|
168
|
-
local evidence="$3"
|
|
169
|
-
local report_file="$4"
|
|
170
133
|
|
|
171
|
-
if [[ $exit_code -eq 0 ]]; then
|
|
172
|
-
echo " Result: PASS"
|
|
173
|
-
echo "- [x] $step (PASS)" >> "$report_file"
|
|
174
|
-
return 0
|
|
175
|
-
else
|
|
176
|
-
echo " Result: FAIL"
|
|
177
|
-
echo "- [ ] $step (FAIL) - $evidence" >> "$report_file"
|
|
178
|
-
return 1
|
|
179
|
-
fi
|
|
180
|
-
}
|
|
181
134
|
|
|
182
135
|
# --- Core Execution ---
|
|
183
136
|
|
|
@@ -214,7 +167,7 @@ process_step() {
|
|
|
214
167
|
fi
|
|
215
168
|
|
|
216
169
|
# Step failed — check if it has a valid waiver
|
|
217
|
-
|
|
170
|
+
has_waiver "$sanitized_step"
|
|
218
171
|
local waiver_code=$?
|
|
219
172
|
|
|
220
173
|
if [[ $waiver_code -eq 0 ]]; then
|
|
@@ -1,26 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# story: e38s04
|
|
3
|
-
# check-blind-spots.sh — TEA-inspired heuristic blind-spot detector
|
|
3
|
+
# check-blind-spots.sh — TEA-inspired heuristic blind-spot detector.
|
|
4
|
+
# Thin bash wrapper — the detection engine lives in scripts/lib/blind-spots.py.
|
|
4
5
|
# Reads execution-status.yaml + traceability-matrix.json, runs 6 structural
|
|
5
|
-
# quality checks
|
|
6
|
+
# quality checks, and emits specs/blind-spots.json.
|
|
6
7
|
#
|
|
7
|
-
# Checks:
|
|
8
|
-
#
|
|
9
|
-
# b. test-gap — code tagged but no test file references
|
|
10
|
-
# c. epic-orphan — capsule task with no story tag anywhere
|
|
11
|
-
# d. stale-tag — story done but tag still in code
|
|
12
|
-
# e. double-tag — same file tagged with multiple stories
|
|
13
|
-
# f. bootstrap-testless — new story without test-related tag or files
|
|
14
|
-
#
|
|
15
|
-
# Severities: HIGH / MEDIUM / LOW
|
|
16
|
-
#
|
|
17
|
-
# Usage:
|
|
18
|
-
# bash scripts/check-blind-spots.sh # full scan, emit JSON
|
|
19
|
-
# bash scripts/check-blind-spots.sh --help # this message
|
|
20
|
-
#
|
|
21
|
-
# Exit codes:
|
|
22
|
-
# 0 — no HIGH-severity findings
|
|
23
|
-
# 1 — one or more HIGH-severity findings (or input files missing)
|
|
8
|
+
# Checks: verify-gap, test-gap, epic-orphan, stale-tag, double-tag, bootstrap-testless.
|
|
9
|
+
# Severities: HIGH / MEDIUM / LOW. Exit 1 on HIGH findings.
|
|
24
10
|
|
|
25
11
|
set -euo pipefail
|
|
26
12
|
|
|
@@ -42,19 +28,9 @@ Run 6 heuristic blind-spot checks on the codebase.
|
|
|
42
28
|
Flags:
|
|
43
29
|
--help Print this message and exit.
|
|
44
30
|
|
|
45
|
-
Checks:
|
|
46
|
-
verify-gap Story done, no verify evidence.
|
|
47
|
-
test-gap Code tagged, no test file references.
|
|
48
|
-
epic-orphan Capsule task with no story tag.
|
|
49
|
-
stale-tag Done story, tag still in code.
|
|
50
|
-
double-tag File tagged with multiple stories.
|
|
51
|
-
bootstrap-testless New story without test coverage.
|
|
52
|
-
|
|
31
|
+
Checks: verify-gap, test-gap, epic-orphan, stale-tag, double-tag, bootstrap-testless.
|
|
53
32
|
Output: specs/blind-spots.json
|
|
54
|
-
|
|
55
|
-
Exit codes:
|
|
56
|
-
0 No HIGH-severity findings.
|
|
57
|
-
1 One or more HIGH-severity findings (or input files missing).
|
|
33
|
+
Exit: 0 = no HIGH findings, 1 = HIGH findings or input files missing.
|
|
58
34
|
USAGE
|
|
59
35
|
exit 0
|
|
60
36
|
;;
|
|
@@ -66,284 +42,10 @@ USAGE
|
|
|
66
42
|
esac
|
|
67
43
|
done
|
|
68
44
|
|
|
69
|
-
# ---------------------------------------------------------------------------
|
|
70
|
-
# Input existence checks
|
|
71
|
-
# ---------------------------------------------------------------------------
|
|
72
45
|
if [[ ! -f "$EXEC_STATUS" ]]; then
|
|
73
46
|
echo "check-blind-spots.sh: execution-status.yaml: not found at $EXEC_STATUS" >&2
|
|
74
47
|
exit 1
|
|
75
48
|
fi
|
|
76
49
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
# ---------------------------------------------------------------------------
|
|
80
|
-
python3 - "$REPO_ROOT" "$BLIND_SPOTS_JSON" "$EXEC_STATUS" "$MATRIX_JSON" "$VERIFICATIONS_DIR" "$EPICS_DIR" <<'PYEOF'
|
|
81
|
-
import json, os, re, sys
|
|
82
|
-
from pathlib import Path
|
|
83
|
-
from datetime import datetime, timezone
|
|
84
|
-
|
|
85
|
-
ROOT = Path(sys.argv[1])
|
|
86
|
-
BLIND_SPOTS_JSON = Path(sys.argv[2])
|
|
87
|
-
EXEC_STATUS = Path(sys.argv[3])
|
|
88
|
-
MATRIX_JSON = Path(sys.argv[4])
|
|
89
|
-
VERIFICATIONS_DIR = Path(sys.argv[5])
|
|
90
|
-
EPICS_DIR = Path(sys.argv[6])
|
|
91
|
-
|
|
92
|
-
findings: list[dict] = []
|
|
93
|
-
|
|
94
|
-
# -----------------------------------------------------------------------
|
|
95
|
-
# Parse execution-status.yaml → flat dict of story_id → status
|
|
96
|
-
# -----------------------------------------------------------------------
|
|
97
|
-
def parse_exec_status(path: Path) -> dict[str, str]:
|
|
98
|
-
status: dict[str, str] = {}
|
|
99
|
-
if not path.exists():
|
|
100
|
-
return status
|
|
101
|
-
text = path.read_text(encoding="utf-8")
|
|
102
|
-
lines = text.splitlines()
|
|
103
|
-
in_dev_status = False
|
|
104
|
-
for line in lines:
|
|
105
|
-
stripped = line.strip()
|
|
106
|
-
if stripped.startswith("development_status:"):
|
|
107
|
-
in_dev_status = True
|
|
108
|
-
continue
|
|
109
|
-
if not in_dev_status:
|
|
110
|
-
# Stop if we hit another top-level key
|
|
111
|
-
if stripped and not stripped.startswith(" ") and not stripped.startswith("#"):
|
|
112
|
-
if ":" in stripped:
|
|
113
|
-
in_dev_status = False
|
|
114
|
-
continue
|
|
115
|
-
if ":" in stripped and not stripped.startswith("#"):
|
|
116
|
-
# Lines like ' e38s04: "done"'
|
|
117
|
-
m = re.match(r'\s+(\S+):\s*"([^"]*)"', stripped)
|
|
118
|
-
if m:
|
|
119
|
-
status[m.group(1)] = m.group(2)
|
|
120
|
-
return status
|
|
121
|
-
|
|
122
|
-
dev_status = parse_exec_status(EXEC_STATUS)
|
|
123
|
-
|
|
124
|
-
# -----------------------------------------------------------------------
|
|
125
|
-
# Read traceability matrix (if available)
|
|
126
|
-
# -----------------------------------------------------------------------
|
|
127
|
-
matrix_data: dict | None = None
|
|
128
|
-
if MATRIX_JSON.exists():
|
|
129
|
-
matrix_data = json.loads(MATRIX_JSON.read_text(encoding="utf-8"))
|
|
130
|
-
|
|
131
|
-
# Build indexes from matrix
|
|
132
|
-
tagged_files: dict[str, list[str]] = {} # story_id → [files]
|
|
133
|
-
file_story_counts: dict[str, set[str]] = {} # file → set of story_ids
|
|
134
|
-
if matrix_data:
|
|
135
|
-
for s in matrix_data.get("stories", []):
|
|
136
|
-
sid = s["id"]
|
|
137
|
-
for link in s.get("links", []):
|
|
138
|
-
f = link["file"]
|
|
139
|
-
tagged_files.setdefault(sid, []).append(f)
|
|
140
|
-
file_story_counts.setdefault(f, set()).add(sid)
|
|
141
|
-
|
|
142
|
-
# Enumerate all tracked code files (skip .git, node_modules, specs/)
|
|
143
|
-
CODE_EXTENSIONS = {".sh", ".py", ".js", ".ts", ".jsx", ".tsx", ".md", ".yaml", ".yml", ".cjs", ".mjs", ".json"}
|
|
144
|
-
SKIP_DIRS = {".git", "node_modules", ".cursor", ".gemini", ".pi", "specs", "dist", "build", "__pycache__", ".bigpowers"}
|
|
145
|
-
|
|
146
|
-
def is_code_file(p: Path) -> bool:
|
|
147
|
-
return p.suffix in CODE_EXTENSIONS and not any(s in p.parts for s in SKIP_DIRS)
|
|
148
|
-
|
|
149
|
-
all_code_files: list[str] = []
|
|
150
|
-
for root, dirs, files in os.walk(str(ROOT)):
|
|
151
|
-
# Prune skip dirs
|
|
152
|
-
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
|
|
153
|
-
rel_root = str(Path(root).relative_to(ROOT))
|
|
154
|
-
for fname in files:
|
|
155
|
-
fpath = Path(root) / fname
|
|
156
|
-
if is_code_file(fpath):
|
|
157
|
-
all_code_files.append(str(fpath.relative_to(ROOT)))
|
|
158
|
-
|
|
159
|
-
# -----------------------------------------------------------------------
|
|
160
|
-
# Helper: find test files for a given code file
|
|
161
|
-
# -----------------------------------------------------------------------
|
|
162
|
-
TEST_PATTERNS = [
|
|
163
|
-
re.compile(r"(^|/)test(s)?/", re.IGNORECASE),
|
|
164
|
-
re.compile(r"(^|/)__tests__/", re.IGNORECASE),
|
|
165
|
-
re.compile(r"\.test\.", re.IGNORECASE),
|
|
166
|
-
re.compile(r"\.spec\.", re.IGNORECASE),
|
|
167
|
-
re.compile(r"_test\.", re.IGNORECASE),
|
|
168
|
-
re.compile(r"test_", re.IGNORECASE),
|
|
169
|
-
]
|
|
170
|
-
|
|
171
|
-
def has_test_files(code_files: list[str]) -> bool:
|
|
172
|
-
"""Check if any file in the repo looks like a test for the given code files."""
|
|
173
|
-
for cf in code_files:
|
|
174
|
-
cf_path = Path(cf)
|
|
175
|
-
cf_dir = cf_path.parent
|
|
176
|
-
cf_stem = cf_path.stem
|
|
177
|
-
for tf in all_code_files:
|
|
178
|
-
tp = Path(tf)
|
|
179
|
-
# Same directory test file
|
|
180
|
-
if tp.parent == cf_dir:
|
|
181
|
-
for pat in TEST_PATTERNS:
|
|
182
|
-
if pat.search(tp.name) and cf_stem in tp.name:
|
|
183
|
-
return True
|
|
184
|
-
# Tests dir sibling
|
|
185
|
-
test_dir_names = ["tests", "test", "__tests__"]
|
|
186
|
-
for tdn in test_dir_names:
|
|
187
|
-
test_dir = cf_dir / tdn
|
|
188
|
-
if str(test_dir) in tf:
|
|
189
|
-
if cf_stem in tp.name:
|
|
190
|
-
return True
|
|
191
|
-
return False
|
|
192
|
-
|
|
193
|
-
# -----------------------------------------------------------------------
|
|
194
|
-
# Check A: verify-gap — story done but no verify evidence
|
|
195
|
-
# -----------------------------------------------------------------------
|
|
196
|
-
for sid, sstatus in dev_status.items():
|
|
197
|
-
if sstatus == "done":
|
|
198
|
-
verify_file = VERIFICATIONS_DIR / f"{sid}-verify.yaml"
|
|
199
|
-
if not verify_file.exists():
|
|
200
|
-
findings.append({
|
|
201
|
-
"check": "verify-gap",
|
|
202
|
-
"story_id": sid,
|
|
203
|
-
"severity": "HIGH",
|
|
204
|
-
"description": f"Story {sid} is marked done but no verification evidence exists at specs/verifications/{sid}-verify.yaml",
|
|
205
|
-
"remediation": f"Run verify-work for {sid} or create the verification evidence file"
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
# -----------------------------------------------------------------------
|
|
209
|
-
# Check B: test-gap — code tagged but no test file references
|
|
210
|
-
# -----------------------------------------------------------------------
|
|
211
|
-
for sid, files in tagged_files.items():
|
|
212
|
-
if dev_status.get(sid) in ("done", "active"):
|
|
213
|
-
if not has_test_files(files):
|
|
214
|
-
# Only flag if there are actual code files (not just docs)
|
|
215
|
-
code_only = [f for f in files if Path(f).suffix in {".sh", ".py", ".js", ".ts", ".jsx", ".tsx"} and "SKILL.md" not in f]
|
|
216
|
-
if code_only:
|
|
217
|
-
findings.append({
|
|
218
|
-
"check": "test-gap",
|
|
219
|
-
"story_id": sid,
|
|
220
|
-
"severity": "MEDIUM",
|
|
221
|
-
"description": f"Story {sid} has {len(code_only)} tagged code file(s) with no matching test files",
|
|
222
|
-
"remediation": f"Add test coverage for the tagged files: {', '.join(code_only[:3])}"
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
# -----------------------------------------------------------------------
|
|
226
|
-
# Check C: epic-orphan — capsule task with no story tag in any code file
|
|
227
|
-
# -----------------------------------------------------------------------
|
|
228
|
-
if EPICS_DIR.exists():
|
|
229
|
-
for epic_dir in sorted(EPICS_DIR.iterdir()):
|
|
230
|
-
if not epic_dir.is_dir() or epic_dir.name == "archive":
|
|
231
|
-
continue
|
|
232
|
-
epic_yaml = epic_dir / "epic.yaml"
|
|
233
|
-
if not epic_yaml.exists():
|
|
234
|
-
continue
|
|
235
|
-
# Extract story IDs from epic capsule task files
|
|
236
|
-
for task_file in sorted(epic_dir.glob("e*[0-9]-tasks.yaml")):
|
|
237
|
-
# e.g., e38s04-tasks.yaml → e38s04
|
|
238
|
-
sid_match = re.match(r"(e\d+s\d+)", task_file.name)
|
|
239
|
-
if not sid_match:
|
|
240
|
-
continue
|
|
241
|
-
sid = sid_match.group(1)
|
|
242
|
-
# Check if story has any tagged files (explicit or heuristic)
|
|
243
|
-
sstatus = dev_status.get(sid, "backlog")
|
|
244
|
-
if sstatus in ("done", "active") and sid not in tagged_files:
|
|
245
|
-
findings.append({
|
|
246
|
-
"check": "epic-orphan",
|
|
247
|
-
"story_id": sid,
|
|
248
|
-
"severity": "LOW",
|
|
249
|
-
"description": f"Story {sid} has capsule tasks but no story tags found in any code file",
|
|
250
|
-
"remediation": f"Add `// story: {sid}` tags to implementing files"
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
# -----------------------------------------------------------------------
|
|
254
|
-
# Check D: stale-tag — story done but tag still in code
|
|
255
|
-
# -----------------------------------------------------------------------
|
|
256
|
-
stale_from_matrix: set[str] = set()
|
|
257
|
-
if matrix_data:
|
|
258
|
-
stale_from_matrix = set(matrix_data.get("summary", {}).get("stale_tags", []))
|
|
259
|
-
|
|
260
|
-
for sid in stale_from_matrix:
|
|
261
|
-
findings.append({
|
|
262
|
-
"check": "stale-tag",
|
|
263
|
-
"story_id": sid,
|
|
264
|
-
"severity": "LOW",
|
|
265
|
-
"description": f"Story {sid} is marked done but still has story tags in code",
|
|
266
|
-
"remediation": f"Remove `// story: {sid}` tags from files or confirm the story should remain done"
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
# -----------------------------------------------------------------------
|
|
270
|
-
# Check E: double-tag — same file tagged with multiple stories
|
|
271
|
-
# -----------------------------------------------------------------------
|
|
272
|
-
for fpath, sids in file_story_counts.items():
|
|
273
|
-
if len(sids) > 1:
|
|
274
|
-
findings.append({
|
|
275
|
-
"check": "double-tag",
|
|
276
|
-
"file": fpath,
|
|
277
|
-
"severity": "MEDIUM",
|
|
278
|
-
"description": f"File '{fpath}' is tagged with multiple stories: {', '.join(sorted(sids))}",
|
|
279
|
-
"remediation": "Review whether this file genuinely implements multiple stories. If it's a shared utility, this is acceptable."
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
# -----------------------------------------------------------------------
|
|
283
|
-
# Check F: bootstrap-testless — new story (active/recent) without test evidence
|
|
284
|
-
# -----------------------------------------------------------------------
|
|
285
|
-
recent_threshold = 5 # flag active/done stories in the last 5 stories if no tests
|
|
286
|
-
active_and_recent = []
|
|
287
|
-
for sid in sorted(dev_status.keys()):
|
|
288
|
-
if dev_status[sid] in ("active",):
|
|
289
|
-
active_and_recent.append(sid)
|
|
290
|
-
|
|
291
|
-
for sid in active_and_recent:
|
|
292
|
-
# Check if story has tagged code AND no test files for those
|
|
293
|
-
if sid in tagged_files:
|
|
294
|
-
code_files = tagged_files[sid]
|
|
295
|
-
code_only = [f for f in code_files if Path(f).suffix in {".sh", ".py", ".js", ".ts", ".jsx", ".tsx"} and "SKILL.md" not in f]
|
|
296
|
-
if code_only and not has_test_files(code_files):
|
|
297
|
-
findings.append({
|
|
298
|
-
"check": "bootstrap-testless",
|
|
299
|
-
"story_id": sid,
|
|
300
|
-
"severity": "HIGH",
|
|
301
|
-
"description": f"Active story {sid} has {len(code_only)} tagged code file(s) but no test files detected",
|
|
302
|
-
"remediation": f"Add test files for: {', '.join(code_only[:3])}"
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
# -----------------------------------------------------------------------
|
|
306
|
-
# De-duplicate findings (same check + story_id)
|
|
307
|
-
# -----------------------------------------------------------------------
|
|
308
|
-
seen: set[tuple] = set()
|
|
309
|
-
deduped: list[dict] = []
|
|
310
|
-
for f in findings:
|
|
311
|
-
key = (f["check"], f.get("story_id", f.get("file", "")))
|
|
312
|
-
if key not in seen:
|
|
313
|
-
seen.add(key)
|
|
314
|
-
deduped.append(f)
|
|
315
|
-
findings = deduped
|
|
316
|
-
|
|
317
|
-
# -----------------------------------------------------------------------
|
|
318
|
-
# Emit blind-spots.json
|
|
319
|
-
# -----------------------------------------------------------------------
|
|
320
|
-
high_count = sum(1 for f in findings if f["severity"] == "HIGH")
|
|
321
|
-
medium_count = sum(1 for f in findings if f["severity"] == "MEDIUM")
|
|
322
|
-
low_count = sum(1 for f in findings if f["severity"] == "LOW")
|
|
323
|
-
|
|
324
|
-
blind_spots = {
|
|
325
|
-
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
326
|
-
"version": "1.0",
|
|
327
|
-
"summary": {
|
|
328
|
-
"total_findings": len(findings),
|
|
329
|
-
"high": high_count,
|
|
330
|
-
"medium": medium_count,
|
|
331
|
-
"low": low_count
|
|
332
|
-
},
|
|
333
|
-
"findings": findings
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
BLIND_SPOTS_JSON.parent.mkdir(parents=True, exist_ok=True)
|
|
337
|
-
BLIND_SPOTS_JSON.write_text(json.dumps(blind_spots, indent=2), encoding="utf-8")
|
|
338
|
-
|
|
339
|
-
# Print summary to stdout
|
|
340
|
-
print(f"check-blind-spots.sh: {len(findings)} findings ({high_count} HIGH, {medium_count} MEDIUM, {low_count} LOW)")
|
|
341
|
-
for f in findings:
|
|
342
|
-
print(f" [{f['severity']}] {f['check']}: {f['description'][:100]}")
|
|
343
|
-
|
|
344
|
-
if high_count > 0:
|
|
345
|
-
print(f"\n⚠️ {high_count} HIGH-severity finding(s) — review specs/blind-spots.json")
|
|
346
|
-
sys.exit(1)
|
|
347
|
-
|
|
348
|
-
sys.exit(0)
|
|
349
|
-
PYEOF
|
|
50
|
+
exec python3 "$(dirname "${BASH_SOURCE[0]}")/lib/blind-spots.py" \
|
|
51
|
+
"$REPO_ROOT" "$BLIND_SPOTS_JSON" "$EXEC_STATUS" "$MATRIX_JSON" "$VERIFICATIONS_DIR" "$EPICS_DIR"
|
package/scripts/land-branch.sh
CHANGED
|
@@ -7,7 +7,7 @@ set -euo pipefail
|
|
|
7
7
|
|
|
8
8
|
CONVENTIONAL_REGEX='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?: .+'
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
usage_land() {
|
|
11
11
|
echo "Usage: $0 <feature-branch> \"<conventional commit message>\" [--skip-verify]" >&2
|
|
12
12
|
echo " Run from primary repo root after release-branch gates (solo-local mode)." >&2
|
|
13
13
|
exit 1
|
|
@@ -31,7 +31,7 @@ done
|
|
|
31
31
|
FEATURE_BRANCH="${ARGS[0]:-}"
|
|
32
32
|
COMMIT_MSG="${ARGS[1]:-}"
|
|
33
33
|
|
|
34
|
-
[ -n "$FEATURE_BRANCH" ] && [ -n "$COMMIT_MSG" ] ||
|
|
34
|
+
[ -n "$FEATURE_BRANCH" ] && [ -n "$COMMIT_MSG" ] || usage_land
|
|
35
35
|
|
|
36
36
|
if [[ ! "$COMMIT_MSG" =~ $CONVENTIONAL_REGEX ]]; then
|
|
37
37
|
deny "Commit message must follow Conventional Commits: <type>(<scope>): <subject>"
|