@holmes-lab/holmes-kit 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/.build-id +1 -1
  3. package/dist/holmes/assoc/assoc-arm.d.ts +94 -0
  4. package/dist/holmes/assoc/assoc-arm.js +187 -0
  5. package/dist/holmes/assoc/explore.d.ts +21 -0
  6. package/dist/holmes/assoc/explore.js +160 -0
  7. package/dist/holmes/assoc/impact-baseline.d.ts +13 -0
  8. package/dist/holmes/assoc/impact-baseline.js +48 -0
  9. package/dist/holmes/assoc/ppr.d.ts +64 -0
  10. package/dist/holmes/assoc/ppr.js +110 -0
  11. package/dist/holmes/cli/agents.d.ts +24 -11
  12. package/dist/holmes/cli/agents.js +93 -17
  13. package/dist/holmes/cli/codex-toml.d.ts +26 -0
  14. package/dist/holmes/cli/codex-toml.js +282 -0
  15. package/dist/holmes/cli/doctor.d.ts +56 -0
  16. package/dist/holmes/cli/doctor.js +348 -18
  17. package/dist/holmes/cli/index.js +16 -2
  18. package/dist/holmes/cli/init.js +78 -0
  19. package/dist/holmes/cli/interactive-prompt.js +4 -4
  20. package/dist/holmes/cli/mcp-launcher.d.ts +2 -2
  21. package/dist/holmes/cli/semantic-key.d.ts +19 -0
  22. package/dist/holmes/cli/semantic-key.js +93 -0
  23. package/dist/holmes/config/config.d.ts +9 -0
  24. package/dist/holmes/config/config.js +8 -1
  25. package/dist/holmes/cpg/language-capability.d.ts +65 -0
  26. package/dist/holmes/cpg/language-capability.js +145 -0
  27. package/dist/holmes/cpg/language-parser-walk.js +179 -34
  28. package/dist/holmes/cpg/language-parser.d.ts +1 -1
  29. package/dist/holmes/governance/ledger-rechain.d.ts +12 -0
  30. package/dist/holmes/governance/ledger-rechain.js +17 -2
  31. package/dist/holmes/governance/provenance-ledger.js +21 -0
  32. package/dist/holmes/guardrail/blind-spots.js +12 -1
  33. package/dist/holmes/guardrail/impact-gate.d.ts +77 -0
  34. package/dist/holmes/guardrail/impact-gate.js +263 -0
  35. package/dist/holmes/guardrail/write-target.d.ts +38 -1
  36. package/dist/holmes/guardrail/write-target.js +48 -4
  37. package/dist/holmes/hooks/adapters/antigravity.js +12 -1
  38. package/dist/holmes/hooks/corrupt-state-run.d.ts +33 -0
  39. package/dist/holmes/hooks/corrupt-state-run.js +16 -0
  40. package/dist/holmes/hooks/pre-tool-use.d.ts +26 -0
  41. package/dist/holmes/hooks/pre-tool-use.js +203 -11
  42. package/dist/holmes/hooks/stop.d.ts +65 -0
  43. package/dist/holmes/hooks/stop.js +200 -2
  44. package/dist/holmes/mcp/handlers.d.ts +88 -0
  45. package/dist/holmes/mcp/handlers.js +597 -16
  46. package/dist/holmes/mcp/maintenance-analyze.d.ts +435 -0
  47. package/dist/holmes/mcp/maintenance-analyze.js +994 -0
  48. package/dist/holmes/mcp/maintenance-evidence.d.ts +140 -0
  49. package/dist/holmes/mcp/maintenance-evidence.js +253 -0
  50. package/dist/holmes/mcp/tool-schemas.js +71 -0
  51. package/dist/holmes/project/root.js +3 -1
  52. package/dist/holmes/review/baseline-arm.d.ts +37 -0
  53. package/dist/holmes/review/baseline-arm.js +51 -0
  54. package/dist/holmes/review/captured-stdin-guard.d.ts +8 -0
  55. package/dist/holmes/review/captured-stdin-guard.js +48 -0
  56. package/dist/holmes/review/coherence-verify.d.ts +31 -0
  57. package/dist/holmes/review/coherence-verify.js +144 -0
  58. package/dist/holmes/review/commit-text.d.ts +50 -0
  59. package/dist/holmes/review/commit-text.js +76 -0
  60. package/dist/holmes/review/confidence-calibration.d.ts +39 -0
  61. package/dist/holmes/review/confidence-calibration.js +39 -0
  62. package/dist/holmes/review/content-baseline.d.ts +38 -0
  63. package/dist/holmes/review/content-baseline.js +103 -0
  64. package/dist/holmes/review/content-verify.d.ts +20 -0
  65. package/dist/holmes/review/content-verify.js +73 -0
  66. package/dist/holmes/review/dense-retrieval.d.ts +66 -0
  67. package/dist/holmes/review/dense-retrieval.js +97 -0
  68. package/dist/holmes/review/edge-quality.d.ts +44 -0
  69. package/dist/holmes/review/edge-quality.js +117 -0
  70. package/dist/holmes/review/evaluation-metrics.d.ts +138 -0
  71. package/dist/holmes/review/evaluation-metrics.js +175 -0
  72. package/dist/holmes/review/graph-verifier.d.ts +34 -0
  73. package/dist/holmes/review/graph-verifier.js +62 -0
  74. package/dist/holmes/review/hop-ablation.d.ts +100 -0
  75. package/dist/holmes/review/hop-ablation.js +89 -0
  76. package/dist/holmes/review/manual-baseline.d.ts +209 -0
  77. package/dist/holmes/review/manual-baseline.js +2846 -0
  78. package/dist/holmes/review/oracle-gap.d.ts +32 -0
  79. package/dist/holmes/review/oracle-gap.js +102 -0
  80. package/dist/holmes/review/point-in-time-replay.d.ts +41 -0
  81. package/dist/holmes/review/point-in-time-replay.js +161 -0
  82. package/dist/holmes/review/rank-diagnosis.d.ts +43 -0
  83. package/dist/holmes/review/rank-diagnosis.js +163 -0
  84. package/dist/holmes/review/replay-calibration.d.ts +62 -0
  85. package/dist/holmes/review/replay-calibration.js +83 -0
  86. package/dist/holmes/review/replay-corpus.d.ts +135 -0
  87. package/dist/holmes/review/replay-corpus.js +210 -0
  88. package/dist/holmes/review/run-replay.d.ts +260 -0
  89. package/dist/holmes/review/run-replay.js +729 -0
  90. package/dist/holmes/review/semantic-arm.d.ts +271 -0
  91. package/dist/holmes/review/semantic-arm.js +717 -0
  92. package/dist/holmes/review/semantic-retrieval.d.ts +55 -0
  93. package/dist/holmes/review/semantic-retrieval.js +156 -0
  94. package/dist/holmes/review/spec-layer-stats.d.ts +38 -0
  95. package/dist/holmes/review/spec-layer-stats.js +52 -0
  96. package/dist/holmes/review/temporal-prior.d.ts +33 -0
  97. package/dist/holmes/review/temporal-prior.js +53 -0
  98. package/dist/holmes/review/test-runner.d.ts +15 -0
  99. package/dist/holmes/review/test-runner.js +41 -4
  100. package/dist/holmes/review/test-selection-breadth.d.ts +75 -0
  101. package/dist/holmes/review/test-selection-breadth.js +57 -0
  102. package/dist/holmes/review/traceability-benchmark.d.ts +81 -0
  103. package/dist/holmes/review/traceability-benchmark.js +135 -0
  104. package/dist/holmes/review/union-verify.d.ts +12 -0
  105. package/dist/holmes/review/union-verify.js +70 -0
  106. package/dist/holmes/rtm/graph-store.d.ts +51 -0
  107. package/dist/holmes/rtm/graph-store.js +122 -0
  108. package/dist/holmes/rtm/incremental.d.ts +25 -1
  109. package/dist/holmes/rtm/incremental.js +18 -1
  110. package/dist/holmes/rtm/localize.d.ts +28 -0
  111. package/dist/holmes/rtm/localize.js +272 -14
  112. package/dist/holmes/rtm/rtm-builder.d.ts +118 -3
  113. package/dist/holmes/rtm/rtm-builder.js +265 -28
  114. package/dist/holmes/rtm/rtm-graph.d.ts +117 -2
  115. package/dist/holmes/rtm/rtm-graph.js +194 -31
  116. package/dist/holmes/rtm/taint-benchmark.d.ts +97 -0
  117. package/dist/holmes/rtm/taint-benchmark.js +141 -0
  118. package/dist/holmes/rtm/test-scope.js +24 -1
  119. package/dist/holmes/semantic/credentials.d.ts +15 -0
  120. package/dist/holmes/semantic/credentials.js +134 -0
  121. package/dist/holmes/semantic/embedder.d.ts +44 -0
  122. package/dist/holmes/semantic/embedder.js +185 -0
  123. package/dist/holmes/semantic/hit-rerank.d.ts +4 -0
  124. package/dist/holmes/semantic/hit-rerank.js +38 -0
  125. package/dist/holmes/semantic/tier.d.ts +37 -0
  126. package/dist/holmes/semantic/tier.js +54 -0
  127. package/dist/holmes/semantic/vector-cache.d.ts +11 -0
  128. package/dist/holmes/semantic/vector-cache.js +91 -0
  129. package/dist/holmes/spec/acceptance-quality.d.ts +81 -0
  130. package/dist/holmes/spec/acceptance-quality.js +169 -0
  131. package/dist/holmes/spec/validator.js +33 -1
  132. package/dist/holmes/spec/yaml-scalar.d.ts +1 -0
  133. package/dist/holmes/spec/yaml-scalar.js +43 -0
  134. package/package.json +1 -1
