@holmes-lab/holmes-kit 0.1.7 → 0.1.9

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 (114) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +11 -7
  3. package/bin/holmes-mcp.js +0 -0
  4. package/dist/.build-id +1 -1
  5. package/dist/holmes/cli/agents.d.ts +50 -0
  6. package/dist/holmes/cli/ci-gate.d.ts +18 -0
  7. package/dist/holmes/cli/doctor.d.ts +44 -0
  8. package/dist/holmes/cli/gitignore-merge.d.ts +18 -0
  9. package/dist/holmes/cli/governed-precondition.d.ts +27 -0
  10. package/dist/holmes/cli/index.d.ts +14 -0
  11. package/dist/holmes/cli/index.js +23 -1
  12. package/dist/holmes/cli/init.d.ts +69 -0
  13. package/dist/holmes/cli/interactive-prompt.d.ts +9 -0
  14. package/dist/holmes/cli/playbook-skills.d.ts +129 -0
  15. package/dist/holmes/cli/roles-readme.d.ts +12 -0
  16. package/dist/holmes/cli/serve.d.ts +14 -0
  17. package/dist/holmes/cli/serve.js +70 -0
  18. package/dist/holmes/cli/settings-merge.d.ts +66 -0
  19. package/dist/holmes/config/config.d.ts +13 -0
  20. package/dist/holmes/context/bundler.d.ts +40 -0
  21. package/dist/holmes/context/render.d.ts +9 -0
  22. package/dist/holmes/context/tiers.d.ts +54 -0
  23. package/dist/holmes/context/tokens.d.ts +2 -0
  24. package/dist/holmes/cpg/ast-mutation.d.ts +31 -0
  25. package/dist/holmes/cpg/ast-mutation.js +126 -0
  26. package/dist/holmes/cpg/cpg-scanner.d.ts +69 -0
  27. package/dist/holmes/cpg/dynamic-cpg.d.ts +18 -0
  28. package/dist/holmes/cpg/dynamic-cpg.js +82 -0
  29. package/dist/holmes/cpg/hash-cache.d.ts +21 -0
  30. package/dist/holmes/cpg/language-parser-walk.d.ts +31 -0
  31. package/dist/holmes/cpg/language-parser-worker.d.ts +1 -0
  32. package/dist/holmes/cpg/language-parser.d.ts +116 -0
  33. package/dist/holmes/cpg/program-slicing.d.ts +21 -0
  34. package/dist/holmes/cpg/program-slicing.js +123 -0
  35. package/dist/holmes/cpg/scan-cache.d.ts +21 -0
  36. package/dist/holmes/cpg/source-path.d.ts +2 -0
  37. package/dist/holmes/cpg/test-files.d.ts +36 -0
  38. package/dist/holmes/governance/constitution-debt.d.ts +3 -0
  39. package/dist/holmes/governance/constitution-report.d.ts +23 -0
  40. package/dist/holmes/governance/constitution.d.ts +61 -0
  41. package/dist/holmes/governance/identity.d.ts +28 -0
  42. package/dist/holmes/governance/ledger-lock.d.ts +55 -0
  43. package/dist/holmes/governance/ledger-store.conformance.d.ts +11 -0
  44. package/dist/holmes/governance/ledger-store.d.ts +114 -0
  45. package/dist/holmes/governance/progress-ledger.d.ts +25 -0
  46. package/dist/holmes/governance/provenance-chain.d.ts +154 -0
  47. package/dist/holmes/governance/provenance-ledger.d.ts +102 -0
  48. package/dist/holmes/governance/provenance-schema.d.ts +21 -0
  49. package/dist/holmes/governance/replica-id.d.ts +20 -0
  50. package/dist/holmes/governance/role-policy.d.ts +52 -0
  51. package/dist/holmes/governance/trust-score.d.ts +32 -0
  52. package/dist/holmes/guardrail/anchors.d.ts +8 -0
  53. package/dist/holmes/guardrail/blind-spots.d.ts +32 -0
  54. package/dist/holmes/guardrail/decision-ledger.d.ts +29 -0
  55. package/dist/holmes/guardrail/executable-artifact.d.ts +1 -0
  56. package/dist/holmes/guardrail/governance-history.d.ts +15 -0
  57. package/dist/holmes/guardrail/phase.d.ts +27 -0
  58. package/dist/holmes/guardrail/risk-classifier.d.ts +52 -0
  59. package/dist/holmes/guardrail/risk-gate.d.ts +77 -0
  60. package/dist/holmes/guardrail/risk-types.d.ts +48 -0
  61. package/dist/holmes/guardrail/tspec-state.d.ts +71 -0
  62. package/dist/holmes/guardrail/write-target.d.ts +77 -0
  63. package/dist/holmes/hooks/adapters/antigravity.d.ts +42 -0
  64. package/dist/holmes/hooks/pre-tool-use.d.ts +144 -0
  65. package/dist/holmes/hooks/stop.d.ts +91 -0
  66. package/dist/holmes/mcp/basis.d.ts +100 -0
  67. package/dist/holmes/mcp/handlers.d.ts +482 -0
  68. package/dist/holmes/mcp/server.d.ts +2 -0
  69. package/dist/holmes/mcp/stdio-client.d.ts +51 -0
  70. package/dist/holmes/mcp/supervisor.d.ts +48 -0
  71. package/dist/holmes/mcp/tool-schemas.d.ts +12 -0
  72. package/dist/holmes/mcp/validate-args.d.ts +27 -0
  73. package/dist/holmes/messages/registry.d.ts +23 -0
  74. package/dist/holmes/project/baseline.d.ts +66 -0
  75. package/dist/holmes/project/change-source.d.ts +89 -0
  76. package/dist/holmes/project/ignore.d.ts +33 -0
  77. package/dist/holmes/project/root.d.ts +34 -0
  78. package/dist/holmes/reverse/anchor.d.ts +47 -0
  79. package/dist/holmes/reverse/cluster.d.ts +45 -0
  80. package/dist/holmes/reverse/draft.d.ts +33 -0
  81. package/dist/holmes/reverse/dynamic-wiring.d.ts +18 -0
  82. package/dist/holmes/reverse/scan.d.ts +103 -0
  83. package/dist/holmes/reverse/surface.d.ts +91 -0
  84. package/dist/holmes/reverse/test-map.d.ts +91 -0
  85. package/dist/holmes/review/coverage.d.ts +21 -0
  86. package/dist/holmes/review/findings.d.ts +49 -0
  87. package/dist/holmes/review/package.d.ts +26 -0
  88. package/dist/holmes/review/review-targets.d.ts +28 -0
  89. package/dist/holmes/review/scope.d.ts +13 -0
  90. package/dist/holmes/review/test-evidence.d.ts +31 -0
  91. package/dist/holmes/review/test-runner.d.ts +141 -0
  92. package/dist/holmes/rtm/dataflow-taint.d.ts +81 -0
  93. package/dist/holmes/rtm/gap-analyzer.d.ts +28 -0
  94. package/dist/holmes/rtm/git-changes.d.ts +33 -0
  95. package/dist/holmes/rtm/heatmap.d.ts +21 -0
  96. package/dist/holmes/rtm/incremental.d.ts +38 -0
  97. package/dist/holmes/rtm/localize.d.ts +36 -0
  98. package/dist/holmes/rtm/rtm-builder.d.ts +101 -0
  99. package/dist/holmes/rtm/rtm-check.d.ts +13 -0
  100. package/dist/holmes/rtm/rtm-graph.d.ts +88 -0
  101. package/dist/holmes/rtm/taint.d.ts +35 -0
  102. package/dist/holmes/rtm/test-scope.d.ts +85 -0
  103. package/dist/holmes/server/dashboard.d.ts +85 -0
  104. package/dist/holmes/server/dashboard.js +2188 -0
  105. package/dist/holmes/spec/approval-blockers.d.ts +62 -0
  106. package/dist/holmes/spec/breaking-change.d.ts +42 -0
  107. package/dist/holmes/spec/legacy-format.d.ts +93 -0
  108. package/dist/holmes/spec/spec-digest.d.ts +29 -0
  109. package/dist/holmes/spec/spec-parser.d.ts +12 -0
  110. package/dist/holmes/spec/spec-store.conformance.d.ts +10 -0
  111. package/dist/holmes/spec/spec-store.d.ts +130 -0
  112. package/dist/holmes/spec/spec-types.d.ts +150 -0
  113. package/dist/holmes/spec/validator.d.ts +65 -0
  114. package/package.json +2 -1
