@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.
- package/CHANGELOG.md +99 -0
- package/README.md +48 -4
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/test-platform.d.ts +25 -0
- package/dist/holmes/cli/test-platform.js +38 -0
- package/dist/holmes/cpg/cpg-scanner.d.ts +41 -0
- package/dist/holmes/cpg/cpg-scanner.js +53 -1
- package/dist/holmes/cpg/forbidden-edges.d.ts +73 -0
- package/dist/holmes/cpg/forbidden-edges.js +140 -0
- package/dist/holmes/cpg/hash-cache.js +13 -5
- package/dist/holmes/cpg/language-parser-walk.js +70 -4
- package/dist/holmes/cpg/proposed-content.d.ts +51 -0
- package/dist/holmes/cpg/proposed-content.js +72 -0
- package/dist/holmes/cpg/required-calls.d.ts +62 -0
- package/dist/holmes/cpg/required-calls.js +93 -0
- package/dist/holmes/guardrail/cspec-change.d.ts +23 -0
- package/dist/holmes/guardrail/cspec-change.js +70 -0
- package/dist/holmes/guardrail/risk-classifier.js +122 -0
- package/dist/holmes/guardrail/write-target.d.ts +42 -0
- package/dist/holmes/guardrail/write-target.js +69 -18
- package/dist/holmes/hooks/pre-tool-use.js +90 -5
- package/dist/holmes/hooks/stop.d.ts +17 -0
- package/dist/holmes/hooks/stop.js +39 -2
- package/dist/holmes/mcp/handlers.d.ts +41 -0
- package/dist/holmes/mcp/handlers.js +173 -3
- package/dist/holmes/mcp/tool-schemas.js +12 -0
- package/dist/holmes/project/dependencies.d.ts +15 -0
- package/dist/holmes/project/dependencies.js +58 -0
- package/dist/holmes/project/json-state.d.ts +24 -0
- package/dist/holmes/project/json-state.js +30 -0
- package/dist/holmes/reverse/scan.js +8 -1
- package/dist/holmes/review/scope.d.ts +29 -0
- package/dist/holmes/review/scope.js +44 -0
- package/dist/holmes/rtm/test-scope.d.ts +44 -0
- package/dist/holmes/rtm/test-scope.js +92 -2
- package/dist/holmes/server/dashboard.d.ts +77 -0
- package/dist/holmes/server/dashboard.js +703 -183
- package/dist/holmes/spec/approval-blockers.d.ts +21 -5
- package/dist/holmes/spec/approval-blockers.js +49 -6
- package/dist/holmes/spec/legacy-format.d.ts +14 -0
- package/dist/holmes/spec/legacy-format.js +15 -1
- package/dist/holmes/spec/nonfunctional.d.ts +70 -0
- package/dist/holmes/spec/nonfunctional.js +119 -0
- package/dist/holmes/spec/spec-parser.d.ts +25 -0
- package/dist/holmes/spec/spec-parser.js +46 -2
- package/dist/holmes/spec/spec-types.d.ts +4 -1
- package/dist/holmes/spec/spec-types.js +13 -1
- package/dist/holmes/testing/effects.d.ts +54 -0
- package/dist/holmes/testing/effects.js +107 -0
- package/package.json +3 -2
- 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[];
|