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.
Files changed (45) hide show
  1. package/README.md +16 -1
  2. package/dist/bin/cli.js +173 -14
  3. package/dist/src/agent-events.js +25 -7
  4. package/dist/src/authenticated-cursor.js +81 -0
  5. package/dist/src/class-consumer-contract.js +18 -0
  6. package/dist/src/class-consumer-cursor.js +91 -0
  7. package/dist/src/class-consumer-delivery.js +22 -0
  8. package/dist/src/class-consumer-page.js +148 -0
  9. package/dist/src/cli-model.js +9 -3
  10. package/dist/src/docs-sections.js +1 -0
  11. package/dist/src/engine/apex-class-uses.js +430 -0
  12. package/dist/src/engine/apex-entry-points.js +98 -0
  13. package/dist/src/engine/apex-receiver.js +301 -0
  14. package/dist/src/engine/embedding-reuse.js +57 -0
  15. package/dist/src/engine/embeddings.js +22 -0
  16. package/dist/src/engine/index-coverage.js +215 -0
  17. package/dist/src/engine/index.js +2495 -252
  18. package/dist/src/engine/salesforce-components.js +460 -0
  19. package/dist/src/engine/seal.js +3 -0
  20. package/dist/src/engine/sqlite.js +44 -0
  21. package/dist/src/evidence-bundle.js +283 -0
  22. package/dist/src/evidence-graph.js +163 -0
  23. package/dist/src/failure-diagnosis.js +80 -5
  24. package/dist/src/file-dependency.js +35 -0
  25. package/dist/src/graph-query-health.js +47 -1
  26. package/dist/src/implementation-search.js +69 -0
  27. package/dist/src/index-coverage-read.js +33 -0
  28. package/dist/src/investigation.js +195 -0
  29. package/dist/src/mcp-reliability.js +4 -0
  30. package/dist/src/mcp-worker-supervisor.js +122 -6
  31. package/dist/src/progressive-evidence.js +4 -4
  32. package/dist/src/response-budget.js +129 -3
  33. package/dist/src/server.js +18 -5
  34. package/dist/src/shared-index/publisher.js +41 -1
  35. package/dist/src/tools/knodin-tools.js +224 -41
  36. package/docs/CLI.md +92 -0
  37. package/docs/MCP.md +67 -0
  38. package/docs/PROGRESSIVE-EVIDENCE.md +62 -0
  39. package/docs/SALESFORCE-BINDINGS.md +121 -0
  40. package/docs/SALESFORCE-DEAD-CODE.md +45 -0
  41. package/docs/SCOPED-INDEXING.md +76 -0
  42. package/docs/apex-receiver-resolution.md +41 -0
  43. package/docs/releases/0.13.0.md +62 -0
  44. package/docs/structural-only-indexing.md +20 -0
  45. package/package.json +11 -5