@@ -0,0 +1,116 @@
1
+ export interface CodeSymbol {
2
+ name: string;
3
+ kind: 'function' | 'class' | 'method';
4
+ qualifiedName: string;
5
+ startLine: number;
6
+ endLine: number;
7
+ }
8
+ /**
9
+ * A call/import relation recovered from a parse tree.
10
+ *
11
+ * @implements A-SPEC-136
12
+ * WIRED END-TO-END. `CpgScanner.scan` populates `ScannedFile.edges` via `extractEdges`;
13
+ * `addCallEdges` resolves each bare callee name to a graph edge under a precision-over-recall rule
14
+ * (same-file first, else the single unique definition across the scan, else nothing); and
15
+ * `impactedBy` traverses `callersOf`, so a change to an unanchored callee reaches its caller's spec.
16
+ * Measured: the self-repo carries ~2260 `calls` edges. Only `calls` edges are traversed for impact;
17
+ * `imports` are recorded but advisory.
18
+ *
19
+ * Traceability remains a file-level `@implements` claim as the FLOOR — a call the walk cannot resolve
20
+ * leaves a gap L6 surfaces, never a wrong edge (a wrong edge poisons every impact answer through it).
21
+ * Call-edge extraction covers TS/JS and Python (bespoke walks, for their import handling) and
22
+ * Go/Rust/Java/C#/C++ (table-driven `EDGE_CONFIG` + `walkEdges` in language-parser-walk.js). The
23
+ * corrected wiring pin lives in rtm-builder.test.ts ('call edges ARE scanned, resolved, and
24
+ * traversed'); a prior version of this note wrongly said edges were unwired and misled an audit.
25
+ */
26
+ export interface CodeEdge {
27
+ from: string;
28
+ to: string;
29
+ rel: 'calls' | 'imports';
30
+ }
31
+ export type TsLang = 'typescript' | 'tsx' | 'python' | 'csharp' | 'java' | 'go' | 'rust' | 'cpp';
32
+ /**
33
+ * Def-use FACTS for one file — what a taint fixpoint needs, with no analysis applied yet.
34
+ *
35
+ * @implements A-SPEC-140.1
36
+ * `fn` is always the enclosing definition's QUALIFIED name, built by the same rule as
37
+ * `CodeEdge.from`, so a fact can be joined to the call graph. `calls[].args` is ORDERED: position is
38
+ * what binds an argument to a parameter, and `params[].index` is the parameter's DECLARATION index —
39
+ * a parameter whose identifier cannot be read is omitted but still consumes its index, so the
40
+ * remaining ones keep their true positions.
41
+ *
42
+ * Every field reports what the tree literally says. A construct the walk cannot read produces NO
43
+ * fact rather than a guessed one (precision over recall): the cost is a propagation hop that does
44
+ * not happen, which is a disclosed false negative, versus a fabricated flow, which is a false
45
+ * positive nobody can distinguish from a real finding.
46
+ */
47
+ export interface DataFlowFacts {
48
+ defs: {
49
+ fn: string;
50
+ name: string;
51
+ expr: Expr;
52
+ line: number;
53
+ }[];
54
+ calls: {
55
+ fn: string;
56
+ callee: string;
57
+ args: Expr[];
58
+ line: number;
59
+ }[];
60
+ params: {
61
+ fn: string;
62
+ index: number;
63
+ name: string;
64
+ }[];
65
+ returns: {
66
+ fn: string;
67
+ expr: Expr;
68
+ line: number;
69
+ }[];
70
+ }
71
+ /**
72
+ * An expression as STRUCTURE rather than text.
73
+ *
74
+ * @implements A-SPEC-140.1
75
+ * Text alone would force the propagation pass to ask "does this mention a tainted variable?"
76
+ * lexically, and two ordinary lines break that: `const note = "raw user input";` falsely taints
77
+ * `note` because the literal contains the word, and `exec(sanitize(raw))` looks tainted because the
78
+ * argument text contains `raw` even though it was sanitized inline. Both are exactly the false
79
+ * positives REQ-140 exists to remove, so the structure is recovered once, from the tree.
80
+ */
81
+ export interface Expr {
82
+ /** Source text, trimmed and bounded (200 chars) — for display, and for the inherently textual source/sink patterns. */
83
+ text: string;
84
+ /** Identifiers READ, deduped, first-appearance order. A property name after a dot is not a read (`req.body.cmd` → `['req']`). */
85
+ refs: string[];
86
+ /** Bare names INVOKED, deduped, first-appearance order (`sanitize(raw)` → `['sanitize']`), by the edge walk's last-segment rule. */
87
+ callees: string[];
88
+ }
89
+ export interface LanguageParser {
90
+ extractSymbols(code: string, lang?: TsLang): CodeSymbol[];
91
+ extractEdges(code: string, lang?: TsLang): CodeEdge[];
92
+ /** `undefined` — never an empty fact set — for a language with no data-flow walk: not analysed ≠ nothing found. */
93
+ extractDataFlow(code: string, lang?: TsLang): DataFlowFacts | undefined;
94
+ }
95
+ /**
96
+ * Thrown for RPC/infrastructure failures on the jest worker_thread path
97
+ * (timeout, dead worker, desynchronized response) — as opposed to a genuine
98
+ * per-file parse exception. Callers such as CpgScanner use this to avoid
99
+ * silently treating a systemic worker failure as "just this file is
100
+ * unparseable".
101
+ */
102
+ export declare class TreeSitterInfraError extends Error {
103
+ }
104
+ /**
105
+ * Whether a language has a data-flow walk at all.
106
+ *
107
+ * @implements A-SPEC-140.1
108
+ * Read straight from the walk's own table so the two can never drift: a caller that guessed the
109
+ * list would eventually claim "analysed, nothing found" for a language nothing ever looked at.
110
+ */
111
+ export declare function hasDataFlowWalk(lang: TsLang): boolean;
112
+ export declare class TreeSitterTsParser implements LanguageParser {
113
+ extractSymbols(code: string, lang?: TsLang): CodeSymbol[];
114
+ extractEdges(code: string, lang?: TsLang): CodeEdge[];
115
+ extractDataFlow(code: string, lang?: TsLang): DataFlowFacts | undefined;
116
+ }
@@ -0,0 +1,21 @@
1
+ import { ScannedFile } from './cpg-scanner';
2
+ export interface SliceCriterion {
3
+ file: string;
4
+ symbolName?: string;
5
+ line?: number;
6
+ direction?: 'backward' | 'forward' | 'both';
7
+ }
8
+ export interface SliceResult {
9
+ criterion: SliceCriterion;
10
+ slicedNodeIds: Set<string>;
11
+ totalOriginalNodes: number;
12
+ prunedNodesCount: number;
13
+ pruningRatioPct: number;
14
+ slicedFiles: ScannedFile[];
15
+ }
16
+ /**
17
+ * Perform precise program slicing over CPG AST nodes and call edges to prune noise and isolate affected nodes.
18
+ *
19
+ * @implements A-SPEC-217
20
+ */
21
+ export declare function sliceProgram(scannedFiles: ScannedFile[] | null | undefined, criterion: SliceCriterion | null | undefined): SliceResult;
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sliceProgram = sliceProgram;
4
+ function isFileMatch(sfSource, sfFull, normTargetFile) {
5
+ if (!normTargetFile)
6
+ return true;
7
+ if (sfSource !== '' && (sfSource === normTargetFile || sfSource.endsWith('/' + normTargetFile)))
8
+ return true;
9
+ if (sfFull !== '' && (sfFull === normTargetFile || sfFull.endsWith('/' + normTargetFile)))
10
+ return true;
11
+ return false;
12
+ }
13
+ function getSymbolId(sf, sym) {
14
+ if (sym.qualifiedName)
15
+ return sym.qualifiedName;
16
+ const sfSource = (sf.sourcePath || '').split(/[\\/]/).filter(Boolean).join('/');
17
+ const sfFull = (sf.path || '').split(/[\\/]/).filter(Boolean).join('/');
18
+ return `${sfSource || sfFull}#${sym.name}`;
19
+ }
20
+ /**
21
+ * Perform precise program slicing over CPG AST nodes and call edges to prune noise and isolate affected nodes.
22
+ *
23
+ * @implements A-SPEC-217
24
+ */
25
+ function sliceProgram(scannedFiles, criterion) {
26
+ const safeFiles = Array.isArray(scannedFiles) ? scannedFiles : [];
27
+ const safeCriterion = criterion || { file: '' };
28
+ const direction = safeCriterion.direction || 'both';
29
+ let totalOriginalNodes = 0;
30
+ // Build symbol lookup maps and adjacency graph
31
+ const nodeMap = new Map();
32
+ const forwardEdges = new Map();
33
+ const backwardEdges = new Map();
34
+ const normTargetFile = (safeCriterion.file || '').split(/[\\/]/).filter(Boolean).join('/');
35
+ const seedNodeIds = new Set();
36
+ for (const sf of safeFiles) {
37
+ if (!sf)
38
+ continue;
39
+ const sfSource = (sf.sourcePath || '').split(/[\\/]/).filter(Boolean).join('/');
40
+ const sfFull = (sf.path || '').split(/[\\/]/).filter(Boolean).join('/');
41
+ const fileMatches = isFileMatch(sfSource, sfFull, normTargetFile);
42
+ const symbols = Array.isArray(sf.symbols) ? sf.symbols : [];
43
+ for (const sym of symbols) {
44
+ totalOriginalNodes++;
45
+ const id = getSymbolId(sf, sym);
46
+ nodeMap.set(id, { file: sf, name: sym.name, qualifiedName: id, line: sym.startLine });
47
+ if (!forwardEdges.has(id))
48
+ forwardEdges.set(id, new Set());
49
+ if (!backwardEdges.has(id))
50
+ backwardEdges.set(id, new Set());
51
+ if (fileMatches) {
52
+ let nameMatch = !safeCriterion.symbolName || sym.name === safeCriterion.symbolName || sym.qualifiedName === safeCriterion.symbolName;
53
+ let lineMatch = safeCriterion.line === undefined || (safeCriterion.line >= sym.startLine && safeCriterion.line <= sym.endLine);
54
+ if (nameMatch && lineMatch) {
55
+ seedNodeIds.add(id);
56
+ }
57
+ }
58
+ }
59
+ const edges = Array.isArray(sf.edges) ? sf.edges : [];
60
+ for (const e of edges) {
61
+ if (e && e.from && e.to) {
62
+ if (!forwardEdges.has(e.from))
63
+ forwardEdges.set(e.from, new Set());
64
+ forwardEdges.get(e.from).add(e.to);
65
+ if (!backwardEdges.has(e.to))
66
+ backwardEdges.set(e.to, new Set());
67
+ backwardEdges.get(e.to).add(e.from);
68
+ }
69
+ }
70
+ }
71
+ // Traversal for transitive closure
72
+ const slicedNodeIds = new Set();
73
+ const queue = Array.from(seedNodeIds);
74
+ for (const id of seedNodeIds) {
75
+ slicedNodeIds.add(id);
76
+ }
77
+ while (queue.length > 0) {
78
+ const curr = queue.shift();
79
+ if (direction === 'forward' || direction === 'both') {
80
+ const nextForward = forwardEdges.get(curr);
81
+ if (nextForward) {
82
+ for (const nextId of nextForward) {
83
+ if (!slicedNodeIds.has(nextId) && nodeMap.has(nextId)) {
84
+ slicedNodeIds.add(nextId);
85
+ queue.push(nextId);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ if (direction === 'backward' || direction === 'both') {
91
+ const nextBackward = backwardEdges.get(curr);
92
+ if (nextBackward) {
93
+ for (const prevId of nextBackward) {
94
+ if (!slicedNodeIds.has(prevId) && nodeMap.has(prevId)) {
95
+ slicedNodeIds.add(prevId);
96
+ queue.push(prevId);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
102
+ const prunedNodesCount = totalOriginalNodes - slicedNodeIds.size;
103
+ const pruningRatioPct = totalOriginalNodes > 0 ? Math.round((prunedNodesCount / totalOriginalNodes) * 100) : 0;
104
+ // Filter scanned files to include only sliced symbols
105
+ const slicedFiles = safeFiles.map((sf) => {
106
+ if (!sf)
107
+ return sf;
108
+ const symbols = Array.isArray(sf.symbols) ? sf.symbols : [];
109
+ const filteredSymbols = symbols.filter((sym) => {
110
+ const id = getSymbolId(sf, sym);
111
+ return slicedNodeIds.has(id);
112
+ });
113
+ return { ...sf, symbols: filteredSymbols };
114
+ });
115
+ return {
116
+ criterion: safeCriterion,
117
+ slicedNodeIds,
118
+ totalOriginalNodes,
119
+ prunedNodesCount,
120
+ pruningRatioPct,
121
+ slicedFiles,
122
+ };
123
+ }
@@ -0,0 +1,21 @@
1
+ import { ScannedFile } from './cpg-scanner';
2
+ export declare const SCAN_CACHE_VERSION = 3;
3
+ export declare class ScanFileCache {
4
+ private readonly dir;
5
+ private readonly file;
6
+ private entries;
7
+ private seen;
8
+ private dirty;
9
+ hits: number;
10
+ misses: number;
11
+ constructor(dir: string);
12
+ static hashOf(content: string): string;
13
+ /** Cached scan result iff the stored hash matches this content. Marks the path as seen. */
14
+ get(sourcePath: string, contentHash: string): Omit<ScannedFile, 'path'> | undefined;
15
+ put(sourcePath: string, contentHash: string, file: Omit<ScannedFile, 'path'>): void;
16
+ /**
17
+ * Persist once per scan. Mark-and-sweep: entries whose path was NOT seen this scan belong to
18
+ * deleted/renamed files — pruned so the cache cannot grow unboundedly. No-op when nothing changed.
19
+ */
20
+ save(scannedScopeDir?: string): void;
21
+ }
@@ -0,0 +1,2 @@
1
+ /** Repo-root-relative, POSIX-normalized path used as the provenance key for graph rows. */
2
+ export declare function toSourcePath(repoRoot: string, absOrRelPath: string): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * What counts as a TEST file, per ecosystem — the single table both the scanner and the phase gate
3
+ * read.
4
+ *
5
+ * It lives in its own module, apart from the scanner, because two very different consumers need it
6
+ * and only one of them can afford the scanner's dependencies. `CpgScanner` uses it to keep test
7
+ * symbols out of the production graph; `classifyAction` (the PreToolUse phase gate, which runs on
8
+ * every Write/Edit and is measured in tens of milliseconds) uses it to tell WRITE_TEST from
9
+ * WRITE_CODE. When the two disagree about what a test is, the harness contradicts itself: measured on
10
+ * a real Python target, `tests/test_briefing.py` was excluded from the graph as a test AND gated as
11
+ * production code, so editing it demanded a stricter chain than the code it tests.
12
+ *
13
+ * Nothing here imports anything, and that is deliberate — it is the property that lets the hook use
14
+ * it without pulling tree-sitter into a latency-critical path.
15
+ */
16
+ /**
17
+ * Test-file conventions, one entry per ecosystem, matched against the path RELATIVE to the project
18
+ * root (POSIX-separated).
19
+ *
20
+ * Test code must never enter the production graph: its symbols would be counted as production, which
21
+ * corrupts change-impact, clustering, and any reverse-engineered spec drafted from a scan. The
22
+ * previous rules covered TS/JS, Python and Go only, so — measured — `AppTest.java`, `AppTests.cs`,
23
+ * `app_test.rs`, `tests/mod.rs` and `app_test.cpp` all leaked through.
24
+ *
25
+ * Kept as one exported table rather than scattered constants so the per-language coverage is
26
+ * auditable at a glance and testable as a unit; `isTestFile` is the only entry point.
27
+ */
28
+ export declare const TEST_FILE_PATTERNS: ReadonlyArray<readonly [string, RegExp]>;
29
+ /**
30
+ * True when a PROJECT-ROOT-RELATIVE POSIX path is a test file by any ecosystem's convention.
31
+ *
32
+ * The path must be relative to the project, not absolute: several rules key on a directory segment,
33
+ * so a repository that merely lives under some `/…/test/` directory on disk would otherwise read as
34
+ * one big test tree.
35
+ */
36
+ export declare function isTestFile(relPosixPath: string): boolean;
@@ -0,0 +1,3 @@
1
+ export declare function writeDebt(root: string, articles: string[]): void;
2
+ export declare function readDebt(root: string): string[] | null;
3
+ export declare function clearDebt(root: string): void;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Renders constitution violations as text a reader can act on.
3
+ *
4
+ * This exists because of a measured failure of diagnosis, not a failure of logic. The suite's
5
+ * corpus assertions compare arrays of violation objects, so a failure printed an opaque diff —
6
+ * `expect([]).toEqual([6 items])`. Twice that failure was recorded as an unidentified intermittent
7
+ * and the cause was never found; the 2026-08-08 run reproduced it eleven times on demand and showed
8
+ * it was never random at all, but a true report about the working tree at the moment it was read.
9
+ *
10
+ * Pure and exported rather than assembled inside the test, because an unverified diagnostic is wrong
11
+ * exactly when it is needed. Extracting it lets a mutation prove the message really names the spec.
12
+ */
13
+ export interface ConstitutionViolation {
14
+ article?: string;
15
+ detail?: string;
16
+ }
17
+ /**
18
+ * Cap on listed violations. A corpus-wide regression across 300+ specs would otherwise bury the
19
+ * terminal in hundreds of lines, which reads as noise and gets skipped — the same outcome as
20
+ * printing nothing.
21
+ */
22
+ export declare const MAX_LISTED = 20;
23
+ export declare function describeViolations(violations: ConstitutionViolation[]): string;
@@ -0,0 +1,61 @@
1
+ import { Spec } from '../spec/spec-parser';
2
+ /**
3
+ * L1 — Governance CONSTITUTION (target-architecture §7 L1).
4
+ *
5
+ * The inviolable principles are defined ONCE, here, as named ARTICLES — deterministic, pure checks —
6
+ * and RE-VERIFIED at every phase boundary by the hooks (the Stop gate calls verifyConstitution each
7
+ * turn; the PreToolUse gates enforce ART-1/ART-5 per-action). This is the spec-kit "constitution" +
8
+ * bkit "BeforePhase re-verification" pattern, realized as CODE, not prompt text: an article cannot be
9
+ * rationalized away by a model because it is executed, not read.
10
+ *
11
+ * Articles (violations carry the article id so remediation is addressable):
12
+ * ART-1 No-Spec-No-Code — enforced per-action by the PreToolUse phase gate (not re-checked here;
13
+ * listed for completeness of the constitution document).
14
+ * ART-2 RTM integrity — the governed spec graph has no orphans/cycles/dangling/duplicates.
15
+ * ART-3 Spec validity — every governed spec passes its type rules (incl. 4-quadrant GWT).
16
+ * ART-4 Coverage honesty — an approved T-SPEC is backed by REAL anchored test cases (evidence).
17
+ * ART-5 Approval out-of-band — spec approval/config writes require HOLMES_APPROVAL (enforced
18
+ * per-action by the PreToolUse gate; listed for completeness).
19
+ *
20
+ * Pure: no I/O, no clocks. The hooks supply loaded specs + evidence.
21
+ */
22
+ export interface ConstitutionViolation {
23
+ article: string;
24
+ detail: string;
25
+ }
26
+ export interface ConstitutionContext {
27
+ specs: Spec[];
28
+ /**
29
+ * @implements A-SPEC-160
30
+ * Recorded review findings, for ART-6. Supplied by the hook, like ART-4's evidence — the
31
+ * constitution stays pure and does no I/O.
32
+ *
33
+ * @implements A-SPEC-191
34
+ * `severity` rides along for ART-7. The ledger always carried it; this type narrowed it away, so
35
+ * the constitution could not legally read the one field that says "this must not ship". Optional:
36
+ * pre-REQ-157 records never had it, and absence stays silent (the A-SPEC-146 trap). `summary`
37
+ * rides along too (§4d) — a violation that names only an opaque id sends the operator to grep.
38
+ */
39
+ findings?: {
40
+ id: string;
41
+ status: 'open' | 'resolved';
42
+ basis?: string;
43
+ severity?: string;
44
+ summary?: string;
45
+ }[];
46
+ /** A-SPEC id -> count of real test cases in anchored test files (H1 syntactic lower bound). */
47
+ testCasesByAspec?: Record<string, number>;
48
+ /**
49
+ * A-SPEC id -> cases that ACTUALLY EXECUTED in a recorded PASSING test run.
50
+ *
51
+ * STRICTLY ADDITIVE (adversarial review C1/C3): this can only ADD violations, never satisfy ART-4
52
+ * on its own. The record lives in an unsigned, gitignored file an agent can rewrite, and its
53
+ * freshness stamp (git HEAD) does not move for uncommitted work — so letting it OVERRIDE the
54
+ * syntactic bound made forgery and staleness able to SUPPRESS real violations (measured: 16 -> 0).
55
+ * As an additive signal both failure modes are defused: forging it unlocks nothing, and a stale
56
+ * record cannot hide a missing test suite because the syntactic bound is still enforced.
57
+ */
58
+ executedByAspec?: Record<string, number>;
59
+ }
60
+ export declare const ARTICLES: Record<string, string>;
61
+ export declare function verifyConstitution(ctx: ConstitutionContext): ConstitutionViolation[];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Who is acting — the port a server-backed identity will eventually fill.
3
+ *
4
+ * `proven` is the load-bearing field. A LOCAL role is a claim the operator made when launching the
5
+ * agent; nothing verified it. Recording that plainly is what keeps a later reader from assuming a
6
+ * local role was checked by something, and it reserves the exact slot where a server answers
7
+ * `proven: true` for "this really is the Reviewer".
8
+ */
9
+ export interface Identity {
10
+ actor?: string;
11
+ role?: string;
12
+ /** Always false locally. A server-backed provider is where this becomes true. */
13
+ proven: boolean;
14
+ }
15
+ export interface IdentityProvider {
16
+ current(): Identity;
17
+ }
18
+ /**
19
+ * Reads the claim from the process environment — the same out-of-band discipline `HOLMES_APPROVAL`
20
+ * established, and for the same reason: a session cannot set its own environment, so it cannot claim
21
+ * its own role. A role carried in the tool payload would be self-assertion, which is precisely what
22
+ * ART-5 forbids on the spec axis.
23
+ */
24
+ export declare class EnvIdentityProvider implements IdentityProvider {
25
+ private readonly env;
26
+ constructor(env?: NodeJS.ProcessEnv);
27
+ current(): Identity;
28
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Cross-process mutual exclusion for one ledger file.
3
+ *
4
+ * WHY this exists, measured rather than assumed. `ProvenanceChain.append()` was read-then-append and
5
+ * the single-use approval gate was check-then-act, so two agents running at once could both observe
6
+ * "nonce not consumed" and both proceed. Two real processes synchronized on a file barrier, 20
7
+ * trials: the single-use approval was spent TWICE 20/20 times, and the tamper-evident chain reported
8
+ * `ok:false` 17/20 times — reporting tampering that never happened. Sequential behaviour was, and
9
+ * remains, correct; the failure only appears when the critical sections overlap, which is precisely
10
+ * the case a harness with "agentic" in its name promises to support.
11
+ *
12
+ * `fs.mkdirSync` is the primitive: atomic on every platform this ships to, `EEXIST` when held, and
13
+ * with none of the file-vs-`O_EXCL` subtleties. A DIRECTORY rather than a lock FILE because a
14
+ * zero-byte file is what a cleanup script or `git clean` deletes mid-hold, while a directory holds
15
+ * its own metadata and cannot be removed non-empty by accident.
16
+ *
17
+ * LIMIT, stated rather than implied: single host, single filesystem with atomic `mkdir`. Networked
18
+ * filesystems (NFS, shared volumes) are out of scope and this does not pretend to cover them.
19
+ */
20
+ /** Thrown when exclusion cannot be obtained within the acquire budget. Distinguishable on purpose. */
21
+ export declare class LedgerLockError extends Error {
22
+ readonly holmesRefusal = true;
23
+ }
24
+ /**
25
+ * A hold older than this is treated as abandoned and broken.
26
+ *
27
+ * 30 seconds against a critical section that reads a few hundred bytes and appends one line —
28
+ * microseconds of real work, so this is roughly five orders of magnitude of headroom and cannot fire
29
+ * on a live holder that is merely slow. The number exists because a `kill -9`'d agent must not wedge
30
+ * every other agent forever.
31
+ */
32
+ export declare const STALE_MS = 30000;
33
+ /** Default budget for waiting on someone else's hold before failing. */
34
+ export declare const ACQUIRE_MS = 5000;
35
+ export declare function lockDirFor(ledgerFile: string): string;
36
+ export interface LockOptions {
37
+ acquireMs?: number;
38
+ staleMs?: number;
39
+ /**
40
+ * Records the breaking of a stale hold. Injected so the lock module does not depend on the
41
+ * provenance chain that depends on IT — `withLedgerLock` is the lower layer.
42
+ */
43
+ onStaleBreak?: (info: {
44
+ pid?: number;
45
+ ageMs: number;
46
+ ledgerFile: string;
47
+ }) => void;
48
+ }
49
+ /**
50
+ * Run `fn` while holding exclusion for `ledgerFile`.
51
+ *
52
+ * The hold is released in `finally`, including when `fn` throws, and a release problem never masks
53
+ * `fn`'s own error — the body's failure is the one the caller needs to see.
54
+ */
55
+ export declare function withLedgerLock<T>(ledgerFile: string, fn: () => T, opts?: LockOptions): T;
@@ -0,0 +1,11 @@
1
+ import { LedgerStore } from './ledger-store';
2
+ /**
3
+ * The ONE conformance suite every `LedgerStore` adapter must pass.
4
+ *
5
+ * Deliberately a single exported function rather than a copy per adapter: two copies drift, and
6
+ * "both adapters pass" quietly stops meaning anything. Everything here is expressed against the
7
+ * CONTRACT — no paths, no files, no knowledge of how an adapter stores anything. If a property
8
+ * cannot be written that way, that is itself the finding: the contract has leaked its
9
+ * implementation, and finding that now is the whole reason a second adapter exists.
10
+ */
11
+ export declare function ledgerStoreConformance(name: string, make: () => LedgerStore): void;
@@ -0,0 +1,114 @@
1
+ import { ProvenanceEvent } from './provenance-chain';
2
+ import { LedgerEvent } from './provenance-ledger';
3
+ /**
4
+ * The ledger as a PORT — ADR-001's deferred half, applied to the one store whose shape was derived
5
+ * from real use rather than guessed at.
6
+ *
7
+ * ADR-001 decided the ports now because "an interface changed later causes core-wide rework, while
8
+ * an adapter can wait". Measured on 2026-08-07: seven of the eight declared ports do not exist, and
9
+ * the one that does carries the very `read/write/list` shape that ADR called insufficient. This is
10
+ * the first to land, and it starts from `ProvenanceLedger` — five operations that five production
11
+ * call sites already use — so it avoids the cost that ADR itself listed as the downside: designing a
12
+ * contract for something nobody calls.
13
+ *
14
+ * DECLARED, with reasons:
15
+ * - `replicaId()` — the filename carries it locally, but a server adapter has no filenames. On the
16
+ * contract it becomes independent of storage.
17
+ * - append-only — free from a file append, an explicit promise for anything else.
18
+ * - `isNonceConsumed` spanning ALL chains — REQ-148's rule. An adapter storing chains separately
19
+ * can get this wrong, so the contract has to require it rather than one implementation happening
20
+ * to do it.
21
+ *
22
+ * NOT DECLARED, and why: transactions, batching, pagination, RBAC. No caller needs them, they cannot
23
+ * be verified by the two adapters, and an unverifiable contract clause is worth less than a comment.
24
+ * ADR-001 named speculative port design as the cost of deciding early; this is where that cost is
25
+ * refused.
26
+ */
27
+ export interface LedgerStore {
28
+ /** This writer's identity — the one used for events this store appends. */
29
+ replicaId(): string;
30
+ /** Known chains by IDENTITY, never by location: a path here is a filesystem assumption a server adapter could not satisfy. */
31
+ chains(): {
32
+ replicaId: string | null;
33
+ id: string;
34
+ }[];
35
+ /** Every event across every chain, deterministically ordered. */
36
+ loadAll(): LedgerEvent[];
37
+ /** Per-chain verification; one broken chain fails the whole store and is named. */
38
+ verifyAll(): {
39
+ ok: boolean;
40
+ broken: {
41
+ replicaId: string | null;
42
+ detail?: string;
43
+ }[];
44
+ };
45
+ /** Append to this writer's chain. Never modifies or removes an existing event. */
46
+ append(body: Omit<ProvenanceEvent, 'prevHash' | 'hash' | 'seq'>): ProvenanceEvent;
47
+ /** Spent on ANY chain — see the interface note. */
48
+ isNonceConsumed(nonce: string): boolean;
49
+ }
50
+ /**
51
+ * Local adapter — DELEGATES to `ProvenanceLedger` rather than re-implementing it.
52
+ *
53
+ * That is load-bearing, not stylistic. REQ-141's per-file lock, REQ-147's read compatibility with the
54
+ * pre-split ledger, and REQ-148's replica split all live inside `ProvenanceLedger`. An adapter that
55
+ * wrote files itself would satisfy every conformance test while silently dropping all three, and
56
+ * nothing in this suite would notice — which is why a structural test pins the delegation too.
57
+ */
58
+ export declare class FileLedgerStore implements LedgerStore {
59
+ private readonly dir;
60
+ private readonly givenId?;
61
+ private readonly inner;
62
+ private cachedId;
63
+ /**
64
+ * The replica id is resolved LAZILY and the root derivation is delegated.
65
+ *
66
+ * Both were regressions I reintroduced here: resolving in the constructor made a mere read create
67
+ * `.ax/state/` (REQ-148 had fixed precisely that in ProvenanceLedger), and deriving the project
68
+ * root with a local regex duplicated a rule that already exists — two copies of one rule drift.
69
+ * The contract now demands the no-footprint property so the NEXT adapter cannot lose it either.
70
+ */
71
+ constructor(dir: string, givenId?: string | undefined);
72
+ replicaId(): string;
73
+ chains(): {
74
+ replicaId: string | null;
75
+ id: string;
76
+ }[];
77
+ loadAll(): LedgerEvent[];
78
+ verifyAll(): {
79
+ ok: boolean;
80
+ broken: {
81
+ replicaId: string | null;
82
+ detail?: string;
83
+ }[];
84
+ };
85
+ append(body: Omit<ProvenanceEvent, 'prevHash' | 'hash' | 'seq'>): ProvenanceEvent;
86
+ isNonceConsumed(nonce: string): boolean;
87
+ }
88
+ /**
89
+ * In-memory adapter — the instrument that makes the contract testable.
90
+ *
91
+ * Uses NO filesystem, deliberately. An in-memory store backed by a temp directory would be the file
92
+ * adapter in disguise and would prove nothing about coupling. It reuses the pure chain functions
93
+ * (`chainNext`, `verifyChain`) because those are the format, not the storage.
94
+ */
95
+ export declare class MemoryLedgerStore implements LedgerStore {
96
+ private readonly id;
97
+ private readonly byReplica;
98
+ constructor(id?: string);
99
+ replicaId(): string;
100
+ chains(): {
101
+ replicaId: string | null;
102
+ id: string;
103
+ }[];
104
+ loadAll(): LedgerEvent[];
105
+ verifyAll(): {
106
+ ok: boolean;
107
+ broken: {
108
+ replicaId: string | null;
109
+ detail?: string;
110
+ }[];
111
+ };
112
+ append(body: Omit<ProvenanceEvent, 'prevHash' | 'hash' | 'seq'>): ProvenanceEvent;
113
+ isNonceConsumed(nonce: string): boolean;
114
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * L4 — append-only PROGRESS ledger + checkpoints (target-architecture §7 L4).
3
+ *
4
+ * Complements the decision ledger (guardrail/decision-ledger.ts, same L4 family): decisions record
5
+ * WHY, this ledger records WHAT HAPPENED — slices completed, findings, checkpoints — as append-only
6
+ * JSONL under .ax/ledger/progress.jsonl (gitignored, like all .ax derived state). Append-only means
7
+ * an agent cannot rewrite history it doesn't like; a checkpoint entry pins the git HEAD + suite count
8
+ * so "where were we / what was green" survives compaction and session loss.
9
+ *
10
+ * Pure half: formatProgressEntry / parseProgressLog (deterministic — the caller supplies the
11
+ * timestamp, this module never reads a clock). I/O half: appendProgress / readProgress (fail-open).
12
+ */
13
+ export type ProgressKind = 'slice' | 'finding' | 'checkpoint' | 'note';
14
+ export interface ProgressEntry {
15
+ ts: string;
16
+ kind: ProgressKind;
17
+ summary: string;
18
+ refs?: string[];
19
+ suite?: number;
20
+ head?: string;
21
+ }
22
+ export declare function formatProgressEntry(e: ProgressEntry): string;
23
+ export declare function parseProgressLog(text: string): ProgressEntry[];
24
+ export declare function appendProgress(root: string, entry: ProgressEntry): boolean;
25
+ export declare function readProgress(root: string, lastN?: number): ProgressEntry[];