@holmes-lab/holmes-kit 0.1.8 → 0.1.10

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 (51) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +48 -4
  3. package/dist/.build-id +1 -1
  4. package/dist/holmes/cli/test-platform.d.ts +25 -0
  5. package/dist/holmes/cli/test-platform.js +38 -0
  6. package/dist/holmes/cpg/cpg-scanner.d.ts +41 -0
  7. package/dist/holmes/cpg/cpg-scanner.js +53 -1
  8. package/dist/holmes/cpg/forbidden-edges.d.ts +73 -0
  9. package/dist/holmes/cpg/forbidden-edges.js +140 -0
  10. package/dist/holmes/cpg/hash-cache.js +13 -5
  11. package/dist/holmes/cpg/language-parser-walk.js +70 -4
  12. package/dist/holmes/cpg/proposed-content.d.ts +51 -0
  13. package/dist/holmes/cpg/proposed-content.js +72 -0
  14. package/dist/holmes/cpg/required-calls.d.ts +62 -0
  15. package/dist/holmes/cpg/required-calls.js +93 -0
  16. package/dist/holmes/guardrail/cspec-change.d.ts +23 -0
  17. package/dist/holmes/guardrail/cspec-change.js +70 -0
  18. package/dist/holmes/guardrail/risk-classifier.js +122 -0
  19. package/dist/holmes/guardrail/write-target.d.ts +42 -0
  20. package/dist/holmes/guardrail/write-target.js +69 -18
  21. package/dist/holmes/hooks/pre-tool-use.js +90 -5
  22. package/dist/holmes/hooks/stop.d.ts +17 -0
  23. package/dist/holmes/hooks/stop.js +39 -2
  24. package/dist/holmes/mcp/handlers.d.ts +41 -0
  25. package/dist/holmes/mcp/handlers.js +173 -3
  26. package/dist/holmes/mcp/tool-schemas.js +12 -0
  27. package/dist/holmes/project/dependencies.d.ts +15 -0
  28. package/dist/holmes/project/dependencies.js +58 -0
  29. package/dist/holmes/project/json-state.d.ts +24 -0
  30. package/dist/holmes/project/json-state.js +30 -0
  31. package/dist/holmes/reverse/scan.js +8 -1
  32. package/dist/holmes/review/scope.d.ts +29 -0
  33. package/dist/holmes/review/scope.js +44 -0
  34. package/dist/holmes/rtm/test-scope.d.ts +44 -0
  35. package/dist/holmes/rtm/test-scope.js +92 -2
  36. package/dist/holmes/server/dashboard.d.ts +77 -0
  37. package/dist/holmes/server/dashboard.js +703 -183
  38. package/dist/holmes/spec/approval-blockers.d.ts +21 -5
  39. package/dist/holmes/spec/approval-blockers.js +49 -6
  40. package/dist/holmes/spec/legacy-format.d.ts +14 -0
  41. package/dist/holmes/spec/legacy-format.js +15 -1
  42. package/dist/holmes/spec/nonfunctional.d.ts +70 -0
  43. package/dist/holmes/spec/nonfunctional.js +119 -0
  44. package/dist/holmes/spec/spec-parser.d.ts +25 -0
  45. package/dist/holmes/spec/spec-parser.js +46 -2
  46. package/dist/holmes/spec/spec-types.d.ts +4 -1
  47. package/dist/holmes/spec/spec-types.js +13 -1
  48. package/dist/holmes/testing/effects.d.ts +54 -0
  49. package/dist/holmes/testing/effects.js +107 -0
  50. package/package.json +3 -2
  51. package/playbooks/promote-slice/PLAYBOOK.md +20 -0
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.SUPPORTED_TEST_FRAMEWORKS = void 0;
37
+ exports.architectureGoverningFiles = architectureGoverningFiles;
37
38
  exports.accountChangedFiles = accountChangedFiles;
38
39
  exports.countTestCases = countTestCases;
39
40
  exports.extractAnchors = extractAnchors;
@@ -42,8 +43,50 @@ exports.computeTestScope = computeTestScope;
42
43
  // @implements A-SPEC-121.2
43
44
  const fs = __importStar(require("node:fs"));
44
45
  const path = __importStar(require("node:path"));
46
+ const spec_types_1 = require("../spec/spec-types");
45
47
  const rtm_check_1 = require("./rtm-check");
