@happyvertical/smrt-scanner 0.47.0 → 0.47.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 CHANGED
@@ -38,6 +38,9 @@ executes the source.
38
38
  by `OxcScanner` and the core manifest preflight.
39
39
  - `extractAgentSurface` / `scanSvelteAgentSurface` / `mergeAgentSurfaces` —
40
40
  the agent-surface matcher (#2591). See below.
41
+ - `checkAgentSurfaceToolNames(surface, { generatedToolNames, uiToolPrefixes })`
42
+ — report an emitted intent whose derived WebMCP tool name a generated model
43
+ tool or a fixed UI tool already registers (#2725). Takes names, never config.
41
44
  - Types (re-exported from `./types`): `RawClassDefinition`,
42
45
  `RawFieldDefinition`, `RawMethodDefinition`, `ResolvedClassDefinition`,
43
46
  `ScanResults`, `FileScanResult`, `OxcScannerOptions`, `InferredFieldType`,
@@ -79,7 +82,9 @@ build output can retain declarations and otherwise win duplicate resolution.
79
82
  ### What it refuses, always with a diagnostic
80
83
 
81
84
  Never a silent omission — every message names `useWebMcpTool`, the escape hatch
82
- for a tool set genuinely derived from computed or fetched data:
85
+ for a tool set genuinely derived from computed or fetched data. Every code here
86
+ means the declaration was NOT emitted; `tool-name-collision` (below) is the one
87
+ exception and is advisory:
83
88
 
84
89
  | Code | Shape |
85
90
  |---|---|
@@ -90,6 +95,7 @@ for a tool set genuinely derived from computed or fetched data:
90
95
  | `invalid-identity` | a declaration the runtime helper itself would reject: an intent `id` that is not lowercase and dot-namespaced, over 128 chars, or resolving into the reserved `smrt_ui_` namespace; an unknown declaration, target, or capability key; a malformed `capability` (bad `effect`, non-boolean flag); a `target` outside the closed `control`/`dataSurface` unions or missing a required `controlId`; a playbook with no steps, an empty/non-array/unknown-plane `planes`, an `onStepFailure` outside `abort`/`continue`, a non-boolean `enabled`, or a step whose `model` is not a qualified pair |
91
96
  | `svelte-declaration` | written inline in a `.svelte` file |
92
97
  | `duplicate-identity` | two modules declare the same `id`/`key`, or two intent ids derive the same WebMCP tool name |
98
+ | `tool-name-collision` | an EMITTED intent whose derived tool name is already registered by a generated model tool or a fixed UI tool (see below) |
93
99
 
94
100
  Mirror `defineIntent` and `definePlaybook` validation without importing their
95
101
  packages; update the scanner whenever those runtime rules tighten. Reject
@@ -120,6 +126,68 @@ cross-profile parity snapshot does not churn on directory order:
120
126
  - paths are recorded `cwd`-relative and POSIX-separated, so a checked-in
121
127
  artifact is neither machine- nor platform-specific.
122
128
 
129
+ ### Collisions with names this pass does not own (#2725)
130
+
131
+ `mergeAgentSurfaces` resolves intent-vs-intent by dropping the loser, because
132
+ `defineIntent` REJECTS the second colliding declaration — only one can exist.
133
+ Two other sources register into the same document and are invisible to the
134
+ declaration scan, so `checkAgentSurfaceToolNames` reports them **without
135
+ dropping anything**:
136
+
137
+ - **generated model tools**, `${className.toLowerCase()}_${action}`, so
138
+ `defineIntent({ id: 'product.list' })` lands on `Product.list`;
139
+ - **the six fixed UI tools** under the configured `webmcp.ui.prefix`.
140
+
141
+ Warnings, not drops, and the asymmetry is the point: `defineIntent` accepts
142
+ these ids, so the declarations are real and belong in the artifact; the
143
+ document-global tool-name lock (#2613) decides which registration survives,
144
+ rejecting the second with a `WebMcpToolNameCollisionError` — a runtime answer to
145
+ a question the build can already see coming, which costs whoever loses its tool;
146
+ and whether it happens at all turns on runtime values no artifact records (a
147
+ WebMCP `namespace`, an `effects` policy, whether a page mounts both). A
148
+ build-time drop would guess all three, and the emitted surface would stop
149
+ matching the source.
150
+
151
+ Because those values are unknowable here, the generated-tool message **states
152
+ the precondition it assumes** — no `namespace`, action within the `effects`
153
+ policy — and says to disregard it when either already separates the pair. A
154
+ `namespace` prefixes generated tools and leaves intents alone, so it dissolves
155
+ the collision outright; without that sentence a namespaced app would get a
156
+ notice that is always wrong where it fires, recommending the remedy it had
157
+ already applied. `smrt doctor`'s header is `Tool name also claimed` for the same
158
+ reason: it must not assert past what the message says.
159
+
160
+ **It takes NAMES, never config.** The caller passes the tool names that will
161
+ really register; this module never derives a generated name and never applies a
162
+ namespace. `namespace` and `ui.prefix` are runtime `<Provider webmcp={…}>`
163
+ values that no build artifact records, so a build-time declaration of either
164
+ would be a second place to say what the provider already says, free to diverge
165
+ silently. Core supplies the names from `buildWebMcpToolDefinitions` — the same
166
+ function that emits the runtime `webMcpToolDefinitions`, and therefore already
167
+ filtered by the exposure policy. Comparing against every verb a class *could*
168
+ expose would invent collisions with tools nobody mounts.
169
+
170
+ `uiToolPrefixes` is supplied for the same reason, defaulting to the registrar's
171
+ `smrt_ui_`. Do NOT replace it with a prefix derived from the name: quantifying
172
+ over every prefix an app could have configured has, under the default, no true
173
+ positive at all — an id flattening to `smrt_ui_*` is rejected during extraction
174
+ — so every diagnostic it emits for a default-configured app is false, persisted
175
+ into the artifact and clearable only by renaming a correct intent. There is no
176
+ build-time source for `ui.prefix` (a runtime `<Provider>` prop), so this half is
177
+ dormant by default and the seam waits for a caller that can fill it.
178
+
179
+ `RESERVED_TOOL_NAME_PREFIX` is **not** configurable and must not become so — it
180
+ mirrors `defineIntent`'s own literal, which rejects the id wherever an app moved
181
+ its UI tools, so accepting one here would emit an entry the runtime refuses to
182
+ construct.
183
+
184
+ Consumers: core's Vite plugin runs the check once the manifest exists and
185
+ appends the diagnostics to the emitted surface; `dev:knowledge-check` maps the
186
+ code to the `agent-surface-tool-name-collision` warning and **excludes it from
187
+ the artifact-drift comparison**, which re-derives declarations from source and
188
+ has no manifest to produce one from; `smrt doctor` prints it under
189
+ `⚠️ Tool name also claimed`, separately from `Not statically emittable`.
190
+
123
191
  The scanner mirrors the #2587 capability vocabulary structurally instead of
124
192
  importing `@happyvertical/smrt-types`: core depends on this package, so the
125
193
  reverse edge would close a cycle. Core reconciles the two shapes in exactly one
@@ -160,7 +228,8 @@ place, `toKnowledgeAgentSurface` in `vite-plugin/index.ts`.
160
228
  field-type inference.
161
229
  - `src/verify-completeness.ts` — `verifyManifestCompleteness` publish guard.
162
230
  - `src/agent-surface.ts` — the `defineIntent` / `definePlaybook` matcher, its
163
- diagnostics, and `mergeAgentSurfaces` (#2591).
231
+ diagnostics, `mergeAgentSurfaces` (#2591), and `checkAgentSurfaceToolNames`
232
+ (#2725).
164
233
  - `src/source-location.ts` — `getLineColumn`, split out so `agent-surface.ts`
165
234
  can resolve a diagnostic's position without importing `oxc-parser.ts`, which
166
235
  imports it back.
@@ -29,6 +29,16 @@ var INTENT_ID_PATTERN = /^[a-z][a-z0-9]*(?:\.[a-z0-9][a-z0-9_]*)+$/;
29
29
  var INTENT_ID_MAX_LENGTH = 128;
30
30
  var DESCRIPTION_MAX_LENGTH = 1024;
31
31
  var RESERVED_TOOL_NAME_PREFIX = "smrt_ui_";
32
+ var FIXED_UI_TOOL_SUFFIXES = [
33
+ "execute_data_surface_control",
34
+ "execute_form_control",
35
+ "inspect_data_surface",
36
+ "inspect_form_control",
37
+ "list_data_surfaces",
38
+ "list_form_controls"
39
+ ];
40
+ var UI_TOOL_PREFIX_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
41
+ var DEFAULT_UI_TOOL_PREFIX = "smrt_ui_";
32
42
  var INTENT_DECLARATION_KEYS = /* @__PURE__ */ new Set([
33
43
  "id",
34
44
  "description",
@@ -668,6 +678,37 @@ function mergeAgentSurfaces(surfaces, relativize = (filePath) => filePath) {
668
678
  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
679
  };
670
680
  }
681
+ function checkAgentSurfaceToolNames(surface, options = {}) {
682
+ const diagnostics = [];
683
+ const generated = /* @__PURE__ */ new Map();
684
+ for (const tool of options.generatedToolNames ?? []) if (!generated.has(tool.name)) generated.set(tool.name, tool);
685
+ const uiTools = /* @__PURE__ */ new Map();
686
+ for (const prefix of options.uiToolPrefixes ?? [DEFAULT_UI_TOOL_PREFIX]) {
687
+ if (!UI_TOOL_PREFIX_PATTERN.test(prefix)) continue;
688
+ for (const suffix of FIXED_UI_TOOL_SUFFIXES) if (!uiTools.has(`${prefix}${suffix}`)) uiTools.set(`${prefix}${suffix}`, prefix);
689
+ }
690
+ for (const intent of surface.intents) {
691
+ const toolName = intentToolName(intent.id);
692
+ const collidingTool = generated.get(toolName);
693
+ if (collidingTool) {
694
+ const owner = collidingTool.declaredBy ? `\`${collidingTool.declaredBy}\`` : "a generated model action";
695
+ diagnostics.push({
696
+ code: "tool-name-collision",
697
+ helper: "defineIntent",
698
+ message: `view intent \`${intent.id}\` derives the WebMCP tool name \`${toolName}\`, which is also the generated model tool for ${owner}. Both are emitted \u2014 \`defineIntent\` accepts the id, so the declaration is real. On a page that mounts both with no WebMCP \`namespace\`, the document-global tool-name lock rejects whichever registers second with a \`WebMcpToolNameCollisionError\`. A build cannot see the provider's \`namespace\` or \`effects\` policy, so if either already separates this pair, disregard this. Otherwise set a \`namespace\`, which prefixes the generated tools and leaves intents alone, or rename the intent.`,
699
+ filePath: intent.filePath
700
+ });
701
+ }
702
+ const uiPrefix = uiTools.get(toolName);
703
+ if (uiPrefix !== void 0) diagnostics.push({
704
+ code: "tool-name-collision",
705
+ helper: "defineIntent",
706
+ message: `view intent \`${intent.id}\` derives the WebMCP tool name \`${toolName}\`, which is also one of the six fixed UI tools mounted under \`ui.prefix\` \`${uiPrefix}\`. \`defineIntent\` accepts the id because it reserves only the default \`smrt_ui_\` prefix, so the two reach the document-global tool-name lock and whichever registers second is rejected with a \`WebMcpToolNameCollisionError\`. Rename the intent, or give the fixed UI tools a different \`ui.prefix\`.`,
707
+ filePath: intent.filePath
708
+ });
709
+ }
710
+ return diagnostics.sort((a, b) => compareStrings(a.filePath, b.filePath) || compareStrings(a.code, b.code) || compareStrings(a.message, b.message));
711
+ }
671
712
  function emptyAgentSurface() {
672
713
  return {
673
714
  intents: [],
@@ -3095,6 +3136,6 @@ var OxcScanner = class {
3095
3136
  }
3096
3137
  };
3097
3138
  //#endregion
3098
- 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 };
3139
+ export { scanSvelteAgentSurface as _, parseFile as a, discoverSourceFiles as c, checkAgentSurfaceToolNames as d, emptyAgentSurface as f, mergeAgentSurfaces as g, isPrunedAgentSurfacePath as h, parseAgentSurfaceFile as i, normalizeGlobSeparators as l, isAgentSurfaceSourcePath as m, ManifestAdapter as n, parseSource as o, extractAgentSurface as p, extractSmrtImports as r, InheritanceResolver as s, OxcScanner as t, relativeGlobToCwd as u, sourceMayDeclareAgentSurface as v };
3099
3140
 
3100
- //# sourceMappingURL=scanner-9p-PUN-X.js.map
3141
+ //# sourceMappingURL=scanner-CxUeI0FI.js.map