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.
Files changed (134) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +76 -163
  3. package/VERSION +1 -1
  4. package/dist/commands/doctor.js +18 -8
  5. package/dist/commands/doctor.js.map +1 -1
  6. package/dist/commands/init.js +7 -3
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/update.js +9 -2
  9. package/dist/commands/update.js.map +1 -1
  10. package/dist/lib/descriptions.js +10 -17
  11. package/dist/lib/descriptions.js.map +1 -1
  12. package/dist/lib/wire.js +166 -138
  13. package/dist/lib/wire.js.map +1 -1
  14. package/dist/manifest.js +41 -29
  15. package/dist/manifest.js.map +1 -1
  16. package/package.json +3 -4
  17. package/template/.claude/agents/adversarial-reviewer.md +44 -50
  18. package/template/.claude/agents/app-architect.md +44 -96
  19. package/template/.claude/agents/data-architect.md +46 -89
  20. package/template/.claude/agents/test-strategist.md +39 -68
  21. package/template/.claude/skills/hstack-adr-new/SKILL.md +23 -73
  22. package/template/.claude/skills/hstack-adversarial-review/SKILL.md +46 -152
  23. package/template/.claude/skills/hstack-adversarial-review/references/finding-categories.md +89 -103
  24. package/template/.claude/skills/hstack-app-architecture/SKILL.md +43 -68
  25. package/template/.claude/skills/hstack-commit/SKILL.md +27 -68
  26. package/template/.claude/skills/hstack-data-architecture/SKILL.md +34 -56
  27. package/template/.claude/skills/hstack-promote/SKILL.md +55 -0
  28. package/template/.claude/skills/hstack-story/SKILL.md +58 -0
  29. package/template/.claude/skills/hstack-test-audit/SKILL.md +64 -0
  30. package/template/.claude/skills/hstack-wrap/SKILL.md +50 -0
  31. package/template/.claude/skills/hstack-wrap/references/pr-description.md +45 -0
  32. package/template/KERNEL.md +75 -318
  33. package/template/templates/adr.md +2 -18
  34. package/template/templates/app-architecture.md +38 -50
  35. package/template/templates/code-standards.md +107 -0
  36. package/template/templates/data-architecture.md +38 -89
  37. package/template/templates/eslint-clean-code.mjs +322 -0
  38. package/template/templates/infrastructure.md +54 -148
  39. package/template/templates/roadmap.md +6 -12
  40. package/template/templates/story.md +7 -13
  41. package/template/templates/tech-debt.md +2 -37
  42. package/template/templates/tech-stack.md +2 -12
  43. package/template/.claude/agents/data-specialist.md +0 -81
  44. package/template/.claude/agents/implementer.md +0 -97
  45. package/template/.claude/agents/kernel-fit-analyst.md +0 -129
  46. package/template/.claude/agents/planner.md +0 -75
  47. package/template/.claude/agents/product-discovery.md +0 -129
  48. package/template/.claude/agents/product-manager.md +0 -76
  49. package/template/.claude/agents/researcher.md +0 -87
  50. package/template/.claude/agents/security-reviewer.md +0 -88
  51. package/template/.claude/agents/spec-author.md +0 -89
  52. package/template/.claude/agents/stack-architect.md +0 -108
  53. package/template/.claude/agents/ui-ux-briefer.md +0 -75
  54. package/template/.claude/agents/verifier.md +0 -81
  55. package/template/.claude/skills/hstack-branch/SKILL.md +0 -76
  56. package/template/.claude/skills/hstack-brownfield-init/SKILL.md +0 -131
  57. package/template/.claude/skills/hstack-change-new/SKILL.md +0 -84
  58. package/template/.claude/skills/hstack-change-plan/SKILL.md +0 -108
  59. package/template/.claude/skills/hstack-configure/SKILL.md +0 -99
  60. package/template/.claude/skills/hstack-coord/SKILL.md +0 -95
  61. package/template/.claude/skills/hstack-data-review/SKILL.md +0 -93
  62. package/template/.claude/skills/hstack-finalize/SKILL.md +0 -140
  63. package/template/.claude/skills/hstack-flag/SKILL.md +0 -99
  64. package/template/.claude/skills/hstack-greenfield-init/SKILL.md +0 -148
  65. package/template/.claude/skills/hstack-help/SKILL.md +0 -153
  66. package/template/.claude/skills/hstack-implement/SKILL.md +0 -155
  67. package/template/.claude/skills/hstack-kernel-fit-promote/SKILL.md +0 -125
  68. package/template/.claude/skills/hstack-kernel-fit-scan/SKILL.md +0 -142
  69. package/template/.claude/skills/hstack-kernel-fit-scan/references/slack-setup.md +0 -42
  70. package/template/.claude/skills/hstack-kernel-fit-triage/SKILL.md +0 -121
  71. package/template/.claude/skills/hstack-module-spec/SKILL.md +0 -78
  72. package/template/.claude/skills/hstack-product-discovery/SKILL.md +0 -75
  73. package/template/.claude/skills/hstack-research/SKILL.md +0 -115
  74. package/template/.claude/skills/hstack-scaffold/SKILL.md +0 -113
  75. package/template/.claude/skills/hstack-security-review/SKILL.md +0 -95
  76. package/template/.claude/skills/hstack-ship/SKILL.md +0 -119
  77. package/template/.claude/skills/hstack-stack-decide/SKILL.md +0 -81
  78. package/template/.claude/skills/hstack-story-draft/SKILL.md +0 -92
  79. package/template/.claude/skills/hstack-tech-debt-new/SKILL.md +0 -95
  80. package/template/.claude/skills/hstack-tech-debt-resolve/SKILL.md +0 -123
  81. package/template/.claude/skills/hstack-tech-debt-stale/SKILL.md +0 -86
  82. package/template/.claude/skills/hstack-tech-debt-wontfix/SKILL.md +0 -77
  83. package/template/.claude/skills/hstack-telemetry/SKILL.md +0 -73
  84. package/template/.claude/skills/hstack-test-plan/SKILL.md +0 -159
  85. package/template/.claude/skills/hstack-ui-brief/SKILL.md +0 -81
  86. package/template/.claude/skills/hstack-verify/SKILL.md +0 -128
  87. package/template/scripts/compute-merge-readiness.mjs +0 -780
  88. package/template/scripts/coord/coord_scan.py +0 -681
  89. package/template/scripts/run-gates.sh +0 -388
  90. package/template/scripts/telemetry/__init__.py +0 -6
  91. package/template/scripts/telemetry/insights/__init__.py +0 -0
  92. package/template/scripts/telemetry/insights/contract_drift.py +0 -137
  93. package/template/scripts/telemetry/insights/kernel_fit.py +0 -438
  94. package/template/scripts/telemetry/insights/overengineering.py +0 -115
  95. package/template/scripts/telemetry/insights/quality_outcomes.py +0 -131
  96. package/template/scripts/telemetry/insights/token_economics.py +0 -302
  97. package/template/scripts/telemetry/insights/workflow_shape.py +0 -198
  98. package/template/scripts/telemetry/parsers/__init__.py +0 -0
  99. package/template/scripts/telemetry/parsers/bodies.py +0 -87
  100. package/template/scripts/telemetry/parsers/commits.py +0 -219
  101. package/template/scripts/telemetry/parsers/frontmatter.py +0 -322
  102. package/template/scripts/telemetry/parsers/sidecars.py +0 -61
  103. package/template/scripts/telemetry/parsers/transcripts.py +0 -294
  104. package/template/scripts/telemetry/render.py +0 -460
  105. package/template/scripts/telemetry/report.py +0 -150
  106. package/template/scripts/telemetry/run_kernel_fit.py +0 -95
  107. package/template/scripts/telemetry/session_id.py +0 -139
  108. package/template/scripts/validate-spec.mjs +0 -3303
  109. package/template/templates/adversarial-review.md +0 -54
  110. package/template/templates/bootstrap.md +0 -105
  111. package/template/templates/change-spec.md +0 -82
  112. package/template/templates/ci-cd.md +0 -41
  113. package/template/templates/coord-message.md +0 -56
  114. package/template/templates/data-review.md +0 -54
  115. package/template/templates/discovery/brainstorm.md +0 -69
  116. package/template/templates/discovery/forcing-questions.md +0 -76
  117. package/template/templates/discovery/project-brief.md +0 -53
  118. package/template/templates/figma-handoff.md +0 -38
  119. package/template/templates/glossary.md +0 -20
  120. package/template/templates/hardening-checklist.md +0 -73
  121. package/template/templates/incident-runbook.md +0 -57
  122. package/template/templates/kernel-fit-finding.md +0 -63
  123. package/template/templates/kernel-fit-flag.md +0 -52
  124. package/template/templates/module-spec.md +0 -49
  125. package/template/templates/persona.md +0 -38
  126. package/template/templates/plan.md +0 -53
  127. package/template/templates/product-brief.md +0 -56
  128. package/template/templates/security-review.md +0 -63
  129. package/template/templates/telemetry-sidecar.md +0 -227
  130. package/template/templates/test-plan.md +0 -119
  131. package/template/templates/threat-model.md +0 -54
  132. package/template/templates/ui-brief.md +0 -49
  133. package/template/templates/verification.md +0 -63
  134. 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}