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.
- package/bin/mcp-server.mjs +41 -5
- package/bin/nodality.js +61 -0
- package/dist/animator.cjs.js.LICENSE.txt +1 -1
- package/dist/animator.esm.js.LICENSE.txt +1 -1
- package/dist/audionew.cjs.js.LICENSE.txt +1 -1
- package/dist/audionew.esm.js.LICENSE.txt +1 -1
- package/dist/base.cjs.js.LICENSE.txt +1 -1
- package/dist/base.esm.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
- package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
- package/dist/bundle.umd.js +1 -1
- package/dist/bundle.umd.js.LICENSE.txt +60 -1
- package/dist/button.cjs.js.LICENSE.txt +1 -1
- package/dist/button.esm.js.LICENSE.txt +1 -1
- package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
- package/dist/card-getter.esm.js.LICENSE.txt +1 -1
- package/dist/center.cjs.js.LICENSE.txt +1 -1
- package/dist/center.esm.js.LICENSE.txt +1 -1
- package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
- package/dist/checkbox.esm.js.LICENSE.txt +1 -1
- package/dist/code.cjs.js.LICENSE.txt +1 -1
- package/dist/code.esm.js.LICENSE.txt +1 -1
- package/dist/container.cjs.js.LICENSE.txt +1 -1
- package/dist/container.esm.js.LICENSE.txt +1 -1
- package/dist/data-list.cjs.js.LICENSE.txt +1 -1
- package/dist/data-list.esm.js.LICENSE.txt +1 -1
- package/dist/designer.cjs.js +1 -1
- package/dist/designer.cjs.js.LICENSE.txt +60 -1
- package/dist/designer.esm.js +1 -1
- package/dist/designer.esm.js.LICENSE.txt +60 -1
- package/dist/element-mapper.cjs.js +1 -1
- package/dist/element-mapper.cjs.js.LICENSE.txt +1 -1
- package/dist/element-mapper.esm.js +1 -1
- package/dist/element-mapper.esm.js.LICENSE.txt +1 -1
- package/dist/finalresult.esm.js +1 -1
- package/dist/finalresult.esm.js.LICENSE.txt +60 -1
- package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-card.esm.js.LICENSE.txt +1 -1
- package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
- package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-row.esm.js.LICENSE.txt +1 -1
- package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
- package/dist/floating-input.esm.js.LICENSE.txt +1 -1
- package/dist/free.cjs.js.LICENSE.txt +1 -1
- package/dist/free.esm.js.LICENSE.txt +1 -1
- package/dist/horizontal-scroller.cjs.js.LICENSE.txt +1 -1
- package/dist/horizontal-scroller.esm.js.LICENSE.txt +1 -1
- package/dist/image-picker.cjs.js.LICENSE.txt +1 -1
- package/dist/image-picker.esm.js.LICENSE.txt +1 -1
- package/dist/image.cjs.js.LICENSE.txt +1 -1
- package/dist/image.esm.js.LICENSE.txt +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.LICENSE.txt +60 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.LICENSE.txt +60 -1
- package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
- package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
- package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
- package/dist/link-getter.esm.js.LICENSE.txt +1 -1
- package/dist/link.cjs.js.LICENSE.txt +1 -1
- package/dist/link.esm.js.LICENSE.txt +1 -1
- package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
- package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
- package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
- package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
- package/dist/picker.cjs.js.LICENSE.txt +1 -1
- package/dist/picker.esm.js.LICENSE.txt +1 -1
- package/dist/progress.cjs.js.LICENSE.txt +1 -1
- package/dist/progress.esm.js.LICENSE.txt +1 -1
- package/dist/radio.cjs.js.LICENSE.txt +1 -1
- package/dist/radio.esm.js.LICENSE.txt +1 -1
- package/dist/range.cjs.js.LICENSE.txt +1 -1
- package/dist/range.esm.js.LICENSE.txt +1 -1
- package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
- package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
- package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/side-bar.esm.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
- package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
- package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
- package/dist/spacer.cjs.js.LICENSE.txt +1 -1
- package/dist/spacer.esm.js.LICENSE.txt +1 -1
- package/dist/stack.cjs.js.LICENSE.txt +1 -1
- package/dist/stack.esm.js.LICENSE.txt +1 -1
- package/dist/stacker.cjs.js.LICENSE.txt +1 -1
- package/dist/stacker.esm.js.LICENSE.txt +1 -1
- package/dist/table.cjs.js.LICENSE.txt +1 -1
- package/dist/table.esm.js.LICENSE.txt +1 -1
- package/dist/text-field.cjs.js +1 -1
- package/dist/text-field.cjs.js.LICENSE.txt +1 -1
- package/dist/text-field.esm.js +1 -1
- package/dist/text-field.esm.js.LICENSE.txt +1 -1
- package/dist/text.cjs.js.LICENSE.txt +1 -1
- package/dist/text.esm.js.LICENSE.txt +1 -1
- package/dist/theme.cjs.js.LICENSE.txt +1 -1
- package/dist/theme.esm.js.LICENSE.txt +1 -1
- package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
- package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
- package/dist/ulist.cjs.js.LICENSE.txt +1 -1
- package/dist/ulist.esm.js.LICENSE.txt +1 -1
- package/dist/video.cjs.js.LICENSE.txt +1 -1
- package/dist/video.esm.js.LICENSE.txt +1 -1
- package/dist/wrap.cjs.js.LICENSE.txt +1 -1
- package/dist/wrap.esm.js.LICENSE.txt +1 -1
- package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
- package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
- package/layout/animator.js +1 -1
- package/layout/audio.js +1 -1
- package/layout/audionew.js +1 -1
- package/layout/base.js +1 -1
- package/layout/beta-desktop-bar.js +1 -1
- package/layout/beta-mobile-bar.js +1 -1
- package/layout/button.js +1 -1
- package/layout/center.js +1 -1
- package/layout/checkbox.js +1 -1
- package/layout/circle.js +1 -1
- package/layout/code.js +1 -1
- package/layout/container.js +1 -1
- package/layout/dropdown-2025.js +1 -1
- package/layout/flex-card.js +1 -1
- package/layout/flex-grid.js +1 -1
- package/layout/flex-row.js +1 -1
- package/layout/form-components/custom.js +1 -1
- package/layout/form-components/data-list.js +1 -1
- package/layout/form-components/floating-input.js +1 -1
- package/layout/form-components/form-all.js +1 -1
- package/layout/form-components/form.js +6 -1
- package/layout/form-components/image-picker.js +1 -1
- package/layout/form-components/picker.js +1 -1
- package/layout/form-components/radio.js +1 -1
- package/layout/form-components/radiogroup.js +1 -1
- package/layout/form-components/range.js +1 -1
- package/layout/free.js +1 -1
- package/layout/grid-switcher.js +1 -1
- package/layout/grid.js +1 -1
- package/layout/horizontal-scroller.js +1 -1
- package/layout/image.js +1 -1
- package/layout/index.js +1 -1
- package/layout/link.js +1 -1
- package/layout/list.js +1 -1
- package/layout/meta-adder.js +1 -1
- package/layout/modal-2025.js +1 -1
- package/layout/morph.js +1 -1
- package/layout/multiswitcher.js +1 -1
- package/layout/nav-bar.js +1 -1
- package/layout/nav-factor/custom-div.js +1 -1
- package/layout/new-nav-bar.js +1 -1
- package/layout/polygon.js +1 -1
- package/layout/prerender-site.js +1 -1
- package/layout/prerender.js +1 -1
- package/layout/progress.js +1 -1
- package/layout/row.js +1 -1
- package/layout/scroll-video.js +1 -1
- package/layout/side-bar.js +1 -1
- package/layout/side-nav-bar.js +1 -1
- package/layout/simple-bar.js +1 -1
- package/layout/slider-2025.js +1 -1
- package/layout/spacer.js +1 -1
- package/layout/stack.js +1 -1
- package/layout/svg.js +1 -1
- package/layout/switcher.js +1 -1
- package/layout/table.js +1 -1
- package/layout/text-field.js +13 -1
- package/layout/text.js +1 -1
- package/layout/ulist.js +1 -1
- package/layout/video.js +1 -1
- package/layout/wrap.js +1 -1
- package/layout/zoom-card.js +1 -1
- package/lib/agent-surface.js +347 -0
- package/lib/card-getter.js +1 -1
- package/lib/codegen.js +1 -1
- package/lib/data.js +1 -1
- package/lib/designer.js +22 -1
- package/lib/element-mapper.js +2 -1
- package/lib/keyframe-animation.js +1 -1
- package/lib/link-getter.js +1 -1
- package/lib/morph-node.js +47 -8
- package/lib/raster-inspect.js +1 -1
- package/lib/raster-ops.js +1 -1
- package/lib/raster-presets.js +1 -1
- package/lib/scroll-video.js +1 -1
- package/lib/seo.js +1 -1
- package/lib/stacker.js +1 -1
- package/lib/suggest.js +1 -1
- package/lib/theme.js +1 -1
- package/lib/transform-anim.js +1 -1
- package/lib/transition.js +1 -1
- package/lib/validate-nodes.js +196 -32
- package/lib/webmcp-adapter.js +307 -0
- package/package.json +1 -1
package/layout/meta-adder.js
CHANGED
package/layout/modal-2025.js
CHANGED
package/layout/morph.js
CHANGED
package/layout/multiswitcher.js
CHANGED
package/layout/nav-bar.js
CHANGED
package/layout/new-nav-bar.js
CHANGED
package/layout/polygon.js
CHANGED
package/layout/prerender-site.js
CHANGED
package/layout/prerender.js
CHANGED
package/layout/progress.js
CHANGED
package/layout/row.js
CHANGED
package/layout/scroll-video.js
CHANGED
package/layout/side-bar.js
CHANGED
package/layout/side-nav-bar.js
CHANGED
package/layout/simple-bar.js
CHANGED
package/layout/slider-2025.js
CHANGED
package/layout/spacer.js
CHANGED
package/layout/stack.js
CHANGED
package/layout/svg.js
CHANGED
package/layout/switcher.js
CHANGED
package/layout/table.js
CHANGED
package/layout/text-field.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.
|
|
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
package/layout/ulist.js
CHANGED
package/layout/video.js
CHANGED
package/layout/wrap.js
CHANGED
package/layout/zoom-card.js
CHANGED
|
@@ -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
|
+
}
|
package/lib/card-getter.js
CHANGED
package/lib/codegen.js
CHANGED
package/lib/data.js
CHANGED
package/lib/designer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.
|
|
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
|
|
package/lib/element-mapper.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.
|
|
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 })));
|