@happyvertical/smrt-scanner 0.43.10 → 0.44.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.
@@ -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 { parseSource as a, normalizeGlobSeparators as c, parseFile as i, relativeGlobToCwd as l, ManifestAdapter as n, InheritanceResolver as o, extractSmrtImports as r, discoverSourceFiles as s, OxcScanner as t };
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-CW9g-vyS.js.map
2980
+ //# sourceMappingURL=scanner-C3VqXyzB.js.map