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,460 @@
|
|
|
1
|
+
const NAME = /^[A-Za-z_][\w:.-]*/;
|
|
2
|
+
const VOID = new Set([
|
|
3
|
+
"area",
|
|
4
|
+
"base",
|
|
5
|
+
"br",
|
|
6
|
+
"col",
|
|
7
|
+
"embed",
|
|
8
|
+
"hr",
|
|
9
|
+
"img",
|
|
10
|
+
"input",
|
|
11
|
+
"link",
|
|
12
|
+
"meta",
|
|
13
|
+
"param",
|
|
14
|
+
"source",
|
|
15
|
+
"track",
|
|
16
|
+
"wbr",
|
|
17
|
+
]);
|
|
18
|
+
const UNSUPPORTED_RAW_TEXT = new Set([
|
|
19
|
+
"script",
|
|
20
|
+
"style",
|
|
21
|
+
"textarea",
|
|
22
|
+
"title",
|
|
23
|
+
"iframe",
|
|
24
|
+
"xmp",
|
|
25
|
+
"noembed",
|
|
26
|
+
"noframes",
|
|
27
|
+
"plaintext",
|
|
28
|
+
]);
|
|
29
|
+
const FOREIGN_CONTENT = new Set(["svg", "math"]);
|
|
30
|
+
// DTDs are unsupported; no declared entities can be safely inferred or loaded.
|
|
31
|
+
function validXmlText(text) {
|
|
32
|
+
for (const char of text) {
|
|
33
|
+
const code = char.charCodeAt(0);
|
|
34
|
+
if (code < 32 && code !== 9 && code !== 10 && code !== 13)
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
for (let offset = text.indexOf("&"); offset !== -1; offset = text.indexOf("&", offset + 1)) {
|
|
38
|
+
const entity = /^&(?:amp|lt|gt|apos|quot|#\d+|#x[\da-fA-F]+);/.exec(text.slice(offset))?.[0];
|
|
39
|
+
if (!entity)
|
|
40
|
+
return false;
|
|
41
|
+
if (entity.startsWith("&#")) {
|
|
42
|
+
const hex = entity.startsWith("&#x");
|
|
43
|
+
const code = Number.parseInt(entity.slice(hex ? 3 : 2, -1), hex ? 16 : 10);
|
|
44
|
+
if (!(code === 9 ||
|
|
45
|
+
code === 10 ||
|
|
46
|
+
code === 13 ||
|
|
47
|
+
(code >= 32 && code <= 0xd7ff) ||
|
|
48
|
+
(code >= 0xe000 && code <= 0xfffd) ||
|
|
49
|
+
(code >= 0x10000 && code <= 0x10ffff)))
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
offset += entity.length - 1;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
function validAttributes(tail, html) {
|
|
57
|
+
let rest = tail.replace(/\/\s*$/, "");
|
|
58
|
+
const names = new Set();
|
|
59
|
+
const attributes = new Map();
|
|
60
|
+
while (rest.trim()) {
|
|
61
|
+
if (!/^\s/.test(rest))
|
|
62
|
+
return false;
|
|
63
|
+
rest = rest.trimStart();
|
|
64
|
+
const name = NAME.exec(rest)?.[0];
|
|
65
|
+
if (!name || names.has(name))
|
|
66
|
+
return false;
|
|
67
|
+
names.add(name);
|
|
68
|
+
rest = rest.slice(name.length);
|
|
69
|
+
const assignment = /^\s*=\s*/.exec(rest);
|
|
70
|
+
if (!assignment) {
|
|
71
|
+
if (!html)
|
|
72
|
+
return false;
|
|
73
|
+
attributes.set(name, "");
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
rest = rest.slice(assignment[0].length);
|
|
77
|
+
if (rest.startsWith('"') || rest.startsWith("'")) {
|
|
78
|
+
const end = rest.indexOf(rest[0], 1);
|
|
79
|
+
if (end < 0)
|
|
80
|
+
return false;
|
|
81
|
+
if (!html && (rest.slice(1, end).includes("<") || !validXmlText(rest.slice(1, end))))
|
|
82
|
+
return false;
|
|
83
|
+
attributes.set(name, rest.slice(1, end));
|
|
84
|
+
rest = rest.slice(end + 1);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const value = html ? /^[^\s"'`=<>]+/.exec(rest)?.[0] : null;
|
|
88
|
+
if (!value)
|
|
89
|
+
return false;
|
|
90
|
+
attributes.set(name, value);
|
|
91
|
+
rest = rest.slice(value.length);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return attributes;
|
|
95
|
+
}
|
|
96
|
+
/** Bounded literal markup traversal. Reject unsupported declarations/malformed trees. */
|
|
97
|
+
export function parseMarkup(source, html = false) {
|
|
98
|
+
const document = {
|
|
99
|
+
name: "#document",
|
|
100
|
+
attributes: new Map(),
|
|
101
|
+
children: [],
|
|
102
|
+
text: "",
|
|
103
|
+
offset: 0,
|
|
104
|
+
evidence: "",
|
|
105
|
+
};
|
|
106
|
+
const stack = [document];
|
|
107
|
+
let cursor = 0;
|
|
108
|
+
let count = 0;
|
|
109
|
+
while (cursor < source.length) {
|
|
110
|
+
const parent = stack.at(-1);
|
|
111
|
+
if (!parent)
|
|
112
|
+
return null;
|
|
113
|
+
if (source[cursor] !== "<") {
|
|
114
|
+
const end = source.indexOf("<", cursor);
|
|
115
|
+
const next = end < 0 ? source.length : end;
|
|
116
|
+
const text = source.slice(cursor, next);
|
|
117
|
+
if (!html && (!validXmlText(text) || text.includes("]]>")))
|
|
118
|
+
return null;
|
|
119
|
+
parent.text += text;
|
|
120
|
+
cursor = next;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (source.startsWith("<!--", cursor) || source.startsWith("<![CDATA[", cursor)) {
|
|
124
|
+
const comment = source.startsWith("<!--", cursor);
|
|
125
|
+
const endToken = comment ? "-->" : "]]>";
|
|
126
|
+
const end = source.indexOf(endToken, cursor + (comment ? 4 : 9));
|
|
127
|
+
if (end < 0)
|
|
128
|
+
return null;
|
|
129
|
+
if (comment && source.slice(cursor + 4, end).includes("--"))
|
|
130
|
+
return null;
|
|
131
|
+
// Never turn a split/commented identity into a literal identifier.
|
|
132
|
+
parent.text += "\0";
|
|
133
|
+
cursor = end + endToken.length;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (source.startsWith("<?xml", cursor) && stack.length === 1 && !document.children.length) {
|
|
137
|
+
const end = source.indexOf("?>", cursor + 5);
|
|
138
|
+
if (end < 0)
|
|
139
|
+
return null;
|
|
140
|
+
cursor = end + 2;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (source.startsWith("<!", cursor) || source.startsWith("<?", cursor))
|
|
144
|
+
return null;
|
|
145
|
+
let end = cursor + 1;
|
|
146
|
+
let quote = "";
|
|
147
|
+
for (; end < source.length; end++) {
|
|
148
|
+
const char = source[end];
|
|
149
|
+
if (quote) {
|
|
150
|
+
if (char === quote)
|
|
151
|
+
quote = "";
|
|
152
|
+
}
|
|
153
|
+
else if (char === '"' || char === "'")
|
|
154
|
+
quote = char;
|
|
155
|
+
else if (char === ">")
|
|
156
|
+
break;
|
|
157
|
+
else if (char === "<")
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (end === source.length || quote)
|
|
161
|
+
return null;
|
|
162
|
+
const raw = source.slice(cursor, end + 1);
|
|
163
|
+
const closing = raw.startsWith("</");
|
|
164
|
+
const body = raw.slice(closing ? 2 : 1, -1);
|
|
165
|
+
const name = NAME.exec(body)?.[0];
|
|
166
|
+
if (!name)
|
|
167
|
+
return null;
|
|
168
|
+
const tail = body.slice(name.length);
|
|
169
|
+
if (closing) {
|
|
170
|
+
if (tail.trim() || stack.length === 1 || parent.name !== name)
|
|
171
|
+
return null;
|
|
172
|
+
stack.pop();
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
if (tail && !/^[\s/]/.test(tail))
|
|
176
|
+
return null;
|
|
177
|
+
if (/\/\s+$/.test(body))
|
|
178
|
+
return null;
|
|
179
|
+
const attributes = validAttributes(tail, html);
|
|
180
|
+
if (!attributes)
|
|
181
|
+
return null;
|
|
182
|
+
const rawText = UNSUPPORTED_RAW_TEXT.has(name.toLowerCase());
|
|
183
|
+
const node = {
|
|
184
|
+
opaque: parent.opaque || rawText || FOREIGN_CONTENT.has(name.toLowerCase()),
|
|
185
|
+
name,
|
|
186
|
+
attributes,
|
|
187
|
+
children: [],
|
|
188
|
+
text: "",
|
|
189
|
+
offset: cursor,
|
|
190
|
+
evidence: raw,
|
|
191
|
+
};
|
|
192
|
+
parent.children.push(node);
|
|
193
|
+
if (++count > 100000)
|
|
194
|
+
return null;
|
|
195
|
+
if (html && rawText) {
|
|
196
|
+
// HTML raw text is not markup, even inside quotes or comments. Stop
|
|
197
|
+
// at its literal closing tag rather than interpreting its contents.
|
|
198
|
+
// Plaintext has no reliable closing boundary in HTML.
|
|
199
|
+
if (name.toLowerCase() === "plaintext")
|
|
200
|
+
return null;
|
|
201
|
+
const closingTag = new RegExp(`</${name}\\s*>`, "gi");
|
|
202
|
+
closingTag.lastIndex = end + 1;
|
|
203
|
+
const close = closingTag.exec(source);
|
|
204
|
+
if (!close)
|
|
205
|
+
return null;
|
|
206
|
+
// HTML script escaped/double-escaped states can make the first
|
|
207
|
+
// apparent closing tag part of script text. We do not implement
|
|
208
|
+
// that state machine: withhold ambiguous scripts, never expose
|
|
209
|
+
// their remaining text as component markup.
|
|
210
|
+
if (name.toLowerCase() === "script" &&
|
|
211
|
+
/<!--|<script\b/i.test(source.slice(end + 1, close.index)))
|
|
212
|
+
return null;
|
|
213
|
+
cursor = close.index + close[0].length;
|
|
214
|
+
node.text = source.slice(end + 1, close.index);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (!/\/\s*$/.test(body) && !(html && VOID.has(name)))
|
|
218
|
+
stack.push(node);
|
|
219
|
+
if (stack.length > 256)
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
cursor = end + 1;
|
|
223
|
+
}
|
|
224
|
+
if (stack.length !== 1 ||
|
|
225
|
+
document.children.length !== 1 ||
|
|
226
|
+
document.text.replaceAll("\0", "").trim())
|
|
227
|
+
return null;
|
|
228
|
+
return document.children[0];
|
|
229
|
+
}
|
|
230
|
+
function descendants(root) {
|
|
231
|
+
const result = [];
|
|
232
|
+
const queue = [root];
|
|
233
|
+
for (const node of queue) {
|
|
234
|
+
// SVG/MathML integration points (including foreignObject) remain opaque.
|
|
235
|
+
if (node.opaque)
|
|
236
|
+
continue;
|
|
237
|
+
result.push(node);
|
|
238
|
+
for (const child of node.children)
|
|
239
|
+
queue.push(child);
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
function localName(value) {
|
|
244
|
+
const name = /^(?:c:)?([A-Za-z]\w*)$/.exec(value)?.[1];
|
|
245
|
+
return name && !name.includes("__") ? name : null;
|
|
246
|
+
}
|
|
247
|
+
function literalChild(parent, name) {
|
|
248
|
+
const children = parent.children.filter((child) => child.name === name);
|
|
249
|
+
if (children.length !== 1 || children[0].children.length)
|
|
250
|
+
return null;
|
|
251
|
+
const child = children[0];
|
|
252
|
+
const value = localName(child.text.trim());
|
|
253
|
+
return value
|
|
254
|
+
? { name: value, offset: child.offset, evidence: `${child.evidence}${child.text}</${name}>` }
|
|
255
|
+
: null;
|
|
256
|
+
}
|
|
257
|
+
export function componentBindings(source, format, validateXml) {
|
|
258
|
+
// XML grammar validates well-formed syntax, while local checks below cover
|
|
259
|
+
// unsupported namespaces/entities. HTML deliberately does not use XML rules.
|
|
260
|
+
if (format !== "lwc" && (!validateXml || !validateXml(source)))
|
|
261
|
+
return [];
|
|
262
|
+
const root = parseMarkup(source, format === "lwc");
|
|
263
|
+
const roots = {
|
|
264
|
+
aura: ["aura:component", "aura:application"],
|
|
265
|
+
lwc: ["template"],
|
|
266
|
+
flexipage: ["FlexiPage"],
|
|
267
|
+
flow: ["Flow"],
|
|
268
|
+
weblink: ["WebLink"],
|
|
269
|
+
};
|
|
270
|
+
if (!root || !roots[format]?.includes(root.name))
|
|
271
|
+
return [];
|
|
272
|
+
// No namespace inference: conventional unbound Salesforce source prefixes are
|
|
273
|
+
// accepted, but explicit rebinding must never masquerade as a local component.
|
|
274
|
+
for (const node of descendants(root)) {
|
|
275
|
+
for (const [name, value] of node.attributes) {
|
|
276
|
+
if (name === "xmlns") {
|
|
277
|
+
const expected = format === "flexipage" || format === "flow" || format === "weblink"
|
|
278
|
+
? "http://soap.sforce.com/2006/04/metadata"
|
|
279
|
+
: format === "lwc"
|
|
280
|
+
? "http://www.w3.org/1999/xhtml"
|
|
281
|
+
: null;
|
|
282
|
+
if (!expected || value !== expected)
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
else if (name.startsWith("xmlns:")) {
|
|
286
|
+
// Salesforce Flow exports declare xsi for nullable, unrelated
|
|
287
|
+
// metadata fields. This does not rebind the component namespace.
|
|
288
|
+
if (!(format === "flow" &&
|
|
289
|
+
name === "xmlns:xsi" &&
|
|
290
|
+
value === "http://www.w3.org/2001/XMLSchema-instance"))
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const bindings = [];
|
|
296
|
+
if (format === "weblink") {
|
|
297
|
+
const type = literalChild(root, "linkType");
|
|
298
|
+
const entry = literalChild(root, "page");
|
|
299
|
+
if (type?.name === "page" &&
|
|
300
|
+
entry &&
|
|
301
|
+
![...root.attributes.keys()].some((name) => name !== "xmlns") &&
|
|
302
|
+
!root.children
|
|
303
|
+
.filter((node) => node.name === "page" || node.name === "linkType")
|
|
304
|
+
.some((node) => node.attributes.size))
|
|
305
|
+
bindings.push({ ...entry, target: "page", kind: "weblink_visualforce_page" });
|
|
306
|
+
}
|
|
307
|
+
else if (format === "flow") {
|
|
308
|
+
const hasSchemaInstanceAttribute = (node) => [...node.attributes.keys()].some((name) => name.startsWith("xsi:"));
|
|
309
|
+
// Do not interpret nil/type annotations on identity-bearing elements.
|
|
310
|
+
// Other Flow values may be nil without suppressing independent bindings.
|
|
311
|
+
if (hasSchemaInstanceAttribute(root))
|
|
312
|
+
return [];
|
|
313
|
+
for (const screen of root.children.filter((node) => node.name === "screens")) {
|
|
314
|
+
if (hasSchemaInstanceAttribute(screen))
|
|
315
|
+
continue;
|
|
316
|
+
for (const field of screen.children.filter((node) => node.name === "fields")) {
|
|
317
|
+
if (hasSchemaInstanceAttribute(field))
|
|
318
|
+
continue;
|
|
319
|
+
const types = field.children.filter((node) => node.name === "fieldType");
|
|
320
|
+
if (types.some(hasSchemaInstanceAttribute))
|
|
321
|
+
continue;
|
|
322
|
+
if (types.length &&
|
|
323
|
+
(types.length !== 1 ||
|
|
324
|
+
types[0].children.length ||
|
|
325
|
+
types[0].text.trim() !== "ComponentInstance"))
|
|
326
|
+
continue;
|
|
327
|
+
const extensions = field.children.filter((node) => node.name === "extensionName");
|
|
328
|
+
if (extensions.some(hasSchemaInstanceAttribute))
|
|
329
|
+
continue;
|
|
330
|
+
if (extensions.length !== 1 || !/^c:[A-Za-z]\w*$/.test(extensions[0].text.trim()))
|
|
331
|
+
continue;
|
|
332
|
+
const entry = literalChild(field, "extensionName");
|
|
333
|
+
if (entry)
|
|
334
|
+
bindings.push({ ...entry, target: "component", kind: "flow_screen_component" });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else if (format === "flexipage") {
|
|
339
|
+
for (const region of root.children.filter((node) => node.name === "flexiPageRegions")) {
|
|
340
|
+
for (const container of region.children) {
|
|
341
|
+
const instances = container.name === "itemInstances"
|
|
342
|
+
? container.children.filter((node) => node.name === "componentInstance")
|
|
343
|
+
: container.name === "componentInstances"
|
|
344
|
+
? [container]
|
|
345
|
+
: [];
|
|
346
|
+
for (const instance of instances) {
|
|
347
|
+
const entry = literalChild(instance, "componentName");
|
|
348
|
+
if (entry)
|
|
349
|
+
bindings.push({ ...entry, target: "component", kind: "flexipage_component" });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const templates = root.children.filter((node) => node.name === "template");
|
|
354
|
+
if (templates.length === 1) {
|
|
355
|
+
const entry = literalChild(templates[0], "name");
|
|
356
|
+
if (entry)
|
|
357
|
+
bindings.push({ ...entry, target: "aura", kind: "flexipage_template" });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
if (format === "aura" && root.name === "aura:application") {
|
|
362
|
+
for (const node of root.children.filter((node) => node.name === "aura:dependency")) {
|
|
363
|
+
const name = /^c:([A-Za-z]\w*)$/.exec(node.attributes.get("resource") ?? "")?.[1];
|
|
364
|
+
if (name &&
|
|
365
|
+
!name.includes("__") &&
|
|
366
|
+
!node.children.length &&
|
|
367
|
+
!node.text.trim() &&
|
|
368
|
+
[...node.attributes.keys()].every((key) => key === "resource" || key === "type") &&
|
|
369
|
+
(!node.attributes.has("type") || node.attributes.get("type") === "COMPONENT"))
|
|
370
|
+
bindings.push({
|
|
371
|
+
name,
|
|
372
|
+
offset: node.offset,
|
|
373
|
+
evidence: node.evidence,
|
|
374
|
+
target: "component",
|
|
375
|
+
kind: "aura_resource_component",
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
for (const node of descendants(root)) {
|
|
380
|
+
const name = format === "aura"
|
|
381
|
+
? /^c:([A-Za-z]\w*)$/.exec(node.name)?.[1]
|
|
382
|
+
: /^c-([a-z][a-z0-9_-]*)$/.exec(node.name)?.[1];
|
|
383
|
+
if (name && !name.includes("__"))
|
|
384
|
+
bindings.push({
|
|
385
|
+
name,
|
|
386
|
+
offset: node.offset,
|
|
387
|
+
evidence: node.evidence,
|
|
388
|
+
target: format === "aura" ? "component" : "lwc-tag",
|
|
389
|
+
kind: format === "aura" ? "aura_component" : "lwc_template_component",
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return bindings;
|
|
394
|
+
}
|
|
395
|
+
/** Real inline script regions only; foreign/raw-text descendants remain opaque. */
|
|
396
|
+
export function visualforceInlineScripts(source) {
|
|
397
|
+
const root = parseMarkup(source, true);
|
|
398
|
+
if (!root || root.name !== "apex:page")
|
|
399
|
+
return null;
|
|
400
|
+
const scripts = [];
|
|
401
|
+
const queue = [root];
|
|
402
|
+
for (const node of queue) {
|
|
403
|
+
if ([...node.attributes.keys()].some((name) => name.startsWith("xmlns")))
|
|
404
|
+
return null;
|
|
405
|
+
if (node.name.toLowerCase() === "script") {
|
|
406
|
+
if (node.name !== "script" ||
|
|
407
|
+
[...node.attributes.keys()].some((name) => name !== "type") ||
|
|
408
|
+
(node.attributes.has("type") &&
|
|
409
|
+
!["text/javascript", "application/javascript"].includes(node.attributes.get("type") ?? "")))
|
|
410
|
+
return null;
|
|
411
|
+
scripts.push(node.text);
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (!node.opaque)
|
|
415
|
+
queue.push(...node.children);
|
|
416
|
+
}
|
|
417
|
+
return scripts;
|
|
418
|
+
}
|
|
419
|
+
/** Input is a single pass-local source inventory; cap each ambiguity set at two. */
|
|
420
|
+
export function componentCatalog(paths) {
|
|
421
|
+
const entries = new Map();
|
|
422
|
+
const add = (key, target) => {
|
|
423
|
+
const matches = entries.get(key) ?? [];
|
|
424
|
+
if (matches.length < 2 && !matches.some((match) => match.file === target.file))
|
|
425
|
+
matches.push(target);
|
|
426
|
+
entries.set(key, matches);
|
|
427
|
+
};
|
|
428
|
+
for (const file of paths) {
|
|
429
|
+
if (!file.startsWith("force-app/main/default/"))
|
|
430
|
+
continue;
|
|
431
|
+
const app = /^force-app\/main\/default\/aura\/([A-Za-z]\w*)\/\1\.app$/.exec(file);
|
|
432
|
+
const page = /^force-app\/main\/default\/pages\/([A-Za-z]\w*)\.page$/.exec(file);
|
|
433
|
+
const metadataName = (app ?? page)?.[1];
|
|
434
|
+
if (metadataName) {
|
|
435
|
+
const name = metadataName;
|
|
436
|
+
if (!name.includes("__"))
|
|
437
|
+
add(`${app ? "application" : "page"}:${name}`, {
|
|
438
|
+
file,
|
|
439
|
+
kind: app ? "application" : "page",
|
|
440
|
+
});
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const aura = /^force-app\/main\/default\/aura\/([A-Za-z]\w*)\/\1\.cmp$/.exec(file);
|
|
444
|
+
const lwc = /^force-app\/main\/default\/lwc\/([a-z]\w*)\/\1\.js$/.exec(file);
|
|
445
|
+
const name = aura?.[1] ?? lwc?.[1];
|
|
446
|
+
if (!name || name.includes("__"))
|
|
447
|
+
continue;
|
|
448
|
+
const target = { file, kind: aura ? "aura" : "lwc" };
|
|
449
|
+
add(`${target.kind}:${name}`, target);
|
|
450
|
+
add(`component:${name}`, target);
|
|
451
|
+
if (lwc)
|
|
452
|
+
add(`lwc-tag:${name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`)}`, target);
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
resolve(binding) {
|
|
456
|
+
const matches = entries.get(`${binding.target}:${binding.name}`) ?? [];
|
|
457
|
+
return matches.length === 1 ? matches[0] : null;
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
}
|
package/dist/src/engine/seal.js
CHANGED
|
@@ -235,6 +235,9 @@ export function sealIndex(options) {
|
|
|
235
235
|
db.run(statement);
|
|
236
236
|
db.run("DELETE FROM sealed_file;");
|
|
237
237
|
db.run("DELETE FROM sealed_source;");
|
|
238
|
+
// This disposable cache can contain vectors from deleted source versions.
|
|
239
|
+
// It is never part of an export, even when current embeddings are retained.
|
|
240
|
+
db.run("DROP TABLE IF EXISTS embedding_reuse_v1;");
|
|
238
241
|
let removedEmbeddings = 0;
|
|
239
242
|
if (stripEmbeddings) {
|
|
240
243
|
removedEmbeddings =
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* preserves the engine's existing typed `query<Row, Params>()` contract while
|
|
7
7
|
* using Node's native `DatabaseSync` implementation underneath.
|
|
8
8
|
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
9
11
|
import { DatabaseSync } from "node:sqlite";
|
|
10
12
|
/**
|
|
11
13
|
* knodin's call sites pass bind params either spread (`stmt.all(a, b)`) or as a
|
|
@@ -42,8 +44,50 @@ class Statement {
|
|
|
42
44
|
}
|
|
43
45
|
export class Database {
|
|
44
46
|
db;
|
|
47
|
+
filePath;
|
|
48
|
+
openedFileIdentity;
|
|
45
49
|
constructor(databasePath, options = {}) {
|
|
46
50
|
this.db = new DatabaseSync(databasePath, { readOnly: options.readonly === true });
|
|
51
|
+
this.filePath = databasePath && databasePath !== ":memory:" ? path.resolve(databasePath) : null;
|
|
52
|
+
try {
|
|
53
|
+
const stat = this.filePath ? fs.statSync(this.filePath, { bigint: true }) : null;
|
|
54
|
+
this.openedFileIdentity = stat ? `${stat.dev}:${stat.ino}` : null;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
this.openedFileIdentity = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Read-only native capability. Unknown runtimes must use byte-scanning validation. */
|
|
61
|
+
readSnapshotIdentity() {
|
|
62
|
+
let transaction = null;
|
|
63
|
+
try {
|
|
64
|
+
const nativeState = this.db.isTransaction;
|
|
65
|
+
if (typeof nativeState === "boolean")
|
|
66
|
+
transaction = nativeState;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
/* Closed or unsupported native handle. */
|
|
70
|
+
}
|
|
71
|
+
if (!this.filePath)
|
|
72
|
+
return { transaction, fileStamp: "memory", replaced: false };
|
|
73
|
+
try {
|
|
74
|
+
const stat = fs.statSync(this.filePath, { bigint: true });
|
|
75
|
+
const identity = `${stat.dev}:${stat.ino}`;
|
|
76
|
+
return {
|
|
77
|
+
transaction,
|
|
78
|
+
fileStamp: this.openedFileIdentity
|
|
79
|
+
? `${identity}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`
|
|
80
|
+
: null,
|
|
81
|
+
replaced: this.openedFileIdentity !== null && identity !== this.openedFileIdentity,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return {
|
|
86
|
+
transaction,
|
|
87
|
+
fileStamp: null,
|
|
88
|
+
replaced: error.code === "ENOENT",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
47
91
|
}
|
|
48
92
|
query(sql) {
|
|
49
93
|
return new Statement(this.db.prepare(sql));
|