46
48
  const DEFAULTS = { partialBlast: 3, fullBlast: 12 };
49
+ /**
50
+ * Architecture-governing documents, picked out of a change set.
51
+ *
52
+ * @implements A-SPEC-233
53
+ * WHY THIS EXISTS. `assessRisk`'s architecture axis already returns `hard-hitl` for exactly these
54
+ * changes — "supersedes accepted ADR(s)" and "C-SPEC Forbidden Edges / Layer Rules changed". Nothing
55
+ * on the test side consumed that verdict. Measured 2026-08-22 on the built dist: a change impacting
56
+ * A-SPEC-224 (the structural-constraint slice itself) got tier `unit`, ONE test file, while
57
+ * `assessRisk` returned `hard-hitl`. The most dangerous change received the narrowest test run.
58
+ *
59
+ * WHY THIS PREDICATE IS COARSER THAN THE RISK AXIS, AND WHY THAT IS RIGHT. The risk axis asks "did
60
+ * the CONSTRAINTS change?" and answers precisely, via `cspecConstraintChanged(current, proposed)`.
61
+ * That needs before-and-after content, which is not available here: the change source is not always
62
+ * git — `change-source.ts` also diffs against a baseline hash tree, where no "before" exists to
63
+ * read. So this asks the weaker question it can actually answer: "was an architecture-governing
64
+ * document touched?" The two are allowed to differ because their cost asymmetries are opposite. A
65
+ * false positive on the risk axis interrupts a human for nothing; a false positive here runs more
66
+ * tests. Widening is this module's standing answer to uncertainty everywhere else, and it is the
67
+ * answer here too.
68
+ *
69
+ * WHAT IT DOES NOT SEE. Whether the edit changed a rule or only its prose. Deletions (`changedFiles`
70
+ * carries added/modified/renamed; `.ax/specs`/`.ax/decisions` deletion is gated separately). Any
71
+ * architecture decision recorded somewhere other than these two locations.
72
+ */
73
+ function architectureGoverningFiles(changedFiles) {
74
+ if (!Array.isArray(changedFiles))
75
+ return [];
76
+ // The C-SPEC folder is READ from the type table, never spelled here. A second spelling of a
77
+ // location is a second thing to keep in step, and this repository has already paid for that
78
+ // (`isTestFile` drifted into two copies; `parseSpec`'s section split was restated and returned an
79
+ // empty section while the check that used it went green).
80
+ const cspecDir = `.ax/specs/${spec_types_1.SPEC_TYPES['C-SPEC'].folder}/`;
81
+ const out = new Set();
82
+ for (const f of changedFiles) {
83
+ if (typeof f !== 'string' || !f.endsWith('.md'))
84
+ continue;
85
+ if (f.startsWith('.ax/decisions/') || f.startsWith(cspecDir))
86
+ out.add(f);
87
+ }
88
+ return [...out].sort();
89
+ }
47
90
  const stripSpecPrefix = (id) => (id.startsWith('SPEC:') ? id.slice('SPEC:'.length) : id);
48
91
  // "Is this a test file?" must be ONE predicate. Two copies had already drifted: the scanner excluded
49
92
  // JVM/.NET/Rust/C++ conventions from the production graph while this side still only recognized