@@ -0,0 +1,148 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { compareClassConsumerKeys, } from "./class-consumer-contract.js";
3
+ import { signClassConsumerCursor } from "./class-consumer-cursor.js";
4
+ const positiveInt = (value, fallback) => value == null || !Number.isFinite(value) ? fallback : Math.max(1, Math.trunc(value));
5
+ const size = (value) => Buffer.byteLength(JSON.stringify(value));
6
+ const invalid = () => new Error("invalid class-consumer page contract");
7
+ function validatePage(page) {
8
+ if ((page.targetKind !== "class" && page.targetKind !== "interface") ||
9
+ page.returnedCount !== page.results.length ||
10
+ !Number.isSafeInteger(page.count) ||
11
+ page.count < page.results.length ||
12
+ !Number.isSafeInteger(page.consumerFileCount) ||
13
+ page.consumerFileCount < page.files.length ||
14
+ (page.hasMore && page.results.length === 0))
15
+ throw invalid();
16
+ const files = new Map(page.files.map((file) => [file.file, file]));
17
+ if (files.size !== page.files.length)
18
+ throw invalid();
19
+ const counts = new Map();
20
+ for (let index = 0; index < page.results.length; index++) {
21
+ const row = page.results[index];
22
+ if (row.file !== row.key.file ||
23
+ row.start.byte !== row.key.startByte ||
24
+ row.end.byte !== row.key.endByte ||
25
+ row.kind !== row.key.kind ||
26
+ (index > 0 && compareClassConsumerKeys(page.results[index - 1].key, row.key) >= 0))
27
+ throw invalid();
28
+ const file = files.get(row.file);
29
+ if (!file || file.isTest !== row.isTest)
30
+ throw invalid();
31
+ counts.set(row.file, (counts.get(row.file) ?? 0) + 1);
32
+ }
33
+ for (const file of files.values()) {
34
+ const count = counts.get(file.file);
35
+ if (count === undefined ||
36
+ file.returnedSiteCount !== count ||
37
+ !Number.isSafeInteger(file.siteCount) ||
38
+ file.siteCount < count)
39
+ throw invalid();
40
+ }
41
+ }
42
+ /**
43
+ * Fits complete rows and all qualifications, never truncating evidence fields.
44
+ * The caller must return this result unchanged: no later generic budget reduction.
45
+ * Backend lease checks precede this pure step; continuation always names the last
46
+ * actually delivered key, not the last row fetched from storage.
47
+ */
48
+ export function finalizeClassConsumerPage(page, options) {
49
+ validatePage(page);
50
+ const request = options.budget ?? {};
51
+ const tokenLimit = positiveInt(request.tokens, 16384);
52
+ const byteLimit = Math.min(positiveInt(request.bytes, 65536), tokenLimit * 4);
53
+ const itemLimit = positiveInt(request.items, 100);
54
+ if (byteLimit < 256)
55
+ throw new Error("class-consumer response budget must be at least 256 bytes");
56
+ let maxRows = Math.min(page.results.length, itemLimit);
57
+ const minimumRows = page.results.length ? 1 : 0;
58
+ // Protected rows alone are a strict lower bound on the complete wire size.
59
+ // Skip impossible prefixes without cloning/decorating/serializing the entire
60
+ // page for each one. Do not assume complete candidate sizes are monotonic:
61
+ // the last-key cursor can get shorter when crossing a filename boundary.
62
+ let rowArrayBytes = 2;
63
+ for (let index = 0; index < maxRows; index++) {
64
+ rowArrayBytes += size(page.results[index]) + (index > 0 ? 1 : 0);
65
+ if (rowArrayBytes > byteLimit) {
66
+ maxRows = Math.max(minimumRows, index);
67
+ break;
68
+ }
69
+ }
70
+ let requiredBytes = 0;
71
+ for (let length = maxRows; length >= minimumRows; length--) {
72
+ const results = page.results.slice(0, length);
73
+ const counts = new Map();
74
+ for (const row of results)
75
+ counts.set(row.file, (counts.get(row.file) ?? 0) + 1);
76
+ const hasMore = page.hasMore || length < page.results.length;
77
+ const last = results.at(-1);
78
+ const continuation = hasMore && last
79
+ ? signClassConsumerCursor({
80
+ v: 1,
81
+ type: "class-consumers",
82
+ queryDigest: page.queryDigest,
83
+ snapshotDigest: page.snapshotDigest,
84
+ after: last.key,
85
+ }, options.secret)
86
+ : null;
87
+ const metadata = {
88
+ byteLimit,
89
+ tokenLimit,
90
+ itemLimit,
91
+ serializedBytes: 0,
92
+ estimatedTokens: 0,
93
+ truncated: hasMore,
94
+ totals: { "$/results": page.count, "$/files": page.consumerFileCount },
95
+ };
96
+ const candidate = structuredClone({
97
+ pattern: "class_consumers",
98
+ targetIdentity: page.targetIdentity,
99
+ targetKind: page.targetKind,
100
+ count: page.count,
101
+ consumerFileCount: page.consumerFileCount,
102
+ returnedCount: length,
103
+ results,
104
+ files: page.files
105
+ .filter((file) => counts.has(file.file))
106
+ .map((file) => ({
107
+ ...file,
108
+ returnedSiteCount: counts.get(file.file),
109
+ })),
110
+ hasMore,
111
+ continuation,
112
+ classConsumerAnalysis: page.analysis,
113
+ indexCoverage: page.indexCoverage,
114
+ freshness: page.freshness,
115
+ responseBudget: metadata,
116
+ });
117
+ const output = options.decorate ? options.decorate(structuredClone(candidate)) : candidate;
118
+ if (!output || Array.isArray(output) || typeof output !== "object")
119
+ throw invalid();
120
+ if (output.available === false ||
121
+ (output.availability &&
122
+ typeof output.availability === "object" &&
123
+ ["unavailable", "repair-needed"].includes(String(output.availability.state))))
124
+ throw new Error("class-consumer evidence became unavailable; restart the query");
125
+ for (const key of Object.keys(candidate)) {
126
+ if (!isDeepStrictEqual(candidate[key], output[key]))
127
+ throw new Error("class-consumer decoration changed protected evidence");
128
+ }
129
+ output.responseBudget = metadata;
130
+ // Metadata changes its own serialized size. Recompute until the byte/token digits stabilize.
131
+ let stable = false;
132
+ for (let attempt = 0; attempt < 16; attempt++) {
133
+ const actual = size(output), tokens = Math.ceil(actual / 4);
134
+ if (metadata.serializedBytes === actual && metadata.estimatedTokens === tokens) {
135
+ stable = true;
136
+ break;
137
+ }
138
+ metadata.serializedBytes = actual;
139
+ metadata.estimatedTokens = tokens;
140
+ }
141
+ if (!stable)
142
+ throw new Error("class-consumer response serialization did not stabilize");
143
+ requiredBytes = metadata.serializedBytes;
144
+ if (requiredBytes <= byteLimit)
145
+ return output;
146
+ }
147
+ throw new Error(`class-consumer response budget ${byteLimit} cannot preserve qualifications and one complete page item; use at least ${requiredBytes} bytes and ${Math.ceil(requiredBytes / 4)} estimated tokens or narrow the query`);
148
+ }
@@ -122,7 +122,10 @@ function addRepositoryCommands(program, capture) {
122
122
  .option("--dry-run", "report actions without mutation");
123
123
  }