@@ -46,6 +46,7 @@ const playbook_skills_1 = require("./playbook-skills");
46
46
  const agents_1 = require("./agents");
47
47
  const roles_readme_1 = require("./roles-readme");
48
48
  const mcp_launcher_1 = require("./mcp-launcher");
49
+ const codex_toml_1 = require("./codex-toml");
49
50
  const pre_tool_use_1 = require("../hooks/pre-tool-use");
50
51
  const governed_precondition_1 = require("./governed-precondition");
51
52
  const risk_gate_1 = require("../guardrail/risk-gate");
@@ -343,6 +344,71 @@ function runInit(opts) {
343
344
  }
344
345
  messages.push('Restart Claude Code — hooks and MCP servers are read at session start.');
345
346
  }
347
+ // @implements A-SPEC-266 — Codex reads MCP servers from `.codex/config.toml` (TOML
348
+ // `[mcp_servers.*]`), NOT the `.codex/mcp_config.json` (JSON) this project used to write and Codex
349
+ // never loaded. That file also holds the user's other Codex settings, so — like `.mcp.json` — we
350
+ // MERGE only our table. The obsolete JSON is migrated away so no dead wiring is left behind.
351
+ let codexJsonToMigrate = null; // deleted only after the write loop confirms config.toml
352
+ {
353
+ const codexDir = path.join(opts.target, '.codex');
354
+ const codexTomlPath = path.join(codexDir, 'config.toml');
355
+ const codexJsonPath = path.join(codexDir, 'mcp_config.json');
356
+ // A `.codex/config.toml` we cannot read (a directory, a permission wall, a dangling symlink) is
357
+ // NOT touched — reading it eagerly with readFileSync threw and crashed the whole init. Mirror the
358
+ // `.mcp.json` "refusing to touch it" discipline: skip codex wiring and say so, migrate nothing.
359
+ let codexTomlBefore = null;
360
+ let codexUnreadable = false;
361
+ if (fs.existsSync(codexTomlPath)) {
362
+ try {
363
+ codexTomlBefore = fs.readFileSync(codexTomlPath, 'utf8');
364
+ }
365
+ catch {
366
+ codexUnreadable = true;
367
+ }
368
+ }
369
+ const wantsCodex = (opts.agents ?? []).includes('codex') && !codexUnreadable;
370
+ if (codexUnreadable)
371
+ messages.push(`Refusing to touch ${codexTomlPath} — it is not a readable file. Fix or remove it, then re-run.`);
372
+ // @implements A-SPEC-266 (round-2 F3) — the obsolete JSON is deleted AFTER the config.toml write is
373
+ // confirmed, never before: an eager delete followed by a failed toml write left the user with NO
374
+ // codex wiring at all (and a message claiming nothing was lost). Here we only PLAN the migration;
375
+ // the deletion runs post-write-loop (dry-run just previews it in `removals`).
376
+ const migrateAwayCodexJson = () => {
377
+ if (!fs.existsSync(codexJsonPath))
378
+ return;
379
+ if (opts.dryRun) {
380
+ removals.push(codexJsonPath);
381
+ return;
382
+ }
383
+ codexJsonToMigrate = codexJsonPath;
384
+ };
385
+ if (opts.remove) {
386
+ if (codexTomlBefore !== null) {
387
+ const after = (0, codex_toml_1.removeCodexToml)(codexTomlBefore);
388
+ if (after.trim() === '' && after !== codexTomlBefore) {
389
+ // Our table was the only content — delete the file rather than leave it empty.
390
+ if (opts.dryRun)
391
+ removals.push(codexTomlPath);
392
+ else if (fs.existsSync(codexTomlPath)) {
393
+ fs.rmSync(codexTomlPath, { force: true });
394
+ messages.push(`Removed ${codexTomlPath} (holmes-kit MCP table was its only content).`);
395
+ }
396
+ }
397
+ else if (after !== codexTomlBefore) {
398
+ changes.push({ path: codexTomlPath, before: codexTomlBefore, after });
399
+ }
400
+ }
401
+ migrateAwayCodexJson();
402
+ }
403
+ else if (wantsCodex && opts.mcp) {
404
+ const mcpBin = path.join(opts.packageRoot, 'bin', 'holmes-mcp.js');
405
+ const entry = (0, mcp_launcher_1.mcpEntryForInstall)({ packageRoot: opts.packageRoot, mcpBinPath: mcpBin, flag: opts.mcpLauncher });
406
+ const after = (0, codex_toml_1.mergeCodexToml)(codexTomlBefore, (0, codex_toml_1.codexMcpBlock)(entry, opts.specsDir));
407
+ if (after !== codexTomlBefore)
408
+ changes.push({ path: codexTomlPath, before: codexTomlBefore, after });
409
+ migrateAwayCodexJson();
410
+ }
411
+ }
346
412
  if (opts.dryRun) {
347
413
  // @implements A-SPEC-190 (round 8) — dry-run must plan the SAME set the real run touches, and
348
414
  // must not print a deletion under the verb 'would write'. Round-7 planned installs only from
@@ -422,6 +488,18 @@ function runInit(opts) {
422
488
  ] };
