@happyvertical/smrt-scanner 0.43.10 → 0.44.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.
- package/AGENTS.md +146 -1
- package/dist/chunks/{scanner-CW9g-vyS.js → scanner-C3VqXyzB.js} +815 -19
- package/dist/chunks/scanner-C3VqXyzB.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +261 -0
- package/dist/index.js +3 -3
- package/dist/types.d.ts +126 -0
- package/package.json +1 -1
- package/dist/chunks/scanner-CW9g-vyS.js.map +0 -1
|
@@ -1,7 +1,681 @@
|
|
|
1
|
-
import { isAbsolute, resolve, sep, win32 } from "node:path";
|
|
2
|
-
import fg from "fast-glob";
|
|
3
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep, win32 } from "node:path";
|
|
3
|
+
import fg from "fast-glob";
|
|
4
4
|
import { parseSync } from "oxc-parser";
|
|
5
|
+
//#region src/source-location.ts
|
|
6
|
+
function getLineColumn(sourceText, offset) {
|
|
7
|
+
if (offset < 0 || offset > sourceText.length) return;
|
|
8
|
+
let line = 1;
|
|
9
|
+
let lastNewlinePos = -1;
|
|
10
|
+
for (let i = 0; i < offset; i++) if (sourceText[i] === "\n") {
|
|
11
|
+
line++;
|
|
12
|
+
lastNewlinePos = i;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
line,
|
|
16
|
+
column: offset - lastNewlinePos
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/agent-surface.ts
|
|
21
|
+
var HELPER_SPECIFIERS = {
|
|
22
|
+
defineIntent: "@happyvertical/smrt-web/intents",
|
|
23
|
+
definePlaybook: "@happyvertical/smrt-playbooks"
|
|
24
|
+
};
|
|
25
|
+
var HELPER_NAMES = Object.keys(HELPER_SPECIFIERS);
|
|
26
|
+
var ESCAPE_HATCH = "A declaration the scanner cannot read without evaluating it is not emittable — use `useWebMcpTool` for a tool set derived from computed or fetched data.";
|
|
27
|
+
var MAX_LITERAL_DEPTH = 32;
|
|
28
|
+
var INTENT_ID_PATTERN = /^[a-z][a-z0-9]*(?:\.[a-z0-9][a-z0-9_]*)+$/;
|
|
29
|
+
var INTENT_ID_MAX_LENGTH = 128;
|
|
30
|
+
var DESCRIPTION_MAX_LENGTH = 1024;
|
|
31
|
+
var RESERVED_TOOL_NAME_PREFIX = "smrt_ui_";
|
|
32
|
+
var INTENT_DECLARATION_KEYS = /* @__PURE__ */ new Set([
|
|
33
|
+
"id",
|
|
34
|
+
"description",
|
|
35
|
+
"inputSchema",
|
|
36
|
+
"capability",
|
|
37
|
+
"target"
|
|
38
|
+
]);
|
|
39
|
+
var CONTROL_TARGET_KEYS = /* @__PURE__ */ new Set([
|
|
40
|
+
"registry",
|
|
41
|
+
"action",
|
|
42
|
+
"formId",
|
|
43
|
+
"controlId"
|
|
44
|
+
]);
|
|
45
|
+
var DATA_SURFACE_TARGET_KEYS = /* @__PURE__ */ new Set([
|
|
46
|
+
"registry",
|
|
47
|
+
"controlId",
|
|
48
|
+
"surfaceId",
|
|
49
|
+
"kind"
|
|
50
|
+
]);
|
|
51
|
+
var CONTROL_ACTIONS = /* @__PURE__ */ new Set([
|
|
52
|
+
"focus",
|
|
53
|
+
"reveal",
|
|
54
|
+
"highlight",
|
|
55
|
+
"explain",
|
|
56
|
+
"validate",
|
|
57
|
+
"stage",
|
|
58
|
+
"apply",
|
|
59
|
+
"discard",
|
|
60
|
+
"clear",
|
|
61
|
+
"undo"
|
|
62
|
+
]);
|
|
63
|
+
var DATA_SURFACE_KINDS = /* @__PURE__ */ new Set([
|
|
64
|
+
"table",
|
|
65
|
+
"list",
|
|
66
|
+
"report",
|
|
67
|
+
"custom"
|
|
68
|
+
]);
|
|
69
|
+
var PLAYBOOK_PLANES = /* @__PURE__ */ new Set(["browser", "server"]);
|
|
70
|
+
var FAILURE_POLICIES = /* @__PURE__ */ new Set(["abort", "continue"]);
|
|
71
|
+
var QUALIFIED_MODEL_PATTERN = /^\S+:\S+$/;
|
|
72
|
+
function intentToolName(id) {
|
|
73
|
+
return id.replace(/[.-]/g, "_");
|
|
74
|
+
}
|
|
75
|
+
var EXCLUDED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
76
|
+
"node_modules",
|
|
77
|
+
"dist",
|
|
78
|
+
"build",
|
|
79
|
+
"coverage",
|
|
80
|
+
"__tests__",
|
|
81
|
+
"__typechecks__"
|
|
82
|
+
]);
|
|
83
|
+
function isAgentSurfaceSourcePath(filePath, rootDir) {
|
|
84
|
+
if (!/\.(?:ts|tsx|js|jsx)$/.test(filePath)) return false;
|
|
85
|
+
if (filePath.endsWith(".d.ts")) return false;
|
|
86
|
+
if (/\.(?:test|spec)\.(?:ts|tsx|js|jsx)$/.test(filePath)) return false;
|
|
87
|
+
return !isPrunedAgentSurfacePath(filePath, rootDir);
|
|
88
|
+
}
|
|
89
|
+
function isPrunedAgentSurfacePath(filePath, rootDir) {
|
|
90
|
+
let scoped = filePath;
|
|
91
|
+
if (rootDir) {
|
|
92
|
+
const relativePath = relative(rootDir, filePath);
|
|
93
|
+
if (relativePath && !relativePath.startsWith("..")) scoped = relativePath;
|
|
94
|
+
}
|
|
95
|
+
return scoped.split(/[\\/]/).some((segment) => EXCLUDED_DIRECTORIES.has(segment) || segment.startsWith(".") && segment !== "." && segment !== "..");
|
|
96
|
+
}
|
|
97
|
+
function isNode(value) {
|
|
98
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
99
|
+
}
|
|
100
|
+
function collectHelperBindings(body) {
|
|
101
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
102
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
103
|
+
for (const node of body) {
|
|
104
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
105
|
+
const source = node.source;
|
|
106
|
+
if (!source || typeof source.value !== "string") continue;
|
|
107
|
+
const specifier = source.value;
|
|
108
|
+
const specifiers = node.specifiers ?? [];
|
|
109
|
+
for (const spec of specifiers) {
|
|
110
|
+
const local = spec.local?.name;
|
|
111
|
+
if (!local) continue;
|
|
112
|
+
if (spec.type === "ImportSpecifier") {
|
|
113
|
+
const imported = spec.imported?.name;
|
|
114
|
+
const helper = HELPER_NAMES.find((name) => name === imported && HELPER_SPECIFIERS[name] === specifier);
|
|
115
|
+
if (helper) bindings.set(local, helper);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (spec.type === "ImportNamespaceSpecifier") namespaces.set(local, specifier);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const [local, specifier] of namespaces) for (const helper of HELPER_NAMES) if (HELPER_SPECIFIERS[helper] === specifier) bindings.set(`${local}.${helper}`, helper);
|
|
122
|
+
return bindings;
|
|
123
|
+
}
|
|
124
|
+
function resolveCallee(callee, bindings) {
|
|
125
|
+
if (!isNode(callee)) return void 0;
|
|
126
|
+
if (callee.type === "Identifier") return bindings.get(String(callee.name));
|
|
127
|
+
if (callee.type === "MemberExpression" && callee.computed !== true) {
|
|
128
|
+
const object = callee.object;
|
|
129
|
+
const property = callee.property;
|
|
130
|
+
if (isNode(object) && object.type === "Identifier" && isNode(property) && property.type === "Identifier") return bindings.get(`${String(object.name)}.${String(property.name)}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function readLiteral(node, failures, path, depth = 0) {
|
|
134
|
+
if (!isNode(node)) {
|
|
135
|
+
failures.push({ reason: `${path} is not a readable expression` });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (depth > MAX_LITERAL_DEPTH) {
|
|
139
|
+
failures.push({
|
|
140
|
+
reason: `${path} nests deeper than ${MAX_LITERAL_DEPTH} levels`,
|
|
141
|
+
start: node.start
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
switch (node.type) {
|
|
146
|
+
case "Literal": {
|
|
147
|
+
const value = node.value;
|
|
148
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number") return value;
|
|
149
|
+
failures.push({
|
|
150
|
+
reason: `${path} is not a JSON literal`,
|
|
151
|
+
start: node.start
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
case "UnaryExpression": {
|
|
156
|
+
const argument = node.argument;
|
|
157
|
+
if (node.operator === "-" && isNode(argument) && argument.type === "Literal" && typeof argument.value === "number") return -argument.value;
|
|
158
|
+
failures.push({
|
|
159
|
+
reason: `${path} is a computed unary expression`,
|
|
160
|
+
start: node.start
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
case "ArrayExpression": {
|
|
165
|
+
const elements = node.elements ?? [];
|
|
166
|
+
const result = [];
|
|
167
|
+
elements.forEach((element, index) => {
|
|
168
|
+
if (isNode(element) && element.type === "SpreadElement") {
|
|
169
|
+
failures.push({
|
|
170
|
+
reason: `${path}[${index}] is a spread`,
|
|
171
|
+
start: element.start
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (element === null) {
|
|
176
|
+
failures.push({ reason: `${path}[${index}] is an array hole` });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
result.push(readLiteral(element, failures, `${path}[${index}]`, depth + 1));
|
|
180
|
+
});
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
case "ObjectExpression": {
|
|
184
|
+
const properties = node.properties ?? [];
|
|
185
|
+
const result = {};
|
|
186
|
+
for (const property of properties) {
|
|
187
|
+
if (property.type === "SpreadElement") {
|
|
188
|
+
failures.push({
|
|
189
|
+
reason: `${path} contains a spread`,
|
|
190
|
+
start: property.start
|
|
191
|
+
});
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (property.type !== "Property") {
|
|
195
|
+
failures.push({
|
|
196
|
+
reason: `${path} contains an unsupported member`,
|
|
197
|
+
start: property.start
|
|
198
|
+
});
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (property.computed === true || property.shorthand === true) {
|
|
202
|
+
failures.push({
|
|
203
|
+
reason: `${path} contains a ${property.computed === true ? "computed" : "shorthand"} key`,
|
|
204
|
+
start: property.start
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const key = readPropertyKey(property.key);
|
|
209
|
+
if (key === void 0) {
|
|
210
|
+
failures.push({
|
|
211
|
+
reason: `${path} contains a non-literal key`,
|
|
212
|
+
start: property.start
|
|
213
|
+
});
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!isSafeKey(key)) {
|
|
217
|
+
failures.push({
|
|
218
|
+
reason: `${path}.${key} uses a reserved prototype key`,
|
|
219
|
+
start: property.start
|
|
220
|
+
});
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
result[key] = readLiteral(property.value, failures, `${path}.${key}`, depth + 1);
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
case "TSAsExpression":
|
|
228
|
+
case "TSSatisfiesExpression":
|
|
229
|
+
case "TSNonNullExpression":
|
|
230
|
+
case "TSTypeAssertion": return readLiteral(node.expression, failures, path, depth);
|
|
231
|
+
case "Identifier": {
|
|
232
|
+
const name = String(node.name);
|
|
233
|
+
if (name === "undefined") return void 0;
|
|
234
|
+
failures.push({
|
|
235
|
+
reason: `${path} references the identifier \`${name}\``,
|
|
236
|
+
start: node.start
|
|
237
|
+
});
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
case "TemplateLiteral":
|
|
241
|
+
failures.push({
|
|
242
|
+
reason: `${path} is a template literal`,
|
|
243
|
+
start: node.start
|
|
244
|
+
});
|
|
245
|
+
return;
|
|
246
|
+
case "ConditionalExpression":
|
|
247
|
+
failures.push({
|
|
248
|
+
reason: `${path} is a conditional expression`,
|
|
249
|
+
start: node.start
|
|
250
|
+
});
|
|
251
|
+
return;
|
|
252
|
+
default:
|
|
253
|
+
failures.push({
|
|
254
|
+
reason: `${path} is a computed \`${node.type}\``,
|
|
255
|
+
start: node.start
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function readPropertyKey(key) {
|
|
261
|
+
if (!isNode(key)) return void 0;
|
|
262
|
+
if (key.type === "Identifier") return String(key.name);
|
|
263
|
+
if (key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
264
|
+
}
|
|
265
|
+
function isSafeKey(key) {
|
|
266
|
+
return key !== "__proto__" && key !== "constructor" && key !== "prototype";
|
|
267
|
+
}
|
|
268
|
+
function unwrapTypeWrappers(node) {
|
|
269
|
+
let current = node;
|
|
270
|
+
while (isNode(current) && (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression" || current.type === "TSTypeAssertion")) current = current.expression;
|
|
271
|
+
return isNode(current) ? current : void 0;
|
|
272
|
+
}
|
|
273
|
+
function collectModuleScopeCalls(body) {
|
|
274
|
+
const calls = /* @__PURE__ */ new Set();
|
|
275
|
+
const addInitializer = (value) => {
|
|
276
|
+
const init = unwrapTypeWrappers(value);
|
|
277
|
+
if (!init) return;
|
|
278
|
+
if (init.type === "CallExpression") {
|
|
279
|
+
calls.add(init);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (init.type === "ArrayExpression") for (const element of init.elements ?? []) {
|
|
283
|
+
const entry = unwrapTypeWrappers(element);
|
|
284
|
+
if (entry && entry.type === "CallExpression") calls.add(entry);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
const addDeclaration = (declaration) => {
|
|
288
|
+
if (!isNode(declaration)) return;
|
|
289
|
+
if (declaration.type !== "VariableDeclaration") return;
|
|
290
|
+
for (const declarator of declaration.declarations ?? []) addInitializer(declarator.init);
|
|
291
|
+
};
|
|
292
|
+
for (const statement of body) {
|
|
293
|
+
if (statement.type === "ExpressionStatement") {
|
|
294
|
+
const expression = unwrapTypeWrappers(statement.expression);
|
|
295
|
+
if (expression && expression.type === "CallExpression") calls.add(expression);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (statement.type === "VariableDeclaration") {
|
|
299
|
+
addDeclaration(statement);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (statement.type === "ExportNamedDeclaration") {
|
|
303
|
+
addDeclaration(statement.declaration);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (statement.type === "ExportDefaultDeclaration") {
|
|
307
|
+
const declaration = unwrapTypeWrappers(statement.declaration);
|
|
308
|
+
if (declaration && declaration.type === "CallExpression") calls.add(declaration);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return calls;
|
|
312
|
+
}
|
|
313
|
+
function collectMatchedCalls(body, bindings) {
|
|
314
|
+
const moduleScope = collectModuleScopeCalls(body);
|
|
315
|
+
const matches = [];
|
|
316
|
+
const seen = /* @__PURE__ */ new Set();
|
|
317
|
+
const visit = (value) => {
|
|
318
|
+
if (Array.isArray(value)) {
|
|
319
|
+
for (const entry of value) visit(entry);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (!isNode(value) || seen.has(value)) return;
|
|
323
|
+
seen.add(value);
|
|
324
|
+
if (value.type === "CallExpression") {
|
|
325
|
+
const helper = resolveCallee(value.callee, bindings);
|
|
326
|
+
if (helper) matches.push({
|
|
327
|
+
helper,
|
|
328
|
+
node: value,
|
|
329
|
+
moduleScope: moduleScope.has(value)
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
for (const key of Object.keys(value)) {
|
|
333
|
+
if (key === "type" || key === "loc" || key === "range") continue;
|
|
334
|
+
visit(value[key]);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
visit(body);
|
|
338
|
+
matches.sort((a, b) => (a.node.start ?? 0) - (b.node.start ?? 0));
|
|
339
|
+
return matches;
|
|
340
|
+
}
|
|
341
|
+
function resolveCapability(declared) {
|
|
342
|
+
const value = typeof declared === "object" && declared !== null ? declared : {};
|
|
343
|
+
const effect = value.effect;
|
|
344
|
+
return {
|
|
345
|
+
effect: effect === "read" || effect === "write" || effect === "destructive" ? effect : "destructive",
|
|
346
|
+
idempotent: value.idempotent === true,
|
|
347
|
+
openWorld: value.openWorld !== false
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function readString(value) {
|
|
351
|
+
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
352
|
+
}
|
|
353
|
+
var IDENTIFIER_MAX_LENGTH = 256;
|
|
354
|
+
function identifierProblem(value, path) {
|
|
355
|
+
if (typeof value !== "string" || value.length === 0) return `${path} must be a non-empty string`;
|
|
356
|
+
if (value.length > IDENTIFIER_MAX_LENGTH) return `${path} is longer than ${IDENTIFIER_MAX_LENGTH} characters, which \`defineIntent\` rejects`;
|
|
357
|
+
for (const character of value) {
|
|
358
|
+
const code = character.charCodeAt(0);
|
|
359
|
+
if (code < 32 || code === 127) return `${path} contains a control character, which \`defineIntent\` rejects`;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function intentDeclarationProblem(declaration, id, description) {
|
|
363
|
+
const identity = intentIdentityProblem(id);
|
|
364
|
+
if (identity) return identity;
|
|
365
|
+
for (const key of Object.keys(declaration)) if (!INTENT_DECLARATION_KEYS.has(key)) return `view intent '${id}' declares unknown key '${key}'. A declaration is data only \u2014 there is no field for an execute function, a URL, a route, or a fetch, and \`defineIntent\` rejects one.`;
|
|
366
|
+
if (description.length > DESCRIPTION_MAX_LENGTH) return `view intent '${id}' has a description longer than ${DESCRIPTION_MAX_LENGTH} characters, which \`defineIntent\` rejects.`;
|
|
367
|
+
if (declaration.inputSchema !== void 0 && !isPlainRecord(declaration.inputSchema)) return `view intent '${id}' has an inputSchema that is not an object literal, which \`defineIntent\` rejects.`;
|
|
368
|
+
const capability = declaration.capability;
|
|
369
|
+
if (capability !== void 0) {
|
|
370
|
+
if (!isPlainRecord(capability)) return `view intent '${id}' has a capability that is not an object literal, which \`defineIntent\` rejects.`;
|
|
371
|
+
for (const key of Object.keys(capability)) if (key !== "effect" && key !== "idempotent" && key !== "openWorld") return `view intent '${id}' declares unknown capability key '${key}', which \`defineIntent\` rejects.`;
|
|
372
|
+
if (capability.effect !== void 0 && capability.effect !== "read" && capability.effect !== "write" && capability.effect !== "destructive") return `view intent '${id}' declares capability.effect '${String(capability.effect)}'; \`defineIntent\` accepts only read, write, or destructive.`;
|
|
373
|
+
for (const flag of ["idempotent", "openWorld"]) if (capability[flag] !== void 0 && typeof capability[flag] !== "boolean") return `view intent '${id}' declares a non-boolean capability.${flag}, which \`defineIntent\` rejects.`;
|
|
374
|
+
}
|
|
375
|
+
const target = declaration.target;
|
|
376
|
+
const registry = target.registry;
|
|
377
|
+
if (registry !== "control" && registry !== "dataSurface") return `view intent '${id}' targets registry '${String(registry)}'; \`defineIntent\` accepts only 'control' or 'dataSurface'. An intent moves mounted browser state and has no path to REST.`;
|
|
378
|
+
const allowed = registry === "control" ? CONTROL_TARGET_KEYS : DATA_SURFACE_TARGET_KEYS;
|
|
379
|
+
for (const key of Object.keys(target)) if (!allowed.has(key)) return `view intent '${id}' declares unknown target key '${key}' for the '${registry}' registry, which \`defineIntent\` rejects.`;
|
|
380
|
+
if (registry === "control") {
|
|
381
|
+
if (!CONTROL_ACTIONS.has(String(target.action))) return `view intent '${id}' declares control action '${String(target.action)}', which is not a \`ControlInteractionRegistry\` command.`;
|
|
382
|
+
for (const key of ["formId", "controlId"]) {
|
|
383
|
+
if (target[key] === void 0) continue;
|
|
384
|
+
const problem = identifierProblem(target[key], `target.${key}`);
|
|
385
|
+
if (problem) return `view intent '${id}': ${problem}.`;
|
|
386
|
+
}
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (!isNonEmptyString(target.controlId)) return `view intent '${id}' targets the dataSurface registry without a \`controlId\`, which \`defineIntent\` requires.`;
|
|
390
|
+
for (const key of ["controlId", "surfaceId"]) {
|
|
391
|
+
if (target[key] === void 0) continue;
|
|
392
|
+
const problem = identifierProblem(target[key], `target.${key}`);
|
|
393
|
+
if (problem) return `view intent '${id}': ${problem}.`;
|
|
394
|
+
}
|
|
395
|
+
if (target.kind !== void 0 && !DATA_SURFACE_KINDS.has(String(target.kind))) return `view intent '${id}' declares data-surface kind '${String(target.kind)}', which \`defineIntent\` rejects.`;
|
|
396
|
+
}
|
|
397
|
+
function isNonEmptyString(value) {
|
|
398
|
+
return typeof value === "string" && value.length > 0;
|
|
399
|
+
}
|
|
400
|
+
function isPlainRecord(value) {
|
|
401
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
402
|
+
}
|
|
403
|
+
function playbookDeclarationProblem(declaration, key, steps) {
|
|
404
|
+
if (steps.length === 0) return `playbook '${key}' declares no steps; \`definePlaybook\` requires at least one.`;
|
|
405
|
+
const planes = declaration.planes;
|
|
406
|
+
if (planes !== void 0 && planes !== null) {
|
|
407
|
+
if (!Array.isArray(planes)) return `playbook '${key}' declares planes that are not an array, which \`definePlaybook\` rejects.`;
|
|
408
|
+
for (const plane of planes) if (!PLAYBOOK_PLANES.has(String(plane))) return `playbook '${key}' declares unknown plane '${String(plane)}'; expected 'browser' or 'server'.`;
|
|
409
|
+
if (planes.length === 0) return `playbook '${key}' declares an empty planes list; \`definePlaybook\` requires at least one, and defaulting it here would assert a validity the author never declared.`;
|
|
410
|
+
}
|
|
411
|
+
const onStepFailure = declaration.onStepFailure;
|
|
412
|
+
if (onStepFailure !== void 0 && !FAILURE_POLICIES.has(String(onStepFailure))) return `playbook '${key}' declares onStepFailure '${String(onStepFailure)}'; \`definePlaybook\` accepts only 'abort' or 'continue'.`;
|
|
413
|
+
const enabled = declaration.enabled;
|
|
414
|
+
if (enabled !== void 0 && typeof enabled !== "boolean") return `playbook '${key}' declares a non-boolean \`enabled\`, which \`definePlaybook\` rejects rather than coercing \u2014 a truthy '"false"' would otherwise read as enabled.`;
|
|
415
|
+
for (const step of declaration.steps ?? []) {
|
|
416
|
+
const record = step;
|
|
417
|
+
if (record.kind === "operation" && !QUALIFIED_MODEL_PATTERN.test(String(record.model))) return `playbook '${key}' has a step whose model '${String(record.model)}' is not a qualified pair such as '@happyvertical/smrt-commerce:Order'.`;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function intentIdentityProblem(id) {
|
|
421
|
+
if (id.length > INTENT_ID_MAX_LENGTH) return `intent id '${id}' is longer than ${INTENT_ID_MAX_LENGTH} characters, which \`defineIntent\` rejects.`;
|
|
422
|
+
if (!INTENT_ID_PATTERN.test(id)) return `intent id '${id}' must be lowercase and namespaced with at least one dot, e.g. 'orders.filter_by_status' \u2014 \`defineIntent\` rejects it as written, so emitting it would advertise an operation that can never register.`;
|
|
423
|
+
if (intentToolName(id).startsWith(RESERVED_TOOL_NAME_PREFIX)) return `intent id '${id}' resolves into the reserved '${RESERVED_TOOL_NAME_PREFIX}' namespace of the six fixed UI tools, which \`defineIntent\` rejects.`;
|
|
424
|
+
}
|
|
425
|
+
function normalizeSteps(value) {
|
|
426
|
+
if (!Array.isArray(value)) return void 0;
|
|
427
|
+
const steps = [];
|
|
428
|
+
for (const entry of value) {
|
|
429
|
+
if (typeof entry !== "object" || entry === null) return void 0;
|
|
430
|
+
const step = entry;
|
|
431
|
+
if (step.kind === "operation") {
|
|
432
|
+
const model = readString(step.model);
|
|
433
|
+
const action = readString(step.action);
|
|
434
|
+
if (!model || !action) return void 0;
|
|
435
|
+
steps.push({
|
|
436
|
+
kind: "operation",
|
|
437
|
+
model,
|
|
438
|
+
action
|
|
439
|
+
});
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (step.kind === "intent") {
|
|
443
|
+
const id = readString(step.id);
|
|
444
|
+
if (!id) return void 0;
|
|
445
|
+
steps.push({
|
|
446
|
+
kind: "intent",
|
|
447
|
+
id
|
|
448
|
+
});
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
return steps;
|
|
454
|
+
}
|
|
455
|
+
function normalizePlanes(value) {
|
|
456
|
+
if (!Array.isArray(value)) return [];
|
|
457
|
+
return value.filter((entry) => entry === "browser" || entry === "server").sort();
|
|
458
|
+
}
|
|
459
|
+
function sourceMayDeclareAgentSurface(sourceText) {
|
|
460
|
+
return HELPER_NAMES.some((helper) => sourceText.includes(helper));
|
|
461
|
+
}
|
|
462
|
+
function extractAgentSurface(options) {
|
|
463
|
+
const { sourceText, filePath } = options;
|
|
464
|
+
const body = options.body.filter(isNode);
|
|
465
|
+
const intents = [];
|
|
466
|
+
const playbooks = [];
|
|
467
|
+
const diagnostics = [];
|
|
468
|
+
const bindings = collectHelperBindings(body);
|
|
469
|
+
if (bindings.size === 0) return {
|
|
470
|
+
intents,
|
|
471
|
+
playbooks,
|
|
472
|
+
diagnostics
|
|
473
|
+
};
|
|
474
|
+
const report = (code, helper, message, start) => {
|
|
475
|
+
const loc = start === void 0 ? void 0 : getLineColumn(sourceText, start);
|
|
476
|
+
diagnostics.push({
|
|
477
|
+
code,
|
|
478
|
+
helper,
|
|
479
|
+
message,
|
|
480
|
+
filePath,
|
|
481
|
+
line: loc?.line,
|
|
482
|
+
column: loc?.column
|
|
483
|
+
});
|
|
484
|
+
};
|
|
485
|
+
for (const match of collectMatchedCalls(body, bindings)) {
|
|
486
|
+
const { helper, node } = match;
|
|
487
|
+
if (!match.moduleScope) {
|
|
488
|
+
report("not-module-scope", helper, `\`${helper}()\` must be called at module scope to be emitted into the manifest and knowledge graph. ${ESCAPE_HATCH}`, node.start);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
const args = node.arguments ?? [];
|
|
492
|
+
if (args.length !== 1) {
|
|
493
|
+
report("argument-count", helper, `\`${helper}()\` takes exactly one object-literal argument; found ${args.length}. ${ESCAPE_HATCH}`, node.start);
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const argument = unwrapTypeWrappers(args[0]);
|
|
497
|
+
if (!argument || argument.type !== "ObjectExpression") {
|
|
498
|
+
report("non-literal-argument", helper, `\`${helper}()\` must be called with an object literal, not a ${argument?.type ?? "unknown expression"}. ${ESCAPE_HATCH}`, (argument ?? node).start);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const failures = [];
|
|
502
|
+
const declaration = readLiteral(argument, failures, helper);
|
|
503
|
+
if (failures.length > 0) {
|
|
504
|
+
for (const failure of failures) report("non-literal-argument", helper, `${failure.reason}. ${ESCAPE_HATCH}`, failure.start ?? node.start);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (!declaration) continue;
|
|
508
|
+
if (helper === "defineIntent") {
|
|
509
|
+
const id = readString(declaration.id);
|
|
510
|
+
const description2 = readString(declaration.description);
|
|
511
|
+
const target = declaration.target;
|
|
512
|
+
if (!id || !description2 || typeof target !== "object" || target === null || Array.isArray(target)) {
|
|
513
|
+
report("incomplete-declaration", helper, "a view intent needs a literal `id`, `description`, and `target` to be emitted. A declaration the scanner cannot read without evaluating it is not emittable — use `useWebMcpTool` for a tool set derived from computed or fetched data.", node.start);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const problem = intentDeclarationProblem(declaration, id, description2);
|
|
517
|
+
if (problem) {
|
|
518
|
+
report("invalid-identity", helper, problem, node.start);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
intents.push({
|
|
522
|
+
kind: "intent",
|
|
523
|
+
id,
|
|
524
|
+
description: description2,
|
|
525
|
+
capability: resolveCapability(declaration.capability),
|
|
526
|
+
target,
|
|
527
|
+
hasInputSchema: typeof declaration.inputSchema === "object" && declaration.inputSchema !== null,
|
|
528
|
+
planes: ["browser"],
|
|
529
|
+
filePath
|
|
530
|
+
});
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const key = readString(declaration.key);
|
|
534
|
+
const title = readString(declaration.title);
|
|
535
|
+
const description = readString(declaration.description);
|
|
536
|
+
const steps = normalizeSteps(declaration.steps);
|
|
537
|
+
if (!key || !title || !description || !steps) {
|
|
538
|
+
report("incomplete-declaration", helper, `a playbook needs a literal \`key\`, \`title\`, \`description\`, and a \`steps\` array of \`{ kind: "operation", model, action }\` / \`{ kind: "intent", id }\` members to be emitted. ${ESCAPE_HATCH}`, node.start);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const playbookProblem = playbookDeclarationProblem(declaration, key, steps);
|
|
542
|
+
if (playbookProblem) {
|
|
543
|
+
report("invalid-identity", helper, playbookProblem, node.start);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const declaredPlanes = normalizePlanes(declaration.planes);
|
|
547
|
+
playbooks.push({
|
|
548
|
+
kind: "playbook",
|
|
549
|
+
key,
|
|
550
|
+
title,
|
|
551
|
+
description,
|
|
552
|
+
steps,
|
|
553
|
+
planes: declaredPlanes.length > 0 ? declaredPlanes : steps.some((step) => step.kind === "intent") ? ["browser"] : ["browser", "server"],
|
|
554
|
+
planesDeclared: declaredPlanes.length > 0,
|
|
555
|
+
onStepFailure: declaration.onStepFailure === "continue" ? "continue" : "abort",
|
|
556
|
+
enabled: declaration.enabled !== false,
|
|
557
|
+
filePath
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
intents,
|
|
562
|
+
playbooks,
|
|
563
|
+
diagnostics
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function svelteCallOffset(text, helper) {
|
|
567
|
+
const names = /* @__PURE__ */ new Set([helper]);
|
|
568
|
+
const importPattern = new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*['"\`]${escapeRegExp(HELPER_SPECIFIERS[helper])}['"\`]`, "g");
|
|
569
|
+
for (const match of text.matchAll(importPattern)) for (const clause of match[1].split(",")) {
|
|
570
|
+
const alias = clause.trim().match(/^(\w+)\s+as\s+(\w+)$/);
|
|
571
|
+
if (alias && alias[1] === helper) names.add(alias[2]);
|
|
572
|
+
}
|
|
573
|
+
let earliest;
|
|
574
|
+
for (const name of names) {
|
|
575
|
+
const call = new RegExp(`\\b${escapeRegExp(name)}\\s*\\(`, "g");
|
|
576
|
+
for (const match of text.matchAll(call)) if (match.index !== void 0 && (earliest === void 0 || match.index < earliest)) earliest = match.index;
|
|
577
|
+
}
|
|
578
|
+
return earliest;
|
|
579
|
+
}
|
|
580
|
+
function escapeRegExp(value) {
|
|
581
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
582
|
+
}
|
|
583
|
+
function scanSvelteAgentSurface(filePath, sourceText) {
|
|
584
|
+
let text;
|
|
585
|
+
try {
|
|
586
|
+
text = sourceText ?? readFileSync(filePath, "utf-8");
|
|
587
|
+
} catch {
|
|
588
|
+
return [];
|
|
589
|
+
}
|
|
590
|
+
const diagnostics = [];
|
|
591
|
+
for (const helper of HELPER_NAMES) {
|
|
592
|
+
if (!text.includes(HELPER_SPECIFIERS[helper])) continue;
|
|
593
|
+
const callIndex = svelteCallOffset(text, helper);
|
|
594
|
+
if (callIndex === void 0) continue;
|
|
595
|
+
const loc = getLineColumn(text, callIndex);
|
|
596
|
+
const sidecar = helper === "defineIntent" ? "intents" : "playbooks";
|
|
597
|
+
diagnostics.push({
|
|
598
|
+
code: "svelte-declaration",
|
|
599
|
+
helper,
|
|
600
|
+
message: `\`${helper}()\` is called in a .svelte file, which the scanner never reads, so this declaration can never reach the manifest or knowledge graph. Move it to a \`.ts\` sidecar (\`Foo.${sidecar}.ts\`) and import it from the component. ${ESCAPE_HATCH}`,
|
|
601
|
+
filePath,
|
|
602
|
+
line: loc?.line,
|
|
603
|
+
column: loc?.column
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
return diagnostics;
|
|
607
|
+
}
|
|
608
|
+
function compareStrings(a, b) {
|
|
609
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
610
|
+
}
|
|
611
|
+
function mergeAgentSurfaces(surfaces, relativize = (filePath) => filePath) {
|
|
612
|
+
const diagnostics = [];
|
|
613
|
+
const intents = /* @__PURE__ */ new Map();
|
|
614
|
+
const playbooks = /* @__PURE__ */ new Map();
|
|
615
|
+
const claim = (bucket, identity, entry, helper, label) => {
|
|
616
|
+
const existing = bucket.get(identity);
|
|
617
|
+
if (!existing) {
|
|
618
|
+
bucket.set(identity, entry);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const [winner, loser] = entry.filePath < existing.filePath ? [entry, existing] : [existing, entry];
|
|
622
|
+
bucket.set(identity, winner);
|
|
623
|
+
diagnostics.push({
|
|
624
|
+
code: "duplicate-identity",
|
|
625
|
+
helper,
|
|
626
|
+
message: `${label} \`${identity}\` is declared in both \`${winner.filePath}\` and \`${loser.filePath}\`. Identity must be unique across the project; the declaration in the first path is emitted and this one is dropped.`,
|
|
627
|
+
filePath: loser.filePath
|
|
628
|
+
});
|
|
629
|
+
};
|
|
630
|
+
for (const surface of surfaces) {
|
|
631
|
+
for (const intent of surface.intents) claim(intents, intent.id, {
|
|
632
|
+
...intent,
|
|
633
|
+
filePath: relativize(intent.filePath)
|
|
634
|
+
}, "defineIntent", "view intent");
|
|
635
|
+
for (const playbook of surface.playbooks) claim(playbooks, playbook.key, {
|
|
636
|
+
...playbook,
|
|
637
|
+
filePath: relativize(playbook.filePath)
|
|
638
|
+
}, "definePlaybook", "playbook");
|
|
639
|
+
for (const diagnostic of surface.diagnostics) diagnostics.push({
|
|
640
|
+
...diagnostic,
|
|
641
|
+
filePath: relativize(diagnostic.filePath)
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
const sortedIntents = [...intents.values()].sort((a, b) => compareStrings(a.id, b.id) || compareStrings(a.filePath, b.filePath));
|
|
645
|
+
const byToolName = /* @__PURE__ */ new Map();
|
|
646
|
+
const survivingIntents = [];
|
|
647
|
+
for (const intent of sortedIntents) {
|
|
648
|
+
const toolName = intentToolName(intent.id);
|
|
649
|
+
const claimed = byToolName.get(toolName);
|
|
650
|
+
if (!claimed) {
|
|
651
|
+
byToolName.set(toolName, intent);
|
|
652
|
+
survivingIntents.push(intent);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const [winner, loser] = intent.filePath < claimed.filePath ? [intent, claimed] : [claimed, intent];
|
|
656
|
+
byToolName.set(toolName, winner);
|
|
657
|
+
if (winner !== claimed) survivingIntents[survivingIntents.indexOf(claimed)] = winner;
|
|
658
|
+
diagnostics.push({
|
|
659
|
+
code: "duplicate-identity",
|
|
660
|
+
helper: "defineIntent",
|
|
661
|
+
message: `view intents \`${winner.id}\` and \`${loser.id}\` both derive the WebMCP tool name \`${toolName}\`, which \`defineIntent\` rejects at registration. The declaration in \`${winner.filePath}\` is emitted and the one in \`${loser.filePath}\` is dropped.`,
|
|
662
|
+
filePath: loser.filePath
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
intents: survivingIntents.sort((a, b) => compareStrings(a.id, b.id) || compareStrings(a.filePath, b.filePath)),
|
|
667
|
+
playbooks: [...playbooks.values()].sort((a, b) => compareStrings(a.key, b.key) || compareStrings(a.filePath, b.filePath)),
|
|
668
|
+
diagnostics: diagnostics.sort((a, b) => compareStrings(a.filePath, b.filePath) || (a.line ?? 0) - (b.line ?? 0) || (a.column ?? 0) - (b.column ?? 0) || compareStrings(a.code, b.code) || compareStrings(a.message, b.message))
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function emptyAgentSurface() {
|
|
672
|
+
return {
|
|
673
|
+
intents: [],
|
|
674
|
+
playbooks: [],
|
|
675
|
+
diagnostics: []
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
//#endregion
|
|
5
679
|
//#region src/discovery.ts
|
|
6
680
|
var MANDATORY_DISCOVERY_EXCLUDES = Object.freeze([
|
|
7
681
|
"**/node_modules/**",
|
|
@@ -367,19 +1041,6 @@ function getLangFromFilename(filename) {
|
|
|
367
1041
|
if (filename.endsWith(".jsx")) return "jsx";
|
|
368
1042
|
return "js";
|
|
369
1043
|
}
|
|
370
|
-
function getLineColumn(sourceText, offset) {
|
|
371
|
-
if (offset < 0 || offset > sourceText.length) return;
|
|
372
|
-
let line = 1;
|
|
373
|
-
let lastNewlinePos = -1;
|
|
374
|
-
for (let i = 0; i < offset; i++) if (sourceText[i] === "\n") {
|
|
375
|
-
line++;
|
|
376
|
-
lastNewlinePos = i;
|
|
377
|
-
}
|
|
378
|
-
return {
|
|
379
|
-
line,
|
|
380
|
-
column: offset - lastNewlinePos
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
1044
|
function getRange(node) {
|
|
384
1045
|
if (node.range) return node.range;
|
|
385
1046
|
if (node.start !== void 0 && node.end !== void 0) return [node.start, node.end];
|
|
@@ -395,6 +1056,7 @@ function parseFile(filePath) {
|
|
|
395
1056
|
const classes = [];
|
|
396
1057
|
let typeAliases = {};
|
|
397
1058
|
let smrtImports;
|
|
1059
|
+
let agentSurface;
|
|
398
1060
|
try {
|
|
399
1061
|
const sourceText = readFileSync(filePath, "utf-8");
|
|
400
1062
|
const result = parseSync(filePath, sourceText, {
|
|
@@ -425,6 +1087,7 @@ function parseFile(filePath) {
|
|
|
425
1087
|
if (extracted) classes.push(extracted);
|
|
426
1088
|
}
|
|
427
1089
|
reportUnresolvedSpreads(ctx.unresolved, filePath, sourceText, errors);
|
|
1090
|
+
agentSurface = maybeExtractAgentSurface(program, sourceText, filePath);
|
|
428
1091
|
}
|
|
429
1092
|
} catch (error) {
|
|
430
1093
|
errors.push({
|
|
@@ -441,14 +1104,44 @@ function parseFile(filePath) {
|
|
|
441
1104
|
typeAliases
|
|
442
1105
|
};
|
|
443
1106
|
if (smrtImports && smrtImports.size > 0) result2.smrtImports = smrtImports;
|
|
1107
|
+
if (agentSurface) result2.agentSurface = agentSurface;
|
|
444
1108
|
return result2;
|
|
445
1109
|
}
|
|
1110
|
+
function maybeExtractAgentSurface(program, sourceText, filePath) {
|
|
1111
|
+
if (!sourceMayDeclareAgentSurface(sourceText)) return void 0;
|
|
1112
|
+
const surface = extractAgentSurface({
|
|
1113
|
+
body: program.body,
|
|
1114
|
+
sourceText,
|
|
1115
|
+
filePath
|
|
1116
|
+
});
|
|
1117
|
+
return surface.intents.length > 0 || surface.playbooks.length > 0 || surface.diagnostics.length > 0 ? surface : void 0;
|
|
1118
|
+
}
|
|
1119
|
+
function parseAgentSurfaceFile(filePath) {
|
|
1120
|
+
let sourceText;
|
|
1121
|
+
try {
|
|
1122
|
+
sourceText = readFileSync(filePath, "utf-8");
|
|
1123
|
+
} catch {
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
if (!sourceMayDeclareAgentSurface(sourceText)) return void 0;
|
|
1127
|
+
try {
|
|
1128
|
+
const program = parseSync(filePath, sourceText, {
|
|
1129
|
+
lang: getLangFromFilename(filePath),
|
|
1130
|
+
preserveParens: false
|
|
1131
|
+
}).program;
|
|
1132
|
+
if (!program?.body) return void 0;
|
|
1133
|
+
return maybeExtractAgentSurface(program, sourceText, filePath);
|
|
1134
|
+
} catch {
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
446
1138
|
function parseSource(sourceText, filename = "test.ts") {
|
|
447
1139
|
const startTime = performance.now();
|
|
448
1140
|
const errors = [];
|
|
449
1141
|
const classes = [];
|
|
450
1142
|
let typeAliases = {};
|
|
451
1143
|
let smrtImports;
|
|
1144
|
+
let agentSurface;
|
|
452
1145
|
try {
|
|
453
1146
|
const result = parseSync(filename, sourceText, {
|
|
454
1147
|
lang: getLangFromFilename(filename),
|
|
@@ -478,6 +1171,7 @@ function parseSource(sourceText, filename = "test.ts") {
|
|
|
478
1171
|
if (extracted) classes.push(extracted);
|
|
479
1172
|
}
|
|
480
1173
|
reportUnresolvedSpreads(ctx.unresolved, filename, sourceText, errors);
|
|
1174
|
+
agentSurface = maybeExtractAgentSurface(program, sourceText, filename);
|
|
481
1175
|
}
|
|
482
1176
|
} catch (error) {
|
|
483
1177
|
errors.push({
|
|
@@ -494,6 +1188,7 @@ function parseSource(sourceText, filename = "test.ts") {
|
|
|
494
1188
|
typeAliases
|
|
495
1189
|
};
|
|
496
1190
|
if (smrtImports && smrtImports.size > 0) result2.smrtImports = smrtImports;
|
|
1191
|
+
if (agentSurface) result2.agentSurface = agentSurface;
|
|
497
1192
|
return result2;
|
|
498
1193
|
}
|
|
499
1194
|
var FORBIDDEN_OBJECT_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -1951,6 +2646,20 @@ var ManifestAdapter = class ManifestAdapter {
|
|
|
1951
2646
|
//#endregion
|
|
1952
2647
|
//#region src/scanner.ts
|
|
1953
2648
|
var DEFAULT_INCLUDE = ["**/*.ts", "**/*.tsx"];
|
|
2649
|
+
var DEFAULT_SVELTE_INCLUDE = ["**/*.svelte"];
|
|
2650
|
+
var DEFAULT_AGENT_SURFACE_INCLUDE = [
|
|
2651
|
+
"**/*.ts",
|
|
2652
|
+
"**/*.tsx",
|
|
2653
|
+
"**/*.js",
|
|
2654
|
+
"**/*.jsx"
|
|
2655
|
+
];
|
|
2656
|
+
var AGENT_SURFACE_PRUNE = [
|
|
2657
|
+
"**/dist/**",
|
|
2658
|
+
"**/build/**",
|
|
2659
|
+
"**/coverage/**",
|
|
2660
|
+
"**/__tests__/**",
|
|
2661
|
+
"**/__typechecks__/**"
|
|
2662
|
+
];
|
|
1954
2663
|
var DEFAULT_EXCLUDE = [
|
|
1955
2664
|
"**/node_modules/**",
|
|
1956
2665
|
"**/dist/**",
|
|
@@ -1984,7 +2693,10 @@ var OxcScanner = class {
|
|
|
1984
2693
|
includePrivateMethods: options.includePrivateMethods ?? false,
|
|
1985
2694
|
includeStaticMethods: options.includeStaticMethods ?? true,
|
|
1986
2695
|
externalManifests: options.externalManifests || /* @__PURE__ */ new Map(),
|
|
1987
|
-
followSymbolicLinks: options.followSymbolicLinks ?? false
|
|
2696
|
+
followSymbolicLinks: options.followSymbolicLinks ?? false,
|
|
2697
|
+
agentSurface: options.agentSurface ?? true,
|
|
2698
|
+
svelteInclude: options.svelteInclude || DEFAULT_SVELTE_INCLUDE,
|
|
2699
|
+
agentSurfaceInclude: options.agentSurfaceInclude || DEFAULT_AGENT_SURFACE_INCLUDE
|
|
1988
2700
|
};
|
|
1989
2701
|
this.resolver = new InheritanceResolver({
|
|
1990
2702
|
baseClasses: this.options.baseClasses,
|
|
@@ -2020,12 +2732,20 @@ var OxcScanner = class {
|
|
|
2020
2732
|
errors: [],
|
|
2021
2733
|
totalParseTimeMs: performance.now() - startTime,
|
|
2022
2734
|
fileCount: files.length,
|
|
2023
|
-
typeAliases: {}
|
|
2735
|
+
typeAliases: {},
|
|
2736
|
+
agentSurface: emptyAgentSurface()
|
|
2024
2737
|
};
|
|
2738
|
+
const surfaces = [];
|
|
2025
2739
|
for (const file of fileResults) {
|
|
2026
2740
|
for (const classDef of file.classes) results.classes.push(classDef);
|
|
2027
2741
|
for (const error of file.errors) results.errors.push(error);
|
|
2028
2742
|
Object.assign(results.typeAliases, file.typeAliases);
|
|
2743
|
+
if (file.agentSurface && isAgentSurfaceSourcePath(file.filePath, this.options.cwd)) surfaces.push(file.agentSurface);
|
|
2744
|
+
}
|
|
2745
|
+
if (this.options.agentSurface) {
|
|
2746
|
+
surfaces.push(...await this.scanDeclarationsOutsideClassGlob(new Set(files)));
|
|
2747
|
+
surfaces.push(await this.scanSvelteDeclarations());
|
|
2748
|
+
results.agentSurface = mergeAgentSurfaces(surfaces, (filePath) => this.relativizeSourcePath(filePath));
|
|
2029
2749
|
}
|
|
2030
2750
|
this.resolver.addClasses(results.classes);
|
|
2031
2751
|
this.scanResults = results;
|
|
@@ -2172,6 +2892,82 @@ var OxcScanner = class {
|
|
|
2172
2892
|
});
|
|
2173
2893
|
}
|
|
2174
2894
|
/**
|
|
2895
|
+
* Find declarations in files the CLASS scan did not cover.
|
|
2896
|
+
*
|
|
2897
|
+
* The class `include` is routinely narrowed to where models live, but an
|
|
2898
|
+
* intent sidecar lives beside its component. Without this pass those
|
|
2899
|
+
* declarations would be missing from every artifact with no diagnostic — a
|
|
2900
|
+
* silent omission, and in the shipped SvelteKit template's own layout at
|
|
2901
|
+
* that. Files already parsed by the class scan are skipped so a declaration
|
|
2902
|
+
* is never counted twice and cannot collide with itself.
|
|
2903
|
+
*
|
|
2904
|
+
* @param alreadyScanned - Absolute paths the class scan already parsed.
|
|
2905
|
+
*/
|
|
2906
|
+
async scanDeclarationsOutsideClassGlob(alreadyScanned) {
|
|
2907
|
+
if (this.options.agentSurfaceInclude.length === 0) return [];
|
|
2908
|
+
let files;
|
|
2909
|
+
try {
|
|
2910
|
+
files = await discoverSourceFiles({
|
|
2911
|
+
cwd: this.options.cwd,
|
|
2912
|
+
include: this.options.agentSurfaceInclude,
|
|
2913
|
+
exclude: [...this.options.exclude, ...AGENT_SURFACE_PRUNE],
|
|
2914
|
+
followSymbolicLinks: this.options.followSymbolicLinks
|
|
2915
|
+
});
|
|
2916
|
+
} catch {
|
|
2917
|
+
return [];
|
|
2918
|
+
}
|
|
2919
|
+
const surfaces = [];
|
|
2920
|
+
for (const filePath of files) {
|
|
2921
|
+
if (alreadyScanned.has(filePath)) continue;
|
|
2922
|
+
if (!isAgentSurfaceSourcePath(filePath, this.options.cwd)) continue;
|
|
2923
|
+
const surface = parseAgentSurfaceFile(filePath);
|
|
2924
|
+
if (surface) surfaces.push(surface);
|
|
2925
|
+
}
|
|
2926
|
+
return surfaces;
|
|
2927
|
+
}
|
|
2928
|
+
/**
|
|
2929
|
+
* Search `.svelte` files for declarations the scanner can never read.
|
|
2930
|
+
*
|
|
2931
|
+
* A `.svelte` file is not a TypeScript program and is never parsed here, so
|
|
2932
|
+
* this pass produces diagnostics only — never an emitted entry. It exists
|
|
2933
|
+
* because the alternative is silence: an intent declared inline in a
|
|
2934
|
+
* component simply would not appear anywhere, with nothing to explain why.
|
|
2935
|
+
*/
|
|
2936
|
+
async scanSvelteDeclarations() {
|
|
2937
|
+
const surface = emptyAgentSurface();
|
|
2938
|
+
if (this.options.svelteInclude.length === 0) return surface;
|
|
2939
|
+
const exclude = [...this.options.exclude.filter((pattern) => !pattern.endsWith(".svelte")), ...AGENT_SURFACE_PRUNE];
|
|
2940
|
+
let files;
|
|
2941
|
+
try {
|
|
2942
|
+
files = await discoverSourceFiles({
|
|
2943
|
+
cwd: this.options.cwd,
|
|
2944
|
+
include: this.options.svelteInclude,
|
|
2945
|
+
exclude,
|
|
2946
|
+
followSymbolicLinks: this.options.followSymbolicLinks
|
|
2947
|
+
});
|
|
2948
|
+
} catch {
|
|
2949
|
+
return surface;
|
|
2950
|
+
}
|
|
2951
|
+
for (const filePath of files) {
|
|
2952
|
+
if (isPrunedAgentSurfacePath(filePath, this.options.cwd)) continue;
|
|
2953
|
+
surface.diagnostics.push(...scanSvelteAgentSurface(filePath));
|
|
2954
|
+
}
|
|
2955
|
+
return surface;
|
|
2956
|
+
}
|
|
2957
|
+
/**
|
|
2958
|
+
* Record a declaring module as a `cwd`-relative POSIX path.
|
|
2959
|
+
*
|
|
2960
|
+
* Emitted entries land in checked-in artifacts, so an absolute path would
|
|
2961
|
+
* make them machine-specific and a Windows separator would make them
|
|
2962
|
+
* platform-specific — either one churns a snapshot that is supposed to prove
|
|
2963
|
+
* two builds agree.
|
|
2964
|
+
*/
|
|
2965
|
+
relativizeSourcePath(filePath) {
|
|
2966
|
+
const relativePath = relative(this.options.cwd, filePath);
|
|
2967
|
+
if (!relativePath || relativePath.startsWith("..")) return filePath;
|
|
2968
|
+
return sep === "/" ? relativePath : relativePath.split(sep).join("/");
|
|
2969
|
+
}
|
|
2970
|
+
/**
|
|
2175
2971
|
* Parse a single file with timing
|
|
2176
2972
|
*/
|
|
2177
2973
|
async parseFileWithTiming(filePath) {
|
|
@@ -2179,6 +2975,6 @@ var OxcScanner = class {
|
|
|
2179
2975
|
}
|
|
2180
2976
|
};
|
|
2181
2977
|
//#endregion
|
|
2182
|
-
export {
|
|
2978
|
+
export { sourceMayDeclareAgentSurface as _, parseFile as a, discoverSourceFiles as c, emptyAgentSurface as d, extractAgentSurface as f, scanSvelteAgentSurface as g, mergeAgentSurfaces as h, parseAgentSurfaceFile as i, normalizeGlobSeparators as l, isPrunedAgentSurfacePath as m, ManifestAdapter as n, parseSource as o, isAgentSurfaceSourcePath as p, extractSmrtImports as r, InheritanceResolver as s, OxcScanner as t, relativeGlobToCwd as u };
|
|
2183
2979
|
|
|
2184
|
-
//# sourceMappingURL=scanner-
|
|
2980
|
+
//# sourceMappingURL=scanner-C3VqXyzB.js.map
|