knodin 0.12.2 → 0.13.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.
Files changed (58) hide show
  1. package/README.md +16 -1
  2. package/dist/bin/cli.js +245 -18
  3. package/dist/src/agent-events.js +25 -7
  4. package/dist/src/authenticated-cursor.js +81 -0
  5. package/dist/src/backup-retention.js +345 -12
  6. package/dist/src/class-consumer-contract.js +18 -0
  7. package/dist/src/class-consumer-cursor.js +91 -0
  8. package/dist/src/class-consumer-delivery.js +22 -0
  9. package/dist/src/class-consumer-page.js +148 -0
  10. package/dist/src/cli-model.js +21 -3
  11. package/dist/src/docs-sections.js +1 -0
  12. package/dist/src/engine/apex-class-uses.js +430 -0
  13. package/dist/src/engine/apex-entry-points.js +98 -0
  14. package/dist/src/engine/apex-receiver.js +301 -0
  15. package/dist/src/engine/candidate-database.js +624 -66
  16. package/dist/src/engine/embedding-reuse.js +57 -0
  17. package/dist/src/engine/embeddings.js +22 -0
  18. package/dist/src/engine/index-coverage.js +215 -0
  19. package/dist/src/engine/index.js +3854 -448
  20. package/dist/src/engine/salesforce-components.js +460 -0
  21. package/dist/src/engine/seal.js +3 -0
  22. package/dist/src/engine/sqlite.js +44 -0
  23. package/dist/src/engine/state-paths.js +192 -31
  24. package/dist/src/evidence-bundle.js +283 -0
  25. package/dist/src/evidence-graph.js +163 -0
  26. package/dist/src/failure-diagnosis.js +80 -5
  27. package/dist/src/file-dependency.js +35 -0
  28. package/dist/src/graph-query-health.js +47 -1
  29. package/dist/src/implementation-search.js +69 -0
  30. package/dist/src/index-coverage-read.js +33 -0
  31. package/dist/src/investigation.js +195 -0
  32. package/dist/src/manager-update.js +9 -10
  33. package/dist/src/mcp-reliability.js +4 -0
  34. package/dist/src/mcp-worker-supervisor.js +122 -6
  35. package/dist/src/mirror.js +2 -0
  36. package/dist/src/progressive-evidence.js +4 -4
  37. package/dist/src/response-budget.js +129 -3
  38. package/dist/src/server.js +18 -5
  39. package/dist/src/shared-index/publisher.js +41 -1
  40. package/dist/src/shared-index/restore.js +16 -6
  41. package/dist/src/storage-budget.js +146 -0
  42. package/dist/src/storage-inventory.js +263 -0
  43. package/dist/src/storage-management.js +204 -0
  44. package/dist/src/storage-policy-contract.js +121 -0
  45. package/dist/src/tools/knodin-tools.js +224 -41
  46. package/dist/src/worktree-seed.js +7 -1
  47. package/docs/BACKUP-RETENTION.md +46 -2
  48. package/docs/CLI.md +98 -2
  49. package/docs/MCP.md +75 -0
  50. package/docs/PROGRESSIVE-EVIDENCE.md +62 -0
  51. package/docs/SALESFORCE-BINDINGS.md +121 -0
  52. package/docs/SALESFORCE-DEAD-CODE.md +45 -0
  53. package/docs/SCOPED-INDEXING.md +76 -0
  54. package/docs/apex-receiver-resolution.md +41 -0
  55. package/docs/releases/0.13.0.md +62 -0
  56. package/docs/releases/0.13.1.md +216 -0
  57. package/docs/structural-only-indexing.md +20 -0
  58. package/package.json +12 -4
@@ -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
+ }