nodality 1.1.15 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/bin/mcp-server.mjs +28 -2
  2. package/bin/nodality.js +61 -0
  3. package/dist/animator.cjs.js.LICENSE.txt +1 -1
  4. package/dist/animator.esm.js.LICENSE.txt +1 -1
  5. package/dist/audionew.cjs.js.LICENSE.txt +1 -1
  6. package/dist/audionew.esm.js.LICENSE.txt +1 -1
  7. package/dist/base.cjs.js.LICENSE.txt +1 -1
  8. package/dist/base.esm.js.LICENSE.txt +1 -1
  9. package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
  10. package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
  11. package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
  12. package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
  13. package/dist/bundle.umd.js +1 -1
  14. package/dist/bundle.umd.js.LICENSE.txt +60 -1
  15. package/dist/button.cjs.js.LICENSE.txt +1 -1
  16. package/dist/button.esm.js.LICENSE.txt +1 -1
  17. package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
  18. package/dist/card-getter.esm.js.LICENSE.txt +1 -1
  19. package/dist/center.cjs.js.LICENSE.txt +1 -1
  20. package/dist/center.esm.js.LICENSE.txt +1 -1
  21. package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
  22. package/dist/checkbox.esm.js.LICENSE.txt +1 -1
  23. package/dist/code.cjs.js.LICENSE.txt +1 -1
  24. package/dist/code.esm.js.LICENSE.txt +1 -1
  25. package/dist/container.cjs.js.LICENSE.txt +1 -1
  26. package/dist/container.esm.js.LICENSE.txt +1 -1
  27. package/dist/data-list.cjs.js.LICENSE.txt +1 -1
  28. package/dist/data-list.esm.js.LICENSE.txt +1 -1
  29. package/dist/designer.cjs.js +1 -1
  30. package/dist/designer.cjs.js.LICENSE.txt +60 -1
  31. package/dist/designer.esm.js +1 -1
  32. package/dist/designer.esm.js.LICENSE.txt +60 -1
  33. package/dist/element-mapper.cjs.js +1 -1
  34. package/dist/element-mapper.cjs.js.LICENSE.txt +1 -1
  35. package/dist/element-mapper.esm.js +1 -1
  36. package/dist/element-mapper.esm.js.LICENSE.txt +1 -1
  37. package/dist/finalresult.esm.js +1 -1
  38. package/dist/finalresult.esm.js.LICENSE.txt +60 -1
  39. package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
  40. package/dist/flex-card.esm.js.LICENSE.txt +1 -1
  41. package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
  42. package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
  43. package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
  44. package/dist/flex-row.esm.js.LICENSE.txt +1 -1
  45. package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
  46. package/dist/floating-input.esm.js.LICENSE.txt +1 -1
  47. package/dist/free.cjs.js.LICENSE.txt +1 -1
  48. package/dist/free.esm.js.LICENSE.txt +1 -1
  49. package/dist/horizontal-scroller.cjs.js.LICENSE.txt +1 -1
  50. package/dist/horizontal-scroller.esm.js.LICENSE.txt +1 -1
  51. package/dist/image-picker.cjs.js.LICENSE.txt +1 -1
  52. package/dist/image-picker.esm.js.LICENSE.txt +1 -1
  53. package/dist/image.cjs.js.LICENSE.txt +1 -1
  54. package/dist/image.esm.js.LICENSE.txt +1 -1
  55. package/dist/index.cjs.js +1 -1
  56. package/dist/index.cjs.js.LICENSE.txt +60 -1
  57. package/dist/index.d.ts +1 -1
  58. package/dist/index.esm.js +1 -1
  59. package/dist/index.esm.js.LICENSE.txt +60 -1
  60. package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
  61. package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
  62. package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
  63. package/dist/link-getter.esm.js.LICENSE.txt +1 -1
  64. package/dist/link.cjs.js.LICENSE.txt +1 -1
  65. package/dist/link.esm.js.LICENSE.txt +1 -1
  66. package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
  67. package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
  68. package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
  69. package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
  70. package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
  71. package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
  72. package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
  73. package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
  74. package/dist/picker.cjs.js.LICENSE.txt +1 -1
  75. package/dist/picker.esm.js.LICENSE.txt +1 -1
  76. package/dist/progress.cjs.js.LICENSE.txt +1 -1
  77. package/dist/progress.esm.js.LICENSE.txt +1 -1
  78. package/dist/radio.cjs.js.LICENSE.txt +1 -1
  79. package/dist/radio.esm.js.LICENSE.txt +1 -1
  80. package/dist/range.cjs.js.LICENSE.txt +1 -1
  81. package/dist/range.esm.js.LICENSE.txt +1 -1
  82. package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
  83. package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
  84. package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
  85. package/dist/side-bar.esm.js.LICENSE.txt +1 -1
  86. package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
  87. package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
  88. package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
  89. package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
  90. package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
  91. package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
  92. package/dist/spacer.cjs.js.LICENSE.txt +1 -1
  93. package/dist/spacer.esm.js.LICENSE.txt +1 -1
  94. package/dist/stack.cjs.js.LICENSE.txt +1 -1
  95. package/dist/stack.esm.js.LICENSE.txt +1 -1
  96. package/dist/stacker.cjs.js.LICENSE.txt +1 -1
  97. package/dist/stacker.esm.js.LICENSE.txt +1 -1
  98. package/dist/table.cjs.js.LICENSE.txt +1 -1
  99. package/dist/table.esm.js.LICENSE.txt +1 -1
  100. package/dist/text-field.cjs.js +1 -1
  101. package/dist/text-field.cjs.js.LICENSE.txt +1 -1
  102. package/dist/text-field.esm.js +1 -1
  103. package/dist/text-field.esm.js.LICENSE.txt +1 -1
  104. package/dist/text.cjs.js.LICENSE.txt +1 -1
  105. package/dist/text.esm.js.LICENSE.txt +1 -1
  106. package/dist/theme.cjs.js.LICENSE.txt +1 -1
  107. package/dist/theme.esm.js.LICENSE.txt +1 -1
  108. package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
  109. package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
  110. package/dist/ulist.cjs.js.LICENSE.txt +1 -1
  111. package/dist/ulist.esm.js.LICENSE.txt +1 -1
  112. package/dist/video.cjs.js.LICENSE.txt +1 -1
  113. package/dist/video.esm.js.LICENSE.txt +1 -1
  114. package/dist/wrap.cjs.js.LICENSE.txt +1 -1
  115. package/dist/wrap.esm.js.LICENSE.txt +1 -1
  116. package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
  117. package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
  118. package/layout/animator.js +1 -1
  119. package/layout/audio.js +1 -1
  120. package/layout/audionew.js +1 -1
  121. package/layout/base.js +1 -1
  122. package/layout/beta-desktop-bar.js +1 -1
  123. package/layout/beta-mobile-bar.js +1 -1
  124. package/layout/button.js +1 -1
  125. package/layout/center.js +1 -1
  126. package/layout/checkbox.js +1 -1
  127. package/layout/circle.js +1 -1
  128. package/layout/code.js +1 -1
  129. package/layout/container.js +1 -1
  130. package/layout/dropdown-2025.js +1 -1
  131. package/layout/flex-card.js +1 -1
  132. package/layout/flex-grid.js +1 -1
  133. package/layout/flex-row.js +1 -1
  134. package/layout/form-components/custom.js +1 -1
  135. package/layout/form-components/data-list.js +1 -1
  136. package/layout/form-components/floating-input.js +1 -1
  137. package/layout/form-components/form-all.js +1 -1
  138. package/layout/form-components/form.js +6 -1
  139. package/layout/form-components/image-picker.js +1 -1
  140. package/layout/form-components/picker.js +1 -1
  141. package/layout/form-components/radio.js +1 -1
  142. package/layout/form-components/radiogroup.js +1 -1
  143. package/layout/form-components/range.js +1 -1
  144. package/layout/free.js +1 -1
  145. package/layout/grid-switcher.js +1 -1
  146. package/layout/grid.js +1 -1
  147. package/layout/horizontal-scroller.js +1 -1
  148. package/layout/image.js +1 -1
  149. package/layout/index.js +1 -1
  150. package/layout/link.js +1 -1
  151. package/layout/list.js +1 -1
  152. package/layout/meta-adder.js +1 -1
  153. package/layout/modal-2025.js +1 -1
  154. package/layout/morph.js +1 -1
  155. package/layout/multiswitcher.js +1 -1
  156. package/layout/nav-bar.js +1 -1
  157. package/layout/nav-factor/custom-div.js +1 -1
  158. package/layout/new-nav-bar.js +1 -1
  159. package/layout/polygon.js +1 -1
  160. package/layout/prerender-site.js +1 -1
  161. package/layout/prerender.js +1 -1
  162. package/layout/progress.js +1 -1
  163. package/layout/row.js +1 -1
  164. package/layout/scroll-video.js +1 -1
  165. package/layout/side-bar.js +1 -1
  166. package/layout/side-nav-bar.js +1 -1
  167. package/layout/simple-bar.js +1 -1
  168. package/layout/slider-2025.js +1 -1
  169. package/layout/spacer.js +1 -1
  170. package/layout/stack.js +1 -1
  171. package/layout/svg.js +1 -1
  172. package/layout/switcher.js +1 -1
  173. package/layout/table.js +1 -1
  174. package/layout/text-field.js +13 -1
  175. package/layout/text.js +1 -1
  176. package/layout/ulist.js +1 -1
  177. package/layout/video.js +1 -1
  178. package/layout/wrap.js +1 -1
  179. package/layout/zoom-card.js +1 -1
  180. package/lib/agent-surface.js +347 -0
  181. package/lib/card-getter.js +1 -1
  182. package/lib/codegen.js +1 -1
  183. package/lib/data.js +1 -1
  184. package/lib/designer.js +22 -1
  185. package/lib/element-mapper.js +2 -1
  186. package/lib/keyframe-animation.js +1 -1
  187. package/lib/link-getter.js +1 -1
  188. package/lib/morph-node.js +15 -2
  189. package/lib/raster-inspect.js +1 -1
  190. package/lib/raster-ops.js +1 -1
  191. package/lib/raster-presets.js +1 -1
  192. package/lib/scroll-video.js +1 -1
  193. package/lib/seo.js +1 -1
  194. package/lib/stacker.js +1 -1
  195. package/lib/suggest.js +1 -1
  196. package/lib/theme.js +1 -1
  197. package/lib/transform-anim.js +1 -1
  198. package/lib/transition.js +1 -1
  199. package/lib/validate-nodes.js +79 -2
  200. package/lib/webmcp-adapter.js +307 -0
  201. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.15
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.15
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/morph-node.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.15
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -837,6 +837,13 @@ function setUpMorph(mount, m, resolve) {
837
837
  get active() { return active; },
838
838
  goTo, goToState,
839
839
  back() { if (tl) tl.to(0); },
840
+ // The control's own entry point, published for the agent surface.
841
+ // `back()` above reverses a RUNNING timeline and does nothing at
842
+ // rest; `goBack` is what the back button calls — queued, one
843
+ // press one level, reusing the live pipeline or rebuilding it.
844
+ // A derived go_back tool has to be the same action as the button
845
+ // or the two would unwind differently from the same state.
846
+ goBack,
840
847
  destroy() {
841
848
  if (tl) tl.destroy();
842
849
  for (const p of activeRasterPipelines()) {
@@ -847,4 +854,10 @@ function setUpMorph(mount, m, resolve) {
847
854
  };
848
855
  }
849
856
 
850
- export { applyMorphNodes };
857
+ // `normalizeEdges` is exported for DERIVATION, not for use: the agent
858
+ // surface reads the same edge list the controller runs, so the two can
859
+ // never describe different graphs. Reimplementing the normalisation
860
+ // there would have drifted the first time either side changed — which
861
+ // is precisely how the validator came to accept a `#id` the runtime
862
+ // refused to resolve.
863
+ export { applyMorphNodes, normalizeEdges };
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.15
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/raster-ops.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.15
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.15
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.15
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/seo.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.15
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/stacker.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.15
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/suggest.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.15
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/theme.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.15
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.15
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/transition.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.15
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.15
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -50,11 +50,22 @@ const EDGE_FIELDS = [
50
50
  "from", "to", "effect", "duration", "back", "live", "fade",
51
51
  ];
52
52
 
53
+ /**
54
+ * Every key an `agent-surface` node reads. The node is small on purpose:
55
+ * what it exposes is DERIVED, so there is nothing here to describe the
56
+ * tools with — only which parts of the page may be reached.
57
+ */
58
+ const AGENT_SURFACE_FIELDS = ["op", "name", "forms", "exclude"];
59
+
53
60
  /** Node families, told apart exactly the way the runtime tells them apart. */
54
61
  const familyOf = (node) => {
55
62
  if (!node || typeof node !== "object") return "invalid";
56
63
  const op = node.op;
57
- if (typeof op === "string") return op === "morph" ? "morph" : "raster";
64
+ if (typeof op === "string") {
65
+ if (op === "morph") return "morph";
66
+ if (op === "agent-surface") return "agent-surface";
67
+ return "raster";
68
+ }
58
69
  if (op && typeof op === "object" && typeof op.name === "string") return "design";
59
70
  return "invalid";
60
71
  };
@@ -135,6 +146,72 @@ export function validateNodes(nodes, elements) {
135
146
 
136
147
  if (family === "design") return; // design nodes carry their own vocabulary
137
148
 
149
+ if (family === "agent-surface") {
150
+ // The ids this node names must exist, for the usual reason:
151
+ // a surface that silently exposes nothing is the agentic
152
+ // form of the silent no-op, and an agent has no way to see
153
+ // that the tool it expected was never registered.
154
+ const formIds = [];
155
+ const walkForms = (list) => {
156
+ if (!Array.isArray(list)) return;
157
+ for (const el of list) {
158
+ if (!el || typeof el !== "object") continue;
159
+ if (el.type === "form" && typeof el.id === "string") formIds.push(el.id);
160
+ walkForms(el.children);
161
+ }
162
+ };
163
+ if (idsKnown) walkForms(elements);
164
+
165
+ if (node.name !== undefined && typeof node.name !== "string") {
166
+ push("BAD_FIELD", `${path}.name`, node.name, [],
167
+ ["a string used to prefix every derived tool name"]);
168
+ }
169
+
170
+ for (const key of ["forms", "exclude"]) {
171
+ if (node[key] === undefined) continue;
172
+ if (!Array.isArray(node[key])) {
173
+ push("BAD_FIELD", `${path}.${key}`, node[key], [], ["an array of ids"]);
174
+ continue;
175
+ }
176
+ node[key].forEach((v, k) => {
177
+ const at = `${path}.${key}[${k}]`;
178
+ if (typeof v !== "string") {
179
+ push("BAD_TARGET", at, v, [], ["an element id"]);
180
+ return;
181
+ }
182
+ const bare = v.startsWith("#") ? v.slice(1) : v;
183
+ if (key === "forms") {
184
+ // Checked against FORMS, not against every id: a
185
+ // node naming a heading would otherwise pass here
186
+ // and derive no tool at all.
187
+ if (idsKnown && !formIds.includes(bare)) {
188
+ push("UNKNOWN_FORM", at, v, suggest(bare, formIds), formIds);
189
+ }
190
+ } else if (idsKnown && !ids.includes(bare)) {
191
+ push("UNKNOWN_STATE", at, v, suggest(bare, ids), ids);
192
+ }
193
+ });
194
+ }
195
+
196
+ // A surface with no graph and no forms still derives
197
+ // `read_view`, which is legitimate — a page can be worth
198
+ // reading and nothing else. Reported so that a node written
199
+ // in the expectation of more says so.
200
+ const hasChain = nodes.some((n) => n && n.op === "morph");
201
+ const hasForms = Array.isArray(node.forms) && node.forms.length > 0;
202
+ if (!hasChain && !hasForms) {
203
+ push("EMPTY_SURFACE", path, node, [],
204
+ ["add a { op: \"morph\" } node, or name a form in `forms`"]);
205
+ }
206
+
207
+ for (const key in node) {
208
+ if (AGENT_SURFACE_FIELDS.includes(key)) continue;
209
+ push("UNKNOWN_PARAM", `${path}.${key}`, key,
210
+ suggest(key, AGENT_SURFACE_FIELDS), AGENT_SURFACE_FIELDS);
211
+ }
212
+ return;
213
+ }
214
+
138
215
  if (family === "morph") {
139
216
  // `from` and `to` are what make a hop; without them it is
140
217
  // inert. Factored out because a chain edge has exactly the
@@ -0,0 +1,307 @@
1
+ /*!
2
+ * nodality v1.2.0
3
+ * (c) 2026 Filip Vabrousek
4
+ * License: MIT
5
+ */
6
+
7
+ /*!
8
+ * webmcp-adapter.js — the derived surface, made callable.
9
+ *
10
+ * Everything protocol-shaped in this library lives HERE, in one file,
11
+ * on purpose. WebMCP is six months old, is a Draft Community Group
12
+ * Report rather than a standard, is available only in an origin trial,
13
+ * and has ALREADY moved its entry point once (`navigator.modelContext`
14
+ * → `document.modelContext`). A library that scattered those facts
15
+ * through the designer and the prerenderer would need archaeology every
16
+ * time the draft moved; here the next move is one file and a failing
17
+ * test.
18
+ *
19
+ * The registration itself is a REQUEST, not a guarantee — exactly the
20
+ * posture the live raster backend takes toward HTML-in-Canvas. Where
21
+ * the API is absent (every stock browser today, and jsdom at build
22
+ * time) nothing registers, nothing warns, and the page behaves
23
+ * identically. What still happens everywhere is the DECLARATION: the
24
+ * manifest is written into the document as data, so a crawler, an
25
+ * indexer, or an agent deciding whether to visit at all can read the
26
+ * site's capabilities without executing anything and without the API
27
+ * existing. That half needs no browser support and no standard.
28
+ */
29
+
30
+ import { deriveSurface } from "./agent-surface.js";
31
+
32
+ /**
33
+ * The draft this adapter is written against. Asserted in ONE place so
34
+ * that a spec move is a single-file change with a failing test, and so
35
+ * that every emitted manifest records what it was derived against
36
+ * rather than implying a stability the spec does not yet have.
37
+ */
38
+ export const SPEC_DRAFT = "2026-07-21";
39
+
40
+ /** Where the declaration lives in the document, JSON-LD style. */
41
+ export const MANIFEST_ID = "nodality-agent-manifest";
42
+
43
+ /**
44
+ * The registry, wherever this browser currently keeps it.
45
+ *
46
+ * Both spellings are accepted because the trial spans the rename: the
47
+ * draft moved to `document.modelContext`, Chrome 150 deprecated the
48
+ * older location, and pages served to both are the normal case for the
49
+ * length of an origin trial.
50
+ */
51
+ function registry() {
52
+ if (typeof document !== "undefined" && document.modelContext) return document.modelContext;
53
+ if (typeof navigator !== "undefined" && navigator.modelContext) return navigator.modelContext;
54
+ return null;
55
+ }
56
+
57
+ /**
58
+ * `CSS.escape`, or a serviceable stand-in.
59
+ *
60
+ * jsdom has no `CSS` global, and the build-time pass runs there — the
61
+ * unguarded call threw, was swallowed by the designer's try/catch, and
62
+ * every prerendered page silently shipped without its declaration. The
63
+ * fallback escapes what an id may legally contain and a selector may
64
+ * not; ids here come from descriptors the author wrote, not from user
65
+ * input, so this is about correctness on ordinary hyphens and dots
66
+ * rather than about hostile strings.
67
+ */
68
+ const esc = (s) => {
69
+ const str = String(s);
70
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(str);
71
+ return str.replace(/([^a-zA-Z0-9_-])/g, "\\$1");
72
+ };
73
+
74
+ /** Text an agent can read, from the subtree that is actually on screen. */
75
+ function readSubtree(root) {
76
+ if (!root) return { text: "", actions: [] };
77
+ const visible = (el) => {
78
+ const r = el.getBoundingClientRect();
79
+ if (!(r.width > 0 && r.height > 0)) return false;
80
+ const cs = getComputedStyle(el);
81
+ return cs.visibility !== "hidden" && cs.display !== "none" && cs.opacity !== "0";
82
+ };
83
+ const lines = [];
84
+ for (const el of root.querySelectorAll("h1,h2,h3,h4,h5,h6,p,li")) {
85
+ if (!visible(el)) continue;
86
+ const t = (el.textContent || "").replace(/\s+/g, " ").trim();
87
+ if (!t) continue;
88
+ const tag = el.tagName.toLowerCase();
89
+ lines.push(/^h[1-6]$/.test(tag) ? `${"#".repeat(+tag[1])} ${t}` : t);
90
+ }
91
+ const actions = [];
92
+ for (const el of root.querySelectorAll("a,button,[role=button]")) {
93
+ if (!visible(el)) continue;
94
+ const t = (el.textContent || "").replace(/\s+/g, " ").trim();
95
+ if (t && !actions.includes(t)) actions.push(t);
96
+ }
97
+ return { text: lines.join("\n"), actions };
98
+ }
99
+
100
+ /** A structured refusal, in the shape every other report in this library uses. */
101
+ const refuse = (code, got, valid) => ({ ok: false, code, got, valid });
102
+
103
+ /**
104
+ * Give a derived surface its handlers and offer it to the browser.
105
+ *
106
+ * @param {object} args
107
+ * @param {Element} args.mount the rendered mount
108
+ * @param {Array} args.elements E
109
+ * @param {Array} args.nodes N
110
+ * @param {Array} args.morphs handles returned by applyMorphNodes
111
+ * @returns {{manifest: object, tools: object[], registered: boolean,
112
+ * destroy: function}|null}
113
+ */
114
+ export function installAgentSurface({ mount, elements, nodes, morphs }) {
115
+ const derived = deriveSurface(elements, nodes, { specDraft: SPEC_DRAFT });
116
+ if (!derived) return null; // opt-in: no node, no surface
117
+
118
+ const { tools, manifest } = derived;
119
+ const handles = Array.isArray(morphs) ? morphs.filter(Boolean) : [];
120
+ const graphFor = (id) => handles.find((h) => h.states && h.states.has(id)) || null;
121
+
122
+ /**
123
+ * The rendered form a descriptor id names, wherever it currently is.
124
+ *
125
+ * A morph state that has not been visited is DETACHED — held by the
126
+ * controller, absent from the document — so a form inside one cannot
127
+ * be found by querying the mount. It is reachable through the state
128
+ * elements the controller holds, and stamping or filling it there
129
+ * works exactly as well: the attributes and values travel with the
130
+ * element when the transition inserts it.
131
+ */
132
+ const findForm = (formId) => {
133
+ const dig = (root) => {
134
+ if (!root || typeof root.querySelector !== "function") return null;
135
+ if (root.id === formId) {
136
+ return root.tagName === "FORM" ? root : root.querySelector("form");
137
+ }
138
+ const host = root.querySelector(`#${esc(formId)}`);
139
+ if (!host) return null;
140
+ return host.tagName === "FORM" ? host : host.querySelector("form");
141
+ };
142
+ const here = dig(mount);
143
+ if (here) return here;
144
+ for (const h of handles) {
145
+ if (!h.states) continue;
146
+ for (const el of h.states.values()) {
147
+ const found = dig(el);
148
+ if (found) return found;
149
+ }
150
+ }
151
+ return null;
152
+ };
153
+
154
+ // ── the declaration, written wherever we are ─────────────────────
155
+ //
156
+ // In jsdom this ends up inside the prerendered HTML, which is the
157
+ // whole point: the static declaration exists because the same code
158
+ // ran at build time, not because a second emitter was written for
159
+ // it. In a browser it is simply inert data in the page.
160
+ if (typeof document !== "undefined") {
161
+ const prev = document.getElementById(MANIFEST_ID);
162
+ if (prev) prev.remove();
163
+ const tag = document.createElement("script");
164
+ tag.type = "application/json";
165
+ tag.id = MANIFEST_ID;
166
+ tag.textContent = JSON.stringify(manifest, null, 2);
167
+ (document.body || document.documentElement).appendChild(tag);
168
+ }
169
+
170
+ // ── the spec's own form annotations ──────────────────────────────
171
+ //
172
+ // Emitted rather than resisted. The draft's declarative path derives
173
+ // a tool from `toolname`/`tooldescription` on a form, and Lighthouse
174
+ // already audits for them; stamping the SAME forms we derive from
175
+ // means the annotated surface and the derived surface describe one
176
+ // thing by construction, and a browser that only implements the
177
+ // declarative path still gets the form.
178
+ for (const t of tools) {
179
+ if (t.kind !== "submit" || !mount) continue;
180
+ const el = findForm(t.formId);
181
+ if (!el) continue;
182
+ el.setAttribute("toolname", t.name);
183
+ el.setAttribute("tooldescription", t.description);
184
+ }
185
+
186
+ // ── handlers ─────────────────────────────────────────────────────
187
+ //
188
+ // Traversals are SERIALISED, and this is an agent-specific hazard
189
+ // rather than a controller bug: a human cannot click twice inside
190
+ // one tick, so the controller's interrupt handling has never had to
191
+ // survive two transitions starting before either has built its
192
+ // pipeline. An agent does it trivially — two executeTool calls, no
193
+ // await — and the observed result was an interface with NOTHING on
194
+ // screen. Queuing here means the second call runs from the state the
195
+ // first actually landed in, which may make it unreachable; that
196
+ // returns a constructive refusal, which is a coherent answer. The
197
+ // controller is left exactly as it is, because for its own callers
198
+ // it is exactly right.
199
+ let queue = Promise.resolve();
200
+ const serial = (fn) => {
201
+ const run = queue.then(fn, fn);
202
+ queue = run.then(() => {}, () => {});
203
+ return run;
204
+ };
205
+
206
+ const execute = {
207
+ navigate: ({ destination } = {}) => serial(async () => {
208
+ const h = graphFor(destination);
209
+ if (!h) {
210
+ return refuse("UNKNOWN_VIEW", destination,
211
+ handles.flatMap((x) => [...x.states.keys()]));
212
+ }
213
+ // Reachability is a property of WHERE THE GRAPH IS, not of
214
+ // the schema, so it is answered here and not by the enum: a
215
+ // destination valid from `work` is invalid from `home`, and
216
+ // the honest answer names what is reachable right now.
217
+ const from = h.current;
218
+ await h.goToState(destination);
219
+ if (h.current !== destination) {
220
+ const edge = [...h.states.keys()];
221
+ return refuse("UNREACHABLE_FROM_HERE", destination,
222
+ edge.filter((id) => id !== from));
223
+ }
224
+ return { ok: true, view: h.current, ...readSubtree(h.stage || mount) };
225
+ }),
226
+
227
+ back: () => serial(async () => {
228
+ const h = handles.find((x) => x.history && x.history.length);
229
+ if (!h) return refuse("NO_HISTORY", null, []);
230
+ await h.goBack();
231
+ return { ok: true, view: h.current, ...readSubtree(h.stage || mount) };
232
+ }),
233
+
234
+ read: async () => ({ ok: true, ...readSubtree(mount) }),
235
+
236
+ submit: async (args = {}, tool) => {
237
+ const form = findForm(tool.formId);
238
+ if (!form) return refuse("NO_FORM", tool.formId, []);
239
+
240
+ const props = (tool.inputSchema && tool.inputSchema.properties) || {};
241
+ const required = (tool.inputSchema && tool.inputSchema.required) || [];
242
+ const missing = required.filter(
243
+ (k) => args[k] === undefined || args[k] === "" || args[k] === null);
244
+ if (missing.length) {
245
+ // The constraint, not a boolean. A generator repairs from
246
+ // this in one turn; "false" costs it a guess.
247
+ return refuse("MISSING_REQUIRED", missing, Object.keys(props));
248
+ }
249
+
250
+ const filled = [];
251
+ for (const key of Object.keys(props)) {
252
+ if (args[key] === undefined) continue;
253
+ const field = form.querySelector(
254
+ `[name="${key}"], #${CSS.escape(key)}`);
255
+ if (!field) continue;
256
+ if (field.type === "checkbox") field.checked = !!args[key];
257
+ else field.value = String(args[key]);
258
+ // Both, because component state may listen for either.
259
+ field.dispatchEvent(new Event("input", { bubbles: true }));
260
+ field.dispatchEvent(new Event("change", { bubbles: true }));
261
+ filled.push(key);
262
+ }
263
+
264
+ // Submitted through the form's OWN path, so its listeners,
265
+ // its validation attributes and the browser's constraint
266
+ // checking all apply. An agent submit is a user submit.
267
+ if (typeof form.requestSubmit === "function") form.requestSubmit();
268
+ else form.submit();
269
+ return { ok: true, submitted: tool.formId, filled };
270
+ },
271
+ };
272
+
273
+ // ── registration, if this browser has anywhere to register ───────
274
+ const reg = registry();
275
+ let registered = false;
276
+ if (reg && typeof reg.registerTool === "function") {
277
+ for (const t of tools) {
278
+ try {
279
+ reg.registerTool({
280
+ name: t.name,
281
+ description: t.description,
282
+ inputSchema: t.inputSchema,
283
+ execute: (args) => execute[t.kind](args, t),
284
+ });
285
+ registered = true;
286
+ } catch (e) {
287
+ // A failed registration must never take the page down.
288
+ console.warn("[nodality] agent surface: tool rejected", t.name, e);
289
+ }
290
+ }
291
+ }
292
+
293
+ return {
294
+ manifest, tools, registered,
295
+ destroy() {
296
+ if (reg && typeof reg.unregisterTool === "function") {
297
+ for (const t of tools) {
298
+ try { reg.unregisterTool(t.name); } catch (e) { /* already gone */ }
299
+ }
300
+ }
301
+ if (typeof document !== "undefined") {
302
+ const tag = document.getElementById(MANIFEST_ID);
303
+ if (tag) tag.remove();
304
+ }
305
+ },
306
+ };
307
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodality",
3
- "version": "1.1.15",
3
+ "version": "1.2.0",
4
4
  "description": "A lightweight library for declarative UI elements.",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.esm.js",