@@ -279,15 +322,62 @@ function computeTestScope(impactedSpecsRaw, testAnchors, specs, coverageGaps = [
279
322
  // incomplete graph is not evidence of no impact — same epistemic state as C3 below, same answer.
280
323
  // Checked FIRST so a contradictory input (empty impact + unresolved present) resolves toward
281
324
  // full, never toward "nothing to re-test".
325
+ const architectureTouched = [...new Set(extras?.architectureTouched ?? [])].sort();
326
+ const obligationGaps = [...new Set(extras?.obligationGaps ?? [])].sort();
282
327
  if (unresolvedFiles.length > 0) {
283
328
  const shown = unresolvedFiles.slice(0, 5).join(', ');
284
329
  const suffix = unresolvedFiles.length > 5 ? ` (${unresolvedFiles.length} unresolved total)` : '';
285
330
  return {
286
331
  tier: 'full', impactedSpecs, impactedTestFiles: selectedTestFiles, blastRadius: 0,
287
- hitsFoundational: false, coverageGaps: [...coverageGaps], unresolvedFiles,
332
+ hitsFoundational: false, coverageGaps: [...coverageGaps], unresolvedFiles, architectureTouched, obligationGaps,
288
333
  reason: `${unresolvedFiles.length} changed source file(s) resolved to no spec — cannot narrow safely, full regression: ${shown}${suffix}`,
289
334
  };
290
335
  }
336
+ // @implements A-SPEC-233
337
+ // This branch RETURNS. That, not its position, is what makes it beat the branches below, which
338
+ // only assign `tier`/`reason` and fall through to the shared return.
339
+ //
340
+ // The first cut of this comment claimed the placement was the contract, and gave a reason that is
341
+ // false: that below `impactedSpecs.length === 0` the signal would vanish. Measured — moving this
342
+ // block to just above the final return left all twelve cases green, because that branch does not
343
+ // return either. A comment asserting a guarantee the code does not make is the exact state this
344
+ // slice's own REQ exists to remove, so it is written as it actually is.
345
+ //
346
+ // The one real ordering relation is with the OTHER early return, `unresolvedFiles`. Both answer
347
+ // `full`, so the tier is never in question between them; what the order decides is which reason
348
+ // is reported, and `unresolvedFiles` wins because it names the files to anchor — an operator can
349
+ // act on that.
350
+ //
351
+ // What carries the weight instead of placement: an architecture document having changed is true
352
+ // regardless of blast, and true even when `impactedSpecs` is empty — the shape of a change that
353
+ // edits ONLY a C-SPEC, which resolves to no impacted spec at all.
354
+ if (architectureTouched.length > 0) {
355
+ const shown = architectureTouched.slice(0, 5).join(', ');
356
+ const suffix = architectureTouched.length > 5 ? ` (${architectureTouched.length} total)` : '';
357
+ return {
358
+ tier: 'full', impactedSpecs, impactedTestFiles: selectedTestFiles, blastRadius: 0,
359
+ hitsFoundational: false, coverageGaps: [...coverageGaps], unresolvedFiles: [], architectureTouched, obligationGaps,
360
+ reason: `architecture-governing document(s) changed — full regression: ${shown}${suffix}`,
361
+ };
362
+ }
363
+ // @implements A-SPEC-235
364
+ // Another early return, so it wins over the assigning branches below for the same reason the last
365
+ // one does. Its position among the OTHER early returns decides only which reason is reported —
366
+ // all three answer `full`. It comes last of the three because the two above signal larger
367
+ // uncertainty: a graph that cannot see a changed file, and an architecture document that moved.
368
+ //
369
+ // The reason names the TAG, not just the spec id. "coverage gap in A-SPEC-235" sends the reader
370
+ // to grep; "A-SPEC-235 [purity]" tells them the case to write.
371
+ if (obligationGaps.length > 0) {
372
+ const shown = obligationGaps.slice(0, 5).join(', ');
373
+ const suffix = obligationGaps.length > 5 ? ` (${obligationGaps.length} total)` : '';
374
+ return {
375
+ tier: 'full', impactedSpecs, impactedTestFiles: selectedTestFiles, blastRadius: 0,
376
+ hitsFoundational: false, coverageGaps: [...coverageGaps], unresolvedFiles: [],
377
+ architectureTouched, obligationGaps,
378
+ reason: `declared obligation(s) with no test — full regression: ${shown}${suffix}`,
379
+ };
380
+ }
291
381
  // Tier on the DIRECTLY-impacted A-SPECs (what the changed code implements), NOT the whole impacted
292
382
  // set — impactedBy always returns the ancestor chain up to the REQ root, so keying on it made every
293
383
  // change "reach a foundational REQ" → always full (verification H4). Downstream blast of the direct
@@ -332,5 +422,5 @@ function computeTestScope(impactedSpecsRaw, testAnchors, specs, coverageGaps = [
332
422
  tier = 'unit';
333
423
  reason = `localized impact (blast=${blastRadius}) — run the ${impactedTestFiles.length} impacted test file(s)`;
334
424
  }
335
- return { tier, impactedSpecs, impactedTestFiles: selectedTestFiles, blastRadius, hitsFoundational, coverageGaps: [...coverageGaps], reason, unresolvedFiles: [] };
425
+ return { tier, impactedSpecs, impactedTestFiles: selectedTestFiles, blastRadius, hitsFoundational, coverageGaps: [...coverageGaps], reason, unresolvedFiles: [], architectureTouched, obligationGaps };
336
426
  }
@@ -28,3 +28,80 @@ export interface GraphEdge {
28
28
  * @implements A-SPEC-219
29
29
  */
30
30
  export declare function startDashboardServer(options: DashboardOptions): Promise<DashboardServerHandle>;
31
+ /**
32
+ * One end-to-end 6-stage trace row: REQ -> H-SPEC -> A-SPEC -> T-SPEC -> File -> AST Symbol.
33
+ *
34
+ * Every stage is nullable ON PURPOSE. A stage that does not exist in the repository is reported as
35
+ * `null` and named in `missingStages`; it is never back-filled with a synthesized spec id or with a
36
+ * placeholder file/symbol, because a traceability matrix that invents its own evidence cannot be
37
+ * audited against the repository it claims to describe.
38
+ *
39
+ * @implements A-SPEC-219
40
+ */
41
+ export interface PipelineRow {
42
+ reqId: string;
43
+ reqTitle: string;
44
+ hspecId: string | null;
45
+ aspecId: string | null;
46
+ tspecId: string | null;
47
+ fileId: string | null;
48
+ symbolId: string | null;
49
+ symbolLine: number | null;
50
+ /** How many of the 6 stages are backed by a real artifact (1..6; REQ itself always counts). */
51
+ stagesComplete: number;
52
+ /** Human-readable names of the stages with no artifact, in pipeline order. */
53
+ missingStages: string[];
54
+ /** COVERED only when all 6 stages resolve to a real artifact. */
55
+ status: 'COVERED' | 'UNCOVERED';
56
+ findingsCount: number;
57
+ criticalCount: number;
58
+ /**
59
+ * false when no findings ledger exists. `findingsCount: 0` then means "not scanned", never
60
+ * "scanned and clean", and the audit lens must render the two differently.
61
+ */
62
+ findingsScanned: boolean;
63
+ mutantCount: number;
64
+ /**
65
+ * `null` — never a number. A mutation SCORE requires executing the test suite once per mutant;
66
+ * this endpoint only generates mutants statically, so it has no basis for a score and says so
67
+ * rather than reporting a constant that reads as a measurement.
68
+ */
69
+ mutationScore: null;
70
+ mutationScoreMeasured: false;
71
+ }
72
+ export declare function isCanonicalStatus(status: unknown): boolean;
73
+ /**
74
+ * The kinds that are governed specs at all.
75
+ *
76
+ * @implements A-SPEC-219.1
77
+ * Held here rather than imported from SPEC_TYPES because REQ is a spec kind that SPEC_TYPES does
78
+ * list — the distinction this set draws is "document in the spec store that the pipeline governs"
79
+ * versus "document that lives there but never will", and JOB is the whole of the second group.
80
+ */
81
+ export declare const SPEC_KINDS: Set<string>;
82
+ /**
83
+ * Drop retired documents before anything reads the corpus.
84
+ *
85
+ * @implements A-SPEC-222.2
86
+ * A-SPEC-222.1 excluded `outdated` from `isGoverned` and claimed every other check would follow.
87
+ * `tspec-mirror.test.ts` refuted that, and the spec was corrected to name one known site. Measured
88
+ * 2026-08-22 it was three: /api/rtm exposed 132 retired ids, the heatmap 108, the graph 132 — a
89
+ * screen still pointing at documents retired the day before. Stated once here so the three sites
90
+ * cannot drift, with the suite enumerating the endpoints so a fourth cannot be added silently.
91
+ */
92
+ export declare function activeSpecs<T extends {
93
+ status?: unknown;
94
+ }>(specs: T[]): T[];
95
+ /** `A-SPEC-219.1` -> `A-SPEC`, `REQ-021` -> `REQ`. */
96
+ export declare function specKindOf(id: string): string;
97
+ /**
98
+ * Build the 6-stage pipeline matrix from the repository's real specs, scanned files and findings.
99
+ *
100
+ * Pure and exported so the truthfulness invariants can be asserted directly, without an HTTP round
101
+ * trip: no row may name an artifact that the inputs do not contain, and no symbol may be dropped.
102
+ *
103
+ * @implements A-SPEC-219
104
+ */
105
+ export declare function buildPipelineRows(specs: any[], files: any[], allFindings: any[], options?: {
106
+ findingsScanned?: boolean;
107
+ }): PipelineRow[];