hstack 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/README.md +76 -163
- package/VERSION +1 -1
- package/dist/commands/doctor.js +18 -8
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +7 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.js +9 -2
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/descriptions.js +10 -17
- package/dist/lib/descriptions.js.map +1 -1
- package/dist/lib/wire.js +166 -138
- package/dist/lib/wire.js.map +1 -1
- package/dist/manifest.js +41 -29
- package/dist/manifest.js.map +1 -1
- package/package.json +3 -4
- package/template/.claude/agents/adversarial-reviewer.md +44 -50
- package/template/.claude/agents/app-architect.md +44 -96
- package/template/.claude/agents/data-architect.md +46 -89
- package/template/.claude/agents/test-strategist.md +39 -68
- package/template/.claude/skills/hstack-adr-new/SKILL.md +23 -73
- package/template/.claude/skills/hstack-adversarial-review/SKILL.md +46 -152
- package/template/.claude/skills/hstack-adversarial-review/references/finding-categories.md +89 -103
- package/template/.claude/skills/hstack-app-architecture/SKILL.md +43 -68
- package/template/.claude/skills/hstack-commit/SKILL.md +27 -68
- package/template/.claude/skills/hstack-data-architecture/SKILL.md +34 -56
- package/template/.claude/skills/hstack-promote/SKILL.md +55 -0
- package/template/.claude/skills/hstack-story/SKILL.md +58 -0
- package/template/.claude/skills/hstack-test-audit/SKILL.md +64 -0
- package/template/.claude/skills/hstack-wrap/SKILL.md +50 -0
- package/template/.claude/skills/hstack-wrap/references/pr-description.md +45 -0
- package/template/KERNEL.md +75 -318
- package/template/templates/adr.md +2 -18
- package/template/templates/app-architecture.md +38 -50
- package/template/templates/code-standards.md +107 -0
- package/template/templates/data-architecture.md +38 -89
- package/template/templates/eslint-clean-code.mjs +322 -0
- package/template/templates/infrastructure.md +54 -148
- package/template/templates/roadmap.md +6 -12
- package/template/templates/story.md +7 -13
- package/template/templates/tech-debt.md +2 -37
- package/template/templates/tech-stack.md +2 -12
- package/template/.claude/agents/data-specialist.md +0 -81
- package/template/.claude/agents/implementer.md +0 -97
- package/template/.claude/agents/kernel-fit-analyst.md +0 -129
- package/template/.claude/agents/planner.md +0 -75
- package/template/.claude/agents/product-discovery.md +0 -129
- package/template/.claude/agents/product-manager.md +0 -76
- package/template/.claude/agents/researcher.md +0 -87
- package/template/.claude/agents/security-reviewer.md +0 -88
- package/template/.claude/agents/spec-author.md +0 -89
- package/template/.claude/agents/stack-architect.md +0 -108
- package/template/.claude/agents/ui-ux-briefer.md +0 -75
- package/template/.claude/agents/verifier.md +0 -81
- package/template/.claude/skills/hstack-branch/SKILL.md +0 -76
- package/template/.claude/skills/hstack-brownfield-init/SKILL.md +0 -131
- package/template/.claude/skills/hstack-change-new/SKILL.md +0 -84
- package/template/.claude/skills/hstack-change-plan/SKILL.md +0 -108
- package/template/.claude/skills/hstack-configure/SKILL.md +0 -99
- package/template/.claude/skills/hstack-coord/SKILL.md +0 -95
- package/template/.claude/skills/hstack-data-review/SKILL.md +0 -93
- package/template/.claude/skills/hstack-finalize/SKILL.md +0 -140
- package/template/.claude/skills/hstack-flag/SKILL.md +0 -99
- package/template/.claude/skills/hstack-greenfield-init/SKILL.md +0 -148
- package/template/.claude/skills/hstack-help/SKILL.md +0 -153
- package/template/.claude/skills/hstack-implement/SKILL.md +0 -155
- package/template/.claude/skills/hstack-kernel-fit-promote/SKILL.md +0 -125
- package/template/.claude/skills/hstack-kernel-fit-scan/SKILL.md +0 -142
- package/template/.claude/skills/hstack-kernel-fit-scan/references/slack-setup.md +0 -42
- package/template/.claude/skills/hstack-kernel-fit-triage/SKILL.md +0 -121
- package/template/.claude/skills/hstack-module-spec/SKILL.md +0 -78
- package/template/.claude/skills/hstack-product-discovery/SKILL.md +0 -75
- package/template/.claude/skills/hstack-research/SKILL.md +0 -115
- package/template/.claude/skills/hstack-scaffold/SKILL.md +0 -113
- package/template/.claude/skills/hstack-security-review/SKILL.md +0 -95
- package/template/.claude/skills/hstack-ship/SKILL.md +0 -119
- package/template/.claude/skills/hstack-stack-decide/SKILL.md +0 -81
- package/template/.claude/skills/hstack-story-draft/SKILL.md +0 -92
- package/template/.claude/skills/hstack-tech-debt-new/SKILL.md +0 -95
- package/template/.claude/skills/hstack-tech-debt-resolve/SKILL.md +0 -123
- package/template/.claude/skills/hstack-tech-debt-stale/SKILL.md +0 -86
- package/template/.claude/skills/hstack-tech-debt-wontfix/SKILL.md +0 -77
- package/template/.claude/skills/hstack-telemetry/SKILL.md +0 -73
- package/template/.claude/skills/hstack-test-plan/SKILL.md +0 -159
- package/template/.claude/skills/hstack-ui-brief/SKILL.md +0 -81
- package/template/.claude/skills/hstack-verify/SKILL.md +0 -128
- package/template/scripts/compute-merge-readiness.mjs +0 -780
- package/template/scripts/coord/coord_scan.py +0 -681
- package/template/scripts/run-gates.sh +0 -388
- package/template/scripts/telemetry/__init__.py +0 -6
- package/template/scripts/telemetry/insights/__init__.py +0 -0
- package/template/scripts/telemetry/insights/contract_drift.py +0 -137
- package/template/scripts/telemetry/insights/kernel_fit.py +0 -438
- package/template/scripts/telemetry/insights/overengineering.py +0 -115
- package/template/scripts/telemetry/insights/quality_outcomes.py +0 -131
- package/template/scripts/telemetry/insights/token_economics.py +0 -302
- package/template/scripts/telemetry/insights/workflow_shape.py +0 -198
- package/template/scripts/telemetry/parsers/__init__.py +0 -0
- package/template/scripts/telemetry/parsers/bodies.py +0 -87
- package/template/scripts/telemetry/parsers/commits.py +0 -219
- package/template/scripts/telemetry/parsers/frontmatter.py +0 -322
- package/template/scripts/telemetry/parsers/sidecars.py +0 -61
- package/template/scripts/telemetry/parsers/transcripts.py +0 -294
- package/template/scripts/telemetry/render.py +0 -460
- package/template/scripts/telemetry/report.py +0 -150
- package/template/scripts/telemetry/run_kernel_fit.py +0 -95
- package/template/scripts/telemetry/session_id.py +0 -139
- package/template/scripts/validate-spec.mjs +0 -3303
- package/template/templates/adversarial-review.md +0 -54
- package/template/templates/bootstrap.md +0 -105
- package/template/templates/change-spec.md +0 -82
- package/template/templates/ci-cd.md +0 -41
- package/template/templates/coord-message.md +0 -56
- package/template/templates/data-review.md +0 -54
- package/template/templates/discovery/brainstorm.md +0 -69
- package/template/templates/discovery/forcing-questions.md +0 -76
- package/template/templates/discovery/project-brief.md +0 -53
- package/template/templates/figma-handoff.md +0 -38
- package/template/templates/glossary.md +0 -20
- package/template/templates/hardening-checklist.md +0 -73
- package/template/templates/incident-runbook.md +0 -57
- package/template/templates/kernel-fit-finding.md +0 -63
- package/template/templates/kernel-fit-flag.md +0 -52
- package/template/templates/module-spec.md +0 -49
- package/template/templates/persona.md +0 -38
- package/template/templates/plan.md +0 -53
- package/template/templates/product-brief.md +0 -56
- package/template/templates/security-review.md +0 -63
- package/template/templates/telemetry-sidecar.md +0 -227
- package/template/templates/test-plan.md +0 -119
- package/template/templates/threat-model.md +0 -54
- package/template/templates/ui-brief.md +0 -49
- package/template/templates/verification.md +0 -63
- package/template/templates/vision.md +0 -34
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
"""Kernel-fit insights: patterns suggesting the kernel itself needs revision.
|
|
2
|
-
|
|
3
|
-
This module is the detection layer of the kernel-fit closed-loop system. It
|
|
4
|
-
pattern-matches across shipped artifacts and emits evidence rows; an LLM
|
|
5
|
-
subagent (`kernel-fit-analyst`) then synthesizes findings from these rows.
|
|
6
|
-
|
|
7
|
-
See ADR-0004 for the full design rationale and `template/KERNEL.md` § How
|
|
8
|
-
hstack improves itself for the loop contract.
|
|
9
|
-
|
|
10
|
-
Three starter patterns:
|
|
11
|
-
|
|
12
|
-
- KF-P1 — `category-a-claim-spans-production-paths`: changes flagged
|
|
13
|
-
`internal-tooling: true` (Category A — engineering-only) whose `in-scope`
|
|
14
|
-
touches production-code paths AND whose `enables` array is empty.
|
|
15
|
-
Under the post-PR-#5 schema (`enables` ↔ `enabled-by`, SP-13/SP-14),
|
|
16
|
-
this is the engineer mis-classifying what should be Category B
|
|
17
|
-
(foundational prerequisite) as Category A. The in-scope-overlap
|
|
18
|
-
heuristic surfaces candidate downstream consumers as evidence.
|
|
19
|
-
- KF-P2 — `halt-reason-cluster-uncovered-by-enum`: halt sentinels with
|
|
20
|
-
`reason=other` whose surrounding prose clusters above the Jaccard
|
|
21
|
-
threshold, suggesting the enum is missing a case.
|
|
22
|
-
- KF-P3 — `skill-precondition-violated-and-recoverable`: adversarial-review
|
|
23
|
-
spec-compliance findings whose resolution commit messages reveal a missed
|
|
24
|
-
upstream gate (the ADR-0002 pattern).
|
|
25
|
-
|
|
26
|
-
Detection is pure read — no writes. Output is a dict consumed by the
|
|
27
|
-
analyst subagent via the scan Skill orchestration.
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
from __future__ import annotations
|
|
31
|
-
|
|
32
|
-
import re
|
|
33
|
-
from collections import defaultdict
|
|
34
|
-
from pathlib import Path
|
|
35
|
-
|
|
36
|
-
from telemetry.parsers import frontmatter as fm_parser
|
|
37
|
-
from telemetry.parsers.bodies import parse_findings_section, split_sections
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# File-path prefixes that count as "internal-only" for KF-P1 classification.
|
|
41
|
-
# A change whose in-scope is entirely under these prefixes is genuine
|
|
42
|
-
# Category A (true internal tooling). Anything outside is candidate Category B
|
|
43
|
-
# (foundational prerequisite — production code with deferred user value).
|
|
44
|
-
INTERNAL_ONLY_PREFIXES = (
|
|
45
|
-
"hstack/",
|
|
46
|
-
"scripts/",
|
|
47
|
-
".github/",
|
|
48
|
-
"template/",
|
|
49
|
-
".claude/",
|
|
50
|
-
"docs/",
|
|
51
|
-
"ci/",
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
# Kernel-rule keywords scanned in resolution commits for KF-P3. A
|
|
55
|
-
# `spec-compliance` adversarial finding whose resolving commit mentions any
|
|
56
|
-
# of these is a candidate "Skill precondition should have halted earlier"
|
|
57
|
-
# signal (the ADR-0002 missing-gate pattern).
|
|
58
|
-
KERNEL_GATE_KEYWORDS = re.compile(
|
|
59
|
-
r"\b(precondition|missed\s+gate|should\s+have\s+halted|upstream|"
|
|
60
|
-
r"ready-for-implementation|ready-for-review|ready-to-ship|status\s+gate)\b",
|
|
61
|
-
re.IGNORECASE,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
# Jaccard threshold for KF-P2 cluster membership. Tunable; documented in the
|
|
65
|
-
# plan as a starting value. Lower → more clustering (more cluster merges,
|
|
66
|
-
# fewer clusters). Higher → tighter clusters (fewer merges, more clusters).
|
|
67
|
-
JACCARD_THRESHOLD = 0.6
|
|
68
|
-
|
|
69
|
-
# Minimum cluster size for KF-P2 to fire. Smaller than this is noise.
|
|
70
|
-
MIN_CLUSTER_SIZE = 3
|
|
71
|
-
|
|
72
|
-
# Minimum candidate-row count for KF-P1 to fire. A single Category-B mislabel
|
|
73
|
-
# is noise; recurrence is signal.
|
|
74
|
-
KF_P1_MIN_ROWS = 2
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def compute(commits: list[dict], changes: dict, tech_debt: list[dict],
|
|
78
|
-
adrs: list[dict], module_specs: list[dict],
|
|
79
|
-
session_rows: list[dict], findings_dir: Path | None) -> dict:
|
|
80
|
-
"""Run all kernel-fit detection patterns.
|
|
81
|
-
|
|
82
|
-
`findings_dir` may be `None` or non-existent on first run — the dedup
|
|
83
|
-
cross-reference returns an empty index in that case, and the analyst
|
|
84
|
-
treats every fired pattern as net-new.
|
|
85
|
-
"""
|
|
86
|
-
existing = _load_existing_findings(findings_dir)
|
|
87
|
-
return {
|
|
88
|
-
"existing_open_findings_by_pattern": existing,
|
|
89
|
-
"kf_p1_category_a_claim_spans_production_paths": _kf_p1(changes, commits),
|
|
90
|
-
"kf_p2_halt_reason_cluster_uncovered_by_enum": _kf_p2(commits, session_rows),
|
|
91
|
-
"kf_p3_skill_precondition_violated_and_recoverable": _kf_p3(changes, commits),
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# ---------------- existing-findings index ----------------
|
|
96
|
-
|
|
97
|
-
def _load_existing_findings(findings_dir: Path | None) -> dict:
|
|
98
|
-
"""Read all KF-NNNN-*.md files in findings_dir and build an index of open
|
|
99
|
-
findings keyed by pattern. Used by the analyst for dedup / supersession
|
|
100
|
-
decisions. Tolerant of missing directory."""
|
|
101
|
-
out: dict[str, list[str]] = defaultdict(list)
|
|
102
|
-
if findings_dir is None or not findings_dir.is_dir():
|
|
103
|
-
return dict(out)
|
|
104
|
-
for path in sorted(findings_dir.glob("KF-*.md")):
|
|
105
|
-
parsed = fm_parser.read_artifact(path)
|
|
106
|
-
if parsed is None:
|
|
107
|
-
continue
|
|
108
|
-
fm, _body = parsed
|
|
109
|
-
status = fm.get("status")
|
|
110
|
-
pattern = fm.get("pattern")
|
|
111
|
-
kid = fm.get("id") or path.stem
|
|
112
|
-
if not pattern:
|
|
113
|
-
continue
|
|
114
|
-
# Only "open" and "acknowledged" findings count for dedup; promoted /
|
|
115
|
-
# dismissed / superseded / archived are terminal and do not suppress
|
|
116
|
-
# re-detection.
|
|
117
|
-
if status in ("open", "acknowledged"):
|
|
118
|
-
out[pattern].append(kid)
|
|
119
|
-
return dict(out)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
# ---------------- KF-P1 ----------------
|
|
123
|
-
|
|
124
|
-
def _classify_inscope_paths(in_scope: list) -> tuple[list[str], list[str]]:
|
|
125
|
-
"""Partition an in-scope list into (internal_only_paths, production_paths)."""
|
|
126
|
-
internal_only: list[str] = []
|
|
127
|
-
production: list[str] = []
|
|
128
|
-
for entry in in_scope or []:
|
|
129
|
-
if not isinstance(entry, str):
|
|
130
|
-
continue
|
|
131
|
-
path = entry.strip()
|
|
132
|
-
if not path:
|
|
133
|
-
continue
|
|
134
|
-
# Normalize leading "./" and any glob suffixes for prefix checking.
|
|
135
|
-
normalized = path[2:] if path.startswith("./") else path
|
|
136
|
-
if any(normalized.startswith(p) for p in INTERNAL_ONLY_PREFIXES):
|
|
137
|
-
internal_only.append(path)
|
|
138
|
-
else:
|
|
139
|
-
production.append(path)
|
|
140
|
-
return internal_only, production
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def _forward_consumers(this_change_id: str, this_in_scope: list[str],
|
|
144
|
-
changes: dict, commits: list[dict]) -> list[str]:
|
|
145
|
-
"""Return change-ids of later changes whose in-scope OR whose commit
|
|
146
|
-
file-lists overlap with this change's in-scope production paths."""
|
|
147
|
-
if not this_in_scope:
|
|
148
|
-
return []
|
|
149
|
-
# Build set of production-path-prefixes for cheap overlap checks. We treat
|
|
150
|
-
# each in-scope entry as a prefix; this is forgiving (catches edits inside
|
|
151
|
-
# subdirs) and matches what `internal-tooling: true` plumbing changes
|
|
152
|
-
# typically introduce (a dir of new types or a new module).
|
|
153
|
-
prefixes = {(p[2:] if p.startswith("./") else p).rstrip("/*") for p in this_in_scope}
|
|
154
|
-
|
|
155
|
-
consumers: set[str] = set()
|
|
156
|
-
|
|
157
|
-
# (1) Other change-specs whose in-scope overlaps.
|
|
158
|
-
for other_id, arts in changes.items():
|
|
159
|
-
if other_id == this_change_id:
|
|
160
|
-
continue
|
|
161
|
-
spec = arts.get("change-spec") or arts.get("spec")
|
|
162
|
-
if not spec:
|
|
163
|
-
continue
|
|
164
|
-
other_in_scope = spec["fm"].get("in-scope") or []
|
|
165
|
-
for entry in other_in_scope:
|
|
166
|
-
if not isinstance(entry, str):
|
|
167
|
-
continue
|
|
168
|
-
normalized = entry[2:] if entry.startswith("./") else entry
|
|
169
|
-
if any(normalized.startswith(p) for p in prefixes):
|
|
170
|
-
consumers.add(other_id)
|
|
171
|
-
break
|
|
172
|
-
|
|
173
|
-
# (2) Commits whose file-list touches our production paths AND whose
|
|
174
|
-
# artifact_id is a different change-spec (avoids self-attribution).
|
|
175
|
-
for c in commits:
|
|
176
|
-
cid = c.get("artifact_id")
|
|
177
|
-
if not cid or cid == this_change_id:
|
|
178
|
-
continue
|
|
179
|
-
for f in c.get("files", []):
|
|
180
|
-
normalized = f[2:] if f.startswith("./") else f
|
|
181
|
-
if any(normalized.startswith(p) for p in prefixes):
|
|
182
|
-
consumers.add(cid)
|
|
183
|
-
break
|
|
184
|
-
|
|
185
|
-
return sorted(consumers)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def _kf_p1(changes: dict, commits: list[dict]) -> dict:
|
|
189
|
-
"""KF-P1 — Category A (`internal-tooling: true`) claims whose `in-scope`
|
|
190
|
-
spans production-code paths AND whose `enables` array is empty. Under
|
|
191
|
-
the post-PR-#5 schema, this is the engineer mis-classifying what should
|
|
192
|
-
be Category B (foundational prerequisite) as Category A. SP-13 makes
|
|
193
|
-
A and B mutually exclusive at the validator level; KF-P1 catches the
|
|
194
|
-
case the validator cannot — claiming A when the in-scope reveals B.
|
|
195
|
-
Fires on >= KF_P1_MIN_ROWS candidate rows.
|
|
196
|
-
"""
|
|
197
|
-
rows: list[dict] = []
|
|
198
|
-
for cid, arts in changes.items():
|
|
199
|
-
spec = arts.get("change-spec") or arts.get("spec")
|
|
200
|
-
if not spec:
|
|
201
|
-
continue
|
|
202
|
-
fm = spec["fm"]
|
|
203
|
-
if fm.get("status") != "shipped":
|
|
204
|
-
continue
|
|
205
|
-
if not fm.get("internal-tooling"):
|
|
206
|
-
continue
|
|
207
|
-
in_scope = fm.get("in-scope") or []
|
|
208
|
-
enables = fm.get("enables") or []
|
|
209
|
-
internal_paths, production_paths = _classify_inscope_paths(in_scope)
|
|
210
|
-
# Classification (post-PR-#5 schema; SP-13 enforces mutual exclusivity):
|
|
211
|
-
# - no production paths → "true-category-a" (correctly classified)
|
|
212
|
-
# - has production paths AND enables empty → "category-b-misclassified" (bug)
|
|
213
|
-
# - has production paths AND enables non-empty → impossible under SP-13;
|
|
214
|
-
# if observed the validator failed and the analyst surfaces it separately
|
|
215
|
-
if not production_paths:
|
|
216
|
-
classification = "true-category-a"
|
|
217
|
-
elif not enables:
|
|
218
|
-
classification = "category-b-misclassified"
|
|
219
|
-
else:
|
|
220
|
-
# SP-13 violation should not reach here in a validated repo; flag
|
|
221
|
-
# explicitly so the analyst can route to a validator-bug finding.
|
|
222
|
-
classification = "sp-13-violation"
|
|
223
|
-
consumers: list[str] = []
|
|
224
|
-
if classification == "category-b-misclassified":
|
|
225
|
-
consumers = _forward_consumers(cid, production_paths or in_scope, changes, commits)
|
|
226
|
-
rows.append({
|
|
227
|
-
"change": cid,
|
|
228
|
-
"internal_only_paths_count": len(internal_paths),
|
|
229
|
-
"production_paths_count": len(production_paths),
|
|
230
|
-
"enables_count": len(enables),
|
|
231
|
-
"downstream_consumers": consumers,
|
|
232
|
-
"classification_candidate": classification,
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
candidate_rows = [r for r in rows
|
|
236
|
-
if r["classification_candidate"] == "category-b-misclassified"]
|
|
237
|
-
fired = len(candidate_rows) >= KF_P1_MIN_ROWS
|
|
238
|
-
return {
|
|
239
|
-
"pattern_id": "KF-P1",
|
|
240
|
-
"pattern_name": "category-a-claim-spans-production-paths",
|
|
241
|
-
"fired": fired,
|
|
242
|
-
"evidence_row_count": len(candidate_rows),
|
|
243
|
-
"min_rows_for_firing": KF_P1_MIN_ROWS,
|
|
244
|
-
"all_rows": rows,
|
|
245
|
-
"evidence_rows": candidate_rows,
|
|
246
|
-
"note": ("Changes flagged `internal-tooling: true` (Category A) whose in-scope "
|
|
247
|
-
"spans production-code paths AND whose `enables` array is empty. Under "
|
|
248
|
-
"the post-PR-#5 schema (Category A vs Category B with `enables`/`enabled-by`), "
|
|
249
|
-
"this is the engineer mis-classifying what should be Category B as Category A. "
|
|
250
|
-
"SP-13 catches the both-set case; KF-P1 catches the claim-A-while-looking-like-B case."),
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
# ---------------- KF-P2 ----------------
|
|
255
|
-
|
|
256
|
-
_TOKEN_RE = re.compile(r"[a-zA-Z][a-zA-Z0-9_-]{2,}")
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def _tokens(text: str) -> set[str]:
|
|
260
|
-
"""Lowercase token set from text; drops tokens <=3 chars and pure numbers."""
|
|
261
|
-
if not text:
|
|
262
|
-
return set()
|
|
263
|
-
return {t.lower() for t in _TOKEN_RE.findall(text)}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def _jaccard(a: set[str], b: set[str]) -> float:
|
|
267
|
-
if not a or not b:
|
|
268
|
-
return 0.0
|
|
269
|
-
inter = len(a & b)
|
|
270
|
-
union = len(a | b)
|
|
271
|
-
return inter / union if union else 0.0
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def _kf_p2(commits: list[dict], session_rows: list[dict]) -> dict:
|
|
275
|
-
"""KF-P2 — cluster halt sentinels with reason=other. Cluster size >=
|
|
276
|
-
MIN_CLUSTER_SIZE is evidence the enum is missing a case.
|
|
277
|
-
|
|
278
|
-
Sources: commit bodies (parser already extracted halt_reasons), and
|
|
279
|
-
session-row halt_reasons. For commits, we use the commit body as the
|
|
280
|
-
surrounding-prose context; for session rows we use the row's halt-context
|
|
281
|
-
if available, falling back to a label-only token set.
|
|
282
|
-
"""
|
|
283
|
-
docs: list[dict] = []
|
|
284
|
-
for c in commits:
|
|
285
|
-
reasons = c.get("halt_reasons") or []
|
|
286
|
-
if not any(r.lower() == "other" for r in reasons):
|
|
287
|
-
continue
|
|
288
|
-
# Use commit body as the prose context — it is what the kernel
|
|
289
|
-
# contract says accompanies the sentinel.
|
|
290
|
-
context = c.get("body", "") or c.get("subject", "")
|
|
291
|
-
docs.append({
|
|
292
|
-
"source": "commit",
|
|
293
|
-
"ref": c.get("sha", "")[:8],
|
|
294
|
-
"context": context,
|
|
295
|
-
"tokens": _tokens(context),
|
|
296
|
-
})
|
|
297
|
-
for s in session_rows:
|
|
298
|
-
reasons = s.get("halt_reasons") or []
|
|
299
|
-
for r in reasons:
|
|
300
|
-
if not isinstance(r, str):
|
|
301
|
-
continue
|
|
302
|
-
if r.lower() != "other":
|
|
303
|
-
continue
|
|
304
|
-
docs.append({
|
|
305
|
-
"source": "session",
|
|
306
|
-
"ref": s.get("file", "") or s.get("skill", "") or "",
|
|
307
|
-
# Session parser does not capture surrounding prose in v1;
|
|
308
|
-
# use the session label as a degraded token source.
|
|
309
|
-
"context": s.get("skill", "") + " " + " ".join(reasons),
|
|
310
|
-
"tokens": _tokens(s.get("skill", "")),
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
# Greedy clustering: each new doc joins the first cluster whose
|
|
314
|
-
# representative has Jaccard >= threshold; else starts a new cluster.
|
|
315
|
-
clusters: list[list[dict]] = []
|
|
316
|
-
for d in docs:
|
|
317
|
-
placed = False
|
|
318
|
-
for cluster in clusters:
|
|
319
|
-
rep_tokens = cluster[0]["tokens"]
|
|
320
|
-
if _jaccard(d["tokens"], rep_tokens) >= JACCARD_THRESHOLD:
|
|
321
|
-
cluster.append(d)
|
|
322
|
-
placed = True
|
|
323
|
-
break
|
|
324
|
-
if not placed:
|
|
325
|
-
clusters.append([d])
|
|
326
|
-
|
|
327
|
-
evidence_clusters = [c for c in clusters if len(c) >= MIN_CLUSTER_SIZE]
|
|
328
|
-
rows = []
|
|
329
|
-
for i, cluster in enumerate(evidence_clusters):
|
|
330
|
-
rows.append({
|
|
331
|
-
"cluster_id": f"C-{i + 1}",
|
|
332
|
-
"size": len(cluster),
|
|
333
|
-
"representative_context": (cluster[0]["context"] or "")[:300],
|
|
334
|
-
"member_refs": [d["ref"] for d in cluster],
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
return {
|
|
338
|
-
"pattern_id": "KF-P2",
|
|
339
|
-
"pattern_name": "halt-reason-cluster-uncovered-by-enum",
|
|
340
|
-
"fired": len(evidence_clusters) > 0,
|
|
341
|
-
"evidence_row_count": len(evidence_clusters),
|
|
342
|
-
"jaccard_threshold": JACCARD_THRESHOLD,
|
|
343
|
-
"min_cluster_size": MIN_CLUSTER_SIZE,
|
|
344
|
-
"total_other_halts": len(docs),
|
|
345
|
-
"evidence_rows": rows,
|
|
346
|
-
"note": ("HSTACK-HALT sentinels with reason=other clustered by surrounding-prose "
|
|
347
|
-
f"token overlap (Jaccard >= {JACCARD_THRESHOLD}). A cluster of "
|
|
348
|
-
f">= {MIN_CLUSTER_SIZE} similar halts means the enum is missing a case."),
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
# ---------------- KF-P3 ----------------
|
|
353
|
-
|
|
354
|
-
def _commits_by_sha_prefix(commits: list[dict]) -> dict[str, dict]:
|
|
355
|
-
"""Index commits by short sha (8 chars) for quick lookup. Falls back to
|
|
356
|
-
full sha if entries collide (rare with realistic repo sizes)."""
|
|
357
|
-
out: dict[str, dict] = {}
|
|
358
|
-
for c in commits:
|
|
359
|
-
sha = c.get("sha") or ""
|
|
360
|
-
if not sha:
|
|
361
|
-
continue
|
|
362
|
-
out[sha] = c
|
|
363
|
-
if len(sha) >= 8:
|
|
364
|
-
out[sha[:8]] = c
|
|
365
|
-
return out
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
def _kf_p3(changes: dict, commits: list[dict]) -> dict:
|
|
369
|
-
"""KF-P3 — spec-compliance adversarial findings resolved via a commit
|
|
370
|
-
whose message reveals a kernel-gate keyword. This is the pattern that
|
|
371
|
-
produced ADR-0002 (the missed `ready-for-review` transition).
|
|
372
|
-
"""
|
|
373
|
-
commit_index = _commits_by_sha_prefix(commits)
|
|
374
|
-
rows: list[dict] = []
|
|
375
|
-
for cid, arts in changes.items():
|
|
376
|
-
ar = arts.get("adversarial-review")
|
|
377
|
-
if not ar:
|
|
378
|
-
continue
|
|
379
|
-
ar_fm = ar["fm"]
|
|
380
|
-
if ar_fm.get("status") != "findings-resolved":
|
|
381
|
-
continue
|
|
382
|
-
# Findings array on frontmatter is authoritative; fall back to body
|
|
383
|
-
# parser when the array is missing or absent.
|
|
384
|
-
findings = ar_fm.get("findings") or []
|
|
385
|
-
if not findings:
|
|
386
|
-
sections = split_sections(ar["body"] or "")
|
|
387
|
-
findings_section = sections.get("Findings", "")
|
|
388
|
-
findings = parse_findings_section(findings_section)
|
|
389
|
-
for f in findings:
|
|
390
|
-
if not isinstance(f, dict):
|
|
391
|
-
continue
|
|
392
|
-
category = (f.get("category") or "").lower()
|
|
393
|
-
if category != "spec-compliance":
|
|
394
|
-
continue
|
|
395
|
-
resolution = (f.get("resolution") or "").strip()
|
|
396
|
-
if not resolution.startswith("commit:"):
|
|
397
|
-
continue
|
|
398
|
-
sha_token = resolution.split(":", 1)[1].strip().split()[0]
|
|
399
|
-
commit = commit_index.get(sha_token) or commit_index.get(sha_token[:8])
|
|
400
|
-
if not commit:
|
|
401
|
-
# Still record the candidate — the analyst can decide whether
|
|
402
|
-
# missing-commit-context is itself a signal.
|
|
403
|
-
if KERNEL_GATE_KEYWORDS.search(resolution):
|
|
404
|
-
rows.append({
|
|
405
|
-
"change": cid,
|
|
406
|
-
"finding_id": f.get("id"),
|
|
407
|
-
"category": category,
|
|
408
|
-
"resolution": resolution,
|
|
409
|
-
"commit_subject": None,
|
|
410
|
-
"matched_keywords": [],
|
|
411
|
-
"commit_resolved": False,
|
|
412
|
-
})
|
|
413
|
-
continue
|
|
414
|
-
haystack = (commit.get("subject") or "") + "\n" + (commit.get("body") or "")
|
|
415
|
-
matches = KERNEL_GATE_KEYWORDS.findall(haystack)
|
|
416
|
-
if not matches:
|
|
417
|
-
continue
|
|
418
|
-
rows.append({
|
|
419
|
-
"change": cid,
|
|
420
|
-
"finding_id": f.get("id"),
|
|
421
|
-
"category": category,
|
|
422
|
-
"resolution": resolution,
|
|
423
|
-
"commit_subject": commit.get("subject"),
|
|
424
|
-
"matched_keywords": [m.lower() for m in matches],
|
|
425
|
-
"commit_resolved": True,
|
|
426
|
-
})
|
|
427
|
-
|
|
428
|
-
return {
|
|
429
|
-
"pattern_id": "KF-P3",
|
|
430
|
-
"pattern_name": "skill-precondition-violated-and-recoverable",
|
|
431
|
-
"fired": len(rows) >= 1,
|
|
432
|
-
"evidence_row_count": len(rows),
|
|
433
|
-
"evidence_rows": rows,
|
|
434
|
-
"note": ("Adversarial spec-compliance findings whose resolving commit message "
|
|
435
|
-
"mentions a kernel gate or precondition. Each row is a candidate "
|
|
436
|
-
"'a Skill precondition should have halted earlier' signal — the "
|
|
437
|
-
"ADR-0002 pattern."),
|
|
438
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
"""Overengineering insights: OE-1 artifact/diff ratio, OE-3 context-load ×
|
|
2
|
-
invocations × downstream-refs, OE-5 trivial-eligible that ran the gauntlet."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
from collections import defaultdict
|
|
7
|
-
|
|
8
|
-
from telemetry.parsers.bodies import approx_token_count
|
|
9
|
-
from telemetry.parsers.commits import diff_line_count
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def compute(commits: list[dict], changes: dict, session_rows: list[dict], repo) -> dict:
|
|
13
|
-
return {
|
|
14
|
-
"oe_1_artifact_to_diff_ratio": _oe_1(changes, commits, repo),
|
|
15
|
-
"oe_3_subagent_context_load_amortization": _oe_3(session_rows, changes),
|
|
16
|
-
"oe_5_trivial_eligible_full_gauntlet": _oe_5(changes, commits),
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _oe_1(changes: dict, commits: list[dict], repo) -> dict:
|
|
21
|
-
"""OE-1: artifact tokens / code-diff lines.
|
|
22
|
-
|
|
23
|
-
Sums artifact-body token counts (approx 4 chars/token) per change, divides
|
|
24
|
-
by total diff lines on implement(<change-id>) commits for that change.
|
|
25
|
-
Ratio above ~50 is a strong overengineering signal; below ~5 the change is
|
|
26
|
-
likely under-specified for its complexity.
|
|
27
|
-
"""
|
|
28
|
-
rows = []
|
|
29
|
-
for cid, arts in sorted(changes.items()):
|
|
30
|
-
artifact_tokens = 0
|
|
31
|
-
for atype, data in arts.items():
|
|
32
|
-
artifact_tokens += approx_token_count(data["body"])
|
|
33
|
-
# Sum implement-commit diffs for this change.
|
|
34
|
-
diff_lines = 0
|
|
35
|
-
for c in commits:
|
|
36
|
-
if c["artifact_type"] != "implement" or c["artifact_id"] != cid:
|
|
37
|
-
continue
|
|
38
|
-
added, removed = diff_line_count(repo, c["sha"])
|
|
39
|
-
diff_lines += added + removed
|
|
40
|
-
ratio = (artifact_tokens / diff_lines) if diff_lines > 0 else None
|
|
41
|
-
rows.append({
|
|
42
|
-
"change": cid,
|
|
43
|
-
"artifact_tokens": artifact_tokens,
|
|
44
|
-
"diff_lines": diff_lines,
|
|
45
|
-
"tokens_per_diff_line": round(ratio, 1) if ratio is not None else None,
|
|
46
|
-
})
|
|
47
|
-
rows.sort(key=lambda r: r["tokens_per_diff_line"] or 0, reverse=True)
|
|
48
|
-
return {"rows": rows}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _oe_3(session_rows: list[dict], changes: dict) -> dict:
|
|
52
|
-
"""OE-3: per subagent, invocations × estimated context-load size.
|
|
53
|
-
|
|
54
|
-
Without sidecars to attribute downstream-reference-count, this v1 metric
|
|
55
|
-
surfaces just the subagent × invocation-count × cost dimension and lets
|
|
56
|
-
the reader eyeball which subagents are paying repeated entry-tax.
|
|
57
|
-
"""
|
|
58
|
-
appearances = defaultdict(int)
|
|
59
|
-
total_cost_in_host = defaultdict(int)
|
|
60
|
-
for s in session_rows:
|
|
61
|
-
for sa in s["subagents"]:
|
|
62
|
-
appearances[sa] += 1
|
|
63
|
-
total_cost_in_host[sa] += s["cost_score"]
|
|
64
|
-
rows = []
|
|
65
|
-
for sa, n in sorted(appearances.items(), key=lambda kv: -kv[1]):
|
|
66
|
-
rows.append({
|
|
67
|
-
"subagent": sa,
|
|
68
|
-
"invocations": n,
|
|
69
|
-
"host_cost_score_total": total_cost_in_host[sa],
|
|
70
|
-
"cost_per_invocation": total_cost_in_host[sa] // n if n else 0,
|
|
71
|
-
})
|
|
72
|
-
return {"rows": rows}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _oe_5(changes: dict, commits: list[dict]) -> dict:
|
|
76
|
-
"""OE-5: trivial-eligible changes that ran the full gauntlet.
|
|
77
|
-
|
|
78
|
-
Heuristic: change-spec at `shipped` (or any terminal state) with `trivial:
|
|
79
|
-
false`, empty `surfaces`, zero or one invariant. These are candidates that
|
|
80
|
-
might have qualified for the trivial-tag escape hatch but ran every gate.
|
|
81
|
-
"""
|
|
82
|
-
candidates = []
|
|
83
|
-
for cid, arts in sorted(changes.items()):
|
|
84
|
-
spec = arts.get("change-spec")
|
|
85
|
-
if not spec:
|
|
86
|
-
continue
|
|
87
|
-
fm = spec["fm"]
|
|
88
|
-
if fm.get("trivial") is True:
|
|
89
|
-
continue
|
|
90
|
-
status = fm.get("status")
|
|
91
|
-
if status not in ("shipped", "ready-to-ship", "archived"):
|
|
92
|
-
continue
|
|
93
|
-
surfaces = fm.get("surfaces") or []
|
|
94
|
-
if surfaces: # any declared surface disqualifies trivial-eligibility
|
|
95
|
-
continue
|
|
96
|
-
# diff size heuristic: a "trivial-eligible" change usually has <50 added lines
|
|
97
|
-
diff_total = 0
|
|
98
|
-
for c in commits:
|
|
99
|
-
if c["artifact_type"] == "implement" and c["artifact_id"] == cid:
|
|
100
|
-
# We can't get accurate line counts here without git access — use file count proxy
|
|
101
|
-
diff_total += len(c.get("files", []))
|
|
102
|
-
candidates.append({
|
|
103
|
-
"change": cid,
|
|
104
|
-
"status": status,
|
|
105
|
-
"surfaces": surfaces,
|
|
106
|
-
"files_touched_in_implement_commits": diff_total,
|
|
107
|
-
})
|
|
108
|
-
return {
|
|
109
|
-
"rows": candidates,
|
|
110
|
-
"note": (
|
|
111
|
-
"Heuristic only. A change with empty `surfaces` AND no `trivial: "
|
|
112
|
-
"true` tag is a candidate for retrospective trivial classification "
|
|
113
|
-
"— or a sign the surfaces list was under-declared."
|
|
114
|
-
),
|
|
115
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
"""Quality-outcomes insights: QO-2 severity-resolution mix, QO-3 test-
|
|
2
|
-
immutability audit, QO-4 verifier observed-vs-promised."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
from collections import defaultdict
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def compute(commits: list[dict], changes: dict) -> dict:
|
|
10
|
-
return {
|
|
11
|
-
"qo_2_severity_resolution_mix": _qo_2(changes),
|
|
12
|
-
"qo_3_test_immutability_audit": _qo_3(commits),
|
|
13
|
-
"qo_4_observed_vs_promised": _qo_4(changes),
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _qo_2(changes: dict) -> dict:
|
|
18
|
-
"""QO-2: per finding, cross-tab severity × resolution-type.
|
|
19
|
-
|
|
20
|
-
Resolution prefix is the part before the colon: commit | tech-debt |
|
|
21
|
-
justified-in-prose.
|
|
22
|
-
"""
|
|
23
|
-
matrix: dict[tuple[str, str], int] = defaultdict(int)
|
|
24
|
-
smell_cases = [] # high/critical severity resolved as justified-in-prose
|
|
25
|
-
for cid, arts in changes.items():
|
|
26
|
-
adv = arts.get("adversarial-review")
|
|
27
|
-
if not adv:
|
|
28
|
-
continue
|
|
29
|
-
findings = adv["fm"].get("findings")
|
|
30
|
-
if not isinstance(findings, list):
|
|
31
|
-
continue
|
|
32
|
-
for f in findings:
|
|
33
|
-
if not isinstance(f, dict):
|
|
34
|
-
continue
|
|
35
|
-
sev = (f.get("severity") or "unknown").lower()
|
|
36
|
-
res = (f.get("resolution") or "unknown")
|
|
37
|
-
res_type = res.split(":", 1)[0].strip().lower() if isinstance(res, str) else "unknown"
|
|
38
|
-
if res_type not in ("commit", "tech-debt", "justified-in-prose"):
|
|
39
|
-
res_type = "other"
|
|
40
|
-
matrix[(sev, res_type)] += 1
|
|
41
|
-
if sev in ("high", "critical") and res_type == "justified-in-prose":
|
|
42
|
-
smell_cases.append({
|
|
43
|
-
"change": cid,
|
|
44
|
-
"finding_id": f.get("id"),
|
|
45
|
-
"category": f.get("category"),
|
|
46
|
-
"severity": sev,
|
|
47
|
-
})
|
|
48
|
-
severities = sorted({s for s, _ in matrix.keys()})
|
|
49
|
-
res_types = ["commit", "tech-debt", "justified-in-prose", "other"]
|
|
50
|
-
rows = []
|
|
51
|
-
for sev in severities:
|
|
52
|
-
row = {"severity": sev}
|
|
53
|
-
for rt in res_types:
|
|
54
|
-
row[rt] = matrix.get((sev, rt), 0)
|
|
55
|
-
rows.append(row)
|
|
56
|
-
return {
|
|
57
|
-
"rows": rows,
|
|
58
|
-
"high_severity_in_prose_smells": smell_cases,
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _qo_3(commits: list[dict]) -> dict:
|
|
63
|
-
"""QO-3: test-immutability audit.
|
|
64
|
-
|
|
65
|
-
For every commit touching a test file, check whether the body carries one
|
|
66
|
-
of the canonical authorization phrases. Unauthorized test-edit commits are
|
|
67
|
-
hard kernel violations.
|
|
68
|
-
"""
|
|
69
|
-
import re
|
|
70
|
-
test_re = re.compile(r"(\.test\.|\.spec\.|/__tests__/|/__snapshots__/|^e2e/|_test\.go$)")
|
|
71
|
-
auth_re = re.compile(
|
|
72
|
-
r"Ok to (change|delete) test\s+\S+"
|
|
73
|
-
r"|Ok to update snapshot\s+\S+"
|
|
74
|
-
r"|Ok to refresh fixture\s+\S+"
|
|
75
|
-
)
|
|
76
|
-
violations = []
|
|
77
|
-
authorized = []
|
|
78
|
-
for c in commits:
|
|
79
|
-
test_files = [f for f in c.get("files", []) if test_re.search(f)]
|
|
80
|
-
if not test_files:
|
|
81
|
-
continue
|
|
82
|
-
is_implement_or_test_plan = c.get("artifact_type") in ("implement", "test-plan")
|
|
83
|
-
# New-tests-in-an-implement-commit are permitted; we can't distinguish
|
|
84
|
-
# new-vs-modified without per-file diff inspection. As a heuristic, an
|
|
85
|
-
# implement commit touching a test file without an auth phrase is
|
|
86
|
-
# treated as a candidate, not a violation. Adversarial-review-time
|
|
87
|
-
# changes to a test file without auth ARE violations regardless.
|
|
88
|
-
has_auth = bool(auth_re.search(c.get("body", "")))
|
|
89
|
-
if has_auth:
|
|
90
|
-
authorized.append({"sha": c["sha"], "subject": c["subject"], "test_files": test_files})
|
|
91
|
-
elif is_implement_or_test_plan:
|
|
92
|
-
# candidate — could be a new test write, which is permitted
|
|
93
|
-
pass
|
|
94
|
-
else:
|
|
95
|
-
violations.append({
|
|
96
|
-
"sha": c["sha"],
|
|
97
|
-
"subject": c["subject"],
|
|
98
|
-
"test_files": test_files,
|
|
99
|
-
"artifact_type": c.get("artifact_type"),
|
|
100
|
-
})
|
|
101
|
-
return {
|
|
102
|
-
"authorized_count": len(authorized),
|
|
103
|
-
"candidate_violations": violations,
|
|
104
|
-
"note": (
|
|
105
|
-
"Implement-phase commits touching test files are not flagged here "
|
|
106
|
-
"(new tests are permitted by the test-immutability protocol). "
|
|
107
|
-
"Non-implement commits touching test files without an authorization "
|
|
108
|
-
"phrase are listed as candidate violations for manual review."
|
|
109
|
-
),
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def _qo_4(changes: dict) -> dict:
|
|
114
|
-
"""QO-4: verifier observed-vs-promised, summarized from
|
|
115
|
-
verification.test-plan-coverage map."""
|
|
116
|
-
counts = defaultdict(lambda: defaultdict(int))
|
|
117
|
-
per_change = []
|
|
118
|
-
for cid, arts in sorted(changes.items()):
|
|
119
|
-
v = arts.get("verification")
|
|
120
|
-
if not v:
|
|
121
|
-
continue
|
|
122
|
-
cov = v["fm"].get("test-plan-coverage")
|
|
123
|
-
if not isinstance(cov, dict):
|
|
124
|
-
continue
|
|
125
|
-
row = {"change": cid}
|
|
126
|
-
for key, value in cov.items():
|
|
127
|
-
counts[key][str(value)] += 1
|
|
128
|
-
row[key] = value
|
|
129
|
-
per_change.append(row)
|
|
130
|
-
summary = {key: dict(buckets) for key, buckets in counts.items()}
|
|
131
|
-
return {"summary": summary, "per_change": per_change}
|