423
489
  }
424
490
  }
491
+ // @implements A-SPEC-266 (round-2 F3) — the config.toml write above is confirmed now, so it is
492
+ // safe to remove the obsolete JSON. Doing it earlier risked deleting codex's only wiring and then
493
+ // failing the toml write.
494
+ if (codexJsonToMigrate !== null && fs.existsSync(codexJsonToMigrate)) {
495
+ try {
496
+ fs.rmSync(codexJsonToMigrate, { force: true });
497
+ messages.push(`Removed obsolete ${codexJsonToMigrate} (Codex never read it — MCP wiring now lives in config.toml).`);
498
+ }
499
+ catch (e) {
500
+ messages.push(`Could not remove obsolete ${codexJsonToMigrate}: ${e instanceof Error ? e.message : String(e)}`);
501
+ }
502
+ }
425
503
  if (!opts.remove && opts.mode === 'governed') {
426
504
  for (const d of SPEC_SUBDIRS)
427
505
  fs.mkdirSync(path.join(opts.target, opts.specsDir, d), { recursive: true });
@@ -52,12 +52,12 @@ function parseAgentList(input) {
52
52
  /**
53
53
  * Renders an interactive TTY checkbox selection menu using standard readline & ANSI codes.
54
54
  */
55
- async function promptAgentSelection(availableAgents = agents_1.AGENTS, currentWired = ['claude']) {
55
+ async function promptAgentSelection(availableAgents = agents_1.AGENTS, currentWired = [...agents_1.AGENTS]) {
56
56
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
57
- return ['claude', 'antigravity']; // Fallback for non-TTY
57
+ return [...availableAgents]; // Fallback for non-TTY — default to all harnesses
58
58
  }
59
59
  return new Promise((resolve) => {
60
- const selected = new Set(currentWired.length ? currentWired : ['claude', 'antigravity']);
60
+ const selected = new Set(currentWired.length ? currentWired : [...availableAgents]);
61
61
  let cursor = 0;
62
62
  const items = [...availableAgents, 'all'];
63
63
  const rl = readline.createInterface({
@@ -91,7 +91,7 @@ async function promptAgentSelection(availableAgents = agents_1.AGENTS, currentWi
91
91
  }
92
92
  else if (item === 'codex') {
93
93
  isChecked = selected.has('codex');
94
- label = '💻 Codex CLI (.codex/mcp_config.json)';
94
+ label = '💻 Codex CLI (.codex/config.toml)';
95
95
  }
96
96
  const box = isChecked ? '[X]' : '[ ]';
97
97
  const line = `${prefix}${box} ${label}\n`;
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * MCP 서버 배선의 launch 방식을 한 곳에서 계산한다.
3
3
  *
4
- * 왜 한 곳인가: `.mcp.json`(Claude)·`.agents/mcp_config.json`(antigravity)·`.codex/mcp_config.json`
5
- * 세 배선이 같은 서버를 띄운다. 셋이 각자 command/args 를 지으면 하나가 npx 로 옮겨갈 때 나머지가
4
+ * 왜 한 곳인가: `.mcp.json`(Claude)·`.agents/mcp_config.json`(antigravity)·`.codex/config.toml`
5
+ * (codex, TOML `[mcp_servers.holmes-kit]` — A-SPEC-266) 세 배선이 같은 서버를 띄운다. 셋이 각자 command/args 를 지으면 하나가 npx 로 옮겨갈 때 나머지가
6
6
  * 절대경로에 남아 어긋난다 — 그 드리프트가 REQ-251 자체의 출발점이었다.
7
7
  *
8
8
  * 왜 두 모드인가: 최종 사용자는 npm 설치본에서 init 하므로 `npx ...@<정확한버전> holmes-mcp` 가
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `holmes-kit semantic-key set|unset|status` — the human's consent act for the cloud semantic
3
+ * tier, made convenient AND safe:
4
+ *
5
+ * - `set` reads the key from STDIN (hidden prompt on a TTY, one piped line otherwise). The key
6
+ * never appears in argv — ps and shell history are exactly the surfaces a third-party PC leaks
7
+ * from. Storage prefers the OS keychain (darwin, fed over stdin too) and falls back to
8
+ * ~/.holmes/credentials.json at 0600 under 0700.
9
+ * - `status` prints the SOURCE only. The value never appears in any output — the same promise
10
+ * doctor's ledger-signing check already makes for HOLMES_LEDGER_KEY.
11
+ * - `unset` clears both stores.
12
+ */
13
+ import { type CredentialOpts } from '../semantic/credentials';
14
+ export interface SemanticKeyIo {
15
+ /** Resolves one secret line. The default hides TTY echo; tests inject. */
16
+ readSecret: (prompt: string) => Promise<string>;
17
+ write: (line: string) => void;
18
+ }
19
+ export declare function runSemanticKey(sub: string | undefined, io?: SemanticKeyIo, cred?: CredentialOpts): Promise<number>;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runSemanticKey = runSemanticKey;
4
+ // @implements A-SPEC-477
5
+ /**
6
+ * `holmes-kit semantic-key set|unset|status` — the human's consent act for the cloud semantic
7
+ * tier, made convenient AND safe:
8
+ *
9
+ * - `set` reads the key from STDIN (hidden prompt on a TTY, one piped line otherwise). The key
10
+ * never appears in argv — ps and shell history are exactly the surfaces a third-party PC leaks
11
+ * from. Storage prefers the OS keychain (darwin, fed over stdin too) and falls back to
12
+ * ~/.holmes/credentials.json at 0600 under 0700.
13
+ * - `status` prints the SOURCE only. The value never appears in any output — the same promise
14
+ * doctor's ledger-signing check already makes for HOLMES_LEDGER_KEY.
15
+ * - `unset` clears both stores.
16
+ */
17
+ const credentials_1 = require("../semantic/credentials");
18
+ const defaultIo = {
19
+ readSecret: (prompt) => new Promise((resolve, reject) => {
20
+ process.stderr.write(prompt);
21
+ const chunks = [];
22
+ const stdin = process.stdin;
23
+ if (stdin.isTTY) {
24
+ stdin.setRawMode?.(true);
25
+ stdin.resume();
26
+ let buf = '';
27
+ const onData = (d) => {
28
+ const c = d.toString('utf8');
29
+ if (c === '\r' || c === '\n' || c === '') {
30
+ stdin.setRawMode?.(false);
31
+ stdin.pause();
32
+ stdin.off('data', onData);
33
+ process.stderr.write('\n');
34
+ resolve(buf);
35
+ }
36
+ else if (c === '') {
37
+ stdin.setRawMode?.(false);
38
+ stdin.pause();
39
+ stdin.off('data', onData);
40
+ reject(new Error('aborted'));
41
+ }
42
+ else if (c === '' || c === '\b') {
43
+ buf = buf.slice(0, -1);
44
+ }
45
+ else {
46
+ buf += c; // no echo — the hiding IS the point
47
+ }
48
+ };
49
+ stdin.on('data', onData);
50
+ }
51
+ else {
52
+ stdin.on('data', (d) => chunks.push(Buffer.from(d)));
53
+ stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8').split('\n')[0] ?? ''));
54
+ stdin.on('error', reject);
55
+ }
56
+ }),
57
+ write: (line) => process.stdout.write(line + '\n'),
58
+ };
59
+ const SOURCE_LABEL = {
60
+ 'env': 'HOLMES_SEMANTIC_API_KEY (환경변수)',
61
+ 'env-compat': 'GEMINI_API_KEY (환경변수)',
62
+ 'keychain': 'macOS 키체인 (holmes-kit/semantic)',
63
+ 'file': '~/.holmes/credentials.json (0600)',
64
+ };
65
+ async function runSemanticKey(sub, io = defaultIo, cred = {}) {
66
+ if (sub === 'set') {
67
+ const key = (await io.readSecret('cloud semantic 티어 API 키 입력(표시되지 않음): ')).trim();
68
+ if (key === '') {
69
+ io.write('빈 키는 저장하지 않습니다.');
70
+ return 2;
71
+ }
72
+ const where = (0, credentials_1.storeSemanticKey)(key, cred);
73
+ io.write(`저장됨: ${SOURCE_LABEL[where]} — 키 설정은 이그레스(스펙 산문·경로·심볼명 외부 전송) 동의입니다. 값은 어떤 출력에도 표시되지 않습니다.`);
74
+ return 0;
75
+ }
76
+ if (sub === 'unset') {
77
+ (0, credentials_1.removeSemanticKey)(cred);
78
+ io.write('semantic 키 제거됨 (키체인·파일 양쪽).');
79
+ return 0;
80
+ }
81
+ if (sub === 'status' || sub === undefined) {
82
+ const r = (0, credentials_1.resolveSemanticKey)(cred);
83
+ if (r === null) {
84
+ io.write('semantic 키: 없음 — cloud 티어 비활성. `holmes-kit semantic-key set` 으로 opt-in 하십시오(키 설정 = 이그레스 동의).');
85
+ }
86
+ else {
87
+ io.write(`semantic 키: 설정됨 — 출처 ${SOURCE_LABEL[r.source]}. 값은 표시되지 않습니다.`);
88
+ }
89
+ return 0;
90
+ }
91
+ io.write(`unknown subcommand: ${sub} (set|unset|status)`);
92
+ return 2;
93
+ }
@@ -3,6 +3,15 @@ export interface AxConfig {
3
3
  enforcement: 'block' | 'warn' | 'off';
4
4
  enforceHighRisk: boolean;
5
5
  overrideRequiresAdr: boolean;
6
+ /**
7
+ * @implements A-SPEC-278
8
+ * Pre-edit impact evidence enforcement. Defaults to `off` — unlike `enforcement`, whose strict
9
+ * default is the safe posture, the safe default HERE is off: switching it on changes the working
10
+ * discipline of every session in the repository, and that belongs to the repository's owner.
11
+ * A malformed config also lands here, so a broken file can neither weaken governance nor
12
+ * silently impose a new one.
13
+ */
14
+ preEditEvidence: 'off' | 'warn' | 'block';
6
15
  };
7
16
  highRiskDomains: string[];
8
17
  storage: {
@@ -35,12 +35,19 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.DEFAULT_CONFIG = void 0;
37
37
  exports.loadConfig = loadConfig;
38
+ // @implements A-SPEC-278
38
39
  // @implements A-SPEC-125.5
39
40
  // src/holmes/config/config.ts
40
41
  const fs = __importStar(require("node:fs"));
41
42
  const path = __importStar(require("node:path"));
42
43
  exports.DEFAULT_CONFIG = {
43
- guardrail: { enforcement: 'block', enforceHighRisk: true, overrideRequiresAdr: true },
44
+ // @implements A-SPEC-332
45
+ // `warn`, not `off` and not `block`. Off left Phase 2 present in code and absent in behaviour;
46
+ // block would refuse every source edit today, because `.ax/evidence` is empty (n=0) and the gate's
47
+ // first refusal is `no-analysis`. Warn reports the missing evidence without stopping work, which
48
+ // is also the only path that accumulates the data a later block decision would need. Raised as an
49
+ // authority question rather than decided by measurement — the user chose this level.
50
+ guardrail: { enforcement: 'block', enforceHighRisk: true, overrideRequiresAdr: true, preEditEvidence: 'warn' },
44
51
  highRiskDomains: ['@auth', '@payment'],
45
52
  storage: { specStore: 'local-markdown' },
46
53
  };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * What the extractor actually recovers, per language.
3
+ *
4
+ * Goal Phase 4 requires that per-language coverage and its limits always travel with the result.
5
+ * They did not, and the capability is genuinely uneven — measured with fixtures 2026-08-28, symbols
6
+ * come out of every advertised language, `imports` only out of Python and TypeScript-family files,
7
+ * and `inherits` out of NONE of them, not even where the declaration is completely unambiguous
8
+ * (`class Child(Base)`, `extends Base`).
9
+ *
10
+ * The problem was never the unevenness; it was the silence. Coverage reported only extensions
11
+ * OUTSIDE the scannable set, so an impact answer over a Java file looked exactly as trustworthy as
12
+ * one over Python while resting on strictly less evidence, and every downstream answer inherited
13
+ * that asymmetry without anyone being told.
14
+ *
15
+ * This table is pinned against the real scanner by fixtures in the sibling test. If it drifts from
16
+ * reality in either direction that test fails — a capability table that cannot fail guarantees
17
+ * nothing.
18
+ */
19
+ export declare const EXTRACTABLE_RELATIONS: readonly ["calls", "imports", "inherits"];
20
+ export type ExtractableRelation = typeof EXTRACTABLE_RELATIONS[number];
21
+ export interface LanguageCapability {
22
+ symbols: boolean;
23
+ /** What the SCANNER recovers from the source text. */
24
+ relations: ExtractableRelation[];
25
+ /**
26
+ * @implements A-SPEC-286
27
+ * What actually becomes a GRAPH edge — always a subset of `relations`.
28
+ *
29
+ * These are different claims and conflating them misleads. Measured 2026-08-28: the scanner emits
30
+ * 1,892 `imports` edges of which zero reach the graph, because their target is a module path
31
+ * rather than a symbol and the builder resolves symbol-to-symbol relations only. Everything
32
+ * downstream — impact, localization, the pre-edit gate — queries the graph, so the graph column is
33
+ * the one a coverage claim has to be made in.
34
+ */
35
+ graphResolved: ExtractableRelation[];
36
+ }
37
+ export declare const LANGUAGE_CAPABILITY: Record<string, LanguageCapability>;
38
+ export declare function capabilityFor(ext: string): LanguageCapability | undefined;
39
+ export interface LanguageGap {
40
+ ext: string;
41
+ /** Relation kinds this language's extractor does NOT recover at all. */
42
+ missing: ExtractableRelation[];
43
+ /**
44
+ * @implements A-SPEC-286
45
+ * Recovered by the scanner but never resolved into a graph edge. A distinct fact from `missing`:
46
+ * the information exists and is still unusable to anything that queries the graph.
47
+ */
48
+ extractedButUnresolved: ExtractableRelation[];
49
+ }
50
+ /**
51
+ * The capability gaps for the languages ACTUALLY present in a scan.
52
+ *
53
+ * Only languages that appear are listed: enumerating every language the tool could theoretically
54
+ * meet would bury the ones this answer actually rests on. A language with nothing missing drops out
55
+ * entirely, so the report shrinks to nothing as extraction catches up rather than becoming noise.
56
+ */
57
+ export declare function capabilityGapsFor(sourcePaths: readonly string[]): LanguageGap[];
58
+ /** Every advertised extension must have an entry; a blank would be a silent overclaim. */
59
+ export declare const ADVERTISED_EXTENSIONS: readonly string[];
60
+ /**
61
+ * An unknown extension gets a family of its own rather than joining a shared bucket: an unfamiliar
62
+ * language must not be able to take another language's definitions. Narrowing is the safe direction
63
+ * — it costs recall, which is disclosed, instead of asserting something false.
64
+ */
65
+ export declare function languageFamilyOf(ext: string): string;
@@ -0,0 +1,145 @@
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.ADVERTISED_EXTENSIONS = exports.LANGUAGE_CAPABILITY = exports.EXTRACTABLE_RELATIONS = void 0;
37
+ exports.capabilityFor = capabilityFor;
38
+ exports.capabilityGapsFor = capabilityGapsFor;
39
+ exports.languageFamilyOf = languageFamilyOf;
40
+ // @implements A-SPEC-289
41
+ // @implements A-SPEC-288
42
+ // @implements A-SPEC-287
43
+ // @implements A-SPEC-286
44
+ // @implements A-SPEC-285
45
+ const path = __importStar(require("node:path"));
46
+ const cpg_scanner_1 = require("./cpg-scanner");
47
+ /**
48
+ * What the extractor actually recovers, per language.
49
+ *
50
+ * Goal Phase 4 requires that per-language coverage and its limits always travel with the result.
51
+ * They did not, and the capability is genuinely uneven — measured with fixtures 2026-08-28, symbols
52
+ * come out of every advertised language, `imports` only out of Python and TypeScript-family files,
53
+ * and `inherits` out of NONE of them, not even where the declaration is completely unambiguous
54
+ * (`class Child(Base)`, `extends Base`).
55
+ *
56
+ * The problem was never the unevenness; it was the silence. Coverage reported only extensions
57
+ * OUTSIDE the scannable set, so an impact answer over a Java file looked exactly as trustworthy as
58
+ * one over Python while resting on strictly less evidence, and every downstream answer inherited
59
+ * that asymmetry without anyone being told.
60
+ *
61
+ * This table is pinned against the real scanner by fixtures in the sibling test. If it drifts from
62
+ * reality in either direction that test fails — a capability table that cannot fail guarantees
63
+ * nothing.
64
+ */
65
+ exports.EXTRACTABLE_RELATIONS = ['calls', 'imports', 'inherits'];
66
+ // @implements A-SPEC-288 — `extends` joins Python's inheritance; `implements` stays a stated gap.
67
+ // @implements A-SPEC-289 — imports now resolve to FILE nodes when the specifier is relative and
68
+ // names a scanned file; bare specifiers stay external and unresolved by design.
69
+ const TS_FAMILY = { symbols: true, relations: ['calls', 'imports', 'inherits'], graphResolved: ['calls', 'imports', 'inherits'] };
70
+ /** Symbols and calls only — no import edges, no inheritance. */
71
+ const CALLS_ONLY = { symbols: true, relations: ['calls'], graphResolved: ['calls'] };
72
+ exports.LANGUAGE_CAPABILITY = {
73
+ '.ts': TS_FAMILY, '.mts': TS_FAMILY, '.cts': TS_FAMILY, '.tsx': TS_FAMILY,
74
+ '.js': TS_FAMILY, '.mjs': TS_FAMILY, '.jsx': TS_FAMILY,
75
+ // CommonJS `require()` is not recovered as an import, unlike ESM `import` — measured.
76
+ '.cjs': CALLS_ONLY,
77
+ // @implements A-SPEC-286 — the first language whose inheritance is both extracted AND resolved.
78
+ '.py': { symbols: true, relations: ['calls', 'imports', 'inherits'], graphResolved: ['calls', 'imports', 'inherits'] },
79
+ '.java': CALLS_ONLY, '.cs': CALLS_ONLY, '.go': CALLS_ONLY, '.rs': CALLS_ONLY,
80
+ // @implements A-SPEC-287 — C++ used to extract calls that never became graph edges, because its
81
+ // edge scope omitted the class node the symbol walk uses. Fixed; it now resolves like the others.
82
+ '.cpp': CALLS_ONLY, '.cc': CALLS_ONLY, '.cxx': CALLS_ONLY,
83
+ '.hpp': CALLS_ONLY, '.hh': CALLS_ONLY, '.h': CALLS_ONLY,
84
+ };
85
+ function capabilityFor(ext) {
86
+ return exports.LANGUAGE_CAPABILITY[ext.toLowerCase()];
87
+ }
88
+ /**
89
+ * The capability gaps for the languages ACTUALLY present in a scan.
90
+ *
91
+ * Only languages that appear are listed: enumerating every language the tool could theoretically
92
+ * meet would bury the ones this answer actually rests on. A language with nothing missing drops out
93
+ * entirely, so the report shrinks to nothing as extraction catches up rather than becoming noise.
94
+ */
95
+ function capabilityGapsFor(sourcePaths) {
96
+ const present = new Set();
97
+ for (const p of sourcePaths) {
98
+ const ext = path.extname(p).toLowerCase();
99
+ if (capabilityFor(ext))
100
+ present.add(ext);
101
+ }
102
+ return [...present].sort()
103
+ .map((ext) => {
104
+ const cap = capabilityFor(ext);
105
+ return {
106
+ ext,
107
+ missing: exports.EXTRACTABLE_RELATIONS.filter((r) => !cap.relations.includes(r)),
108
+ extractedButUnresolved: cap.relations.filter((r) => !cap.graphResolved.includes(r)),
109
+ };
110
+ })
111
+ .filter((gap) => gap.missing.length > 0 || gap.extractedButUnresolved.length > 0);
112
+ }
113
+ /** Every advertised extension must have an entry; a blank would be a silent overclaim. */
114
+ exports.ADVERTISED_EXTENSIONS = cpg_scanner_1.SCANNABLE_EXTENSIONS;
115
+ // @implements A-SPEC-300
116
+ /**
117
+ * Extensions that can actually call each other, grouped.
118
+ *
119
+ * Name resolution links a call to a definition when the name is defined in exactly one file. That
120
+ * rule had no notion of language, and measured on a five-language fixture (2026-08-28) it asserted
121
+ * CODE:Child.run@Child.java --calls--> CODE:helper@lib.rs
122
+ * A Java method calling a Rust function. Worse, the correct target sat in the SAME file: it was
123
+ * missed on qualification, leaving the Rust definition as the only candidate, so the precision rule
124
+ * actively confirmed a wrong answer. A missing edge widens scope; a wrong edge narrows it onto the
125
+ * wrong place.
126
+ *
127
+ * The groups follow real interop, not file-name similarity: TypeScript and JavaScript genuinely call
128
+ * each other, and a C++ implementation genuinely calls its header's declarations.
129
+ */
130
+ const LANGUAGE_FAMILIES = {
131
+ '.ts': 'ts-js', '.mts': 'ts-js', '.cts': 'ts-js', '.tsx': 'ts-js',
132
+ '.js': 'ts-js', '.mjs': 'ts-js', '.jsx': 'ts-js', '.cjs': 'ts-js',
133
+ '.py': 'python',
134
+ '.java': 'java', '.cs': 'csharp', '.go': 'go', '.rs': 'rust',
135
+ '.cpp': 'cpp', '.cc': 'cpp', '.cxx': 'cpp', '.hpp': 'cpp', '.hh': 'cpp', '.h': 'cpp',
136
+ };
137
+ /**
138
+ * An unknown extension gets a family of its own rather than joining a shared bucket: an unfamiliar
139
+ * language must not be able to take another language's definitions. Narrowing is the safe direction
140
+ * — it costs recall, which is disclosed, instead of asserting something false.
141
+ */
142
+ function languageFamilyOf(ext) {
143
+ const lower = ext.toLowerCase();
144
+ return LANGUAGE_FAMILIES[lower] ?? `ext:${lower}`;
145
+ }