knodin 0.12.2 → 0.13.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.
- package/README.md +16 -1
- package/dist/bin/cli.js +173 -14
- package/dist/src/agent-events.js +25 -7
- package/dist/src/authenticated-cursor.js +81 -0
- package/dist/src/class-consumer-contract.js +18 -0
- package/dist/src/class-consumer-cursor.js +91 -0
- package/dist/src/class-consumer-delivery.js +22 -0
- package/dist/src/class-consumer-page.js +148 -0
- package/dist/src/cli-model.js +9 -3
- package/dist/src/docs-sections.js +1 -0
- package/dist/src/engine/apex-class-uses.js +430 -0
- package/dist/src/engine/apex-entry-points.js +98 -0
- package/dist/src/engine/apex-receiver.js +301 -0
- package/dist/src/engine/embedding-reuse.js +57 -0
- package/dist/src/engine/embeddings.js +22 -0
- package/dist/src/engine/index-coverage.js +215 -0
- package/dist/src/engine/index.js +2495 -252
- package/dist/src/engine/salesforce-components.js +460 -0
- package/dist/src/engine/seal.js +3 -0
- package/dist/src/engine/sqlite.js +44 -0
- package/dist/src/evidence-bundle.js +283 -0
- package/dist/src/evidence-graph.js +163 -0
- package/dist/src/failure-diagnosis.js +80 -5
- package/dist/src/file-dependency.js +35 -0
- package/dist/src/graph-query-health.js +47 -1
- package/dist/src/implementation-search.js +69 -0
- package/dist/src/index-coverage-read.js +33 -0
- package/dist/src/investigation.js +195 -0
- package/dist/src/mcp-reliability.js +4 -0
- package/dist/src/mcp-worker-supervisor.js +122 -6
- package/dist/src/progressive-evidence.js +4 -4
- package/dist/src/response-budget.js +129 -3
- package/dist/src/server.js +18 -5
- package/dist/src/shared-index/publisher.js +41 -1
- package/dist/src/tools/knodin-tools.js +224 -41
- package/docs/CLI.md +92 -0
- package/docs/MCP.md +67 -0
- package/docs/PROGRESSIVE-EVIDENCE.md +62 -0
- package/docs/SALESFORCE-BINDINGS.md +121 -0
- package/docs/SALESFORCE-DEAD-CODE.md +45 -0
- package/docs/SCOPED-INDEXING.md +76 -0
- package/docs/apex-receiver-resolution.md +41 -0
- package/docs/releases/0.13.0.md +62 -0
- package/docs/structural-only-indexing.md +20 -0
- package/package.json +11 -5
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { compareBytes } from "../compare.js";
|
|
4
|
+
export const INDEX_COVERAGE_KEY = "indexCoverageIntent";
|
|
5
|
+
export const INDEX_COVERAGE_PENDING_KEY = "indexCoveragePending";
|
|
6
|
+
export const INDEX_COVERAGE_COMPLETE_KEY = "indexCoverageComplete";
|
|
7
|
+
export function canonicalCoveragePath(value) {
|
|
8
|
+
return (value.length > 0 &&
|
|
9
|
+
value.length <= 4096 &&
|
|
10
|
+
!/[\\\0]/.test(value) &&
|
|
11
|
+
!path.posix.isAbsolute(value) &&
|
|
12
|
+
!/^[A-Za-z]:/.test(value) &&
|
|
13
|
+
path.posix.normalize(value) === value &&
|
|
14
|
+
value !== "." &&
|
|
15
|
+
!value.split("/").includes(".."));
|
|
16
|
+
}
|
|
17
|
+
// Preserve Array.sort's UTF-16 code-unit ordering; locale ordering changes digests.
|
|
18
|
+
const sort = (values) => [...new Set(values)].sort(compareBytes);
|
|
19
|
+
function normalize(intent) {
|
|
20
|
+
if (intent.mode === "full")
|
|
21
|
+
return { ...intent, prefixes: [], files: [] };
|
|
22
|
+
const prefixes = [];
|
|
23
|
+
const accepted = new Set();
|
|
24
|
+
for (const value of sort(intent.prefixes)) {
|
|
25
|
+
let parent = value;
|
|
26
|
+
let covered = false;
|
|
27
|
+
while (parent.includes("/")) {
|
|
28
|
+
parent = parent.slice(0, parent.lastIndexOf("/"));
|
|
29
|
+
if (accepted.has(parent)) {
|
|
30
|
+
covered = true;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!covered) {
|
|
35
|
+
prefixes.push(value);
|
|
36
|
+
accepted.add(value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const prefixIntent = { ...intent, prefixes, files: [] };
|
|
40
|
+
return {
|
|
41
|
+
...intent,
|
|
42
|
+
prefixes,
|
|
43
|
+
files: sort(intent.files).filter((file) => !coverageContains(prefixIntent, file)),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function readIndexCoverage(raw, pending, complete) {
|
|
47
|
+
if (raw === null && pending === null && complete === null)
|
|
48
|
+
return {
|
|
49
|
+
intent: { version: 1, mode: "full", revision: 0, prefixes: [], files: [] },
|
|
50
|
+
origin: "legacy",
|
|
51
|
+
intentState: "unknown",
|
|
52
|
+
};
|
|
53
|
+
try {
|
|
54
|
+
if (raw === null || Buffer.byteLength(raw) > 16_000_000)
|
|
55
|
+
throw new Error("missing or oversized coverage intent");
|
|
56
|
+
const value = JSON.parse(raw);
|
|
57
|
+
if (!value ||
|
|
58
|
+
Object.keys(value).some((key) => !["version", "mode", "revision", "prefixes", "files"].includes(key)) ||
|
|
59
|
+
value.version !== 1 ||
|
|
60
|
+
!["full", "scoped"].includes(value.mode) ||
|
|
61
|
+
!Number.isSafeInteger(value.revision) ||
|
|
62
|
+
value.revision < 1 ||
|
|
63
|
+
!Array.isArray(value.prefixes) ||
|
|
64
|
+
!Array.isArray(value.files) ||
|
|
65
|
+
value.prefixes.length + value.files.length > 100_000 ||
|
|
66
|
+
![...value.prefixes, ...value.files].every((item) => typeof item === "string" && canonicalCoveragePath(item)) ||
|
|
67
|
+
(value.mode === "full" && (value.prefixes.length > 0 || value.files.length > 0)) ||
|
|
68
|
+
(value.mode === "scoped" && value.prefixes.length + value.files.length === 0))
|
|
69
|
+
throw new Error("invalid coverage intent");
|
|
70
|
+
const normalized = normalize(value);
|
|
71
|
+
if (JSON.stringify(normalized.prefixes) !== JSON.stringify(value.prefixes) ||
|
|
72
|
+
JSON.stringify(normalized.files) !== JSON.stringify(value.files) ||
|
|
73
|
+
(pending !== null && pending !== String(value.revision)) ||
|
|
74
|
+
(complete !== null &&
|
|
75
|
+
(!/^[1-9]\d*$/.test(complete) ||
|
|
76
|
+
!Number.isSafeInteger(Number(complete)) ||
|
|
77
|
+
Number(complete) > value.revision)))
|
|
78
|
+
throw new Error("noncanonical coverage intent");
|
|
79
|
+
return {
|
|
80
|
+
intent: normalized,
|
|
81
|
+
origin: "explicit",
|
|
82
|
+
intentState: pending !== null ? "pending" : complete === String(value.revision) ? "complete" : "unknown",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return { intent: null, origin: "invalid", intentState: "unknown" };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function requireIndexCoverage(state) {
|
|
90
|
+
if (!state.intent)
|
|
91
|
+
throw new Error("knodin: malformed index coverage intent; refusing automatic indexing or scope expansion. Restore valid coverage metadata or explicitly rebuild a full candidate.");
|
|
92
|
+
return state.intent;
|
|
93
|
+
}
|
|
94
|
+
const matchers = new WeakMap();
|
|
95
|
+
function matcher(intent) {
|
|
96
|
+
let known = matchers.get(intent);
|
|
97
|
+
if (!known) {
|
|
98
|
+
known = {
|
|
99
|
+
files: new Set(intent.files),
|
|
100
|
+
prefixes: new Set(intent.prefixes),
|
|
101
|
+
parents: new Set(),
|
|
102
|
+
};
|
|
103
|
+
for (const selector of [...intent.files, ...intent.prefixes]) {
|
|
104
|
+
let parent = selector;
|
|
105
|
+
while (parent.includes("/")) {
|
|
106
|
+
parent = parent.slice(0, parent.lastIndexOf("/"));
|
|
107
|
+
known.parents.add(parent);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
matchers.set(intent, known);
|
|
111
|
+
}
|
|
112
|
+
return known;
|
|
113
|
+
}
|
|
114
|
+
export function coverageContains(intent, file) {
|
|
115
|
+
if (!canonicalCoveragePath(file))
|
|
116
|
+
return false;
|
|
117
|
+
if (intent.mode === "full")
|
|
118
|
+
return true;
|
|
119
|
+
const known = matcher(intent);
|
|
120
|
+
if (known.files.has(file))
|
|
121
|
+
return true;
|
|
122
|
+
let prefix = file;
|
|
123
|
+
for (;;) {
|
|
124
|
+
if (known.prefixes.has(prefix))
|
|
125
|
+
return true;
|
|
126
|
+
const end = prefix.lastIndexOf("/");
|
|
127
|
+
if (end < 0)
|
|
128
|
+
return false;
|
|
129
|
+
prefix = prefix.slice(0, end);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export function coverageMayContain(intent, directory) {
|
|
133
|
+
return (directory === "" ||
|
|
134
|
+
intent.mode === "full" ||
|
|
135
|
+
coverageContains(intent, directory) ||
|
|
136
|
+
matcher(intent).parents.has(directory));
|
|
137
|
+
}
|
|
138
|
+
export function mergeIndexCoverage(previous, requested, fresh) {
|
|
139
|
+
const old = requireIndexCoverage(previous);
|
|
140
|
+
if (requested &&
|
|
141
|
+
(requested.prefixes.length + requested.files.length > 100_000 ||
|
|
142
|
+
![...requested.prefixes, ...requested.files].every(canonicalCoveragePath)))
|
|
143
|
+
throw new Error("knodin: invalid or oversized requested index coverage selectors");
|
|
144
|
+
const full = requested === null || (!fresh && old.mode === "full");
|
|
145
|
+
const next = normalize({
|
|
146
|
+
version: 1,
|
|
147
|
+
mode: full ? "full" : "scoped",
|
|
148
|
+
revision: Math.max(1, old.revision + 1),
|
|
149
|
+
prefixes: full ? [] : [...(fresh ? [] : old.prefixes), ...(requested?.prefixes ?? [])],
|
|
150
|
+
files: full ? [] : [...(fresh ? [] : old.files), ...(requested?.files ?? [])],
|
|
151
|
+
});
|
|
152
|
+
if (!readIndexCoverage(JSON.stringify(next), String(next.revision), null).intent)
|
|
153
|
+
throw new Error("knodin: merged index coverage exceeds the supported intent bounds");
|
|
154
|
+
if (previous.origin === "explicit" &&
|
|
155
|
+
JSON.stringify({ ...next, revision: old.revision }) === JSON.stringify(old))
|
|
156
|
+
return old;
|
|
157
|
+
return next;
|
|
158
|
+
}
|
|
159
|
+
/** Bound argv size independently of the OS ARG_MAX; every pathspec remains literal. */
|
|
160
|
+
export function coverageGitPathspecBatches(intent) {
|
|
161
|
+
if (intent?.mode !== "scoped")
|
|
162
|
+
return [[]];
|
|
163
|
+
const batches = [];
|
|
164
|
+
let batch = ["--"];
|
|
165
|
+
let bytes = 3;
|
|
166
|
+
for (const selector of [...intent.prefixes, ...intent.files]) {
|
|
167
|
+
const value = `:(literal)${selector}`;
|
|
168
|
+
const size = Buffer.byteLength(value) + 1;
|
|
169
|
+
if (batch.length > 1 && (bytes + size > 32_768 || batch.length > 256)) {
|
|
170
|
+
batches.push(batch);
|
|
171
|
+
batch = ["--"];
|
|
172
|
+
bytes = 3;
|
|
173
|
+
}
|
|
174
|
+
batch.push(value);
|
|
175
|
+
bytes += size;
|
|
176
|
+
}
|
|
177
|
+
if (batch.length > 1)
|
|
178
|
+
batches.push(batch);
|
|
179
|
+
return batches;
|
|
180
|
+
}
|
|
181
|
+
const digests = new WeakMap();
|
|
182
|
+
export function indexCoverageSummary(state) {
|
|
183
|
+
const intent = state.intent;
|
|
184
|
+
const selected = [
|
|
185
|
+
...(intent?.prefixes ?? []).slice(0, 8).map((value) => ({ kind: "prefix", value })),
|
|
186
|
+
...(intent?.files ?? [])
|
|
187
|
+
.slice(0, Math.max(0, 8 - (intent?.prefixes.length ?? 0)))
|
|
188
|
+
.map((value) => ({ kind: "file", value })),
|
|
189
|
+
];
|
|
190
|
+
if (intent && !digests.has(intent))
|
|
191
|
+
digests.set(intent, crypto.createHash("sha256").update(JSON.stringify(intent)).digest("hex"));
|
|
192
|
+
return {
|
|
193
|
+
version: 1,
|
|
194
|
+
mode: intent?.mode ?? "unknown",
|
|
195
|
+
revision: intent?.revision ?? null,
|
|
196
|
+
digest: intent ? (digests.get(intent) ?? null) : null,
|
|
197
|
+
origin: state.origin,
|
|
198
|
+
prefixCount: intent?.prefixes.length ?? 0,
|
|
199
|
+
fileCount: intent?.files.length ?? 0,
|
|
200
|
+
selectorPreview: {
|
|
201
|
+
prefixes: selected.filter((x) => x.kind === "prefix").map((x) => x.value.slice(0, 160)),
|
|
202
|
+
files: selected.filter((x) => x.kind === "file").map((x) => x.value.slice(0, 160)),
|
|
203
|
+
truncated: !!intent &&
|
|
204
|
+
(intent.prefixes.length + intent.files.length > 8 ||
|
|
205
|
+
selected.some((x) => x.value.length > 160)),
|
|
206
|
+
},
|
|
207
|
+
intentState: state.intentState,
|
|
208
|
+
repositoryComplete: intent?.mode === "full" && state.intentState === "complete",
|
|
209
|
+
negativeScope: !intent || state.intentState !== "complete"
|
|
210
|
+
? "unknown"
|
|
211
|
+
: intent.mode === "full"
|
|
212
|
+
? "repository"
|
|
213
|
+
: "declared-scope",
|
|
214
|
+
};
|
|
215
|
+
}
|