124
124
  function addGraphCommands(program, capture) {
125
- leaf(program, "context <task> [base]", "build compact task orientation", capture);
125
+ leaf(program, "context <task> [base]", "build compact task orientation or a target investigation", capture)
126
+ .option("--symbol <name>", "investigate a target's source, change sites, impact and tests")
127
+ .option("--implementations", "rank possible implementations for the task; do not select a target")
128
+ .addOption(option("--offset <count>", "implementation candidate page offset", "integer"));
126
129
  leaf(program, "explain <symbol> [detail]", "explain one ambiguity-safe symbol", capture);
127
130
  leaf(program, "review [base] [detail]", "review an explicit Git diff scope", capture)
128
131
  .option("--scope <scope>", "unstaged, staged, all, or compare")
@@ -165,7 +168,8 @@ function addGraphCommands(program, capture) {
165
168
  .option("--kinds <values>", "comma-separated symbol kinds")
166
169
  .option("--path <prefix>", "repo-relative path prefix")
167
170
  .option("--base <ref>", "compare file_metrics with the merge-base of this Git ref")
168
- .option("--test-scope <scope>", "file_metrics scope: all, test, or production")
171
+ .option("--test-scope <scope>", "file_metrics/class_consumers: all, test, or production")
172
+ .option("--continuation <handle>", "class_consumers: resume the same target and filters")
169
173
  .option("--format <format>", "file_metrics machine output format; currently json")
170
174
  .option("--variable <name>", "flow-analysis variable")
171
175
  .option("--facets <values>", "comma-separated architecture facets")
@@ -199,7 +203,9 @@ function addBackupCommands(program, capture) {
199
203
  leaf(retention, "run", "run the installed retention policy", capture);
200
204
  }
201
205
  function addArtifactCommands(program, capture) {
202
- leaf(program, "evidence <level> <file>", "deliver progressive source evidence", capture)
206
+ leaf(program, "evidence <level> [file]", "deliver progressive source evidence", capture)
207
+ .option("--files <paths>", "comma-separated bundle files; evidence or expand level")
208
+ .option("--already-present <handles>", "comma-separated verified bundle file handles")
203
209
  .option("--continuation <handle>", "resume an exact prior response")
204
210
  .option("--baseline-hash <sha256>", "complete baseline SHA-256")
205
211
  .addOption(option("--baseline-bytes <count>", "complete baseline UTF-8 bytes", "integer"))
@@ -24,6 +24,7 @@ The \`query\` operation lets you ask precise, structured questions about your co
24
24
 
25
25
  - **callers_of <target>**: Who invokes this symbol (transitive/direct).
26
26
  - **callees_of <target>**: What other symbols does this symbol invoke.
27
+ - **class_consumers <target>**: Direct, typed Apex class-use sites, separate from method callers and Java configuration consumers. Count means observed sites; consumerFileCount means distinct files. All tests are included by default; testScope can select test or production files. Use limit (1–1000 sites), canonical path prefix, and signed continuation with the same target/filters. Pages preserve full site ranges, source-owner identities and scope/extraction qualifications; stale continuations are rejected. Nine supported syntax roles only, not complete class-use or runtime reachability proof.
27
28
  - **tests_for <target>**: Find test files that cover this symbol.
28
29
  - **file_summary <target>**: Lists all symbols defined in a file.
29
30
  - **shortest_path <from> <to>**: Find the call-graph chain connecting two symbols.
@@ -0,0 +1,430 @@
1
+ import { createHash } from "node:crypto";
2
+ const fold = (value) => value.toLowerCase();
3
+ const qualifiedIdentifier = /^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*$/;
4
+ const spelling = (node) => node.text.replace(/\s+/g, "");
5
+ const typeDeclarations = new Set([
6
+ "class_declaration",
7
+ "interface_declaration",
8
+ "enum_declaration",
9
+ ]);
10
+ const isTypeDeclaration = (declaration) => ["class", "interface", "enum"].includes(declaration.kind);
11
+ function owner(node, includeSelf = false) {
12
+ const names = [];
13
+ for (let parent = includeSelf ? node : node.parent; parent; parent = parent.parent) {
14
+ if (typeDeclarations.has(parent.type)) {
15
+ const name = parent.childForFieldName("name")?.text;
16
+ if (name)
17
+ names.unshift(name);
18
+ }
19
+ }
20
+ return names.join(".") || null;
21
+ }
22
+ function positionReader(source) {
23
+ const lines = [0];
24
+ const bytes = new Uint32Array(source.length + 1);
25
+ let byte = 0;
26
+ for (let offset = 0; offset < source.length;) {
27
+ const point = source.codePointAt(offset) ?? 0;
28
+ const width = point > 0xffff ? 2 : 1;
29
+ bytes[offset] = byte;
30
+ if (width === 2)
31
+ bytes[offset + 1] = byte;
32
+ byte += point <= 0x7f ? 1 : point <= 0x7ff ? 2 : point <= 0xffff ? 3 : 4;
33
+ offset += width;
34
+ bytes[offset] = byte;
35
+ if (point === 10)
36
+ lines.push(offset);
37
+ }
38
+ return (offset) => {
39
+ let low = 0;
40
+ let high = lines.length;
41
+ while (low + 1 < high) {
42
+ const middle = (low + high) >>> 1;
43
+ if (lines[middle] <= offset)
44
+ low = middle;
45
+ else
46
+ high = middle;
47
+ }
48
+ return {
49
+ line: low + 1,
50
+ column: offset - lines[low] + 1,
51
+ utf16Offset: offset,
52
+ byte: bytes[offset],
53
+ };
54
+ };
55
+ }
56
+ /** No filesystem, database, graph identity, or inference work occurs during extraction. */
57
+ export function extractApexClassUses(root, source, file) {
58
+ const position = positionReader(source);
59
+ const nodes = [];
60
+ const stack = [root];
61
+ while (stack.length) {
62
+ const node = stack.pop();
63
+ if (!node)
64
+ break;
65
+ nodes.push(node);
66
+ for (let index = node.namedChildren.length - 1; index >= 0; index--)
67
+ stack.push(node.namedChildren[index]);
68
+ }
69
+ const malformed = root.hasError() || root.text !== source;
70
+ const sites = [];
71
+ const declarations = [];
72
+ const heritage = [];
73
+ let heritageComplete = !malformed;
74
+ const unsupportedUses = [];
75
+ const valueBindings = nodes.filter((node) => [
76
+ "variable_declarator",
77
+ "formal_parameter",
78
+ "enhanced_for_statement",
79
+ "catch_formal_parameter",
80
+ ].includes(node.type));
81
+ const scopeBindings = new Map();
82
+ for (const binding of valueBindings) {
83
+ const name = binding.childForFieldName("name")?.text;
84
+ if (!name)
85
+ continue;
86
+ for (let scope = binding.parent; scope; scope = scope.parent) {
87
+ if (![
88
+ "class_declaration",
89
+ "method_declaration",
90
+ "constructor_declaration",
91
+ "trigger_declaration",
92
+ ].includes(scope.type))
93
+ continue;
94
+ const names = scopeBindings.get(scope.id) ?? new Set();
95
+ names.add(fold(name));
96
+ scopeBindings.set(scope.id, names);
97
+ break;
98
+ }
99
+ }
100
+ const range = (node) => ({
101
+ start: position(node.startIndex),
102
+ end: position(node.endIndex),
103
+ });
104
+ const unsupported = (node, kind) => {
105
+ unsupportedUses.push({ kind, qualifiedSpelling: node.text, ...range(node) });
106
+ };
107
+ const blockReason = (node, typePath) => {
108
+ if (malformed)
109
+ return "malformed-source";
110
+ const first = fold(typePath.split(".")[0]);
111
+ for (let scope = node.parent; scope; scope = scope.parent) {
112
+ if (![
113
+ "class_declaration",
114
+ "method_declaration",
115
+ "constructor_declaration",
116
+ "trigger_declaration",
117
+ ].includes(scope.type))
118
+ continue;
119
+ // Deliberately conservative across lexical blocks: false negatives remain explicit blocked facts.
120
+ if (scopeBindings.get(scope.id)?.has(first))
121
+ return "value-shadow";
122
+ }
123
+ return null;
124
+ };
125
+ const add = (node, kind, typePath = spelling(node), memberName = null) => {
126
+ if (!qualifiedIdentifier.test(typePath))
127
+ return;
128
+ sites.push({
129
+ version: 1,
130
+ kind,
131
+ qualifiedSpelling: node.text,
132
+ typePath,
133
+ memberName,
134
+ lexicalOwner: owner(node),
135
+ ...range(node),
136
+ blockedReason: kind === "static_field_read"
137
+ ? blockReason(node, typePath)
138
+ : malformed
139
+ ? "malformed-source"
140
+ : null,
141
+ });
142
+ };
143
+ const typeUses = (node, kind, generic = false) => {
144
+ if (!node)
145
+ return;
146
+ if (node.type === "scoped_type_identifier" || node.type === "type_identifier") {
147
+ add(node, generic
148
+ ? kind === "parameter_type"
149
+ ? "generic_parameter_type"
150
+ : "generic_type_argument"
151
+ : kind);
152
+ return;
153
+ }
154
+ for (const child of node.namedChildren)
155
+ typeUses(child, kind, generic || node.type === "type_arguments");
156
+ };
157
+ let isTest = false;
158
+ for (const node of nodes) {
159
+ const modifiers = node.namedChildren.find((child) => child.type === "modifiers");
160
+ const annotations = modifiers?.namedChildren
161
+ .filter((child) => child.type === "annotation")
162
+ .map((child) => child.childForFieldName("name")?.text ?? "") ?? [];
163
+ const modifierWords = modifiers?.namedChildren
164
+ .filter((child) => child.type === "modifier")
165
+ .map((child) => fold(child.text)) ?? [];
166
+ if (annotations.some((annotation) => fold(annotation) === "istest") ||
167
+ modifierWords.includes("testmethod"))
168
+ isTest = true;
169
+ if (typeDeclarations.has(node.type)) {
170
+ const name = node.childForFieldName("name");
171
+ if (name)
172
+ declarations.push({
173
+ version: 1,
174
+ kind: node.type === "class_declaration"
175
+ ? "class"
176
+ : node.type === "enum_declaration"
177
+ ? "enum"
178
+ : "interface",
179
+ ownerPath: owner(node, true) ?? name.text,
180
+ name: name.text,
181
+ static: false,
182
+ annotations,
183
+ superclass: node.childForFieldName("superclass")?.namedChildren[0]?.text ?? null,
184
+ ...range(name),
185
+ ...(node.type === "interface_declaration" ||
186
+ (() => {
187
+ for (let parent = node.parent; parent; parent = parent.parent)
188
+ if (parent.type === "interface_declaration")
189
+ return true;
190
+ return false;
191
+ })()
192
+ ? { declarationRange: range(node) }
193
+ : {}),
194
+ });
195
+ for (const field of ["superclass", "interfaces", "extends_interfaces"]) {
196
+ const inherited = field === "extends_interfaces"
197
+ ? node.namedChildren.find((child) => child.type === field)
198
+ : node.childForFieldName(field);
199
+ if (inherited) {
200
+ unsupported(inherited, "inheritance");
201
+ const list = inherited.namedChildren.find((child) => child.type === "type_list") ?? inherited;
202
+ for (const base of list.namedChildren) {
203
+ const typePath = spelling(base);
204
+ const ownerPath = owner(node, true);
205
+ if (ownerPath && qualifiedIdentifier.test(typePath))
206
+ heritage.push({
207
+ kind: field === "interfaces" ? "implements" : "extends",
208
+ ownerPath,
209
+ typePath,
210
+ ...range(base),
211
+ });
212
+ else
213
+ heritageComplete = false;
214
+ }
215
+ }
216
+ }
217
+ }
218
+ if (node.type === "field_declaration") {
219
+ for (const declarator of node.namedChildren.filter((child) => child.type === "variable_declarator")) {
220
+ const name = declarator.childForFieldName("name");
221
+ const ownerPath = owner(node);
222
+ if (name && ownerPath)
223
+ declarations.push({
224
+ version: 1,
225
+ kind: node.namedChildren.some((child) => child.type === "accessor_list")
226
+ ? "property"
227
+ : "field",
228
+ ownerPath,
229
+ name: name.text,
230
+ static: modifierWords.includes("static"),
231
+ annotations,
232
+ superclass: null,
233
+ ...range(name),
234
+ });
235
+ }
236
+ }
237
+ if (node.type === "field_declaration" || node.type === "local_variable_declaration")
238
+ typeUses(node.childForFieldName("type"), "variable_type");
239
+ if (node.type === "formal_parameter")
240
+ typeUses(node.childForFieldName("type"), "parameter_type");
241
+ if (node.type === "enhanced_for_statement")
242
+ typeUses(node.childForFieldName("type"), "loop_variable_type");
243
+ if (node.type === "cast_expression")
244
+ typeUses(node.childForFieldName("type"), "cast");
245
+ if (node.type === "object_creation_expression")
246
+ typeUses(node.childForFieldName("type"), "constructor");
247
+ if (node.type === "method_declaration") {
248
+ const type = node.childForFieldName("type");
249
+ if (type)
250
+ unsupported(type, "return_type");
251
+ }
252
+ if (node.type === "method_invocation") {
253
+ const receiver = node.childForFieldName("object");
254
+ if (receiver && qualifiedIdentifier.test(spelling(receiver)))
255
+ unsupported(receiver, "static_method_receiver");
256
+ }
257
+ if (node.type !== "field_access" || node.parent?.type === "field_access")
258
+ continue;
259
+ const object = node.childForFieldName("object");
260
+ const fieldNode = node.childForFieldName("field");
261
+ if (!object || !fieldNode || !qualifiedIdentifier.test(spelling(object)))
262
+ continue;
263
+ if (fold(fieldNode.text) === "class") {
264
+ add(node, "class_literal", spelling(object));
265
+ continue;
266
+ }
267
+ if ((node.parent?.type === "assignment_expression" &&
268
+ node.parent.childForFieldName("left")?.id === node.id) ||
269
+ node.parent?.type === "update_expression") {
270
+ unsupported(node, "static_field_write");
271
+ continue;
272
+ }
273
+ add(node, "static_field_read", spelling(object), fieldNode.text);
274
+ }
275
+ const unique = new Map(sites.map((site) => [`${site.start.utf16Offset}:${site.end.utf16Offset}:${site.kind}`, site]));
276
+ return {
277
+ version: 1,
278
+ file,
279
+ sourceHash: createHash("sha256").update(source).digest("hex"),
280
+ parseState: malformed ? "partial" : "complete",
281
+ isTest: malformed ? null : isTest,
282
+ sites: [...unique.values()].sort((a, b) => a.start.utf16Offset - b.start.utf16Offset ||
283
+ a.end.utf16Offset - b.end.utf16Offset ||
284
+ a.kind.localeCompare(b.kind)),
285
+ declarations,
286
+ unsupportedUses,
287
+ heritage,
288
+ heritageComplete,
289
+ coverage: "supported-syntax-only",
290
+ };
291
+ }
292
+ /** Build once per indexed catalog revision, never once per use or by walking the filesystem. */
293
+ export function createApexConsumerCatalog(files) {
294
+ const classes = new Map();
295
+ const members = new Map();
296
+ for (const file of files) {
297
+ for (const declaration of file.declarations) {
298
+ // Invalid source still contributes competitors; it must not make a valid duplicate look unique.
299
+ const map = isTypeDeclaration(declaration) ? classes : members;
300
+ const key = fold(isTypeDeclaration(declaration)
301
+ ? declaration.ownerPath
302
+ : `${declaration.ownerPath}.${declaration.name}`);
303
+ const entries = map.get(key) ?? [];
304
+ entries.push({ file: file.file, parseState: file.parseState, declaration });
305
+ map.set(key, entries);
306
+ }
307
+ }
308
+ return { classes, members };
309
+ }
310
+ export function resolveApexClassUses(catalog, facts) {
311
+ const uniqueOwner = (entry) => {
312
+ const parts = entry.declaration.ownerPath.split(".");
313
+ for (let length = 1; length <= parts.length; length++) {
314
+ const owners = catalog.classes.get(fold(parts.slice(0, length).join(".")));
315
+ if (owners?.length !== 1 ||
316
+ owners[0].parseState !== "complete" ||
317
+ owners[0].file !== entry.file)
318
+ return false;
319
+ }
320
+ return true;
321
+ };
322
+ return facts.sites.map((site) => {
323
+ const result = (status, target = null, member = null) => ({ site, status, target, member });
324
+ if (site.blockedReason || facts.parseState !== "complete")
325
+ return result("blocked");
326
+ // A lexical first-segment binding must never fall through to an unrelated global suffix.
327
+ let candidates;
328
+ let lexicalOwner = site.lexicalOwner;
329
+ let scoped = false;
330
+ const first = site.typePath.split(".")[0];
331
+ while (lexicalOwner) {
332
+ const localPrefix = catalog.classes.get(fold(`${lexicalOwner}.${first}`));
333
+ if (localPrefix?.length) {
334
+ if (localPrefix.length !== 1 || localPrefix[0].parseState !== "complete")
335
+ return result("ambiguous");
336
+ if (localPrefix[0].declaration.kind === "enum")
337
+ return result("blocked");
338
+ candidates = catalog.classes.get(fold(`${lexicalOwner}.${site.typePath}`));
339
+ scoped = true;
340
+ break;
341
+ }
342
+ const own = catalog.classes.get(fold(lexicalOwner));
343
+ if (!own || own.length !== 1 || !uniqueOwner(own[0]) || own[0].file !== facts.file)
344
+ return result("blocked");
345
+ let ancestorOwner = own[0];
346
+ let ancestor = ancestorOwner.declaration.superclass;
347
+ const seen = new Set([fold(lexicalOwner)]);
348
+ while (ancestor) {
349
+ if (!qualifiedIdentifier.test(ancestor) || seen.has(fold(ancestor)) || seen.size >= 32)
350
+ return result("blocked");
351
+ seen.add(fold(ancestor));
352
+ // A lexical superclass prefix also blocks global fallback, even if its suffix is absent.
353
+ let parentScope = ancestorOwner.declaration.ownerPath.split(".").slice(0, -1);
354
+ let parents;
355
+ let parentScoped = false;
356
+ while (parentScope.length) {
357
+ const prefix = `${parentScope.join(".")}.${ancestor.split(".")[0]}`;
358
+ if (catalog.classes.has(fold(prefix))) {
359
+ parents = catalog.classes.get(fold(`${parentScope.join(".")}.${ancestor}`));
360
+ parentScoped = true;
361
+ break;
362
+ }
363
+ parentScope = parentScope.slice(0, -1);
364
+ }
365
+ if (!parentScoped) {
366
+ // A nested class can inherit a type through an enclosing scope. Without a lexical
367
+ // match, do not guess that an unqualified superclass names a global declaration.
368
+ if (!ancestor.includes(".") && ancestorOwner.declaration.ownerPath.includes("."))
369
+ return result("blocked");
370
+ parents = catalog.classes.get(fold(ancestor));
371
+ }
372
+ if (!parents ||
373
+ parents.length !== 1 ||
374
+ parents[0].parseState !== "complete" ||
375
+ !uniqueOwner(parents[0]) ||
376
+ parents[0].declaration.kind !== "class")
377
+ return result("blocked");
378
+ const ancestorPath = parents[0].declaration.ownerPath;
379
+ if (site.kind === "static_field_read" &&
380
+ catalog.members.has(fold(`${ancestorPath}.${first}`)))
381
+ return result("blocked");
382
+ const inheritedPrefix = catalog.classes.get(fold(`${ancestorPath}.${first}`));
383
+ if (inheritedPrefix?.length) {
384
+ if (inheritedPrefix.length !== 1)
385
+ return result("ambiguous");
386
+ if (inheritedPrefix[0].declaration.kind === "enum")
387
+ return result("blocked");
388
+ candidates = catalog.classes.get(fold(`${ancestorPath}.${site.typePath}`));
389
+ scoped = true;
390
+ break;
391
+ }
392
+ ancestorOwner = parents[0];
393
+ ancestor = ancestorOwner.declaration.superclass;
394
+ }
395
+ if (scoped)
396
+ break;
397
+ lexicalOwner = lexicalOwner.includes(".")
398
+ ? lexicalOwner.slice(0, lexicalOwner.lastIndexOf("."))
399
+ : null;
400
+ }
401
+ if (!scoped)
402
+ candidates = catalog.classes.get(fold(site.typePath));
403
+ if (!candidates?.length)
404
+ return result("missing");
405
+ if (candidates.length !== 1)
406
+ return result("ambiguous");
407
+ const target = candidates[0];
408
+ if (target.parseState !== "complete" ||
409
+ !["class", "interface"].includes(target.declaration.kind))
410
+ return result("blocked");
411
+ if (target.declaration.kind === "interface" &&
412
+ (site.kind === "constructor" || site.kind === "static_field_read"))
413
+ return result("blocked");
414
+ // Every owning class must be unique too: nested declarations cannot bypass an outer collision.
415
+ if (!uniqueOwner(target))
416
+ return result("ambiguous");
417
+ if (!site.memberName)
418
+ return result("resolved", target);
419
+ const members = catalog.members.get(fold(`${target.declaration.ownerPath}.${site.memberName}`));
420
+ if (!members?.length)
421
+ return result("missing");
422
+ if (members.length !== 1)
423
+ return result("ambiguous");
424
+ if (!members[0].declaration.static ||
425
+ members[0].file !== target.file ||
426
+ members[0].parseState !== "complete")
427
+ return result("blocked");
428
+ return result("resolved", target, members[0]);
429
+ });
430
+ }