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.
- package/bin/mcp-server.mjs +28 -2
- 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 +15 -2
- 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 +79 -2
- package/lib/webmcp-adapter.js +307 -0
- package/package.json +1 -1
package/lib/link-getter.js
CHANGED
package/lib/morph-node.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
|
*/
|
|
@@ -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
|
-
|
|
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 };
|
package/lib/raster-inspect.js
CHANGED
package/lib/raster-ops.js
CHANGED
package/lib/raster-presets.js
CHANGED
package/lib/scroll-video.js
CHANGED
package/lib/seo.js
CHANGED
package/lib/stacker.js
CHANGED
package/lib/suggest.js
CHANGED
package/lib/theme.js
CHANGED
package/lib/transform-anim.js
CHANGED
package/lib/transition.js
CHANGED
package/lib/validate-nodes.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
|
*/
|
|
@@ -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")
|
|
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
|
+
}
|