@holmes-lab/holmes-kit 0.1.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 (107) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +102 -0
  4. package/bin/holmes-hook-antigravity.js +31 -0
  5. package/bin/holmes-kit.js +23 -0
  6. package/bin/holmes-mcp.js +34 -0
  7. package/bin/holmes-stop-antigravity.js +29 -0
  8. package/dist/.build-id +1 -0
  9. package/dist/holmes/cli/agents.js +168 -0
  10. package/dist/holmes/cli/doctor.js +625 -0
  11. package/dist/holmes/cli/gitignore-merge.js +84 -0
  12. package/dist/holmes/cli/governed-precondition.js +157 -0
  13. package/dist/holmes/cli/index.js +384 -0
  14. package/dist/holmes/cli/init.js +462 -0
  15. package/dist/holmes/cli/playbook-skills.js +711 -0
  16. package/dist/holmes/cli/roles-readme.js +134 -0
  17. package/dist/holmes/cli/settings-merge.js +122 -0
  18. package/dist/holmes/config/config.js +70 -0
  19. package/dist/holmes/context/bundler.js +114 -0
  20. package/dist/holmes/context/render.js +29 -0
  21. package/dist/holmes/context/tiers.js +110 -0
  22. package/dist/holmes/context/tokens.js +8 -0
  23. package/dist/holmes/cpg/cpg-scanner.js +213 -0
  24. package/dist/holmes/cpg/hash-cache.js +86 -0
  25. package/dist/holmes/cpg/language-parser-walk.js +917 -0
  26. package/dist/holmes/cpg/language-parser-worker.js +81 -0
  27. package/dist/holmes/cpg/language-parser.js +234 -0
  28. package/dist/holmes/cpg/scan-cache.js +108 -0
  29. package/dist/holmes/cpg/source-path.js +44 -0
  30. package/dist/holmes/cpg/test-files.js +84 -0
  31. package/dist/holmes/governance/constitution-debt.js +73 -0
  32. package/dist/holmes/governance/constitution-report.js +25 -0
  33. package/dist/holmes/governance/constitution.js +129 -0
  34. package/dist/holmes/governance/identity.js +30 -0
  35. package/dist/holmes/governance/ledger-lock.js +165 -0
  36. package/dist/holmes/governance/ledger-store.conformance.js +90 -0
  37. package/dist/holmes/governance/ledger-store.js +106 -0
  38. package/dist/holmes/governance/progress-ledger.js +83 -0
  39. package/dist/holmes/governance/provenance-chain.js +365 -0
  40. package/dist/holmes/governance/provenance-ledger.js +0 -0
  41. package/dist/holmes/governance/provenance-schema.js +47 -0
  42. package/dist/holmes/governance/replica-id.js +106 -0
  43. package/dist/holmes/governance/role-policy.js +137 -0
  44. package/dist/holmes/governance/trust-score.js +43 -0
  45. package/dist/holmes/guardrail/anchors.js +31 -0
  46. package/dist/holmes/guardrail/blind-spots.js +38 -0
  47. package/dist/holmes/guardrail/decision-ledger.js +107 -0
  48. package/dist/holmes/guardrail/executable-artifact.js +129 -0
  49. package/dist/holmes/guardrail/governance-history.js +101 -0
  50. package/dist/holmes/guardrail/phase.js +169 -0
  51. package/dist/holmes/guardrail/risk-classifier.js +450 -0
  52. package/dist/holmes/guardrail/risk-gate.js +160 -0
  53. package/dist/holmes/guardrail/risk-types.js +6 -0
  54. package/dist/holmes/guardrail/tspec-state.js +392 -0
  55. package/dist/holmes/guardrail/write-target.js +224 -0
  56. package/dist/holmes/hooks/adapters/antigravity.js +194 -0
  57. package/dist/holmes/hooks/pre-tool-use.js +1262 -0
  58. package/dist/holmes/hooks/stop.js +416 -0
  59. package/dist/holmes/mcp/basis.js +162 -0
  60. package/dist/holmes/mcp/handlers.js +1831 -0
  61. package/dist/holmes/mcp/server.js +71 -0
  62. package/dist/holmes/mcp/stdio-client.js +165 -0
  63. package/dist/holmes/mcp/supervisor.js +178 -0
  64. package/dist/holmes/mcp/tool-schemas.js +394 -0
  65. package/dist/holmes/mcp/validate-args.js +281 -0
  66. package/dist/holmes/messages/registry.js +50 -0
  67. package/dist/holmes/project/baseline.js +210 -0
  68. package/dist/holmes/project/change-source.js +233 -0
  69. package/dist/holmes/project/ignore.js +145 -0
  70. package/dist/holmes/project/root.js +113 -0
  71. package/dist/holmes/reverse/anchor.js +162 -0
  72. package/dist/holmes/reverse/cluster.js +187 -0
  73. package/dist/holmes/reverse/draft.js +151 -0
  74. package/dist/holmes/reverse/dynamic-wiring.js +47 -0
  75. package/dist/holmes/reverse/scan.js +194 -0
  76. package/dist/holmes/reverse/surface.js +154 -0
  77. package/dist/holmes/reverse/test-map.js +263 -0
  78. package/dist/holmes/review/coverage.js +33 -0
  79. package/dist/holmes/review/findings.js +123 -0
  80. package/dist/holmes/review/package.js +40 -0
  81. package/dist/holmes/review/review-targets.js +92 -0
  82. package/dist/holmes/review/scope.js +57 -0
  83. package/dist/holmes/review/test-evidence.js +77 -0
  84. package/dist/holmes/review/test-runner.js +572 -0
  85. package/dist/holmes/rtm/dataflow-taint.js +262 -0
  86. package/dist/holmes/rtm/gap-analyzer.js +27 -0
  87. package/dist/holmes/rtm/git-changes.js +72 -0
  88. package/dist/holmes/rtm/incremental.js +45 -0
  89. package/dist/holmes/rtm/localize.js +100 -0
  90. package/dist/holmes/rtm/rtm-builder.js +191 -0
  91. package/dist/holmes/rtm/rtm-check.js +89 -0
  92. package/dist/holmes/rtm/rtm-graph.js +232 -0
  93. package/dist/holmes/rtm/taint.js +92 -0
  94. package/dist/holmes/rtm/test-scope.js +336 -0
  95. package/dist/holmes/spec/approval-blockers.js +204 -0
  96. package/dist/holmes/spec/breaking-change.js +89 -0
  97. package/dist/holmes/spec/legacy-format.js +87 -0
  98. package/dist/holmes/spec/spec-digest.js +71 -0
  99. package/dist/holmes/spec/spec-parser.js +106 -0
  100. package/dist/holmes/spec/spec-store.conformance.js +118 -0
  101. package/dist/holmes/spec/spec-store.js +331 -0
  102. package/dist/holmes/spec/spec-types.js +177 -0
  103. package/dist/holmes/spec/validator.js +280 -0
  104. package/package.json +76 -0
  105. package/playbooks/adopt/PLAYBOOK.md +125 -0
  106. package/playbooks/author-slice/PLAYBOOK.md +119 -0
  107. package/playbooks/promote-slice/PLAYBOOK.md +134 -0
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+ /* eslint-disable @typescript-eslint/no-var-requires */
3
+ // Runs INSIDE a dedicated worker_thread, used ONLY when running under Jest
4
+ // (see language-parser.ts for why: the tree-sitter native addon corrupts its
5
+ // own parse results if it is ever used a second time inside a Jest-controlled
6
+ // process/vm-context). Production parses inline instead — this file exists
7
+ // purely as a test-infrastructure accommodation.
8
+ //
9
+ // The actual symbol/edge tree-walking logic lives in language-parser-walk.js
10
+ // and is shared with the inline (production) path; this file must not
11
+ // reimplement it.
12
+ const { workerData } = require('worker_threads');
13
+ const { extractSymbolsFromTree, extractEdgesFromTree, extractDataFlowFromTree, parseWithBuffer } = require('./language-parser-walk');
14
+ const Parser = require(workerData.parserModulePath);
15
+ // @implements A-SPEC-121.6
16
+ // Lang -> { modulePath, exportKey } map (see GRAMMAR_MODULE_PATHS in
17
+ // language-parser.ts). Loading is generic over this map, so a new language
18
+ // is one entry on the language-parser.ts side — nothing here changes.
19
+ const grammarModulePaths = workerData.grammarModulePaths;
20
+ // Cache loaded grammar modules by modulePath (typescript and tsx share one
21
+ // module) and Parser instances by lang, for the life of this worker — the
22
+ // same "load each native language binding exactly once per process" rule
23
+ // that motivates this worker's existence in the first place (see the
24
+ // language-parser.ts comment for the full story on tree-sitter + Jest).
25
+ const grammarModules = new Map();
26
+ const parsers = new Map();
27
+ function loadGrammar(lang) {
28
+ const spec = grammarModulePaths[lang] || grammarModulePaths.typescript;
29
+ let mod = grammarModules.get(spec.modulePath);
30
+ if (!mod) {
31
+ mod = require(spec.modulePath);
32
+ grammarModules.set(spec.modulePath, mod);
33
+ }
34
+ return spec.exportKey ? mod[spec.exportKey] : mod;
35
+ }
36
+ function getParser(lang) {
37
+ const key = grammarModulePaths[lang] ? lang : 'typescript';
38
+ let parser = parsers.get(key);
39
+ if (!parser) {
40
+ parser = new Parser();
41
+ parser.setLanguage(loadGrammar(key));
42
+ parsers.set(key, parser);
43
+ }
44
+ return parser;
45
+ }
46
+ function extractSymbolsImpl(code, lang) {
47
+ // @implements A-SPEC-131 — same bufferSize form as the inline path, so jest and production see the same tree.
48
+ return extractSymbolsFromTree(parseWithBuffer(getParser(lang), code), lang);
49
+ }
50
+ function extractEdgesImpl(code, lang) {
51
+ return extractEdgesFromTree(parseWithBuffer(getParser(lang), code), lang);
52
+ }
53
+ // @implements A-SPEC-140.1
54
+ function extractDataFlowImpl(code, lang) {
55
+ return extractDataFlowFromTree(parseWithBuffer(getParser(lang), code), lang);
56
+ }
57
+ // Explicit dispatch. The previous ternary treated "not extractSymbols" as "edges", which silently
58
+ // made any third method return edges instead of failing — a dispatch table cannot do that.
59
+ const METHODS = {
60
+ extractSymbols: extractSymbolsImpl,
61
+ extractEdges: extractEdgesImpl,
62
+ extractDataFlow: extractDataFlowImpl,
63
+ };
64
+ const { sharedBuffer, port } = workerData;
65
+ const sync = new Int32Array(sharedBuffer);
66
+ port.on('message', (msg) => {
67
+ let result;
68
+ let error;
69
+ try {
70
+ const impl = METHODS[msg.method];
71
+ if (!impl)
72
+ throw new Error(`unknown worker method: ${msg.method}`);
73
+ result = impl(msg.code, msg.lang);
74
+ }
75
+ catch (e) {
76
+ error = e && e.stack ? String(e.stack) : String(e);
77
+ }
78
+ port.postMessage({ id: msg.id, result, error });
79
+ Atomics.store(sync, 0, 1);
80
+ Atomics.notify(sync, 0);
81
+ });
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.TreeSitterTsParser = exports.TreeSitterInfraError = void 0;
37
+ exports.hasDataFlowWalk = hasDataFlowWalk;
38
+ /* eslint-disable @typescript-eslint/no-var-requires */
39
+ const path = __importStar(require("path"));
40
+ const worker_threads_1 = require("worker_threads");
41
+ /**
42
+ * Thrown for RPC/infrastructure failures on the jest worker_thread path
43
+ * (timeout, dead worker, desynchronized response) — as opposed to a genuine
44
+ * per-file parse exception. Callers such as CpgScanner use this to avoid
45
+ * silently treating a systemic worker failure as "just this file is
46
+ * unparseable".
47
+ */
48
+ class TreeSitterInfraError extends Error {
49
+ }
50
+ exports.TreeSitterInfraError = TreeSitterInfraError;
51
+ // Single source of truth for the symbol/edge tree-walking logic, shared by
52
+ // both the inline (production) path below and the jest-only worker_thread
53
+ // path (language-parser-worker.js) — guarantees the two paths produce
54
+ // byte-identical results.
55
+ const walk = require('./language-parser-walk');
56
+ /**
57
+ * Whether a language has a data-flow walk at all.
58
+ *
59
+ * @implements A-SPEC-140.1
60
+ * Read straight from the walk's own table so the two can never drift: a caller that guessed the
61
+ * list would eventually claim "analysed, nothing found" for a language nothing ever looked at.
62
+ */
63
+ function hasDataFlowWalk(lang) {
64
+ return walk.DATAFLOW_LANGS.has(lang);
65
+ }
66
+ /**
67
+ * WHY the jest-only worker_thread path exists: the tree-sitter native addon
68
+ * (node-tree-sitter + tree-sitter-typescript) silently corrupts its own
69
+ * parse results — nodes with undefined children, empty symbol lists — the
70
+ * SECOND time it is used inside the same OS process by a fresh Jest
71
+ * per-test-file sandbox/vm context (confirmed empirically: whichever test
72
+ * file's tree-sitter usage runs first in a process succeeds, every
73
+ * subsequent one in that same process fails, consistently and
74
+ * reproducibly). This is a JEST-only phenomenon: the identical
75
+ * repeated-require-and-parse pattern is harmless in a plain Node process or
76
+ * a hand-rolled `vm.Context` outside Jest's control (verified directly).
77
+ *
78
+ * So production parses INLINE, in-process, the simple way — no worker, no
79
+ * IPC, no Atomics on the hot path. Only when running under Jest
80
+ * (`JEST_WORKER_ID` set) do we route through a persistent worker_thread,
81
+ * whose V8 isolate is a plain Node realm Jest never creates, resets, or
82
+ * tears down, so tree-sitter only ever sees "one real load" for the life of
83
+ * that worker regardless of how many Jest test files share the process.
84
+ */
85
+ const IN_JEST = !!process.env.JEST_WORKER_ID;
86
+ // @implements A-SPEC-121.6
87
+ // Lang -> grammar-loader map. Adding a new language (C#/Java/Go/Rust/C++,
88
+ // later slices) is exactly one new entry here — no branching logic to touch.
89
+ // Any lang not present falls back to the typescript loader, preserving the
90
+ // pre-refactor default (the old ensure() treated every non-python lang as
91
+ // TypeScript/TSX).
92
+ const GRAMMAR_LOADERS = {
93
+ typescript: () => require('tree-sitter-typescript').typescript,
94
+ tsx: () => require('tree-sitter-typescript').tsx,
95
+ python: () => require('tree-sitter-python'),
96
+ csharp: () => require('tree-sitter-c-sharp'),
97
+ java: () => require('tree-sitter-java'),
98
+ go: () => require('tree-sitter-go'),
99
+ rust: () => require('tree-sitter-rust'),
100
+ cpp: () => require('tree-sitter-cpp'),
101
+ };
102
+ class InlineTreeSitterParser {
103
+ parsers = new Map();
104
+ ensure(lang) {
105
+ let parser = this.parsers.get(lang);
106
+ if (!parser) {
107
+ const Parser = require('tree-sitter');
108
+ parser = new Parser();
109
+ const loadGrammar = GRAMMAR_LOADERS[lang] ?? GRAMMAR_LOADERS.typescript;
110
+ parser.setLanguage(loadGrammar());
111
+ this.parsers.set(lang, parser);
112
+ }
113
+ return parser;
114
+ }
115
+ extractSymbols(code, lang = 'typescript') {
116
+ // @implements A-SPEC-131 — bufferSize form: parse(string) throws above 32,767 chars.
117
+ return walk.extractSymbolsFromTree(walk.parseWithBuffer(this.ensure(lang), code), lang);
118
+ }
119
+ extractEdges(code, lang = 'typescript') {
120
+ return walk.extractEdgesFromTree(walk.parseWithBuffer(this.ensure(lang), code), lang);
121
+ }
122
+ // @implements A-SPEC-140.1
123
+ extractDataFlow(code, lang = 'typescript') {
124
+ return walk.extractDataFlowFromTree(walk.parseWithBuffer(this.ensure(lang), code), lang);
125
+ }
126
+ }
127
+ const GRAMMAR_MODULE_PATHS = {
128
+ typescript: { modulePath: require.resolve('tree-sitter-typescript'), exportKey: 'typescript' },
129
+ tsx: { modulePath: require.resolve('tree-sitter-typescript'), exportKey: 'tsx' },
130
+ python: { modulePath: require.resolve('tree-sitter-python') },
131
+ csharp: { modulePath: require.resolve('tree-sitter-c-sharp') },
132
+ java: { modulePath: require.resolve('tree-sitter-java') },
133
+ go: { modulePath: require.resolve('tree-sitter-go') },
134
+ rust: { modulePath: require.resolve('tree-sitter-rust') },
135
+ cpp: { modulePath: require.resolve('tree-sitter-cpp') },
136
+ };
137
+ class TreeSitterWorkerClient {
138
+ worker;
139
+ port;
140
+ sync;
141
+ nextId = 0;
142
+ ensureWorker() {
143
+ if (this.worker)
144
+ return;
145
+ const { port1, port2 } = new worker_threads_1.MessageChannel();
146
+ const sharedBuffer = new SharedArrayBuffer(4);
147
+ this.sync = new Int32Array(sharedBuffer);
148
+ this.port = port1;
149
+ const worker = new worker_threads_1.Worker(path.join(__dirname, 'language-parser-worker.js'), {
150
+ workerData: {
151
+ sharedBuffer,
152
+ port: port2,
153
+ parserModulePath: require.resolve('tree-sitter'),
154
+ grammarModulePaths: GRAMMAR_MODULE_PATHS,
155
+ },
156
+ transferList: [port2],
157
+ });
158
+ // A worker crash must not surface as an uncaught exception on the main
159
+ // thread, and must not permanently degrade parsing: drop the dead
160
+ // worker's references so the next call transparently respawns one.
161
+ worker.on('error', () => this.resetWorker());
162
+ worker.on('exit', () => this.resetWorker());
163
+ // Don't let this background worker keep the host process alive on its own.
164
+ worker.unref();
165
+ this.worker = worker;
166
+ }
167
+ resetWorker() {
168
+ try {
169
+ this.worker?.terminate();
170
+ }
171
+ catch { /* best-effort; worker may already be gone */ }
172
+ this.worker = undefined;
173
+ this.port = undefined;
174
+ this.sync = undefined;
175
+ }
176
+ call(method, code, lang = 'typescript') {
177
+ this.ensureWorker();
178
+ const id = ++this.nextId;
179
+ Atomics.store(this.sync, 0, 0);
180
+ this.port.postMessage({ id, method, code, lang });
181
+ const status = Atomics.wait(this.sync, 0, 0, 10000);
182
+ if (status === 'timed-out') {
183
+ this.resetWorker();
184
+ throw new TreeSitterInfraError(`TreeSitterTsParser: worker did not respond within timeout (method=${method}); worker has been reset`);
185
+ }
186
+ const received = (0, worker_threads_1.receiveMessageOnPort)(this.port);
187
+ if (!received) {
188
+ this.resetWorker();
189
+ throw new TreeSitterInfraError(`TreeSitterTsParser: no message received from worker (method=${method}); worker has been reset`);
190
+ }
191
+ const msg = received.message;
192
+ if (msg.id !== id) {
193
+ // A response belonging to a different (e.g. previously timed-out)
194
+ // request arrived on the channel. Never hand back data the caller
195
+ // didn't ask for: reset the worker so no further stale messages can
196
+ // surface, and fail loudly for this call.
197
+ this.resetWorker();
198
+ throw new TreeSitterInfraError(`TreeSitterTsParser: stale worker response (expected id=${id}, got id=${msg.id}); worker has been reset`);
199
+ }
200
+ if (msg.error) {
201
+ // A genuine exception from inside the worker's own parse/walk logic —
202
+ // equivalent to what would be thrown by the inline path — not an
203
+ // infrastructure failure.
204
+ throw new Error(msg.error);
205
+ }
206
+ return msg.result;
207
+ }
208
+ }
209
+ const inlineParser = new InlineTreeSitterParser();
210
+ // One worker per process, lazily started and reused by every TreeSitterTsParser
211
+ // instance — mirrors the fact that tree-sitter's native parser has no
212
+ // meaningful per-instance state of its own. Only ever constructed/used under
213
+ // Jest; production never touches this.
214
+ const sharedWorkerClient = new TreeSitterWorkerClient();
215
+ // @implements A-SPEC-121.1
216
+ class TreeSitterTsParser {
217
+ extractSymbols(code, lang = 'typescript') {
218
+ return IN_JEST
219
+ ? sharedWorkerClient.call('extractSymbols', code, lang)
220
+ : inlineParser.extractSymbols(code, lang);
221
+ }
222
+ extractEdges(code, lang = 'typescript') {
223
+ return IN_JEST
224
+ ? sharedWorkerClient.call('extractEdges', code, lang)
225
+ : inlineParser.extractEdges(code, lang);
226
+ }
227
+ // @implements A-SPEC-140.1
228
+ extractDataFlow(code, lang = 'typescript') {
229
+ return IN_JEST
230
+ ? sharedWorkerClient.call('extractDataFlow', code, lang)
231
+ : inlineParser.extractDataFlow(code, lang);
232
+ }
233
+ }
234
+ exports.TreeSitterTsParser = TreeSitterTsParser;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ScanFileCache = exports.SCAN_CACHE_VERSION = void 0;
37
+ // @implements A-SPEC-113.1
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ const node_crypto_1 = require("node:crypto");
41
+ // Bump on ANY change to symbol extraction, grammar versions, or the ScannedFile schema (review D3:
42
+ // without a version stamp, cached entries kept serving OLD-parser symbols indefinitely after an
43
+ // upgrade — content hashes match, so nothing else invalidates them). A mismatch drops the whole
44
+ // cache (one cold scan), which is exactly the correct cost of a parser upgrade.
45
+ exports.SCAN_CACHE_VERSION = 3;
46
+ class ScanFileCache {
47
+ dir;
48
+ file;
49
+ entries;
50
+ seen = new Set();
51
+ dirty = false;
52
+ hits = 0;
53
+ misses = 0;
54
+ constructor(dir) {
55
+ this.dir = dir;
56
+ this.file = path.join(dir, 'scan-cache.json');
57
+ try {
58
+ const raw = JSON.parse(fs.readFileSync(this.file, 'utf8'));
59
+ this.entries = raw && raw.v === exports.SCAN_CACHE_VERSION && raw.entries && typeof raw.entries === 'object'
60
+ ? raw.entries
61
+ : {}; // version/schema mismatch (incl. the old un-versioned flat format) -> cold scan
62
+ }
63
+ catch {
64
+ this.entries = {};
65
+ }
66
+ }
67
+ static hashOf(content) {
68
+ return (0, node_crypto_1.createHash)('sha256').update(content, 'utf8').digest('hex');
69
+ }
70
+ /** Cached scan result iff the stored hash matches this content. Marks the path as seen. */
71
+ get(sourcePath, contentHash) {
72
+ this.seen.add(sourcePath);
73
+ const e = this.entries[sourcePath];
74
+ if (e && e.hash === contentHash) {
75
+ this.hits++;
76
+ // Default array fields so an entry from a slightly older writer can never yield `undefined`
77
+ // where a fresh scan yields [] (review D3b — downstream iterates these).
78
+ return { ...e.file, symbols: e.file.symbols ?? [], implementsSpecs: e.file.implementsSpecs ?? [], unanchoredImplements: e.file.unanchoredImplements ?? [] };
79
+ }
80
+ this.misses++;
81
+ return undefined;
82
+ }
83
+ put(sourcePath, contentHash, file) {
84
+ this.seen.add(sourcePath);
85
+ this.entries[sourcePath] = { hash: contentHash, file };
86
+ this.dirty = true;
87
+ }
88
+ /**
89
+ * Persist once per scan. Mark-and-sweep: entries whose path was NOT seen this scan belong to
90
+ * deleted/renamed files — pruned so the cache cannot grow unboundedly. No-op when nothing changed.
91
+ */
92
+ save() {
93
+ const stale = Object.keys(this.entries).filter((p) => !this.seen.has(p));
94
+ for (const p of stale) {
95
+ delete this.entries[p];
96
+ this.dirty = true;
97
+ }
98
+ if (!this.dirty)
99
+ return;
100
+ try {
101
+ fs.mkdirSync(this.dir, { recursive: true });
102
+ fs.writeFileSync(this.file, JSON.stringify({ v: exports.SCAN_CACHE_VERSION, entries: this.entries }), 'utf8');
103
+ this.dirty = false;
104
+ }
105
+ catch { /* cache persistence is best-effort — next scan just runs colder */ }
106
+ }
107
+ }
108
+ exports.ScanFileCache = ScanFileCache;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.toSourcePath = toSourcePath;
37
+ const path = __importStar(require("node:path"));
38
+ /** Repo-root-relative, POSIX-normalized path used as the provenance key for graph rows. */
39
+ // @implements A-SPEC-121.1
40
+ function toSourcePath(repoRoot, absOrRelPath) {
41
+ const abs = path.isAbsolute(absOrRelPath) ? absOrRelPath : path.resolve(repoRoot, absOrRelPath);
42
+ const rel = path.relative(repoRoot, abs);
43
+ return rel.split(path.sep).join('/').replace(/^\.\//, '');
44
+ }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ // @implements A-SPEC-121.1
3
+ /**
4
+ * What counts as a TEST file, per ecosystem — the single table both the scanner and the phase gate
5
+ * read.
6
+ *
7
+ * It lives in its own module, apart from the scanner, because two very different consumers need it
8
+ * and only one of them can afford the scanner's dependencies. `CpgScanner` uses it to keep test
9
+ * symbols out of the production graph; `classifyAction` (the PreToolUse phase gate, which runs on
10
+ * every Write/Edit and is measured in tens of milliseconds) uses it to tell WRITE_TEST from
11
+ * WRITE_CODE. When the two disagree about what a test is, the harness contradicts itself: measured on
12
+ * a real Python target, `tests/test_briefing.py` was excluded from the graph as a test AND gated as
13
+ * production code, so editing it demanded a stricter chain than the code it tests.
14
+ *
15
+ * Nothing here imports anything, and that is deliberate — it is the property that lets the hook use
16
+ * it without pulling tree-sitter into a latency-critical path.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.TEST_FILE_PATTERNS = void 0;
20
+ exports.isTestFile = isTestFile;
21
+ /**
22
+ * Test-file conventions, one entry per ecosystem, matched against the path RELATIVE to the project
23
+ * root (POSIX-separated).
24
+ *
25
+ * Test code must never enter the production graph: its symbols would be counted as production, which
26
+ * corrupts change-impact, clustering, and any reverse-engineered spec drafted from a scan. The
27
+ * previous rules covered TS/JS, Python and Go only, so — measured — `AppTest.java`, `AppTests.cs`,
28
+ * `app_test.rs`, `tests/mod.rs` and `app_test.cpp` all leaked through.
29
+ *
30
+ * Kept as one exported table rather than scattered constants so the per-language coverage is
31
+ * auditable at a glance and testable as a unit; `isTestFile` is the only entry point.
32
+ */
33
+ exports.TEST_FILE_PATTERNS = [
34
+ // TS/JS and the generic `.test.`/`.spec.` infix, which several ecosystems also use.
35
+ ['infix', /\.(test|spec)\.(ts|mts|cts|tsx|jsx|js|mjs|cjs|py|cs|java|go|rs|cpp|cc|cxx|hpp|hh|h)$/],
36
+ // pytest / unittest. `tests.py` is Django's per-app convention and `conftest.py` is pytest's
37
+ // fixture/config module — neither carries a `test_`/`_test` affix, so both need naming by hand.
38
+ ['python', /(^|\/)(test_[^/]*|[^/]*_test|tests?|conftest)\.py$/],
39
+ // `go test`
40
+ ['go', /(^|\/)[^/]*_test\.go$/],
41
+ // JUnit / xUnit / NUnit / MSTest. Case-sensitive on the `Test` suffix, so `Manifest.java` and
42
+ // `Attest.cs` are not misread as tests.
43
+ ['jvm-dotnet', /(^|\/)[^/]*(Test|Tests|TestCase|TestCases)\.(java|cs)$/],
44
+ // Cargo unit-test files and googletest. `_unittest` is gtest's other common suffix.
45
+ ['rust-cpp', /(^|\/)([^/]*_(tests?|unittest)|test_[^/]*)\.(rs|cpp|cc|cxx)$/],
46
+ // @implements A-SPEC-164
47
+ // googletest's other common shape. `jvm-dotnet` already does this for .java/.cs, so `ATest.java`
48
+ // was a test while `ATest.cpp` was not. CASE-SENSITIVE on `Test`, which is what keeps `Attest.cpp`
49
+ // and `Fastest.cpp` out — over-recognition is the dangerous direction here, since a source file
50
+ // read as a test can be written with no approved A-SPEC.
51
+ ['cpp-suffix', /(^|\/)[^/]*(Test|Tests)\.(cpp|cc|cxx)$/],
52
+ // Directory conventions, each SCOPED TO ITS LANGUAGE'S EXTENSION. These are load-bearing where
53
+ // the filename carries no signal at all — a Rust integration test is just `tests/foo.rs`, and a
54
+ // Maven/Gradle test is just `src/test/java/Foo.java`.
55
+ //
56
+ // They are language-scoped because a blanket directory rule over-excludes: an earlier version
57
+ // matched any `spec/` segment and silently dropped this project's own `src/holmes/spec/**`
58
+ // production files from the graph — the exact "silently dropping real source is the worse
59
+ // failure" hazard that the vendor-skip set was written to avoid. Tying each directory rule to the
60
+ // extensions of the ecosystem that uses that convention keeps the signal and removes the hazard.
61
+ ['python-dir', /(^|\/)tests?\/.*\.py$/],
62
+ // Any depth: a cargo integration test may pull in helper modules (`tests/common/mod.rs`), and a
63
+ // one-level rule would let those helpers through as production code.
64
+ ['rust-dir', /(^|\/)tests\/.*\.rs$/],
65
+ ['cpp-dir', /(^|\/)tests?\/.*\.(cpp|cc|cxx|hpp|hh|h)$/],
66
+ ['jvm-dir', /(^|\/)src\/test\/.*\.(java|kt|cs)$/], // Maven/Gradle standard layout
67
+ ['js-dir', /(^|\/)__tests__\/.*\.(ts|mts|cts|tsx|jsx|js|mjs|cjs)$/],
68
+ // @implements A-SPEC-164
69
+ // mocha's and `node:test`'s default directory. Measured before adding it: `test/a.ts` was gated as
70
+ // production while `test/a.cpp` was already a test — the same convention answered differently by
71
+ // language, which blocks test-first work in TypeScript. Scoped to the extensions like every other
72
+ // directory rule, because an unscoped one is exactly what once swallowed `src/holmes/spec/**`.
73
+ ['js-test-dir', /(^|\/)tests?\/.*\.(ts|mts|cts|tsx|jsx|js|mjs|cjs)$/],
74
+ ];
75
+ /**
76
+ * True when a PROJECT-ROOT-RELATIVE POSIX path is a test file by any ecosystem's convention.
77
+ *
78
+ * The path must be relative to the project, not absolute: several rules key on a directory segment,
79
+ * so a repository that merely lives under some `/…/test/` directory on disk would otherwise read as
80
+ * one big test tree.
81
+ */
82
+ function isTestFile(relPosixPath) {
83
+ return exports.TEST_FILE_PATTERNS.some(([, re]) => re.test(relPosixPath));
84
+ }
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.writeDebt = writeDebt;
37
+ exports.readDebt = readDebt;
38
+ exports.clearDebt = clearDebt;
39
+ // @implements A-SPEC-134
40
+ const fs = __importStar(require("node:fs"));
41
+ const path = __importStar(require("node:path"));
42
+ /**
43
+ * The constitution-debt state (REQ-134 P3): when the Stop gate exhausts its consecutive-block cap it
44
+ * no longer yields silently — it records the unresolved articles here and ends the turn. While debt
45
+ * is outstanding the PreToolUse gate denies WRITE_CODE, so new code cannot be layered on an
46
+ * unverified constitution. A clean Stop clears it. The state is a small JSON file under .ax/state;
47
+ * every read fails safe (a corrupt/absent file is "no debt readable" → null, never a throw — a
48
+ * crash here would be a DoS on the very gate the debt is meant to keep honest).
49
+ */
50
+ const debtFile = (root) => path.join(root, '.ax', 'state', 'constitution-debt.json');
51
+ function writeDebt(root, articles) {
52
+ const f = debtFile(root);
53
+ fs.mkdirSync(path.dirname(f), { recursive: true });
54
+ fs.writeFileSync(f, `${JSON.stringify({ articles, recordedAt: new Date().toISOString() }, null, 2)}\n`);
55
+ }
56
+ function readDebt(root) {
57
+ try {
58
+ const text = fs.readFileSync(debtFile(root), 'utf8');
59
+ const parsed = JSON.parse(text);
60
+ if (!Array.isArray(parsed.articles) || parsed.articles.length === 0)
61
+ return null;
62
+ return parsed.articles.filter((a) => typeof a === 'string');
63
+ }
64
+ catch {
65
+ return null; // absent or corrupt → no readable debt
66
+ }
67
+ }
68
+ function clearDebt(root) {
69
+ try {
70
+ fs.rmSync(debtFile(root), { force: true });
71
+ }
72
+ catch { /* idempotent: nothing to clear */ }
73
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_LISTED = void 0;
4
+ exports.describeViolations = describeViolations;
5
+ /**
6
+ * Cap on listed violations. A corpus-wide regression across 300+ specs would otherwise bury the
7
+ * terminal in hundreds of lines, which reads as noise and gets skipped — the same outcome as
8
+ * printing nothing.
9
+ */
10
+ exports.MAX_LISTED = 20;
11
+ function describeViolations(violations) {
12
+ if (violations.length === 0)
13
+ return '';
14
+ const lines = violations.slice(0, exports.MAX_LISTED).map((v) => {
15
+ // `detail` already embeds the spec id (`spec REQ-154: citation-bad-kind — ...`), so it is passed
16
+ // through whole. Parsing it apart and reassembling would silently yield an empty field the day
17
+ // the detail format changes — and that day is precisely when someone is reading this output.
18
+ const article = v.article ?? '(article 미상)';
19
+ return v.detail ? ` - ${article}: ${v.detail}` : ` - ${article}: (detail 없음)`;
20
+ });
21
+ const rest = violations.length - exports.MAX_LISTED;
22
+ if (rest > 0)
23
+ lines.push(` ... 외 ${rest}건`);
24
+ return lines.join('\n');
25
+ }