nodality 1.1.14 → 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 +41 -5
  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 +47 -8
  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 +196 -32
  200. package/lib/webmcp-adapter.js +307 -0
  201. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
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.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/morph.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
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.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/nav-bar.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
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.14
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.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/polygon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
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.14
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.14
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.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/row.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
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.14
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.14
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.14
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.14
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.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/spacer.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/stack.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/svg.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
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.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/table.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
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.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -44,6 +44,18 @@ class TextField extends Animator {
44
44
  this.res.setAttribute("type", obj.type ?? "text");
45
45
  //@ placeholder: Placeholder text shown while the field is empty.
46
46
  obj.placeholder && (this.res.placeholder = obj.placeholder);
47
+ //@ name: Form field name. What a submitted form actually SENDS.
48
+ //@ id: DOM id on the input.
49
+ //@ required: Marks the field required for native validation.
50
+ // These were dropped, and `name` is not cosmetic: a field without
51
+ // one contributes nothing to FormData, so every text input in
52
+ // every Nodality form submitted an empty payload — for a human
53
+ // filling it in, not only for an agent. Forwarded here rather
54
+ // than through the style map because they are attributes, not
55
+ // styles.
56
+ obj.name && this.res.setAttribute("name", obj.name);
57
+ obj.id && this.res.setAttribute("id", obj.id);
58
+ obj.required && this.res.setAttribute("required", "");
47
59
  //@ arrayPadding: Padding on named sides. {sides: ["left","top"], value: "1rem"}.
48
60
  //@ arrayMargin: Margin on named sides. {sides: ["left"], value: "1rem"}.
49
61
  obj.pad && this.pad(obj.pad);
package/layout/text.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/ulist.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/video.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/wrap.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
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.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -0,0 +1,347 @@
1
+ /*!
2
+ * nodality v1.2.0
3
+ * (c) 2026 Filip Vabrousek
4
+ * License: MIT
5
+ */
6
+
7
+ /*!
8
+ * agent-surface.js — derive a site's agent-operable surface from (E, N).
9
+ *
10
+ * The premise this file exists to prove: a page's interaction structure
11
+ * is ALREADY data here, so the tool surface an agent operates need not
12
+ * be authored a second time. Every other approach in this space is
13
+ * either hand-written JavaScript registrations or per-element HTML
14
+ * annotations; both restate, by hand, something the pair already says.
15
+ *
16
+ * Three things are derived, and each answers a different half of what
17
+ * an agent can do with a static site:
18
+ *
19
+ * navigate / go_back — the morph chain's VIEW graph. Not routing:
20
+ * Nodality has no router, and between PAGES an
21
+ * agent already has URLs. What it cannot reach
22
+ * by fetching is a chain's non-root states,
23
+ * which are in-page views; the graph is their
24
+ * only machine door.
25
+ * submit_<form> — the one action a fetch cannot perform. The
26
+ * schema is read from the field descriptors,
27
+ * not from annotations added to the DOM.
28
+ * read_view — what is on screen now, as structured text.
29
+ *
30
+ * Pure and synchronous: no DOM, no browser API, no WebMCP. It returns
31
+ * declarations plus the binding facts a runtime adapter needs to hang
32
+ * handlers on them, so the same derivation serves both the live
33
+ * registration and the build-time manifest — they cannot disagree,
34
+ * because there is only one of them.
35
+ *
36
+ * WebMCP is deliberately not named anywhere below. The spec is six
37
+ * months old, in origin trial, and has already moved its entry point
38
+ * once; everything spec-shaped belongs in the adapter, and the only
39
+ * concession here is `specDraft`, which the CALLER supplies and this
40
+ * module merely records.
41
+ */
42
+
43
+ import { normalizeEdges } from "./morph-node.js";
44
+
45
+ /** Descriptor types that carry a value an agent would fill. */
46
+ const FIELD_TYPES = new Set(["input", "labelInput", "checkbox", "radio", "picker"]);
47
+
48
+ /**
49
+ * `inputType` → JSON Schema. Kept small and explicit: a guessed schema
50
+ * is worse than an absent one, because an agent will believe it.
51
+ */
52
+ const INPUT_SCHEMA = {
53
+ text: { type: "string" },
54
+ email: { type: "string", format: "email" },
55
+ tel: { type: "string" },
56
+ url: { type: "string", format: "uri" },
57
+ number: { type: "number" },
58
+ password: { type: "string" },
59
+ date: { type: "string", format: "date" },
60
+ time: { type: "string", format: "time" },
61
+ search: { type: "string" },
62
+ };
63
+
64
+ const isObj = (v) => !!v && typeof v === "object";
65
+
66
+ /** Depth-first walk of a descriptor subtree, children included. */
67
+ function* walk(el) {
68
+ if (!isObj(el)) return;
69
+ yield el;
70
+ const kids = Array.isArray(el.children) ? el.children : [];
71
+ for (const k of kids) yield* walk(k);
72
+ }
73
+
74
+ /** Every descriptor in E, flattened — ids are unique across the tree. */
75
+ function flatten(elements) {
76
+ const out = [];
77
+ for (const el of Array.isArray(elements) ? elements : []) {
78
+ for (const d of walk(el)) out.push(d);
79
+ }
80
+ return out;
81
+ }
82
+
83
+ /**
84
+ * One form field → one JSON Schema property.
85
+ *
86
+ * The property NAME is `name ?? id`: `name` is what a submitted form
87
+ * actually sends, and where a descriptor declares one it is the honest
88
+ * key for an agent to use.
89
+ */
90
+ function fieldSchema(el) {
91
+ const key = typeof el.name === "string" ? el.name : el.id;
92
+ if (typeof key !== "string" || !key) return null;
93
+
94
+ let schema;
95
+ if (el.type === "checkbox") {
96
+ schema = { type: "boolean" };
97
+ } else if (el.type === "radio" || el.type === "picker") {
98
+ const items = Array.isArray(el.items) ? el.items.filter((i) => typeof i === "string") : [];
99
+ schema = items.length ? { type: "string", enum: items } : { type: "string" };
100
+ if (el.type === "radio" && el.multiple === true) {
101
+ schema = { type: "array", items: schema };
102
+ }
103
+ } else {
104
+ // input / labelInput. An unknown inputType degrades to a plain
105
+ // string rather than inventing a format.
106
+ schema = { ...(INPUT_SCHEMA[el.inputType] || INPUT_SCHEMA.text) };
107
+ }
108
+
109
+ // Whatever the descriptor says the field is FOR, in the agent's
110
+ // words. Label first: it is what a human reader is shown.
111
+ const desc = [el.label, el.placeholder, el.title]
112
+ .find((d) => typeof d === "string" && d.trim());
113
+ if (desc) schema.description = desc.trim();
114
+
115
+ return { key, schema, required: el.required === true };
116
+ }
117
+
118
+ /**
119
+ * The morph chains in N, normalised into one edge list per node.
120
+ *
121
+ * Normalisation is IMPORTED, never reimplemented: the runtime resolves
122
+ * `#id` spellings, per-edge defaults and the two `to` forms in
123
+ * `normalizeEdges`, and a second copy of those rules here would drift
124
+ * from the controller the first time either changed. That drift is the
125
+ * exact failure that made the validator accept a node the runtime
126
+ * silently refused to run.
127
+ */
128
+ function graphOf(nodes, exclude) {
129
+ const morphs = (Array.isArray(nodes) ? nodes : [])
130
+ .filter((n) => isObj(n) && n.op === "morph");
131
+ if (!morphs.length) return null;
132
+
133
+ const edges = [];
134
+ for (const m of morphs) {
135
+ for (const e of normalizeEdges(m)) {
136
+ if (typeof e.from !== "string") continue;
137
+ edges.push(e);
138
+ }
139
+ }
140
+ if (!edges.length) return null;
141
+
142
+ const root = edges[0].from;
143
+ const edgesFrom = (id) => edges.filter((e) => e.from === id);
144
+
145
+ // Reachability from the root, forward edges only. A state no edge
146
+ // leads to is not a destination an agent can be offered, however
147
+ // well-formed its own edge may be.
148
+ const reachable = new Set([root]);
149
+ const queue = [root];
150
+ while (queue.length) {
151
+ const at = queue.shift();
152
+ for (const e of edgesFrom(at)) {
153
+ for (const id of e.toIds) {
154
+ if (typeof id !== "string" || reachable.has(id)) continue;
155
+ reachable.add(id);
156
+ queue.push(id);
157
+ }
158
+ }
159
+ }
160
+
161
+ // Routes, in declaration order, as the USER sees them: the labels
162
+ // in the description are the same strings the visible controls
163
+ // carry, so an agent's vocabulary and a human's cannot drift.
164
+ const routes = [];
165
+ for (const e of edges) {
166
+ if (!reachable.has(e.from) || exclude.has(e.from)) continue;
167
+ const pairs = [];
168
+ if (e.byLabel) {
169
+ for (const [label, id] of Object.entries(e.byLabel)) {
170
+ const to = typeof id === "string" && id.startsWith("#") ? id.slice(1) : id;
171
+ if (exclude.has(to)) continue;
172
+ pairs.push({ label, to });
173
+ }
174
+ } else {
175
+ for (const to of e.toIds) {
176
+ if (exclude.has(to)) continue;
177
+ pairs.push({ label: null, to });
178
+ }
179
+ }
180
+ if (pairs.length) routes.push({ from: e.from, to: pairs, back: e.back === true });
181
+ }
182
+
183
+ const destinations = [];
184
+ for (const r of routes) {
185
+ for (const p of r.to) if (!destinations.includes(p.to)) destinations.push(p.to);
186
+ }
187
+
188
+ return {
189
+ root,
190
+ states: [root, ...destinations.filter((d) => d !== root)],
191
+ destinations,
192
+ routes,
193
+ canGoBack: routes.some((r) => r.back),
194
+ };
195
+ }
196
+
197
+ /** "from home: Work → work, Contact → contact; from work: Aurora → aurora" */
198
+ function describeRoutes(graph) {
199
+ return graph.routes
200
+ .map((r) => {
201
+ const to = r.to
202
+ .map((p) => (p.label ? `${p.label} → ${p.to}` : p.to))
203
+ .join(", ");
204
+ return `from ${r.from}: ${to}`;
205
+ })
206
+ .join("; ");
207
+ }
208
+
209
+ /**
210
+ * Derive the tool surface for one (E, N) pair.
211
+ *
212
+ * @param {Array} elements the E array
213
+ * @param {Array} nodes the N array
214
+ * @param {object} [opts]
215
+ * @param {string|null} [opts.specDraft] a label recorded in the manifest
216
+ * to say which draft of the consuming spec it was derived against.
217
+ * Supplied by the caller precisely so this module stays ignorant of
218
+ * any particular protocol.
219
+ * @returns {{tools: object[], manifest: object}|null} null when no
220
+ * `agent-surface` node is present — the surface is opt-in, because
221
+ * turning a page's interaction structure into callable tools is the
222
+ * page's decision to make and never the framework's.
223
+ */
224
+ export function deriveSurface(elements, nodes, { specDraft = null } = {}) {
225
+ const node = (Array.isArray(nodes) ? nodes : [])
226
+ .find((n) => isObj(n) && n.op === "agent-surface");
227
+ if (!node) return null;
228
+
229
+ const flat = flatten(elements);
230
+ const byId = new Map();
231
+ for (const el of flat) if (typeof el.id === "string") byId.set(el.id, el);
232
+
233
+ const exclude = new Set(
234
+ (Array.isArray(node.exclude) ? node.exclude : []).filter((s) => typeof s === "string"),
235
+ );
236
+ const prefix = typeof node.name === "string" && node.name.trim()
237
+ ? node.name.trim().replace(/[^a-zA-Z0-9_-]/g, "_")
238
+ : null;
239
+ const named = (base) => (prefix ? `${prefix}_${base}` : base);
240
+
241
+ const tools = [];
242
+ const graph = graphOf(nodes, exclude);
243
+
244
+ // ── the view graph ───────────────────────────────────────────────
245
+ if (graph) {
246
+ const enumStates = graph.destinations.filter((d) => !exclude.has(d));
247
+ if (enumStates.length) {
248
+ tools.push({
249
+ name: named("navigate"),
250
+ kind: "navigate",
251
+ description:
252
+ "Move to another view of this page. These are in-page views " +
253
+ "reached by a transition, not separate URLs. Routes: " +
254
+ describeRoutes(graph) + ".",
255
+ inputSchema: {
256
+ type: "object",
257
+ properties: {
258
+ destination: {
259
+ type: "string",
260
+ enum: enumStates,
261
+ description: "The view to move to.",
262
+ },
263
+ },
264
+ required: ["destination"],
265
+ },
266
+ });
267
+ }
268
+ if (graph.canGoBack) {
269
+ tools.push({
270
+ name: named("go_back"),
271
+ kind: "back",
272
+ description:
273
+ "Return to the previous view, unwinding the path actually " +
274
+ "taken rather than following an edge.",
275
+ inputSchema: { type: "object", properties: {} },
276
+ });
277
+ }
278
+ }
279
+
280
+ // ── the forms, allowlisted only ──────────────────────────────────
281
+ //
282
+ // A derived submit tool is an agent ACTING, the one derived
283
+ // capability with a real side effect, so nothing here is inferred:
284
+ // a form is exposed because the node named it.
285
+ const wanted = (Array.isArray(node.forms) ? node.forms : [])
286
+ .filter((f) => typeof f === "string");
287
+ for (const id of wanted) {
288
+ const form = byId.get(id);
289
+ if (!form || form.type !== "form") continue;
290
+
291
+ const properties = {};
292
+ const required = [];
293
+ for (const el of walk(form)) {
294
+ if (!FIELD_TYPES.has(el.type)) continue;
295
+ const f = fieldSchema(el);
296
+ if (!f || properties[f.key]) continue;
297
+ properties[f.key] = f.schema;
298
+ if (f.required) required.push(f.key);
299
+ }
300
+ if (!Object.keys(properties).length) continue;
301
+
302
+ tools.push({
303
+ name: named(`submit_${id.replace(/[^a-zA-Z0-9_-]/g, "_")}`),
304
+ kind: "submit",
305
+ formId: id,
306
+ description:
307
+ (typeof form.title === "string" && form.title.trim())
308
+ ? form.title.trim()
309
+ : `Fill and submit the "${id}" form on this page.`,
310
+ inputSchema: {
311
+ type: "object",
312
+ properties,
313
+ ...(required.length ? { required } : {}),
314
+ },
315
+ });
316
+ }
317
+
318
+ // ── what is on screen ────────────────────────────────────────────
319
+ tools.push({
320
+ name: named("read_view"),
321
+ kind: "read",
322
+ description:
323
+ "Read the content currently on screen as structured text: " +
324
+ "headings, body text and the labels of anything actionable.",
325
+ inputSchema: { type: "object", properties: {} },
326
+ });
327
+
328
+ // The manifest is the same derivation with the handler-facing
329
+ // binding facts dropped — a static declaration of capability that
330
+ // needs no script to have run, which is what a crawler or an agent
331
+ // deciding whether to visit at all can actually read.
332
+ const manifest = {
333
+ spec: specDraft,
334
+ tools: tools.map(({ kind, formId, ...decl }) => decl),
335
+ ...(graph
336
+ ? {
337
+ views: {
338
+ root: graph.root,
339
+ states: graph.states.filter((s) => !exclude.has(s)),
340
+ routes: graph.routes,
341
+ },
342
+ }
343
+ : {}),
344
+ };
345
+
346
+ return { tools, manifest };
347
+ }
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/codegen.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/data.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/designer.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -11,6 +11,7 @@ ElementMapper
11
11
  // CORE
12
12
  import { ElementMapper } from "../lib/element-mapper.js";
13
13
  import { applyMorphNodes } from "../lib/morph-node.js";
14
+ import { installAgentSurface } from "../lib/webmcp-adapter.js";
14
15
  import { Animator } from "../layout/animator.js";
15
16
  import { Base } from "../layout/base.js";
16
17
  import { Text } from "../layout/text.js";
@@ -1405,6 +1406,26 @@ const components = {
1405
1406
  } catch (e) {
1406
1407
  console.warn("[nodality] morph node skipped:", e);
1407
1408
  }
1409
+
1410
+ // The agent surface runs after the morphs because it needs their
1411
+ // handles: a derived `navigate` calls the controller's own
1412
+ // `goToState`, so an agent's traversal is the user's traversal
1413
+ // and inherits every rule that path already enforces.
1414
+ //
1415
+ // Opt-in — absent `{ op: "agent-surface" }` this is a no-op, and
1416
+ // it must stay that way: turning a page's interaction structure
1417
+ // into callable tools is the page's decision, never the
1418
+ // framework's, and least of all a side effect of upgrading.
1419
+ try {
1420
+ this._agentSurface = installAgentSurface({
1421
+ mount: document.querySelector((obj && obj.mount) || "#mount"),
1422
+ elements: this._elements,
1423
+ nodes: this.protoOptions,
1424
+ morphs: this._morphs,
1425
+ });
1426
+ } catch (e) {
1427
+ console.warn("[nodality] agent surface skipped:", e);
1428
+ }
1408
1429
  }
1409
1430
  }
1410
1431
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.1.14
2
+ * nodality v1.2.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1744,6 +1744,7 @@ static form(obj){
1744
1744
  const form = new Form().set({
1745
1745
  action: el.action ?? "",
1746
1746
  ...(el.method !== undefined ? { method: el.method } : {}),
1747
+ ...(el.id !== undefined ? { id: el.id } : {}),
1747
1748
  });
1748
1749
  const kids = Array.isArray(el.children) ? el.children : null;
1749
1750
  if (kids) form.add(kids.map((child) => this.mapType({ ...obj, el: child })));