@holmes-lab/holmes-kit 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/assoc/assoc-arm.d.ts +94 -0
- package/dist/holmes/assoc/assoc-arm.js +187 -0
- package/dist/holmes/assoc/explore.d.ts +21 -0
- package/dist/holmes/assoc/explore.js +160 -0
- package/dist/holmes/assoc/impact-baseline.d.ts +13 -0
- package/dist/holmes/assoc/impact-baseline.js +48 -0
- package/dist/holmes/assoc/ppr.d.ts +64 -0
- package/dist/holmes/assoc/ppr.js +110 -0
- package/dist/holmes/cli/agents.d.ts +24 -11
- package/dist/holmes/cli/agents.js +93 -21
- package/dist/holmes/cli/doctor.d.ts +56 -0
- package/dist/holmes/cli/doctor.js +311 -5
- package/dist/holmes/cli/index.js +13 -1
- package/dist/holmes/cli/semantic-key.d.ts +19 -0
- package/dist/holmes/cli/semantic-key.js +93 -0
- package/dist/holmes/config/config.d.ts +9 -0
- package/dist/holmes/config/config.js +8 -1
- package/dist/holmes/cpg/language-capability.d.ts +65 -0
- package/dist/holmes/cpg/language-capability.js +145 -0
- package/dist/holmes/cpg/language-parser-walk.js +179 -34
- package/dist/holmes/cpg/language-parser.d.ts +1 -1
- package/dist/holmes/governance/ledger-rechain.d.ts +12 -0
- package/dist/holmes/governance/ledger-rechain.js +17 -2
- package/dist/holmes/governance/provenance-ledger.js +21 -0
- package/dist/holmes/guardrail/blind-spots.js +12 -1
- package/dist/holmes/guardrail/impact-gate.d.ts +77 -0
- package/dist/holmes/guardrail/impact-gate.js +263 -0
- package/dist/holmes/guardrail/write-target.d.ts +38 -1
- package/dist/holmes/guardrail/write-target.js +41 -4
- package/dist/holmes/hooks/adapters/antigravity.js +12 -1
- package/dist/holmes/hooks/corrupt-state-run.d.ts +33 -0
- package/dist/holmes/hooks/corrupt-state-run.js +16 -0
- package/dist/holmes/hooks/pre-tool-use.d.ts +26 -0
- package/dist/holmes/hooks/pre-tool-use.js +203 -11
- package/dist/holmes/hooks/stop.d.ts +65 -0
- package/dist/holmes/hooks/stop.js +200 -2
- package/dist/holmes/mcp/elicit-approval.d.ts +32 -2
- package/dist/holmes/mcp/elicit-approval.js +30 -2
- package/dist/holmes/mcp/handlers.d.ts +88 -0
- package/dist/holmes/mcp/handlers.js +636 -35
- package/dist/holmes/mcp/maintenance-analyze.d.ts +435 -0
- package/dist/holmes/mcp/maintenance-analyze.js +994 -0
- package/dist/holmes/mcp/maintenance-evidence.d.ts +140 -0
- package/dist/holmes/mcp/maintenance-evidence.js +253 -0
- package/dist/holmes/mcp/server.js +13 -10
- package/dist/holmes/mcp/tool-schemas.js +71 -0
- package/dist/holmes/project/root.js +3 -1
- package/dist/holmes/review/baseline-arm.d.ts +37 -0
- package/dist/holmes/review/baseline-arm.js +51 -0
- package/dist/holmes/review/captured-stdin-guard.d.ts +8 -0
- package/dist/holmes/review/captured-stdin-guard.js +48 -0
- package/dist/holmes/review/coherence-verify.d.ts +31 -0
- package/dist/holmes/review/coherence-verify.js +144 -0
- package/dist/holmes/review/commit-text.d.ts +50 -0
- package/dist/holmes/review/commit-text.js +76 -0
- package/dist/holmes/review/confidence-calibration.d.ts +39 -0
- package/dist/holmes/review/confidence-calibration.js +39 -0
- package/dist/holmes/review/content-baseline.d.ts +38 -0
- package/dist/holmes/review/content-baseline.js +103 -0
- package/dist/holmes/review/content-verify.d.ts +20 -0
- package/dist/holmes/review/content-verify.js +73 -0
- package/dist/holmes/review/dense-retrieval.d.ts +66 -0
- package/dist/holmes/review/dense-retrieval.js +97 -0
- package/dist/holmes/review/edge-quality.d.ts +44 -0
- package/dist/holmes/review/edge-quality.js +117 -0
- package/dist/holmes/review/evaluation-metrics.d.ts +138 -0
- package/dist/holmes/review/evaluation-metrics.js +175 -0
- package/dist/holmes/review/graph-verifier.d.ts +34 -0
- package/dist/holmes/review/graph-verifier.js +62 -0
- package/dist/holmes/review/hop-ablation.d.ts +100 -0
- package/dist/holmes/review/hop-ablation.js +89 -0
- package/dist/holmes/review/manual-baseline.d.ts +209 -0
- package/dist/holmes/review/manual-baseline.js +2846 -0
- package/dist/holmes/review/oracle-gap.d.ts +32 -0
- package/dist/holmes/review/oracle-gap.js +102 -0
- package/dist/holmes/review/point-in-time-replay.d.ts +41 -0
- package/dist/holmes/review/point-in-time-replay.js +161 -0
- package/dist/holmes/review/rank-diagnosis.d.ts +43 -0
- package/dist/holmes/review/rank-diagnosis.js +163 -0
- package/dist/holmes/review/replay-calibration.d.ts +62 -0
- package/dist/holmes/review/replay-calibration.js +83 -0
- package/dist/holmes/review/replay-corpus.d.ts +135 -0
- package/dist/holmes/review/replay-corpus.js +210 -0
- package/dist/holmes/review/run-replay.d.ts +260 -0
- package/dist/holmes/review/run-replay.js +729 -0
- package/dist/holmes/review/semantic-arm.d.ts +271 -0
- package/dist/holmes/review/semantic-arm.js +717 -0
- package/dist/holmes/review/semantic-retrieval.d.ts +55 -0
- package/dist/holmes/review/semantic-retrieval.js +156 -0
- package/dist/holmes/review/spec-layer-stats.d.ts +38 -0
- package/dist/holmes/review/spec-layer-stats.js +52 -0
- package/dist/holmes/review/temporal-prior.d.ts +33 -0
- package/dist/holmes/review/temporal-prior.js +53 -0
- package/dist/holmes/review/test-runner.d.ts +15 -0
- package/dist/holmes/review/test-runner.js +41 -4
- package/dist/holmes/review/test-selection-breadth.d.ts +75 -0
- package/dist/holmes/review/test-selection-breadth.js +57 -0
- package/dist/holmes/review/traceability-benchmark.d.ts +81 -0
- package/dist/holmes/review/traceability-benchmark.js +135 -0
- package/dist/holmes/review/union-verify.d.ts +12 -0
- package/dist/holmes/review/union-verify.js +70 -0
- package/dist/holmes/rtm/graph-store.d.ts +51 -0
- package/dist/holmes/rtm/graph-store.js +122 -0
- package/dist/holmes/rtm/incremental.d.ts +25 -1
- package/dist/holmes/rtm/incremental.js +18 -1
- package/dist/holmes/rtm/localize.d.ts +28 -0
- package/dist/holmes/rtm/localize.js +272 -14
- package/dist/holmes/rtm/rtm-builder.d.ts +118 -3
- package/dist/holmes/rtm/rtm-builder.js +265 -28
- package/dist/holmes/rtm/rtm-graph.d.ts +117 -2
- package/dist/holmes/rtm/rtm-graph.js +194 -31
- package/dist/holmes/rtm/taint-benchmark.d.ts +97 -0
- package/dist/holmes/rtm/taint-benchmark.js +141 -0
- package/dist/holmes/rtm/test-scope.js +24 -1
- package/dist/holmes/semantic/credentials.d.ts +15 -0
- package/dist/holmes/semantic/credentials.js +134 -0
- package/dist/holmes/semantic/embedder.d.ts +44 -0
- package/dist/holmes/semantic/embedder.js +185 -0
- package/dist/holmes/semantic/hit-rerank.d.ts +4 -0
- package/dist/holmes/semantic/hit-rerank.js +38 -0
- package/dist/holmes/semantic/tier.d.ts +37 -0
- package/dist/holmes/semantic/tier.js +54 -0
- package/dist/holmes/semantic/vector-cache.d.ts +11 -0
- package/dist/holmes/semantic/vector-cache.js +91 -0
- package/dist/holmes/spec/acceptance-quality.d.ts +81 -0
- package/dist/holmes/spec/acceptance-quality.js +169 -0
- package/dist/holmes/spec/validator.js +33 -1
- package/dist/holmes/spec/yaml-scalar.d.ts +1 -0
- package/dist/holmes/spec/yaml-scalar.js +43 -0
- package/package.json +1 -1
|
@@ -1,4 +1,37 @@
|
|
|
1
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
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.DEFAULT_IMPACT = void 0;
|
|
4
37
|
exports.buildFileSubgraph = buildFileSubgraph;
|
|
@@ -6,6 +39,28 @@ exports.buildRtm = buildRtm;
|
|
|
6
39
|
exports.addCallEdges = addCallEdges;
|
|
7
40
|
exports.impactedBy = impactedBy;
|
|
8
41
|
exports.explainImpact = explainImpact;
|
|
42
|
+
exports.addCommitHistory = addCommitHistory;
|
|
43
|
+
exports.addImportEdges = addImportEdges;
|
|
44
|
+
exports.addDecisionEdges = addDecisionEdges;
|
|
45
|
+
const path = __importStar(require("node:path"));
|
|
46
|
+
const language_capability_1 = require("../cpg/language-capability");
|
|
47
|
+
/**
|
|
48
|
+
* @implements A-SPEC-281
|
|
49
|
+
* Build one fact's provenance from the shared observation context plus what this call site knows.
|
|
50
|
+
* `confidence: null` for an inferred fact is deliberate — see `Provenance` in rtm-graph.ts.
|
|
51
|
+
*/
|
|
52
|
+
function fact(opts, sourceLocation, derivation, confidence, evidenceDigest = null) {
|
|
53
|
+
return {
|
|
54
|
+
sourceCommit: opts?.provenance?.sourceCommit ?? null,
|
|
55
|
+
extractorVersion: opts?.provenance?.extractorVersion ?? null,
|
|
56
|
+
sourceLocation,
|
|
57
|
+
confidence,
|
|
58
|
+
evidenceDigest,
|
|
59
|
+
observedAt: opts?.provenance?.observedAt ?? null,
|
|
60
|
+
validUntil: opts?.provenance?.validUntil ?? null,
|
|
61
|
+
derivation,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
9
64
|
/**
|
|
10
65
|
* Adds one scanned file's CODE nodes and `implements` edges to the graph,
|
|
11
66
|
* tagged with that file's sourcePath so RtmGraph.removeBySource(f.sourcePath)
|
|
@@ -13,16 +68,18 @@ exports.explainImpact = explainImpact;
|
|
|
13
68
|
* orchestrator — see Task 4).
|
|
14
69
|
*/
|
|
15
70
|
// @implements A-SPEC-121.2
|
|
16
|
-
function buildFileSubgraph(f, specs, graph) {
|
|
71
|
+
function buildFileSubgraph(f, specs, graph, opts) {
|
|
17
72
|
for (const sym of f.symbols) {
|
|
18
73
|
// A-SPEC-121.3: fold source_path into the node id itself so same-named
|
|
19
74
|
// symbols in different files get distinct CODE nodes instead of
|
|
20
75
|
// colliding on a bare `CODE:<qualifiedName>` id (see RtmGraph's class
|
|
21
76
|
// comment). Resolve a bare qn back to these ids via `graph.codeNodeIds`.
|
|
22
77
|
const codeId = `CODE:${sym.qualifiedName}@${f.sourcePath}`;
|
|
23
|
-
|
|
78
|
+
const digest = opts?.fileDigest?.(f.sourcePath) ?? null;
|
|
79
|
+
graph.addNode(codeId, 'CODE', f.sourcePath, fact(opts, `${f.sourcePath}:${sym.startLine}`, 'ast-scan', 1, digest));
|
|
24
80
|
for (const aspec of f.implementsSpecs) {
|
|
25
|
-
|
|
81
|
+
// DECLARED by an `@implements` anchor: the fact is stated in the file, not inferred.
|
|
82
|
+
graph.addEdge(codeId, `SPEC:${aspec}`, 'implements', f.sourcePath, fact(opts, `${f.sourcePath}:${sym.startLine}`, 'anchor', 1, digest));
|
|
26
83
|
}
|
|
27
84
|
}
|
|
28
85
|
}
|
|
@@ -31,22 +88,28 @@ function buildRtm(specs, scanned, graph, opts) {
|
|
|
31
88
|
// ONE transaction for the whole build: measured, per-insert commits were ~20% of the insert cost
|
|
32
89
|
// on a 56k-insert graph (216 ms -> 173 ms with a transaction; 88 -> 52 ms with cached statements).
|
|
33
90
|
// It also makes a failed build atomic — no half-graph that could be mistaken for a complete one.
|
|
91
|
+
let resolution = {
|
|
92
|
+
resolved: 0, ambiguous: 0, unknownTarget: 0, callerNotNamed: 0, selfReference: 0, moduleScoped: 0,
|
|
93
|
+
};
|
|
34
94
|
graph.transaction(() => {
|
|
35
95
|
// Add spec nodes and dependencies
|
|
36
96
|
for (const s of specs) {
|
|
37
97
|
const sourcePath = opts?.specSourcePath?.(s.id);
|
|
38
|
-
graph.addNode(`SPEC:${s.id}`, s.type, sourcePath);
|
|
98
|
+
graph.addNode(`SPEC:${s.id}`, s.type, sourcePath, fact(opts, sourcePath ?? null, 'spec-store', 1));
|
|
99
|
+
// DECLARED in frontmatter.
|
|
39
100
|
for (const p of s.dependsOn) {
|
|
40
|
-
graph.addEdge(`SPEC:${s.id}`, `SPEC:${p}`, 'depends_on', sourcePath);
|
|
101
|
+
graph.addEdge(`SPEC:${s.id}`, `SPEC:${p}`, 'depends_on', sourcePath, fact(opts, sourcePath ?? null, 'frontmatter', 1));
|
|
41
102
|
}
|
|
42
103
|
}
|
|
43
104
|
// Add code nodes and implements edges, one file at a time
|
|
44
105
|
for (const f of scanned) {
|
|
45
|
-
buildFileSubgraph(f, specs, graph);
|
|
106
|
+
buildFileSubgraph(f, specs, graph, opts);
|
|
46
107
|
}
|
|
47
108
|
// Call edges last: resolving a callee needs every file's symbols to exist as nodes first.
|
|
48
|
-
addCallEdges(scanned, graph);
|
|
109
|
+
resolution = addCallEdges(scanned, graph, opts);
|
|
110
|
+
addImportEdges(scanned, graph, opts);
|
|
49
111
|
});
|
|
112
|
+
return { resolution };
|
|
50
113
|
}
|
|
51
114
|
/**
|
|
52
115
|
* Resolve each file's recovered call relations to graph edges.
|
|
@@ -68,56 +131,98 @@ function buildRtm(specs, scanned, graph, opts) {
|
|
|
68
131
|
* exact (src,dst,rel) triple — so the first-writer-wins hazard documented on RtmGraph cannot arise
|
|
69
132
|
* for call edges, and `removeBySource(caller)` drops exactly them.
|
|
70
133
|
*/
|
|
71
|
-
function addCallEdges(scanned, graph) {
|
|
72
|
-
|
|
134
|
+
function addCallEdges(scanned, graph, opts) {
|
|
135
|
+
const report = {
|
|
136
|
+
resolved: 0, ambiguous: 0, unknownTarget: 0, callerNotNamed: 0, selfReference: 0, moduleScoped: 0,
|
|
137
|
+
};
|
|
138
|
+
// @implements A-SPEC-300
|
|
139
|
+
// Indexes are built PER LANGUAGE FAMILY. Resolution used to look across the whole scan, so a Java
|
|
140
|
+
// call could bind to a Rust definition — measured, and it did. Ambiguity is judged inside the
|
|
141
|
+
// family too, which is why two same-named functions in Python and Go now each resolve correctly
|
|
142
|
+
// instead of cancelling each other out: this filter raises precision AND recall here.
|
|
143
|
+
const familyOf = (sourcePath) => (0, language_capability_1.languageFamilyOf)(path.extname(sourcePath));
|
|
73
144
|
const definedIn = new Map();
|
|
74
145
|
for (const f of scanned) {
|
|
146
|
+
const family = familyOf(f.sourcePath);
|
|
147
|
+
let table = definedIn.get(family);
|
|
148
|
+
if (!table) {
|
|
149
|
+
table = new Map();
|
|
150
|
+
definedIn.set(family, table);
|
|
151
|
+
}
|
|
75
152
|
for (const sym of f.symbols) {
|
|
76
|
-
const list =
|
|
153
|
+
const list = table.get(sym.qualifiedName);
|
|
77
154
|
if (list) {
|
|
78
155
|
if (!list.includes(f.sourcePath))
|
|
79
156
|
list.push(f.sourcePath);
|
|
80
157
|
}
|
|
81
158
|
else
|
|
82
|
-
|
|
159
|
+
table.set(sym.qualifiedName, [f.sourcePath]);
|
|
83
160
|
}
|
|
84
161
|
}
|
|
85
162
|
// A bare callee `m` may correspond to a qualified `Class.m`. Index the last segment too, but only
|
|
86
163
|
// to a UNIQUE owner — a method name shared by several classes stays unresolvable on purpose.
|
|
87
164
|
const byLastSegment = new Map();
|
|
88
|
-
for (const [
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
165
|
+
for (const [family, table] of definedIn) {
|
|
166
|
+
const index = new Map();
|
|
167
|
+
byLastSegment.set(family, index);
|
|
168
|
+
for (const [qn, paths] of table) {
|
|
169
|
+
const last = qn.includes('.') ? qn.slice(qn.lastIndexOf('.') + 1) : qn;
|
|
170
|
+
if (index.has(last))
|
|
171
|
+
index.set(last, null); // ambiguous — never resolved
|
|
172
|
+
else
|
|
173
|
+
index.set(last, { qn, paths });
|
|
174
|
+
}
|
|
94
175
|
}
|
|
95
176
|
for (const f of scanned) {
|
|
96
177
|
for (const e of f.edges ?? []) {
|
|
97
|
-
|
|
178
|
+
// @implements A-SPEC-286 — `inherits` resolves by the SAME rule as `calls`: symbol-to-symbol,
|
|
179
|
+
// and only when the name is defined in exactly one file across the scan. `imports` is excluded
|
|
180
|
+
// because its target is a module path, not a symbol; resolving that needs module->file
|
|
181
|
+
// resolution this scanner does not do, and the capability table reports the gap rather than
|
|
182
|
+
// letting it pass as coverage.
|
|
183
|
+
if (e.rel !== 'calls' && e.rel !== 'inherits')
|
|
98
184
|
continue;
|
|
185
|
+
if (!e.from || !e.to || e.from === '<module>') {
|
|
186
|
+
report.moduleScoped++;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
99
189
|
const fromId = `CODE:${e.from}@${f.sourcePath}`;
|
|
100
190
|
// The caller must be a real node in THIS file; otherwise the walk produced a name the symbol
|
|
101
191
|
// extractor never emitted and the edge cannot be trusted.
|
|
102
|
-
if (!graph.codeNodeIds(e.from).includes(fromId))
|
|
192
|
+
if (!graph.codeNodeIds(e.from).includes(fromId)) {
|
|
193
|
+
report.callerNotNamed++;
|
|
103
194
|
continue;
|
|
195
|
+
}
|
|
196
|
+
const family = familyOf(f.sourcePath);
|
|
197
|
+
const table = definedIn.get(family);
|
|
104
198
|
let target = null;
|
|
105
|
-
if (
|
|
106
|
-
target = { qn: e.to, paths:
|
|
199
|
+
if (table?.has(e.to))
|
|
200
|
+
target = { qn: e.to, paths: table.get(e.to) };
|
|
107
201
|
else
|
|
108
|
-
target = byLastSegment.get(e.to) ?? null;
|
|
109
|
-
if (!target)
|
|
202
|
+
target = byLastSegment.get(family)?.get(e.to) ?? null;
|
|
203
|
+
if (!target) {
|
|
204
|
+
report.unknownTarget++;
|
|
110
205
|
continue;
|
|
206
|
+
}
|
|
111
207
|
const sameFile = target.paths.includes(f.sourcePath);
|
|
112
208
|
const path = sameFile ? f.sourcePath : (target.paths.length === 1 ? target.paths[0] : null);
|
|
113
|
-
if (!path)
|
|
114
|
-
|
|
209
|
+
if (!path) {
|
|
210
|
+
report.ambiguous++;
|
|
211
|
+
continue;
|
|
212
|
+
} // defined in several files — emit nothing
|
|
115
213
|
const toId = `CODE:${target.qn}@${path}`;
|
|
116
|
-
if (toId === fromId)
|
|
117
|
-
|
|
118
|
-
|
|
214
|
+
if (toId === fromId) {
|
|
215
|
+
report.selfReference++;
|
|
216
|
+
continue;
|
|
217
|
+
} // self-recursion carries no impact
|
|
218
|
+
// INFERRED by name resolution. Its precision has never been measured in this repository, so
|
|
219
|
+
// the confidence stays null rather than carrying a number nobody can defend — an invented
|
|
220
|
+
// figure would be indistinguishable from a measured one to every downstream consumer.
|
|
221
|
+
graph.addEdge(fromId, toId, e.rel, f.sourcePath, fact(opts, `${f.sourcePath}`, 'name-resolution', null));
|
|
222
|
+
report.resolved++;
|
|
119
223
|
}
|
|
120
224
|
}
|
|
225
|
+
return report;
|
|
121
226
|
}
|
|
122
227
|
exports.DEFAULT_IMPACT = { maxCallDepth: 3, hubInDegree: 12 };
|
|
123
228
|
/**
|
|
@@ -189,3 +294,135 @@ function explainImpact(graph, changedSymbolQNames, opts) {
|
|
|
189
294
|
// `rtm_impact` emits this directly, and determinism is the harness's thesis.
|
|
190
295
|
return { specs: [...set].sort(), reachedByDepth, stoppedAt, ...(seedIsHub ? { seedIsHub } : {}) };
|
|
191
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* @implements A-SPEC-284
|
|
299
|
+
* The graph's first temporal layer: `COMMIT:<sha>` nodes and `changed_by` edges to the code they
|
|
300
|
+
* touched.
|
|
301
|
+
*
|
|
302
|
+
* Kept as a SEPARATE ingestion rather than folded into the per-file subgraph. Commit history is not
|
|
303
|
+
* a product of scanning a file, and entangling it with the incremental path would put the
|
|
304
|
+
* incremental/full-rebuild convergence A-SPEC-280 proved at risk for no benefit — the code layer
|
|
305
|
+
* must come out identical whether or not history was ever ingested.
|
|
306
|
+
*
|
|
307
|
+
* An edge is emitted ONLY when the changed file is present in the scan. Precision over recall, the
|
|
308
|
+
* same rule call resolution follows: an edge pointing at a node that does not exist poisons every
|
|
309
|
+
* traversal that walks it, while a missing edge merely leaves a gap. Commits touching files nobody
|
|
310
|
+
* scanned are still recorded as nodes — they happened.
|
|
311
|
+
*/
|
|
312
|
+
function addCommitHistory(history, scanned, graph, opts) {
|
|
313
|
+
const codeBySource = new Map();
|
|
314
|
+
for (const f of scanned) {
|
|
315
|
+
codeBySource.set(f.sourcePath, f.symbols.map((s) => `CODE:${s.qualifiedName}@${f.sourcePath}`));
|
|
316
|
+
}
|
|
317
|
+
graph.transaction(() => {
|
|
318
|
+
for (const record of history) {
|
|
319
|
+
const commitId = `COMMIT:${record.commit}`;
|
|
320
|
+
// Git STATES which files a commit touched — a declared fact, not an inference, so confidence 1.
|
|
321
|
+
graph.addNode(commitId, 'COMMIT', undefined, fact(opts, record.commit, 'git-log', 1));
|
|
322
|
+
for (const file of record.files) {
|
|
323
|
+
for (const codeId of codeBySource.get(file) ?? []) {
|
|
324
|
+
graph.addEdge(commitId, codeId, 'changed_by', file, fact(opts, `${file}@${record.commit}`, 'git-log', 1));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* @implements A-SPEC-289
|
|
332
|
+
* FILE nodes and resolved `imports` edges.
|
|
333
|
+
*
|
|
334
|
+
* Import relations were extracted and then dropped, because their endpoints are a file and a module
|
|
335
|
+
* path while the graph knew only symbols and specs — measured on this repository, 1,892 extracted
|
|
336
|
+
* and 0 in the graph. `File` is a node kind Goal Phase 3 names, so it becomes one.
|
|
337
|
+
*
|
|
338
|
+
* ONLY relative specifiers are resolved, and only to a file the scan actually contains. A bare
|
|
339
|
+
* specifier (`node:fs`, `js-yaml`) is an external package with no node to point at, and inventing
|
|
340
|
+
* one would be a guess — the same precision-over-recall rule call resolution follows. Measured: 441
|
|
341
|
+
* of 1,892 specifiers (23%) resolve inside the repository, and the rest failing to resolve is a
|
|
342
|
+
* fact about the imports, not a defect in the resolver.
|
|
343
|
+
*/
|
|
344
|
+
function addImportEdges(scanned, graph, opts) {
|
|
345
|
+
const known = new Set(scanned.map((f) => f.sourcePath));
|
|
346
|
+
const EXTENSIONS = ['', '.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs', '.jsx', '.py',
|
|
347
|
+
'/index.ts', '/index.js', '/__init__.py'];
|
|
348
|
+
const firstKnown = (base) => EXTENSIONS.map((ext) => `${base}${ext}`).find((candidate) => known.has(candidate)) ?? null;
|
|
349
|
+
const resolve = (fromFile, spec) => {
|
|
350
|
+
// @implements A-SPEC-406
|
|
351
|
+
// A specifier without a leading dot used to be refused outright, on the ground that it names an
|
|
352
|
+
// external package. That holds for `node:fs` and `js-yaml`; it does NOT hold for Python, which
|
|
353
|
+
// writes intra-repository modules absolutely — `from src.core.memory_audit import X`. Measured
|
|
354
|
+
// on the jarvis corpus: 1060 specifiers, 0 relative, 542 naming a file the scan already had, and
|
|
355
|
+
// 0 import edges in the graph. The whole import layer was missing for that language because the
|
|
356
|
+
// resolver's shape was JS's.
|
|
357
|
+
//
|
|
358
|
+
// The rule the original was written under is unchanged and is what makes this safe: resolve
|
|
359
|
+
// ONLY to a file the scan actually contains. `os.path` becomes `os/path.py`, which is in no
|
|
360
|
+
// scan, so it still emits nothing. Nothing is invented; the dotted form is simply also read.
|
|
361
|
+
if (!spec.startsWith('.')) {
|
|
362
|
+
if (spec.includes(':'))
|
|
363
|
+
return null; // `node:fs` and friends are never a repository path
|
|
364
|
+
return firstKnown(path.posix.normalize(spec.split('.').filter(Boolean).join('/')));
|
|
365
|
+
}
|
|
366
|
+
// @implements A-SPEC-289
|
|
367
|
+
// Python writes a relative import as `.b` / `..pkg.mod`, where the leading dots are LEVELS and
|
|
368
|
+
// the remaining dots are path separators — not the `./b` form JS uses. Treating `.b` as a path
|
|
369
|
+
// yields the literal name `.b`, which matches nothing. Detected by the absence of a slash: a
|
|
370
|
+
// specifier that already contains one is a JS-style path and is left alone.
|
|
371
|
+
let relative = spec;
|
|
372
|
+
if (!spec.includes('/')) {
|
|
373
|
+
const dots = spec.match(/^\.+/)[0].length;
|
|
374
|
+
const rest = spec.slice(dots).split('.').filter(Boolean).join('/');
|
|
375
|
+
relative = `${'../'.repeat(Math.max(0, dots - 1)) || './'}${rest}`;
|
|
376
|
+
}
|
|
377
|
+
return firstKnown(path.posix.normalize(path.posix.join(path.posix.dirname(fromFile), relative)));
|
|
378
|
+
};
|
|
379
|
+
graph.transaction(() => {
|
|
380
|
+
for (const f of scanned) {
|
|
381
|
+
for (const e of f.edges ?? []) {
|
|
382
|
+
if (e.rel !== 'imports')
|
|
383
|
+
continue;
|
|
384
|
+
const target = resolve(f.sourcePath, e.to);
|
|
385
|
+
if (!target)
|
|
386
|
+
continue;
|
|
387
|
+
// DECLARED: the specifier is written in the source. What is inferred is only WHICH file it
|
|
388
|
+
// names, and an unresolvable one emits nothing rather than a guess.
|
|
389
|
+
graph.addNode(`FILE:${f.sourcePath}`, 'FILE', f.sourcePath, fact(opts, f.sourcePath, 'ast-scan', 1));
|
|
390
|
+
graph.addNode(`FILE:${target}`, 'FILE', target, fact(opts, target, 'ast-scan', 1));
|
|
391
|
+
graph.addEdge(`FILE:${f.sourcePath}`, `FILE:${target}`, 'imports', f.sourcePath, fact(opts, `${f.sourcePath} -> ${e.to}`, 'import-resolution', 1));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* @implements A-SPEC-293
|
|
398
|
+
* DECISION nodes and the `constrained_by` / `supersedes` edges around them.
|
|
399
|
+
*
|
|
400
|
+
* Citation is the link, chosen by measurement rather than by taste: on this repository `governs`
|
|
401
|
+
* resolves only 9 of its 65 names to symbols (the rest are free-form design concepts), while 37
|
|
402
|
+
* source files and 71 spec files cite an ADR outright — and a citation sits exactly where the
|
|
403
|
+
* decision actually bears on the work.
|
|
404
|
+
*
|
|
405
|
+
* A citation naming a decision that does not exist creates neither node nor edge: an edge to a node
|
|
406
|
+
* nobody can open is a guess, the same rule import and call resolution follow. `supersedes` is
|
|
407
|
+
* declared in frontmatter and linked only when both decisions exist; this repository currently has
|
|
408
|
+
* none, and the edge kind is supported anyway rather than waiting for the first one.
|
|
409
|
+
*/
|
|
410
|
+
function addDecisionEdges(decisions, citations, graph, opts) {
|
|
411
|
+
const known = new Map(decisions.map((d) => [d.id, d]));
|
|
412
|
+
graph.transaction(() => {
|
|
413
|
+
for (const d of decisions) {
|
|
414
|
+
graph.addNode(`DECISION:${d.id}`, 'DECISION', undefined, fact(opts, d.id, 'decision-record', 1));
|
|
415
|
+
}
|
|
416
|
+
for (const d of decisions) {
|
|
417
|
+
if (!d.supersedes || !known.has(d.supersedes))
|
|
418
|
+
continue;
|
|
419
|
+
graph.addEdge(`DECISION:${d.id}`, `DECISION:${d.supersedes}`, 'supersedes', undefined, fact(opts, d.id, 'decision-record', 1));
|
|
420
|
+
}
|
|
421
|
+
for (const c of citations) {
|
|
422
|
+
if (!known.has(c.adr))
|
|
423
|
+
continue;
|
|
424
|
+
// DECLARED: the citation is written in the file or spec that carries it.
|
|
425
|
+
graph.addEdge(c.from, `DECISION:${c.adr}`, 'constrained_by', undefined, fact(opts, c.from, 'citation', 1));
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
}
|
|
@@ -22,13 +22,41 @@
|
|
|
22
22
|
* The result is identical to the full build, so the incremental pass is currently redundant work
|
|
23
23
|
* rather than a saving. It becomes meaningful the moment a persisted graph exists.
|
|
24
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* @implements A-SPEC-281
|
|
27
|
+
* What is known about HOW a fact got into the graph.
|
|
28
|
+
*
|
|
29
|
+
* `confidence: null` is load-bearing: it means the derivation gave us no measured basis for a
|
|
30
|
+
* number, and it must stay distinguishable from `1`. A `calls` edge is INFERRED by name resolution
|
|
31
|
+
* whose precision this repository has never measured, so it records the derivation and leaves the
|
|
32
|
+
* number empty rather than inventing one — the same rule the ablation follows for an unmeasured
|
|
33
|
+
* recall. An `implements` or `depends_on` edge is DECLARED (an anchor, a frontmatter field), so it
|
|
34
|
+
* carries 1: the fact is stated, not guessed.
|
|
35
|
+
*
|
|
36
|
+
* `observedAt` is supplied by the CALLER. Reading a clock in here would make the same inputs build a
|
|
37
|
+
* different graph on every run and break the incremental/full-rebuild convergence A-SPEC-280 proved.
|
|
38
|
+
*/
|
|
39
|
+
export interface Provenance {
|
|
40
|
+
sourceCommit: string | null;
|
|
41
|
+
extractorVersion: string | null;
|
|
42
|
+
sourceLocation: string | null;
|
|
43
|
+
confidence: number | null;
|
|
44
|
+
evidenceDigest: string | null;
|
|
45
|
+
observedAt: string | null;
|
|
46
|
+
validUntil: string | null;
|
|
47
|
+
derivation: string | null;
|
|
48
|
+
}
|
|
25
49
|
export declare class RtmGraph {
|
|
26
50
|
private readonly db;
|
|
27
51
|
constructor(dbPath?: string);
|
|
28
52
|
private nodeStmt?;
|
|
29
53
|
private edgeStmt?;
|
|
30
|
-
addNode(id: string, kind: string, sourcePath?: string): void;
|
|
31
|
-
addEdge(src: string, dst: string, rel: string, sourcePath?: string): void;
|
|
54
|
+
addNode(id: string, kind: string, sourcePath?: string, provenance?: Provenance): void;
|
|
55
|
+
addEdge(src: string, dst: string, rel: string, sourcePath?: string, provenance?: Provenance): void;
|
|
56
|
+
/** @implements A-SPEC-281 — how this node got here, or null if the node is unknown. */
|
|
57
|
+
provenanceOfNode(id: string): Provenance | null;
|
|
58
|
+
/** @implements A-SPEC-281 — how this edge got here, or null if the edge is unknown. */
|
|
59
|
+
provenanceOfEdge(src: string, dst: string, rel: string): Provenance | null;
|
|
32
60
|
/**
|
|
33
61
|
* @implements A-SPEC-139
|
|
34
62
|
* Run `fn` inside ONE SQLite transaction, so a build commits once instead of once per insert.
|
|
@@ -45,6 +73,64 @@ export declare class RtmGraph {
|
|
|
45
73
|
* "who calls me" question into "what is attached to me in any way at all".
|
|
46
74
|
*/
|
|
47
75
|
callersOf(id: string): string[];
|
|
76
|
+
/**
|
|
77
|
+
* @implements A-SPEC-288
|
|
78
|
+
* Everything a change to this symbol can reach: its callers PLUS its subclasses. Changing a base
|
|
79
|
+
* method affects a subclass that never calls it, so impact must follow inheritance.
|
|
80
|
+
*
|
|
81
|
+
* Deliberately SEPARATE from `callersOf`, which stays calls-only. Taint follows data flow and hub
|
|
82
|
+
* detection counts call sites; folding inheritance into either would change what those two words
|
|
83
|
+
* mean rather than making them more complete.
|
|
84
|
+
*
|
|
85
|
+
* @implements A-SPEC-304
|
|
86
|
+
* A `references` relation (a type-position use of a contract) was built, wired in here, and
|
|
87
|
+
* measured: 586 correct edges on this repository, and EVERY replay metric identical with the
|
|
88
|
+
* relation on and off — including the impact-reach count, 26 truth files either way. The reason is
|
|
89
|
+
* now understood and is not about the edges: the replay enters from a REQUEST STRING, so impact is
|
|
90
|
+
* never seeded from contract nodes at all. What this feature improves, nothing measures yet.
|
|
91
|
+
*/
|
|
92
|
+
impactSourcesOf(id: string): string[];
|
|
93
|
+
/**
|
|
94
|
+
* @implements A-SPEC-309
|
|
95
|
+
* Both directions of impact: who reaches this symbol, and what this symbol reaches.
|
|
96
|
+
*
|
|
97
|
+
* Goal Phase 5 asks for upstream callers AND downstream dependencies, and only the upstream half
|
|
98
|
+
* existed. Changing a function usually means changing what it calls, so the callee side is not a
|
|
99
|
+
* refinement — it is half the question.
|
|
100
|
+
*
|
|
101
|
+
* Kept as its own name rather than widening `impactSourcesOf`, for the same reason A-SPEC-288 split
|
|
102
|
+
* `impactSourcesOf` off `callersOf`: once one function answers two questions, both get blurred.
|
|
103
|
+
*/
|
|
104
|
+
impactNeighborsOf(id: string, opts?: {
|
|
105
|
+
/**
|
|
106
|
+
* @implements A-SPEC-311
|
|
107
|
+
* Drop callees with more callers than this. A function everything calls is a shared tool, not a
|
|
108
|
+
* thing that changes when you change one of its callers — changing a logger does not change
|
|
109
|
+
* every caller, and calling a logger does not change the logger.
|
|
110
|
+
*
|
|
111
|
+
* The same number means different things in the two directions, which is why the upstream side
|
|
112
|
+
* applies it past depth 1 and this applies at depth 1: upstream, a high in-degree node is one
|
|
113
|
+
* many things depend ON and the damping stops the walk spreading THROUGH it; downstream, a high
|
|
114
|
+
* in-degree node is a utility and is not a signal at all.
|
|
115
|
+
*
|
|
116
|
+
* Optional, and off by default: silently filtering would change what this method means for
|
|
117
|
+
* every other caller of it.
|
|
118
|
+
*/
|
|
119
|
+
maxCalleeInDegree?: number;
|
|
120
|
+
}): string[];
|
|
121
|
+
/**
|
|
122
|
+
* @implements A-SPEC-289
|
|
123
|
+
* Repo-relative paths of the files that import this one, via resolved FILE->FILE `imports` edges.
|
|
124
|
+
* A coarser dependency than a call, and the only one that reaches a file which depends on another
|
|
125
|
+
* without calling any symbol the resolver could pin down.
|
|
126
|
+
*/
|
|
127
|
+
importersOf(sourcePath: string): string[];
|
|
128
|
+
/**
|
|
129
|
+
* @implements A-SPEC-293
|
|
130
|
+
* The decisions constraining any of the given node ids (`FILE:<path>` / `SPEC:<id>`), by ADR id.
|
|
131
|
+
* What a change must not break, rather than what it touches.
|
|
132
|
+
*/
|
|
133
|
+
decisionsConstraining(nodeIds: readonly string[]): string[];
|
|
48
134
|
/** How many distinct symbols call this one. The hub signal for bounded propagation (REQ-127). */
|
|
49
135
|
callerCount(id: string): number;
|
|
50
136
|
/**
|
|
@@ -82,6 +168,35 @@ export declare class RtmGraph {
|
|
|
82
168
|
allCodeNodeIds(): string[];
|
|
83
169
|
/** @implements A-SPEC-138 — direct calls-only callees of a node (one hop), for BFS path assembly. */
|
|
84
170
|
callees(fromId: string): string[];
|
|
171
|
+
/**
|
|
172
|
+
* @implements A-SPEC-280
|
|
173
|
+
* A canonical, order-independent dump of the whole graph: sorted nodes then sorted edges. Exists
|
|
174
|
+
* so two graphs built by different routes (a full rebuild vs. an incremental update) can be
|
|
175
|
+
* compared as VALUES rather than by spot-checking a few lookups — the only way the convergence
|
|
176
|
+
* property Goal completion criterion 5 asks for can actually be evidence.
|
|
177
|
+
*/
|
|
178
|
+
dumpCanonical(): string;
|
|
179
|
+
/**
|
|
180
|
+
* @implements A-SPEC-280
|
|
181
|
+
* Drop every edge of one relation. Needed because call resolution is GLOBAL: whether `B.two`
|
|
182
|
+
* resolves depends on how many files define it across the whole scan, so a file-local update
|
|
183
|
+
* cannot repair a call edge owned by a file that did not change. Clearing the relation and
|
|
184
|
+
* re-resolving is the only way an incremental update lands on the full-rebuild answer.
|
|
185
|
+
*/
|
|
186
|
+
removeEdgesByRel(rel: string): void;
|
|
187
|
+
/** @implements A-SPEC-282 — every stored meta key/value. */
|
|
188
|
+
readMeta(): Record<string, string>;
|
|
189
|
+
/** @implements A-SPEC-282 — replace the stored meta wholesale, so a stale key cannot survive. */
|
|
190
|
+
writeMeta(meta: Record<string, string>): void;
|
|
191
|
+
/**
|
|
192
|
+
* @implements A-SPEC-282
|
|
193
|
+
* Empty the graph, keeping the file. Used when a stored graph's basis no longer holds: the caller
|
|
194
|
+
* has just been told it cannot trust these facts, so leaving any of them behind to be built on top
|
|
195
|
+
* of would be worse than having had no cache at all.
|
|
196
|
+
*/
|
|
197
|
+
clear(): void;
|
|
198
|
+
/** @implements A-SPEC-283 — the journal mode actually in force, so the pragma can be asserted. */
|
|
199
|
+
journalMode(): string;
|
|
85
200
|
nodeCount(): number;
|
|
86
201
|
edgeCount(): number;
|
|
87
202
|
close(): void;
|