@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,66 @@
1
+ /**
2
+ * PURE merge logic for a target project's Claude Code settings + .mcp.json.
3
+ *
4
+ * The installer must NEVER clobber a user's existing configuration: a target project's
5
+ * `settings.local.json` typically already holds `permissions` and possibly other hook groups. These
6
+ * functions take the parsed existing object and return a NEW object with only holmes-kit's entries
7
+ * added/replaced/removed. All fs, path resolution, backups and atomic writes live in init.ts.
8
+ *
9
+ * Idempotency key is the COMMAND STRING (it embeds the installed package path), not an injected
10
+ * marker key — Claude Code's settings schema is not documented to tolerate unknown keys inside hook
11
+ * objects, and a rejected settings file would break the whole session.
12
+ */
13
+ export interface HookEntry {
14
+ type: 'command';
15
+ command: string;
16
+ }
17
+ export interface HookGroup {
18
+ matcher?: string;
19
+ hooks: HookEntry[];
20
+ }
21
+ export interface Settings {
22
+ hooks?: Record<string, HookGroup[]>;
23
+ enabledMcpjsonServers?: string[];
24
+ [k: string]: unknown;
25
+ }
26
+ export interface McpConfig {
27
+ mcpServers?: Record<string, unknown>;
28
+ [k: string]: unknown;
29
+ }
30
+ /** The wiring holmes-kit wants in place. Commands are absolute paths resolved by the caller. */
31
+ export interface HookPlan {
32
+ preToolUseMatcher: string;
33
+ preToolUseCommand: string;
34
+ stopCommand: string;
35
+ }
36
+ /** A hook entry belongs to holmes-kit iff its command references our hook scripts. */
37
+ export declare function isHolmesCommand(command: string): boolean;
38
+ /**
39
+ * The script path out of a hook command line, e.g. `node "/pkg/dist/.../stop.js"` -> `/pkg/dist/.../stop.js`.
40
+ * `init` quotes the path (installs live under paths that may contain spaces), so an unquoting parse is
41
+ * mandatory, not defensive: a naive `\S+` capture keeps the quotes and every existence check then fails.
42
+ * Returns '' when no argument is present — callers treat that as unresolvable.
43
+ */
44
+ export declare function hookScriptPath(command: string): string;
45
+ /**
46
+ * Add or replace holmes-kit's PreToolUse + Stop entries. Every other hook group, every other event,
47
+ * and all unrelated top-level keys (permissions, …) are preserved byte-for-byte in value.
48
+ */
49
+ export declare function mergeHooks(existing: Settings | undefined, plan: HookPlan): Settings;
50
+ /** Remove every holmes-owned hook entry, dropping groups that become empty. Inverse of mergeHooks. */
51
+ export declare function removeHooks(existing: Settings | undefined): Settings;
52
+ /** Union — never replace a user's other enabled servers. */
53
+ export declare function enableMcpServer(existing: Settings | undefined, name: string): Settings;
54
+ export declare function disableMcpServer(existing: Settings | undefined, name: string): Settings;
55
+ /**
56
+ * Merge only `mcpServers['holmes-kit']`. `args` MUST be absolute: the MCP server is spawned with the
57
+ * TARGET project's cwd, so a repo-relative path (as this project's own .mcp.json uses) would not resolve.
58
+ */
59
+ /**
60
+ * @implements A-SPEC-179
61
+ * The env keys holmes itself decides in the server entry. Everything else there belongs to whoever
62
+ * put it there.
63
+ */
64
+ export declare const HOLMES_OWNED_MCP_ENV: readonly ["HOLMES_SPECS"];
65
+ export declare function mergeMcpServers(existing: McpConfig | undefined, name: string, mcpBinPath: string, specsDir: string): McpConfig;
66
+ export declare function removeMcpServer(existing: McpConfig | undefined, name: string): McpConfig;
@@ -0,0 +1,13 @@
1
+ export interface AxConfig {
2
+ guardrail: {
3
+ enforcement: 'block' | 'warn' | 'off';
4
+ enforceHighRisk: boolean;
5
+ overrideRequiresAdr: boolean;
6
+ };
7
+ highRiskDomains: string[];
8
+ storage: {
9
+ specStore: string;
10
+ };
11
+ }
12
+ export declare const DEFAULT_CONFIG: AxConfig;
13
+ export declare function loadConfig(root: string): AxConfig;
@@ -0,0 +1,40 @@
1
+ import { RtmGraph } from '../rtm/rtm-graph';
2
+ import { ContextTier } from './tiers';
3
+ /** Returns a node's raw content, or null if the node has none. */
4
+ export type ContentSource = (nodeId: string) => string | null;
5
+ export interface BundleEntry {
6
+ nodeId: string;
7
+ tier: ContextTier;
8
+ /**
9
+ * Traversal COST from the seed, not a hop count: a spec-chain hop costs 1 and a `calls` hop costs
10
+ * 2 (see REL_COST). Ordering and tiering both read it as "cheaper is closer", which holds either
11
+ * way, but a reader treating it as hops will misjudge every call edge.
12
+ */
13
+ distance: number;
14
+ detail: string;
15
+ tokens: number;
16
+ }
17
+ export interface ContextBundle {
18
+ seedId: string;
19
+ budget: number;
20
+ usedTokens: number;
21
+ /** Included nodes, ordered by (tier priority hot<domain<cold), then (distance, nodeId). */
22
+ entries: BundleEntry[];
23
+ /** Classified nodes that did not fit the budget. Sorted ascending. Never silently omitted. */
24
+ dropped: string[];
25
+ }
26
+ /**
27
+ * Assembles a tiered, budget-filled context bundle around a seed node.
28
+ *
29
+ * Pure/deterministic given the injected content source: no Date, randomness,
30
+ * or LLM calls. Same (seedId, tokenBudget) always yields a deep-equal bundle.
31
+ */
32
+ export declare class ContextBundler {
33
+ private readonly graph;
34
+ private readonly content;
35
+ private readonly maxDistance;
36
+ constructor(graph: RtmGraph, content: ContentSource, opts?: {
37
+ maxDistance?: number;
38
+ });
39
+ getContextBundle(seedId: string, tokenBudget: number): ContextBundle;
40
+ }
@@ -0,0 +1,9 @@
1
+ import { ContextTier } from './tiers';
2
+ /**
3
+ * Renders a node's content at the detail level appropriate for its context
4
+ * tier. Deterministic: fixed char limits, no ellipsis, no randomness.
5
+ * - hot: full content (nodeId alone if content is null).
6
+ * - domain: nodeId + up to 200-char preview of content.
7
+ * - cold: nodeId + first non-empty line, truncated to 80 chars.
8
+ */
9
+ export declare function renderDetail(nodeId: string, content: string | null, tier: ContextTier): string;
@@ -0,0 +1,54 @@
1
+ import { RtmGraph } from '../rtm/rtm-graph';
2
+ export type ContextTier = 'hot' | 'domain' | 'cold';
3
+ /**
4
+ * Distance→tier classifier. 0|1 -> hot, 2|3 -> domain, else cold.
5
+ *
6
+ * NOTE: since edge weighting landed, the input is a traversal COST, not a hop count. A spec-chain
7
+ * hop costs 1 and a `calls` hop costs 2, so a directly-called symbol arrives at 2 and lands in
8
+ * `domain` rather than `hot` — deliberately: what a file implements is the hot context, what calls it
9
+ * is surrounding context. Reading this parameter as "number of hops" would misjudge every call edge.
10
+ */
11
+ export declare function tierForDistance(distance: number): ContextTier;
12
+ /**
13
+ * Deterministic BFS from seedId over graph.neighbors(), recording the shortest
14
+ * hop distance to each reachable node (seed distance is 0). Nodes beyond
15
+ * maxDistance are not enqueued (and thus not included in the result). Ties
16
+ * are broken by visiting neighbors in sorted order (neighbors() already
17
+ * returns sorted, deduped ids), so results are deterministic.
18
+ */
19
+ /**
20
+ * What one hop of each relation costs.
21
+ *
22
+ * `implements` and `depends_on` are the traceability chain the bundle exists to carry, so they cost
23
+ * one. A `calls` hop costs more, because call edges make the graph far denser and a distance
24
+ * calibrated for the spec chain then reaches nearly everything: measured on this repository, adding
25
+ * call edges took a budget-8000 bundle from 164 entries to 252 — 91% of the whole graph — while the
26
+ * distance bound of 6 stayed untouched. A token budget alone does not fix that; it just fills the
27
+ * budget with distant nodes instead of overflowing.
28
+ *
29
+ * This is REQ-127's edge-type weighting: bounding propagation by KIND, not only by depth and hubs.
30
+ */
31
+ export declare const REL_COST: Record<string, number>;
32
+ /**
33
+ * Deterministic weighted shortest-path from seedId, recording the cheapest cost to each reachable
34
+ * node (seed cost 0). Nodes beyond maxDistance are never enqueued.
35
+ *
36
+ * Costs are small integers, so this uses a bucket queue rather than a comparison heap — O(V+E+maxD)
37
+ * and no ordering ambiguity. Ties are broken by visiting neighbours in sorted order, which
38
+ * `neighborsWithRel` already guarantees, so results stay byte-identical across runs.
39
+ */
40
+ /**
41
+ * Degree above which a node is a HUB and the walk stops expanding past it.
42
+ *
43
+ * Measured after call edges landed: from every seed tried, distance 6 reached 100% of a 1,159-node
44
+ * graph, so the distance bound contributed nothing and only the token budget was holding the bundle
45
+ * down. The connector is not `calls` — it is SPEC nodes acting as `implements` hubs, since every file
46
+ * anchoring to a spec is two cheap hops from every other file anchoring to it. Weighting call edges
47
+ * alone therefore could not fix it.
48
+ *
49
+ * A hub is still INCLUDED (it is genuinely adjacent); the walk simply does not travel THROUGH it,
50
+ * which is the same rule impact propagation uses and the reason a bound can stay meaningful on a
51
+ * small-world graph.
52
+ */
53
+ export declare const HUB_DEGREE = 24;
54
+ export declare function bfsDistances(graph: RtmGraph, seedId: string, maxDistance: number, hubDegree?: number): Map<string, number>;
@@ -0,0 +1,2 @@
1
+ /** Approximate token count (~4 chars/token). Deterministic heuristic, not a real tokenizer. */
2
+ export declare function estimateTokens(text: string): number;
@@ -0,0 +1,31 @@
1
+ import { ScannedFile } from './cpg-scanner';
2
+ export type MutationOperator = 'relational' | 'arithmetic' | 'logical' | 'unary';
3
+ export interface AstMutant {
4
+ id: string;
5
+ file: string;
6
+ line: number;
7
+ symbolName: string;
8
+ operator: MutationOperator;
9
+ originalToken: string;
10
+ mutatedToken: string;
11
+ status: 'killed' | 'survived' | 'untested';
12
+ }
13
+ export interface MutationResult {
14
+ totalMutants: number;
15
+ killedMutants: number;
16
+ survivedMutants: number;
17
+ mutationScorePct: number;
18
+ mutants: AstMutant[];
19
+ }
20
+ /**
21
+ * Generate AST mutants for static CPG scanned files.
22
+ *
23
+ * @implements A-SPEC-214
24
+ */
25
+ export declare function generateAstMutants(scannedFiles: ScannedFile[] | null | undefined, operators?: MutationOperator[]): AstMutant[];
26
+ /**
27
+ * Evaluate mutation score for a set of AST mutants using a test runner callback.
28
+ *
29
+ * @implements A-SPEC-214
30
+ */
31
+ export declare function evaluateMutationScore(mutants: AstMutant[] | null | undefined, testRunner?: ((mutant: AstMutant) => boolean) | null): MutationResult;
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateAstMutants = generateAstMutants;
4
+ exports.evaluateMutationScore = evaluateMutationScore;
5
+ const MUTATION_MAP = {
6
+ relational: {
7
+ '>': ['<', '>='],
8
+ '<': ['>', '<='],
9
+ '>=': ['<'],
10
+ '<=': ['>'],
11
+ '==': ['!='],
12
+ '===': ['!=='],
13
+ '!=': ['=='],
14
+ '!==': ['==='],
15
+ },
16
+ arithmetic: {
17
+ '+': ['-'],
18
+ '-': ['+'],
19
+ '*': ['/'],
20
+ '/': ['*'],
21
+ },
22
+ logical: {
23
+ '&&': ['||'],
24
+ '||': ['&&'],
25
+ },
26
+ unary: {
27
+ '!': ['EMPTY'],
28
+ },
29
+ };
30
+ /**
31
+ * Generate AST mutants for static CPG scanned files.
32
+ *
33
+ * @implements A-SPEC-214
34
+ */
35
+ function generateAstMutants(scannedFiles, operators) {
36
+ const safeFiles = Array.isArray(scannedFiles) ? scannedFiles : [];
37
+ const targetOps = Array.isArray(operators) && operators.length > 0
38
+ ? operators
39
+ : ['relational', 'arithmetic', 'logical', 'unary'];
40
+ const mutants = [];
41
+ const mutantIdSet = new Set();
42
+ for (const sf of safeFiles) {
43
+ if (!sf)
44
+ continue;
45
+ const filePath = sf.sourcePath || sf.path || 'unknown.ts';
46
+ const symbols = Array.isArray(sf.symbols) ? sf.symbols : [];
47
+ for (const sym of symbols) {
48
+ let mutIndex = 0;
49
+ for (const opType of targetOps) {
50
+ const opRules = MUTATION_MAP[opType];
51
+ if (!opRules)
52
+ continue;
53
+ for (const [origToken, replacements] of Object.entries(opRules)) {
54
+ for (const mutToken of replacements) {
55
+ mutIndex++;
56
+ const line = sym.startLine || 1;
57
+ const mutantId = `${filePath}:${line}:${sym.name}:${mutIndex}:${opType}:${origToken}=>${mutToken}`;
58
+ if (!mutantIdSet.has(mutantId)) {
59
+ mutantIdSet.add(mutantId);
60
+ mutants.push({
61
+ id: mutantId,
62
+ file: filePath,
63
+ line,
64
+ symbolName: sym.name,
65
+ operator: opType,
66
+ originalToken: origToken,
67
+ mutatedToken: mutToken,
68
+ status: 'untested',
69
+ });
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+ return mutants;
77
+ }
78
+ /**
79
+ * Evaluate mutation score for a set of AST mutants using a test runner callback.
80
+ *
81
+ * @implements A-SPEC-214
82
+ */
83
+ function evaluateMutationScore(mutants, testRunner) {
84
+ const safeMutants = Array.isArray(mutants) ? mutants : [];
85
+ if (safeMutants.length === 0) {
86
+ return {
87
+ totalMutants: 0,
88
+ killedMutants: 0,
89
+ survivedMutants: 0,
90
+ mutationScorePct: 100,
91
+ mutants: [],
92
+ };
93
+ }
94
+ let killedMutants = 0;
95
+ let survivedMutants = 0;
96
+ const evaluatedMutants = safeMutants.map((m) => {
97
+ if (typeof testRunner === 'function') {
98
+ let isKilled = false;
99
+ try {
100
+ isKilled = testRunner(m);
101
+ }
102
+ catch {
103
+ isKilled = true;
104
+ }
105
+ if (isKilled) {
106
+ killedMutants++;
107
+ return { ...m, status: 'killed' };
108
+ }
109
+ else {
110
+ survivedMutants++;
111
+ return { ...m, status: 'survived' };
112
+ }
113
+ }
114
+ return { ...m, status: 'untested' };
115
+ });
116
+ const totalMutants = safeMutants.length;
117
+ // Floor rounding so 99.5% never rounds up to false 100%
118
+ const mutationScorePct = totalMutants === 0 ? 100 : (killedMutants === totalMutants ? 100 : Math.floor((killedMutants / totalMutants) * 100));
119
+ return {
120
+ totalMutants,
121
+ killedMutants,
122
+ survivedMutants,
123
+ mutationScorePct,
124
+ mutants: evaluatedMutants,
125
+ };
126
+ }
@@ -0,0 +1,69 @@
1
+ import { LanguageParser, CodeSymbol, CodeEdge, DataFlowFacts } from './language-parser';
2
+ export interface ScannedFile {
3
+ path: string;
4
+ /** Repo-root-relative POSIX path — provenance key used by RtmGraph.removeBySource. */
5
+ sourcePath: string;
6
+ symbols: CodeSymbol[];
7
+ /**
8
+ * Call/import relations recovered from this file, with `from` a qualified name defined HERE and
9
+ * `to` a bare callee name or module path. Resolution to graph node ids happens in rtm-builder,
10
+ * where the whole scan is visible; a single file cannot know where a callee lives.
11
+ *
12
+ * Optional so a cache entry written by an older scanner still loads — such an entry simply carries
13
+ * no edges, which degrades to the previous anchor-only behaviour rather than crashing.
14
+ */
15
+ edges?: CodeEdge[];
16
+ /**
17
+ * Def-use facts, present ONLY when the scanner was constructed with `{ dataflow: true }` AND the
18
+ * file's language has a data-flow walk.
19
+ *
20
+ * @implements A-SPEC-140.1
21
+ * Opt-in by measurement, not taste: the scan already parses each file twice (symbols, edges), and
22
+ * an unconditional third parse would regress the REQ-139 cold-scan baseline by ~50% to produce a
23
+ * signal only the taint pass reads. `undefined` therefore means "not extracted" — for the option
24
+ * being off, for a language with no walk, or for a cache entry from an older writer. It never
25
+ * means "extracted, found nothing"; that is `{defs:[],calls:[],params:[],returns:[]}`.
26
+ */
27
+ dataflow?: DataFlowFacts;
28
+ implementsSpecs: string[];
29
+ /**
30
+ * `@implements` tags shaped like a well-formed governed spec id (REQ/H-SPEC/
31
+ * C-SPEC/T-SPEC-NNN) instead of an A-SPEC anchor — a wrong-kind anchor that
32
+ * would otherwise be silently dropped (audit finding D). Populated
33
+ * alongside `implementsSpecs`, never overlapping with it.
34
+ */
35
+ unanchoredImplements: string[];
36
+ }
37
+ /**
38
+ * Directories that never contain hand-written source for the project under analysis.
39
+ *
40
+ * The original set (`node_modules`, `dist`, `.git`) was JS-shaped, and that silently made the
41
+ * scanner unusable on other ecosystems: measured on a real brownfield Python target, a scan
42
+ * ingested **20,631 files instead of 315** — 98.5% of it third-party code inside `.venv`. That is
43
+ * not merely slow; the RTM graph fills with library symbols, clustering becomes meaningless, and
44
+ * reverse-engineered specs would describe someone else's code.
45
+ *
46
+ * Entries here must be *unambiguous* vendor or cache directories. Names that are conventional but
47
+ * genuinely ambiguous as source roots — `build`, `out`, `bin`, `obj`, `target`, `lib` — are
48
+ * deliberately EXCLUDED: silently dropping real source is the worse failure, and `isVendorDir`
49
+ * below catches the case that actually matters through evidence rather than by name.
50
+ */
51
+ export declare const SKIP: Set<string>;
52
+ /** Single source of truth for which file extensions CpgScanner ingests (REQ-124 gate 2). */
53
+ export declare const SCANNABLE_EXTENSIONS: readonly [".ts", ".mts", ".cts", ".tsx", ".jsx", ".js", ".mjs", ".cjs", ".py", ".cs", ".java", ".go", ".rs", ".cpp", ".cc", ".cxx", ".hpp", ".hh", ".h"];
54
+ export { TEST_FILE_PATTERNS, isTestFile } from './test-files';
55
+ export declare class CpgScanner {
56
+ private readonly parser;
57
+ private readonly cache?;
58
+ private readonly opts;
59
+ constructor(parser?: LanguageParser, cache?: import("./scan-cache").ScanFileCache | undefined, opts?: {
60
+ dataflow?: boolean;
61
+ });
62
+ private skipped;
63
+ /** `{file, reason}` for every file the LAST scan claimed but could not ingest. Empty on a clean tree. */
64
+ scanSkipped(): {
65
+ file: string;
66
+ reason: string;
67
+ }[];
68
+ scan(rootDir: string, repoRoot?: string): ScannedFile[];
69
+ }
@@ -0,0 +1,18 @@
1
+ import { ScannedFile } from './cpg-scanner';
2
+ export interface CoverageTrace {
3
+ file: string;
4
+ linesHit: number[];
5
+ functionsHit: string[];
6
+ }
7
+ export interface HybridCpgResult {
8
+ totalNodes: number;
9
+ dynamicallyVerifiedNodes: number;
10
+ dynamicPrecisionPct: number;
11
+ fusedCpg: ScannedFile[];
12
+ }
13
+ /**
14
+ * Fuse static AST CPG nodes with dynamic execution coverage traces to elevate impact precision to >99%.
15
+ *
16
+ * @implements A-SPEC-216
17
+ */
18
+ export declare function fuseDynamicTrace(scannedFiles: ScannedFile[] | null | undefined, traces: CoverageTrace[] | null | undefined): HybridCpgResult;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fuseDynamicTrace = fuseDynamicTrace;
4
+ /**
5
+ * Fuse static AST CPG nodes with dynamic execution coverage traces to elevate impact precision to >99%.
6
+ *
7
+ * @implements A-SPEC-216
8
+ */
9
+ function fuseDynamicTrace(scannedFiles, traces) {
10
+ const safeFiles = Array.isArray(scannedFiles) ? scannedFiles : [];
11
+ const safeTraces = Array.isArray(traces) ? traces : [];
12
+ let totalNodes = 0;
13
+ let dynamicallyVerifiedNodes = 0;
14
+ // Merge multiple trace entries for the same file (e.g. from different test suites)
15
+ const traceMap = new Map();
16
+ for (const t of safeTraces) {
17
+ if (t && typeof t.file === 'string') {
18
+ const normKey = t.file.split(/[\\/]/).filter(Boolean).join('/');
19
+ let entry = traceMap.get(normKey);
20
+ if (!entry) {
21
+ entry = { linesHit: new Set(), functionsHit: new Set() };
22
+ traceMap.set(normKey, entry);
23
+ }
24
+ if (Array.isArray(t.linesHit))
25
+ for (const l of t.linesHit)
26
+ entry.linesHit.add(l);
27
+ if (Array.isArray(t.functionsHit))
28
+ for (const f of t.functionsHit)
29
+ entry.functionsHit.add(f);
30
+ }
31
+ }
32
+ const fusedCpg = safeFiles.map((sf) => {
33
+ if (!sf)
34
+ return sf;
35
+ const sfSource = (sf.sourcePath || '').split(/[\\/]/).filter(Boolean).join('/');
36
+ const sfFull = (sf.path || '').split(/[\\/]/).filter(Boolean).join('/');
37
+ // Aggregate ALL matching traces for this exact file path
38
+ const matchedLines = new Set();
39
+ const matchedFuncs = new Set();
40
+ let hasTraceMatch = false;
41
+ for (const [tKey, trace] of traceMap.entries()) {
42
+ // Exact path matching or exact tail match from repo root
43
+ const isMatch = (sfSource !== '' && (sfSource === tKey || sfSource.endsWith('/' + tKey) && tKey.includes('/'))) ||
44
+ (sfFull !== '' && (sfFull === tKey || sfFull.endsWith('/' + tKey) && tKey.includes('/')));
45
+ if (isMatch) {
46
+ hasTraceMatch = true;
47
+ for (const l of trace.linesHit)
48
+ matchedLines.add(l);
49
+ for (const f of trace.functionsHit)
50
+ matchedFuncs.add(f);
51
+ }
52
+ }
53
+ const symbols = Array.isArray(sf.symbols) ? sf.symbols : [];
54
+ const updatedSymbols = symbols.map((sym) => {
55
+ totalNodes++;
56
+ if (hasTraceMatch) {
57
+ let lineHit = false;
58
+ // O(1) Set lookup per line in symbol range
59
+ for (let l = sym.startLine; l <= sym.endLine; l++) {
60
+ if (matchedLines.has(l)) {
61
+ lineHit = true;
62
+ break;
63
+ }
64
+ }
65
+ const funcHit = matchedFuncs.has(sym.name) || matchedFuncs.has(sym.qualifiedName);
66
+ if (lineHit || funcHit) {
67
+ dynamicallyVerifiedNodes++;
68
+ return { ...sym, dynamicVerified: true };
69
+ }
70
+ }
71
+ return sym;
72
+ });
73
+ return { ...sf, symbols: updatedSymbols };
74
+ });
75
+ const dynamicPrecisionPct = totalNodes > 0 ? Math.round((dynamicallyVerifiedNodes / totalNodes) * 100) : 100;
76
+ return {
77
+ totalNodes,
78
+ dynamicallyVerifiedNodes,
79
+ dynamicPrecisionPct,
80
+ fusedCpg,
81
+ };
82
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Content-hash skip cache used by the reindex path to avoid re-scanning
3
+ * files whose content hasn't changed since the last recorded hash. Keyed by
4
+ * repo-relative path, persisted as a single JSON file under `dir` (callers
5
+ * pass `.ax/cpg_cache`, which is gitignored — cache files are derived, never
6
+ * committed).
7
+ *
8
+ * Deterministic: SHA-256 of the exact content string, no timestamps or other
9
+ * nondeterministic inputs recorded.
10
+ */
11
+ export declare class HashCache {
12
+ private readonly dir;
13
+ private readonly file;
14
+ private hashes;
15
+ constructor(dir: string);
16
+ private load;
17
+ private static hashOf;
18
+ /** True iff a hash was previously stored for relPath and it matches content's hash. */
19
+ unchanged(relPath: string, content: string): boolean;
20
+ put(relPath: string, content: string): void;
21
+ }
@@ -0,0 +1,31 @@
1
+ export namespace LANG_CONFIG {
2
+ import tsx = LANG_CONFIG.typescript;
3
+ export { tsx };
4
+ }
5
+ export const DATAFLOW_LANGS: Set<string>;
6
+ export function extractSymbolsFromTree(tree: any, lang?: string): any[];
7
+ export function extractEdgesFromTree(tree: any, lang?: string): any[];
8
+ export function extractDataFlowFromTree(tree: any, lang?: string): {
9
+ defs: any[];
10
+ calls: any[];
11
+ params: any[];
12
+ returns: any[];
13
+ } | undefined;
14
+ export function walkGoSymbols(tree: any): any[];
15
+ export function walkRustSymbols(tree: any): any[];
16
+ /**
17
+ * Python call/import edges.
18
+ *
19
+ * Mirrors the TS walk's contract exactly: `from` is the enclosing definition's QUALIFIED name (so it
20
+ * matches what extractSymbolsFromTree emitted for the same node and can be resolved to a graph node
21
+ * id), `to` is the bare callee name for calls and the module path for imports.
22
+ *
23
+ * PRECISION OVER RECALL, per the target architecture's D-CPG principle. Only forms whose target is
24
+ * syntactically evident are emitted: a direct `f()` or `obj.m()` call, and an `import x` /
25
+ * `from x import y`. Dynamic dispatch (`getattr(o, name)()`), `importlib.import_module(var)` and
26
+ * string-keyed registries produce NOTHING rather than a guessed edge — a wrong edge is worse than a
27
+ * missing one, because the missing one shows up as an unanchored gap while the wrong one silently
28
+ * corrupts every impact answer that traverses it.
29
+ */
30
+ export function walkPythonEdges(tree: any): any[];
31
+ export function parseWithBuffer(parser: any, code: any): any;
@@ -0,0 +1 @@
1
+ export {};