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,98 @@
|
|
|
1
|
+
const normalized = (value) => value.replace(/^@/, "").toLowerCase();
|
|
2
|
+
const ownerKey = (row) => JSON.stringify([row.file, normalized(row.declaration.owner)]);
|
|
3
|
+
const callbacks = new Map([
|
|
4
|
+
["queueable", ["execute"]],
|
|
5
|
+
["system.queueable", ["execute"]],
|
|
6
|
+
["database.batchable", ["start", "execute", "finish"]],
|
|
7
|
+
["schedulable", ["execute"]],
|
|
8
|
+
["system.schedulable", ["execute"]],
|
|
9
|
+
["messaging.inboundemailhandler", ["handleinboundemail"]],
|
|
10
|
+
]);
|
|
11
|
+
function callbackSignature(contract, declaration) {
|
|
12
|
+
const name = normalized(declaration.name);
|
|
13
|
+
const types = declaration.parameterTypes.map((type) => normalized(type)
|
|
14
|
+
.replace(/\s/g, "")
|
|
15
|
+
.replace(/^system\./, ""));
|
|
16
|
+
if (declaration.arity !== types.length)
|
|
17
|
+
return false;
|
|
18
|
+
if (contract === "queueable" || contract === "system.queueable")
|
|
19
|
+
return name === "execute" && types.length === 1 && types[0] === "queueablecontext";
|
|
20
|
+
if (contract === "schedulable" || contract === "system.schedulable")
|
|
21
|
+
return name === "execute" && types.length === 1 && types[0] === "schedulablecontext";
|
|
22
|
+
if (contract === "messaging.inboundemailhandler")
|
|
23
|
+
return (name === "handleinboundemail" &&
|
|
24
|
+
types.length === 2 &&
|
|
25
|
+
types[0] === "messaging.inboundemail" &&
|
|
26
|
+
types[1] === "messaging.inboundenvelope");
|
|
27
|
+
if (contract === "database.batchable") {
|
|
28
|
+
if (types[0] !== "database.batchablecontext")
|
|
29
|
+
return false;
|
|
30
|
+
if (name === "start" || name === "finish")
|
|
31
|
+
return types.length === 1;
|
|
32
|
+
return (name === "execute" &&
|
|
33
|
+
types.length === 2 &&
|
|
34
|
+
(/^list<.+>$/.test(types[1]) || types[1].endsWith("[]")));
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
/** Static org-facing declarations, never a claim that an org actually invokes them. */
|
|
39
|
+
export function apexOrgEntryPoints(rows) {
|
|
40
|
+
const protectedSymbols = new Map();
|
|
41
|
+
const classes = new Map();
|
|
42
|
+
for (const row of rows) {
|
|
43
|
+
const decl = row.declaration;
|
|
44
|
+
if (decl.kind !== "class")
|
|
45
|
+
continue;
|
|
46
|
+
const key = ownerKey(row);
|
|
47
|
+
classes.set(key, [...(classes.get(key) ?? []), row]);
|
|
48
|
+
if (decl.annotations.some((name) => normalized(name) === "restresource"))
|
|
49
|
+
protectedSymbols.set(row.key, "REST resource declaration");
|
|
50
|
+
if (decl.implements.some((name) => callbacks.has(normalized(name))))
|
|
51
|
+
protectedSymbols.set(row.key, "Salesforce platform callback contract");
|
|
52
|
+
}
|
|
53
|
+
for (const row of rows) {
|
|
54
|
+
const decl = row.declaration;
|
|
55
|
+
if (decl.kind !== "method")
|
|
56
|
+
continue;
|
|
57
|
+
const owners = classes.get(ownerKey(row)) ?? [];
|
|
58
|
+
// Duplicate declarations cannot establish a specific enclosing contract.
|
|
59
|
+
if (owners.length !== 1)
|
|
60
|
+
continue;
|
|
61
|
+
const owner = owners[0];
|
|
62
|
+
const annotations = new Set(decl.annotations.map(normalized));
|
|
63
|
+
const modifiers = new Set(decl.modifiers.map(normalized));
|
|
64
|
+
let reason;
|
|
65
|
+
if (annotations.has("invocablemethod") && decl.static)
|
|
66
|
+
reason = "Invocable Apex declaration";
|
|
67
|
+
if (annotations.has("auraenabled") && decl.static)
|
|
68
|
+
reason = "Lightning Apex API declaration";
|
|
69
|
+
if (annotations.has("future") && decl.static)
|
|
70
|
+
reason = "Future Apex callback declaration";
|
|
71
|
+
if (modifiers.has("webservice") && decl.static)
|
|
72
|
+
reason = "SOAP Apex declaration";
|
|
73
|
+
if (decl.static &&
|
|
74
|
+
owner.declaration.annotations.some((name) => normalized(name) === "restresource") &&
|
|
75
|
+
["httpget", "httppost", "httpput", "httppatch", "httpdelete"].some((name) => annotations.has(name)))
|
|
76
|
+
reason = "REST Apex method declaration";
|
|
77
|
+
if (!decl.static &&
|
|
78
|
+
(modifiers.has("public") || modifiers.has("global")) &&
|
|
79
|
+
owner.declaration.implements.some((name) => callbackSignature(normalized(name), decl)))
|
|
80
|
+
reason = "Salesforce platform callback method";
|
|
81
|
+
if (reason) {
|
|
82
|
+
protectedSymbols.set(row.key, reason);
|
|
83
|
+
protectedSymbols.set(owner.key, "Owner of an org-facing Apex declaration");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// A constructor of an org-facing class is a boundary, not an ordinary helper.
|
|
87
|
+
for (const row of rows) {
|
|
88
|
+
const decl = row.declaration;
|
|
89
|
+
if (decl.kind !== "method" || decl.static)
|
|
90
|
+
continue;
|
|
91
|
+
const owners = classes.get(ownerKey(row)) ?? [];
|
|
92
|
+
if (owners.length === 1 &&
|
|
93
|
+
protectedSymbols.has(owners[0].key) &&
|
|
94
|
+
normalized(decl.name) === normalized(owners[0].declaration.name))
|
|
95
|
+
protectedSymbols.set(row.key, "Constructor of an org-facing Apex class");
|
|
96
|
+
}
|
|
97
|
+
return protectedSymbols;
|
|
98
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
const identifier = /^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*$/;
|
|
2
|
+
const fold = (value) => value.toLowerCase();
|
|
3
|
+
const countArguments = (node, field) => node.childForFieldName(field)?.namedChildren.filter((child) => !child.type.includes("comment"))
|
|
4
|
+
.length ?? 0;
|
|
5
|
+
function ownerPath(node, includeSelf = false) {
|
|
6
|
+
const names = [];
|
|
7
|
+
for (let parent = includeSelf ? node : node.parent; parent; parent = parent.parent) {
|
|
8
|
+
if (parent.type === "class_declaration") {
|
|
9
|
+
const name = parent.childForFieldName("name")?.text;
|
|
10
|
+
if (name)
|
|
11
|
+
names.unshift(name);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return names.join(".");
|
|
15
|
+
}
|
|
16
|
+
/** Extract an implemented type identity from grammar nodes, not sanitized source text. */
|
|
17
|
+
function implementedTypeName(node) {
|
|
18
|
+
if (node.type === "generic_type") {
|
|
19
|
+
const children = node.namedChildren.filter((child) => !child.type.includes("comment"));
|
|
20
|
+
if (children.length !== 2 || children[1].type !== "type_arguments")
|
|
21
|
+
return null;
|
|
22
|
+
return implementedTypeName(children[0]);
|
|
23
|
+
}
|
|
24
|
+
if (node.type !== "type_identifier" && node.type !== "scoped_type_identifier")
|
|
25
|
+
return null;
|
|
26
|
+
const name = node.text.trim();
|
|
27
|
+
return identifier.test(name) ? name : null;
|
|
28
|
+
}
|
|
29
|
+
export function apexDeclaration(node) {
|
|
30
|
+
const modifiers = node.namedChildren.find((child) => child.type === "modifiers");
|
|
31
|
+
const words = modifiers?.namedChildren
|
|
32
|
+
.filter((child) => child.type === "modifier")
|
|
33
|
+
.map((child) => fold(child.text)) ?? [];
|
|
34
|
+
const annotations = modifiers?.namedChildren
|
|
35
|
+
.filter((child) => child.type === "annotation")
|
|
36
|
+
.map((child) => child.childForFieldName("name")?.text ?? "") ?? [];
|
|
37
|
+
const kind = node.type === "class_declaration" ? "class" : "method";
|
|
38
|
+
const interfaces = node.childForFieldName("interfaces");
|
|
39
|
+
const types = interfaces?.hasError()
|
|
40
|
+
? undefined
|
|
41
|
+
: interfaces?.namedChildren.find((child) => child.type === "type_list");
|
|
42
|
+
return {
|
|
43
|
+
version: 1,
|
|
44
|
+
kind,
|
|
45
|
+
owner: ownerPath(node, kind === "class"),
|
|
46
|
+
name: node.childForFieldName("name")?.text ?? "",
|
|
47
|
+
arity: kind === "method" ? countArguments(node, "parameters") : null,
|
|
48
|
+
parameterTypes: kind === "method"
|
|
49
|
+
? (node.childForFieldName("parameters")?.namedChildren ?? [])
|
|
50
|
+
.filter((child) => !child.type.includes("comment"))
|
|
51
|
+
.map((child) => child.childForFieldName("type")?.text ?? "")
|
|
52
|
+
: [],
|
|
53
|
+
static: words.includes("static"),
|
|
54
|
+
constructors: kind === "class"
|
|
55
|
+
? (node.childForFieldName("body")?.namedChildren ?? [])
|
|
56
|
+
.filter((child) => child.type === "constructor_declaration")
|
|
57
|
+
.map((child) => countArguments(child, "parameters"))
|
|
58
|
+
: [],
|
|
59
|
+
modifiers: words,
|
|
60
|
+
annotations,
|
|
61
|
+
implements: types?.namedChildren
|
|
62
|
+
.map(implementedTypeName)
|
|
63
|
+
.filter((name) => name !== null) ?? [],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Conservative lexical proof: inherited/unknown scopes and value shadowing do not resolve. */
|
|
67
|
+
export function apexCall(node) {
|
|
68
|
+
const isConstructor = node.type === "object_creation_expression";
|
|
69
|
+
const receiver = node.childForFieldName(isConstructor ? "type" : "object")?.text ?? "";
|
|
70
|
+
const member = isConstructor
|
|
71
|
+
? (receiver.split(".").at(-1) ?? receiver)
|
|
72
|
+
: (node.childForFieldName("name")?.text ?? "");
|
|
73
|
+
let blocked = !identifier.test(receiver) || !identifier.test(member);
|
|
74
|
+
if (!receiver.includes(".")) {
|
|
75
|
+
for (let scope = node.parent; scope; scope = scope.parent) {
|
|
76
|
+
if (scope.type !== "class_declaration")
|
|
77
|
+
continue;
|
|
78
|
+
if (scope
|
|
79
|
+
.descendantsOfType("class_declaration")
|
|
80
|
+
.some((candidate) => ownerPath(candidate, true).includes(".") &&
|
|
81
|
+
fold(candidate.childForFieldName("name")?.text ?? "") === fold(receiver)))
|
|
82
|
+
blocked = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!isConstructor) {
|
|
86
|
+
const first = fold(receiver.split(".")[0] ?? "");
|
|
87
|
+
for (let scope = node.parent; scope; scope = scope.parent) {
|
|
88
|
+
if (scope.type === "class_declaration" && scope.childForFieldName("superclass"))
|
|
89
|
+
blocked = true;
|
|
90
|
+
if (![
|
|
91
|
+
"class_declaration",
|
|
92
|
+
"method_declaration",
|
|
93
|
+
"constructor_declaration",
|
|
94
|
+
"trigger_declaration",
|
|
95
|
+
].includes(scope.type))
|
|
96
|
+
continue;
|
|
97
|
+
for (const binding of scope.descendantsOfType([
|
|
98
|
+
"formal_parameter",
|
|
99
|
+
"variable_declarator",
|
|
100
|
+
"property_declaration",
|
|
101
|
+
"enhanced_for_statement",
|
|
102
|
+
"catch_formal_parameter",
|
|
103
|
+
])) {
|
|
104
|
+
const name = binding.childForFieldName("name")?.text;
|
|
105
|
+
if (name && fold(name) === first)
|
|
106
|
+
blocked = true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
version: 1,
|
|
112
|
+
kind: isConstructor ? "constructor" : "static",
|
|
113
|
+
receiver,
|
|
114
|
+
member,
|
|
115
|
+
arity: countArguments(node, "arguments"),
|
|
116
|
+
blocked,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function payload(raw) {
|
|
120
|
+
if (!raw || raw.length > 16_384)
|
|
121
|
+
return null;
|
|
122
|
+
try {
|
|
123
|
+
const value = JSON.parse(raw);
|
|
124
|
+
return value && typeof value === "object" && value.version === 1 ? value : null;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const strings = (value) => Array.isArray(value) &&
|
|
131
|
+
value.length <= 1000 &&
|
|
132
|
+
value.every((entry) => typeof entry === "string" && entry.length <= 500);
|
|
133
|
+
const arity = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 1000;
|
|
134
|
+
export function parseApexDeclaration(raw) {
|
|
135
|
+
const value = payload(raw);
|
|
136
|
+
if (!value ||
|
|
137
|
+
!["class", "method"].includes(String(value.kind)) ||
|
|
138
|
+
typeof value.owner !== "string" ||
|
|
139
|
+
!identifier.test(value.owner) ||
|
|
140
|
+
typeof value.name !== "string" ||
|
|
141
|
+
!identifier.test(value.name) ||
|
|
142
|
+
typeof value.static !== "boolean" ||
|
|
143
|
+
!(value.arity === null || arity(value.arity)) ||
|
|
144
|
+
!Array.isArray(value.constructors) ||
|
|
145
|
+
!value.constructors.every(arity) ||
|
|
146
|
+
!strings(value.modifiers) ||
|
|
147
|
+
!strings(value.annotations) ||
|
|
148
|
+
!strings(value.implements))
|
|
149
|
+
return null;
|
|
150
|
+
if (!strings(value.parameterTypes) ||
|
|
151
|
+
(value.kind === "method" &&
|
|
152
|
+
(!arity(value.arity) || value.parameterTypes.length !== value.arity)) ||
|
|
153
|
+
(value.kind === "class" && (value.arity !== null || value.parameterTypes.length !== 0)))
|
|
154
|
+
return null;
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
export function parseApexCall(raw) {
|
|
158
|
+
const value = payload(raw);
|
|
159
|
+
if (!value ||
|
|
160
|
+
!["static", "constructor"].includes(String(value.kind)) ||
|
|
161
|
+
typeof value.receiver !== "string" ||
|
|
162
|
+
value.receiver.length > 500 ||
|
|
163
|
+
typeof value.member !== "string" ||
|
|
164
|
+
value.member.length > 500 ||
|
|
165
|
+
typeof value.blocked !== "boolean" ||
|
|
166
|
+
!arity(value.arity))
|
|
167
|
+
return null;
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
/** Read-time defense against damaged provenance retaining an older canonical endpoint. */
|
|
171
|
+
export function apexReferenceReadGuard(db) {
|
|
172
|
+
const targets = new Map();
|
|
173
|
+
return (reference) => {
|
|
174
|
+
if (reference.apexCall == null)
|
|
175
|
+
return true;
|
|
176
|
+
const call = parseApexCall(reference.apexCall);
|
|
177
|
+
if (!call ||
|
|
178
|
+
call.blocked ||
|
|
179
|
+
!identifier.test(call.receiver) ||
|
|
180
|
+
!identifier.test(call.member) ||
|
|
181
|
+
!reference.calleeIdentity ||
|
|
182
|
+
!reference.calleeFile)
|
|
183
|
+
return false;
|
|
184
|
+
const key = JSON.stringify([reference.calleeIdentity, reference.calleeFile]);
|
|
185
|
+
if (!targets.has(key)) {
|
|
186
|
+
const row = db
|
|
187
|
+
.query("SELECT * FROM symbols WHERE identity=? AND filePath=?")
|
|
188
|
+
.get(reference.calleeIdentity, reference.calleeFile);
|
|
189
|
+
targets.set(key, parseApexDeclaration(row?.apexDeclaration));
|
|
190
|
+
}
|
|
191
|
+
const target = targets.get(key);
|
|
192
|
+
if (!target || fold(target.owner) !== fold(call.receiver))
|
|
193
|
+
return false;
|
|
194
|
+
if (call.kind === "constructor")
|
|
195
|
+
return (target.kind === "class" &&
|
|
196
|
+
!target.modifiers.includes("abstract") &&
|
|
197
|
+
(target.constructors.length === 0
|
|
198
|
+
? call.arity === 0
|
|
199
|
+
: target.constructors.filter((value) => value === call.arity).length === 1));
|
|
200
|
+
return (target.kind === "method" &&
|
|
201
|
+
target.static &&
|
|
202
|
+
target.arity === call.arity &&
|
|
203
|
+
fold(target.name) === fold(call.member));
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/** One indexed catalog and raw-call scan per changed-Apex batch; never opens source files. */
|
|
207
|
+
export function persistApexReferenceEndpoints(db) {
|
|
208
|
+
const declarations = db
|
|
209
|
+
.query("SELECT name,filePath,identity,apexDeclaration FROM symbols WHERE apexDeclaration IS NOT NULL")
|
|
210
|
+
.all()
|
|
211
|
+
.flatMap((row) => {
|
|
212
|
+
const declaration = parseApexDeclaration(row.apexDeclaration);
|
|
213
|
+
return declaration ? [{ row, declaration }] : [];
|
|
214
|
+
});
|
|
215
|
+
const classes = new Map();
|
|
216
|
+
const methods = new Map();
|
|
217
|
+
for (const entry of declarations) {
|
|
218
|
+
const map = entry.declaration.kind === "class" ? classes : methods;
|
|
219
|
+
const key = fold(entry.declaration.owner);
|
|
220
|
+
const list = map.get(key) ?? [];
|
|
221
|
+
list.push(entry);
|
|
222
|
+
map.set(key, list);
|
|
223
|
+
}
|
|
224
|
+
const refs = db
|
|
225
|
+
.query('SELECT id,apexCall,calleeSymbol FROM "references" WHERE apexCall IS NOT NULL')
|
|
226
|
+
.all();
|
|
227
|
+
const update = db.query('UPDATE "references" SET calleeSymbol=?, calleeFile=?, calleeIdentity=? WHERE id=?');
|
|
228
|
+
for (const ref of refs) {
|
|
229
|
+
const call = parseApexCall(ref.apexCall);
|
|
230
|
+
let target;
|
|
231
|
+
if (call && !call.blocked && identifier.test(call.receiver) && identifier.test(call.member)) {
|
|
232
|
+
const owners = classes.get(fold(call.receiver)) ?? [];
|
|
233
|
+
if (owners.length === 1) {
|
|
234
|
+
const owner = owners[0];
|
|
235
|
+
if (call.kind === "constructor") {
|
|
236
|
+
const ctors = owner.declaration.constructors;
|
|
237
|
+
if (!owner.declaration.modifiers.includes("abstract") &&
|
|
238
|
+
(ctors.length
|
|
239
|
+
? ctors.filter((count) => count === call.arity).length === 1
|
|
240
|
+
: call.arity === 0))
|
|
241
|
+
target = owner.row;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const candidates = (methods.get(fold(call.receiver)) ?? []).filter((entry) => entry.row.filePath === owner.row.filePath &&
|
|
245
|
+
fold(entry.declaration.name) === fold(call.member) &&
|
|
246
|
+
entry.declaration.arity === call.arity);
|
|
247
|
+
if (candidates.length === 1 && candidates[0].declaration.static)
|
|
248
|
+
target = candidates[0].row;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
update.run(target?.name ?? call?.member ?? ref.calleeSymbol, target?.identity ? target.filePath : null, target?.identity ?? null, ref.id);
|
|
253
|
+
}
|
|
254
|
+
update.finalize();
|
|
255
|
+
}
|
|
256
|
+
export function apexReceiverCapability(db, filePath) {
|
|
257
|
+
const scope = filePath === undefined ? [] : [filePath];
|
|
258
|
+
const epochTable = !!db
|
|
259
|
+
.query("SELECT name FROM sqlite_master WHERE type='table' AND name='apex_file_capabilities'")
|
|
260
|
+
.get();
|
|
261
|
+
const hasColumn = (table, name) => db
|
|
262
|
+
.query(`PRAGMA table_info("${table}")`)
|
|
263
|
+
.all()
|
|
264
|
+
.some((column) => column.name === name);
|
|
265
|
+
const provenance = epochTable && hasColumn("symbols", "apexDeclaration") && hasColumn("references", "apexCall");
|
|
266
|
+
const predicate = `FROM index_state i ${provenance ? "LEFT JOIN apex_file_capabilities a ON a.filePath=i.filePath" : ""} WHERE (i.filePath LIKE '%.cls' OR i.filePath LIKE '%.trigger')${provenance ? " AND (a.version IS NULL OR a.version<>1)" : ""}${filePath === undefined ? "" : " AND i.filePath=?"}`;
|
|
267
|
+
const missingFiles = db.query(`SELECT COUNT(*) count ${predicate}`).get(...scope)
|
|
268
|
+
?.count ?? 0;
|
|
269
|
+
const missingPaths = db
|
|
270
|
+
.query(`SELECT i.filePath ${predicate} ORDER BY i.filePath LIMIT 10`)
|
|
271
|
+
.all(...scope)
|
|
272
|
+
.map((row) => row.filePath);
|
|
273
|
+
// Keyset pages bound memory and use the exact same strict parser as resolution.
|
|
274
|
+
// This is an indexed-payload validation scan, not a source-filesystem traversal.
|
|
275
|
+
const invalid = (table, column, fileColumn, parse) => {
|
|
276
|
+
let count = 0, last = 0;
|
|
277
|
+
for (;;) {
|
|
278
|
+
const rows = db
|
|
279
|
+
.query(`SELECT id,substr(${column},1,16385) raw FROM "${table}" WHERE id>? AND ${column} IS NOT NULL${filePath === undefined ? "" : ` AND ${fileColumn}=?`} ORDER BY id LIMIT 128`)
|
|
280
|
+
.all(last, ...scope);
|
|
281
|
+
const lastRow = rows.at(-1);
|
|
282
|
+
if (!lastRow)
|
|
283
|
+
return count;
|
|
284
|
+
for (const row of rows)
|
|
285
|
+
if (!parse(row.raw))
|
|
286
|
+
count++;
|
|
287
|
+
last = lastRow.id;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const invalidPayloads = provenance
|
|
291
|
+
? invalid("symbols", "apexDeclaration", "filePath", parseApexDeclaration) +
|
|
292
|
+
invalid("references", "apexCall", "callerFile", parseApexCall)
|
|
293
|
+
: 0;
|
|
294
|
+
return {
|
|
295
|
+
partial: missingFiles > 0 || invalidPayloads > 0,
|
|
296
|
+
missingFiles,
|
|
297
|
+
missingPaths,
|
|
298
|
+
invalidPayloads,
|
|
299
|
+
repair: "Run knodin index <Apex files missing receiver provenance> --force --skip-embeddings (or a scoped --under rebuild) to populate Apex receiver capability v1.",
|
|
300
|
+
};
|
|
301
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { EMBEDDING_DIMENSIONS } from "./embeddings.js";
|
|
3
|
+
// ~12 MiB of vector payload, plus fixed-size keys/digests and SQLite overhead.
|
|
4
|
+
// A disposable per-graph acceleration table, not another source of graph truth.
|
|
5
|
+
export const EMBEDDING_REUSE_MAX_ENTRIES = 8192;
|
|
6
|
+
// Bump whenever embeddingInputFor/source preparation changes, even if model stays pinned.
|
|
7
|
+
export const EMBEDDING_INPUT_REPRESENTATION = "name-kind-summary-source:1000:v1";
|
|
8
|
+
const VECTOR_BYTES = EMBEDDING_DIMENSIONS * Float32Array.BYTES_PER_ELEMENT;
|
|
9
|
+
function digest(bytes) {
|
|
10
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
export function embeddingReuseKey(modelIdentity, input, representation = EMBEDDING_INPUT_REPRESENTATION) {
|
|
13
|
+
return digest(JSON.stringify([modelIdentity, representation, input]));
|
|
14
|
+
}
|
|
15
|
+
/** Exact-input cache independent of ephemeral symbol IDs. Stores no source text. */
|
|
16
|
+
export class EmbeddingReuseCache {
|
|
17
|
+
db;
|
|
18
|
+
maxEntries;
|
|
19
|
+
constructor(db, maxEntries = EMBEDDING_REUSE_MAX_ENTRIES) {
|
|
20
|
+
this.db = db;
|
|
21
|
+
this.maxEntries = maxEntries;
|
|
22
|
+
if (!Number.isSafeInteger(maxEntries) ||
|
|
23
|
+
maxEntries < 1 ||
|
|
24
|
+
maxEntries > EMBEDDING_REUSE_MAX_ENTRIES)
|
|
25
|
+
throw new Error("Invalid embedding reuse cache bound");
|
|
26
|
+
db.run(`CREATE TABLE IF NOT EXISTS embedding_reuse_v1 (
|
|
27
|
+
cacheKey TEXT PRIMARY KEY CHECK(length(cacheKey) = 64),
|
|
28
|
+
vector BLOB NOT NULL CHECK(length(vector) = ${VECTOR_BYTES}),
|
|
29
|
+
digest TEXT NOT NULL CHECK(length(digest) = 64)
|
|
30
|
+
)`);
|
|
31
|
+
this.prune();
|
|
32
|
+
}
|
|
33
|
+
get(key) {
|
|
34
|
+
const row = this.db
|
|
35
|
+
.query("SELECT vector, digest FROM embedding_reuse_v1 WHERE cacheKey = ?")
|
|
36
|
+
.get(key);
|
|
37
|
+
if (!row)
|
|
38
|
+
return;
|
|
39
|
+
if (row.vector.byteLength === VECTOR_BYTES && digest(row.vector) === row.digest) {
|
|
40
|
+
const vector = new Float32Array(Uint8Array.from(row.vector).buffer);
|
|
41
|
+
if (vector.every(Number.isFinite))
|
|
42
|
+
return vector;
|
|
43
|
+
}
|
|
44
|
+
this.db.run("DELETE FROM embedding_reuse_v1 WHERE cacheKey = ?", [key]);
|
|
45
|
+
}
|
|
46
|
+
/** Call within the embedding batch transaction; reinsertion refreshes LRU order. */
|
|
47
|
+
put(key, vector) {
|
|
48
|
+
if (vector.length !== EMBEDDING_DIMENSIONS || !vector.every(Number.isFinite))
|
|
49
|
+
return;
|
|
50
|
+
const bytes = new Uint8Array(vector.buffer, vector.byteOffset, vector.byteLength);
|
|
51
|
+
this.db.run("INSERT OR REPLACE INTO embedding_reuse_v1(cacheKey, vector, digest) VALUES (?, ?, ?)", [key, bytes, digest(bytes)]);
|
|
52
|
+
}
|
|
53
|
+
/** Bound all generations together; stale model/input keys compete in the same LRU. */
|
|
54
|
+
prune() {
|
|
55
|
+
this.db.run("DELETE FROM embedding_reuse_v1 WHERE rowid IN (SELECT rowid FROM embedding_reuse_v1 ORDER BY rowid DESC LIMIT -1 OFFSET ?)", [this.maxEntries]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* and reads as a natural pro-tier feature precisely because enabling it is a
|
|
10
10
|
* deliberate trade against this file's local/no-egress guarantee.
|
|
11
11
|
*/
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
12
13
|
import os from "node:os";
|
|
13
14
|
import path from "node:path";
|
|
14
15
|
let embedderPromise = null;
|
|
@@ -20,6 +21,27 @@ export const EMBEDDING_MODEL_ID = "Xenova/all-MiniLM-L6-v2";
|
|
|
20
21
|
export const EMBEDDING_MODEL_REVISION = "751bff37182d3f1213fa05d7196b954e230abad9";
|
|
21
22
|
export const EMBEDDING_MODEL_DTYPE = "q8";
|
|
22
23
|
const DEFAULT_MODEL_REMOTE_HOST = "https://huggingface.co/";
|
|
24
|
+
const overrideCacheIdentities = new WeakMap();
|
|
25
|
+
/** Pin reuse to the pipeline contract; test overrides never alias the real model. */
|
|
26
|
+
export function getEmbeddingCacheIdentity() {
|
|
27
|
+
if (embedderOverride) {
|
|
28
|
+
let identity = overrideCacheIdentities.get(embedderOverride);
|
|
29
|
+
if (!identity) {
|
|
30
|
+
identity = `override:${randomUUID()}`;
|
|
31
|
+
overrideCacheIdentities.set(embedderOverride, identity);
|
|
32
|
+
}
|
|
33
|
+
return identity;
|
|
34
|
+
}
|
|
35
|
+
return JSON.stringify([
|
|
36
|
+
EMBEDDING_MODEL_ID,
|
|
37
|
+
EMBEDDING_MODEL_REVISION,
|
|
38
|
+
EMBEDDING_MODEL_DTYPE,
|
|
39
|
+
EMBEDDING_DIMENSIONS,
|
|
40
|
+
"mean",
|
|
41
|
+
"normalize:true",
|
|
42
|
+
getModelRemoteHost(),
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
23
45
|
/**
|
|
24
46
|
* One machine-level model cache shared by every repository and every installed
|
|
25
47
|
* knodin version. An explicit override makes managed/offline environments and
|