nodality 1.2.9 → 1.2.11

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 (205) hide show
  1. package/bin/mcp-server.mjs +86 -0
  2. package/bin/nodality.js +12 -0
  3. package/bin/schema-cli.mjs +72 -0
  4. package/dist/animator.cjs.js.LICENSE.txt +1 -1
  5. package/dist/animator.esm.js.LICENSE.txt +1 -1
  6. package/dist/audionew.cjs.js.LICENSE.txt +1 -1
  7. package/dist/audionew.esm.js.LICENSE.txt +1 -1
  8. package/dist/base.cjs.js.LICENSE.txt +1 -1
  9. package/dist/base.esm.js.LICENSE.txt +1 -1
  10. package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
  11. package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
  12. package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
  13. package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
  14. package/dist/bundle.umd.js +1 -1
  15. package/dist/bundle.umd.js.LICENSE.txt +1 -28
  16. package/dist/button.cjs.js.LICENSE.txt +1 -1
  17. package/dist/button.esm.js.LICENSE.txt +1 -1
  18. package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
  19. package/dist/card-getter.esm.js.LICENSE.txt +1 -1
  20. package/dist/center.cjs.js.LICENSE.txt +1 -1
  21. package/dist/center.esm.js.LICENSE.txt +1 -1
  22. package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
  23. package/dist/checkbox.esm.js.LICENSE.txt +1 -1
  24. package/dist/code.cjs.js.LICENSE.txt +1 -1
  25. package/dist/code.esm.js.LICENSE.txt +1 -1
  26. package/dist/container.cjs.js.LICENSE.txt +1 -1
  27. package/dist/container.esm.js.LICENSE.txt +1 -1
  28. package/dist/data-list.cjs.js.LICENSE.txt +1 -1
  29. package/dist/data-list.esm.js.LICENSE.txt +1 -1
  30. package/dist/designer.cjs.js +1 -1
  31. package/dist/designer.cjs.js.LICENSE.txt +1 -28
  32. package/dist/designer.esm.js +1 -1
  33. package/dist/designer.esm.js.LICENSE.txt +1 -28
  34. package/dist/element-mapper.cjs.js +1 -1
  35. package/dist/element-mapper.cjs.js.LICENSE.txt +1 -1
  36. package/dist/element-mapper.esm.js +1 -1
  37. package/dist/element-mapper.esm.js.LICENSE.txt +1 -1
  38. package/dist/finalresult.esm.js +1 -1
  39. package/dist/finalresult.esm.js.LICENSE.txt +1 -28
  40. package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
  41. package/dist/flex-card.esm.js.LICENSE.txt +1 -1
  42. package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
  43. package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
  44. package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
  45. package/dist/flex-row.esm.js.LICENSE.txt +1 -1
  46. package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
  47. package/dist/floating-input.esm.js.LICENSE.txt +1 -1
  48. package/dist/free.cjs.js.LICENSE.txt +1 -1
  49. package/dist/free.esm.js.LICENSE.txt +1 -1
  50. package/dist/horizontal-scroller.cjs.js.LICENSE.txt +1 -1
  51. package/dist/horizontal-scroller.esm.js.LICENSE.txt +1 -1
  52. package/dist/image-picker.cjs.js.LICENSE.txt +1 -1
  53. package/dist/image-picker.esm.js.LICENSE.txt +1 -1
  54. package/dist/image.cjs.js.LICENSE.txt +1 -1
  55. package/dist/image.esm.js.LICENSE.txt +1 -1
  56. package/dist/index.cjs.js +1 -1
  57. package/dist/index.cjs.js.LICENSE.txt +1 -28
  58. package/dist/index.d.ts +1 -1
  59. package/dist/index.esm.js +1 -1
  60. package/dist/index.esm.js.LICENSE.txt +1 -28
  61. package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
  62. package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
  63. package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
  64. package/dist/link-getter.esm.js.LICENSE.txt +1 -1
  65. package/dist/link.cjs.js.LICENSE.txt +1 -1
  66. package/dist/link.esm.js.LICENSE.txt +1 -1
  67. package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
  68. package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
  69. package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
  70. package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
  71. package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
  72. package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
  73. package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
  74. package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
  75. package/dist/picker.cjs.js.LICENSE.txt +1 -1
  76. package/dist/picker.esm.js.LICENSE.txt +1 -1
  77. package/dist/progress.cjs.js.LICENSE.txt +1 -1
  78. package/dist/progress.esm.js.LICENSE.txt +1 -1
  79. package/dist/radio.cjs.js.LICENSE.txt +1 -1
  80. package/dist/radio.esm.js.LICENSE.txt +1 -1
  81. package/dist/range.cjs.js.LICENSE.txt +1 -1
  82. package/dist/range.esm.js.LICENSE.txt +1 -1
  83. package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
  84. package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
  85. package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
  86. package/dist/side-bar.esm.js.LICENSE.txt +1 -1
  87. package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
  88. package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
  89. package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
  90. package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
  91. package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
  92. package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
  93. package/dist/spacer.cjs.js.LICENSE.txt +1 -1
  94. package/dist/spacer.esm.js.LICENSE.txt +1 -1
  95. package/dist/stack.cjs.js.LICENSE.txt +1 -1
  96. package/dist/stack.esm.js.LICENSE.txt +1 -1
  97. package/dist/stacker.cjs.js.LICENSE.txt +1 -1
  98. package/dist/stacker.esm.js.LICENSE.txt +1 -1
  99. package/dist/table.cjs.js.LICENSE.txt +1 -1
  100. package/dist/table.esm.js.LICENSE.txt +1 -1
  101. package/dist/text-field.cjs.js.LICENSE.txt +1 -1
  102. package/dist/text-field.esm.js.LICENSE.txt +1 -1
  103. package/dist/text.cjs.js.LICENSE.txt +1 -1
  104. package/dist/text.esm.js.LICENSE.txt +1 -1
  105. package/dist/theme.cjs.js.LICENSE.txt +1 -1
  106. package/dist/theme.esm.js.LICENSE.txt +1 -1
  107. package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
  108. package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
  109. package/dist/ulist.cjs.js.LICENSE.txt +1 -1
  110. package/dist/ulist.esm.js.LICENSE.txt +1 -1
  111. package/dist/video.cjs.js.LICENSE.txt +1 -1
  112. package/dist/video.esm.js.LICENSE.txt +1 -1
  113. package/dist/wrap.cjs.js.LICENSE.txt +1 -1
  114. package/dist/wrap.esm.js.LICENSE.txt +1 -1
  115. package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
  116. package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
  117. package/layout/animator.js +1 -1
  118. package/layout/audio.js +1 -1
  119. package/layout/audionew.js +1 -1
  120. package/layout/base.js +1 -1
  121. package/layout/beta-desktop-bar.js +1 -1
  122. package/layout/beta-mobile-bar.js +1 -1
  123. package/layout/button.js +1 -1
  124. package/layout/center.js +1 -1
  125. package/layout/checkbox.js +1 -1
  126. package/layout/circle.js +1 -1
  127. package/layout/code.js +1 -1
  128. package/layout/container.js +1 -1
  129. package/layout/dropdown-2025.js +1 -1
  130. package/layout/flex-card.js +1 -1
  131. package/layout/flex-grid.js +1 -1
  132. package/layout/flex-row.js +1 -1
  133. package/layout/form-components/custom.js +1 -1
  134. package/layout/form-components/data-list.js +1 -1
  135. package/layout/form-components/floating-input.js +1 -1
  136. package/layout/form-components/form-all.js +1 -1
  137. package/layout/form-components/form.js +1 -1
  138. package/layout/form-components/image-picker.js +1 -1
  139. package/layout/form-components/picker.js +1 -1
  140. package/layout/form-components/radio.js +1 -1
  141. package/layout/form-components/radiogroup.js +1 -1
  142. package/layout/form-components/range.js +1 -1
  143. package/layout/free.js +1 -1
  144. package/layout/grid-switcher.js +1 -1
  145. package/layout/grid.js +1 -1
  146. package/layout/horizontal-scroller.js +1 -1
  147. package/layout/image.js +1 -1
  148. package/layout/index.js +1 -1
  149. package/layout/link.js +1 -1
  150. package/layout/list.js +1 -1
  151. package/layout/meta-adder.js +1 -1
  152. package/layout/modal-2025.js +1 -1
  153. package/layout/morph.js +1 -1
  154. package/layout/multiswitcher.js +1 -1
  155. package/layout/nav-bar.js +1 -1
  156. package/layout/nav-factor/custom-div.js +1 -1
  157. package/layout/new-nav-bar.js +1 -1
  158. package/layout/polygon.js +1 -1
  159. package/layout/prerender-site.js +1 -1
  160. package/layout/prerender.js +1 -1
  161. package/layout/progress.js +1 -1
  162. package/layout/row.js +1 -1
  163. package/layout/scroll-video.js +1 -1
  164. package/layout/side-bar.js +1 -1
  165. package/layout/side-nav-bar.js +1 -1
  166. package/layout/simple-bar.js +1 -1
  167. package/layout/slider-2025.js +1 -1
  168. package/layout/spacer.js +1 -1
  169. package/layout/stack.js +1 -1
  170. package/layout/svg.js +1 -1
  171. package/layout/switcher.js +1 -1
  172. package/layout/table.js +1 -1
  173. package/layout/text-field.js +1 -1
  174. package/layout/text.js +1 -1
  175. package/layout/ulist.js +1 -1
  176. package/layout/video.js +1 -1
  177. package/layout/wrap.js +1 -1
  178. package/layout/zoom-card.js +1 -1
  179. package/lib/agent-surface.js +1 -1
  180. package/lib/card-getter.js +1 -1
  181. package/lib/codegen.js +1 -1
  182. package/lib/data.js +1 -1
  183. package/lib/designer.js +21 -1
  184. package/lib/element-mapper.js +173 -16
  185. package/lib/element-params.generated.js +11 -0
  186. package/lib/keyframe-animation.js +1 -1
  187. package/lib/link-getter.js +1 -1
  188. package/lib/morph-node.js +1 -1
  189. package/lib/parse-html.js +151 -0
  190. package/lib/raster-inspect.js +1 -1
  191. package/lib/raster-ops.js +1 -1
  192. package/lib/raster-presets.js +1 -1
  193. package/lib/scroll-video.js +1 -1
  194. package/lib/seo.js +1 -1
  195. package/lib/stacker.js +1 -1
  196. package/lib/suggest.js +1 -1
  197. package/lib/theme.js +1 -1
  198. package/lib/transform-anim.js +1 -1
  199. package/lib/transition.js +1 -1
  200. package/lib/validate-nodes.js +112 -2
  201. package/lib/webmcp-adapter.js +1 -1
  202. package/package.json +5 -2
  203. package/schema.json +7953 -0
  204. package/scripts/generate-schema.mjs +251 -0
  205. package/skills/nodality/SKILL.md +71 -1
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Stage 2 of AGENTIC-FIRST-PLAN.md — the machine-readable schema, DERIVED.
4
+ *
5
+ * A schema maintained beside the code drifts from it; the op registry did
6
+ * exactly that until 1.2.8, when `copy` turned out to read a parameter it
7
+ * never declared. So nothing here is hand-written. Every entry is recovered
8
+ * from the source that will actually run:
9
+ *
10
+ * types ELEMENT_TYPES in lib/element-mapper.js — the same list the
11
+ * validator checks against, so the two cannot disagree.
12
+ * dispatch the `obj.el.type === "x"` chain in mapType(), giving the
13
+ * mapper method for each type.
14
+ * components the `new X(` calls inside that method, resolved to files
15
+ * through element-mapper's own import statements.
16
+ * parameters `obj.<name>` reads in those files, comments stripped —
17
+ * the technique already proven in the docs repo's
18
+ * audit-options.mjs, which exists because a fifth of the
19
+ * `obj.*` references in these files are commented out.
20
+ * descriptions `//@ name: text` annotations, and `//@deprecated name: why`.
21
+ *
22
+ * What it will NOT do is guess. A type whose component cannot be resolved is
23
+ * emitted with `"resolved": false` and an empty parameter list rather than a
24
+ * plausible-looking one, because a schema that is confidently wrong is worse
25
+ * for an agent than a schema that admits a gap: the agent trusts it either
26
+ * way.
27
+ *
28
+ * node scripts/generate-schema.mjs # write schema.json
29
+ * node scripts/generate-schema.mjs --check # exit 1 if it would change
30
+ * node scripts/generate-schema.mjs --type cards # print one type
31
+ */
32
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
33
+ import { join, dirname } from "node:path";
34
+ import { fileURLToPath } from "node:url";
35
+
36
+ const HERE = dirname(fileURLToPath(import.meta.url));
37
+ const ROOT = join(HERE, "..");
38
+ const MAPPER = join(ROOT, "lib", "element-mapper.js");
39
+ const OUT = join(ROOT, "schema.json");
40
+
41
+ const src = readFileSync(MAPPER, "utf8");
42
+
43
+ /** Strip comments line-wise, the way audit-options.mjs does. */
44
+ function liveLines(text) {
45
+ const out = [];
46
+ let inBlock = false;
47
+ for (let line of text.split("\n")) {
48
+ const t = line.trim();
49
+ if (inBlock) { if (t.includes("*/")) inBlock = false; continue; }
50
+ if (t.startsWith("/*")) { if (!t.includes("*/")) inBlock = true; continue; }
51
+ if (t.startsWith("//") || t.startsWith("*")) continue;
52
+ out.push(line.replace(/\/\/.*$/, ""));
53
+ }
54
+ return out;
55
+ }
56
+
57
+ // ── 1. the authoritative type list ───────────────────────────────────
58
+ const typesBlock = src.slice(src.indexOf("const ELEMENT_TYPES = ["));
59
+ const TYPES = [...typesBlock.slice(0, typesBlock.indexOf("]")).matchAll(/"([^"]+)"/g)]
60
+ .map((m) => m[1]);
61
+ if (!TYPES.length) { console.error("could not read ELEMENT_TYPES"); process.exit(1); }
62
+
63
+ // ── 2. type -> mapper method, from the dispatch chain ────────────────
64
+ const mapperLive = liveLines(src).join("\n");
65
+ const DISPATCH = {};
66
+ {
67
+ // `obj.el.type === "x"` … `return this.mapY(obj)` — take the first return
68
+ // after each comparison, which is how the chain is written throughout.
69
+ const re = /obj\.el\.type\s*===\s*"([a-zA-Z0-9]+)"[\s\S]{0,200}?return\s+this\.([a-zA-Z0-9_]+)\(/g;
70
+ let m;
71
+ while ((m = re.exec(mapperLive))) if (!DISPATCH[m[1]]) DISPATCH[m[1]] = m[2];
72
+
73
+ // The text family is not dispatched by comparison but by membership:
74
+ // let headings = ["h1", … , "p"]; if (headings.includes(obj.el.type))
75
+ // Missing this left the seven most-used types unresolved, which would
76
+ // have been the schema's largest and least excusable hole.
77
+ const grp = /let\s+(\w+)\s*=\s*\[([^\]]+)\][\s\S]{0,120}?\1\.includes\(obj\.el\.type\)[\s\S]{0,120}?return\s+this\.([a-zA-Z0-9_]+)\(/g;
78
+ while ((m = grp.exec(mapperLive))) {
79
+ const method = m[3];
80
+ for (const t of [...m[2].matchAll(/"([^"]+)"/g)].map((x) => x[1])) {
81
+ if (!DISPATCH[t]) DISPATCH[t] = method;
82
+ }
83
+ }
84
+ }
85
+
86
+ // ── 3. mapper method -> component classes it constructs ──────────────
87
+ function methodBody(name) {
88
+ const at = mapperLive.indexOf(`static ${name}(`);
89
+ if (at < 0) return "";
90
+ // Balance braces from the method's opening brace.
91
+ let i = mapperLive.indexOf("{", at), depth = 0;
92
+ for (let j = i; j < mapperLive.length; j++) {
93
+ if (mapperLive[j] === "{") depth++;
94
+ else if (mapperLive[j] === "}") { depth--; if (!depth) return mapperLive.slice(i, j); }
95
+ }
96
+ return mapperLive.slice(i);
97
+ }
98
+ const classesIn = (body) =>
99
+ [...new Set([...body.matchAll(/new\s+([A-Z][A-Za-z0-9]*)\s*\(/g)].map((m) => m[1]))];
100
+
101
+ // ── 4. class -> file, from element-mapper's own imports ──────────────
102
+ const IMPORTS = {};
103
+ for (const m of src.matchAll(/import\s*\{([^}]+)\}\s*from\s*"([^"]+)"/g)) {
104
+ const file = m[2];
105
+ for (const raw of m[1].split(",")) {
106
+ const name = raw.trim().split(/\s+as\s+/).pop().trim();
107
+ if (name) IMPORTS[name] = file;
108
+ }
109
+ }
110
+ function fileFor(cls) {
111
+ const rel = IMPORTS[cls];
112
+ if (!rel) return null;
113
+ const p = join(ROOT, "lib", rel);
114
+ return existsSync(p) ? p : null;
115
+ }
116
+
117
+ // ── 5. parameters a file actually reads ──────────────────────────────
118
+ const paramCache = new Map();
119
+ function paramsOf(file) {
120
+ if (paramCache.has(file)) return paramCache.get(file);
121
+ const text = readFileSync(file, "utf8");
122
+ const found = new Set();
123
+ for (const line of liveLines(text)) {
124
+ for (const m of line.matchAll(/\bobj\.([a-zA-Z][a-zA-Z0-9]*)/g)) found.add(m[1]);
125
+ for (const m of line.matchAll(/\bthis\.options\.([a-zA-Z][a-zA-Z0-9]*)/g)) found.add(m[1]);
126
+ }
127
+ for (const junk of ["options", "el", "customOptions", "storage", "i"]) found.delete(junk);
128
+ paramCache.set(file, found);
129
+ return found;
130
+ }
131
+
132
+ // ── 6. descriptions from //@ annotations, wherever they live ─────────
133
+ const DESCRIPTIONS = {}, DEPRECATED = {};
134
+ {
135
+ const files = new Set(Object.values(IMPORTS)
136
+ .map((rel) => join(ROOT, "lib", rel)).filter(existsSync));
137
+ files.add(MAPPER);
138
+ for (const f of files) {
139
+ for (const line of readFileSync(f, "utf8").split("\n")) {
140
+ let m = line.match(/\/\/@deprecated\s+([a-zA-Z][a-zA-Z0-9]*)\s*:\s*(.+)$/);
141
+ if (m) { DEPRECATED[m[1]] ??= m[2].trim(); continue; }
142
+ m = line.match(/\/\/@\s+([a-zA-Z][a-zA-Z0-9]*)\s*:\s*(.+)$/);
143
+ if (m) DESCRIPTIONS[m[1]] ??= m[2].trim();
144
+ }
145
+ }
146
+ }
147
+
148
+ // ── 7. assemble ──────────────────────────────────────────────────────
149
+ const schema = { generated: "scripts/generate-schema.mjs", types: {} };
150
+ let resolved = 0;
151
+ for (const type of TYPES) {
152
+ const method = DISPATCH[type] || null;
153
+ // A mapper's helpers are part of the mapper: `mapGrid` reads `items` only
154
+ // through `gridItemsSource`, so scanning the dispatched method alone
155
+ // reported that `cards` has no content slot. Follow `this.x(` one level.
156
+ const bodyOf = (name, seen = new Set()) => {
157
+ if (!name || seen.has(name)) return "";
158
+ seen.add(name);
159
+ const b = methodBody(name);
160
+ let out = b;
161
+ for (const m of b.matchAll(/\bthis\.([a-zA-Z][a-zA-Z0-9_]*)\s*\(/g)) {
162
+ if (seen.size < 12) out += "\n" + bodyOf(m[1], seen);
163
+ }
164
+ return out;
165
+ };
166
+ const body = method ? bodyOf(method) : "";
167
+ const classes = body ? classesIn(body) : [];
168
+ const files = classes.map(fileFor).filter(Boolean);
169
+
170
+ const params = new Set();
171
+ for (const f of files) for (const p of paramsOf(f)) params.add(p);
172
+
173
+ // The mapper's OWN `el.<name>` reads. Scanning components alone missed
174
+ // these, because a mapper often passes an element field as a constructor
175
+ // argument rather than an option — `new Text(el.text)`. That left `text`
176
+ // off every heading, which would have been the single worst error the
177
+ // schema could contain: the most-used parameter of the most-used type.
178
+ for (const m of body.matchAll(/\bel\.([a-zA-Z][a-zA-Z0-9]*)/g)) params.add(m[1]);
179
+
180
+ // Read off the element by every mapper regardless of component.
181
+ for (const p of ["type", "id"]) params.add(p);
182
+
183
+ const ok = files.length > 0;
184
+ if (ok) resolved++;
185
+ schema.types[type] = {
186
+ resolved: ok,
187
+ mapper: method,
188
+ components: classes,
189
+ params: [...params].sort().map((name) => ({
190
+ name,
191
+ ...(DESCRIPTIONS[name] ? { description: DESCRIPTIONS[name] } : {}),
192
+ ...(DEPRECATED[name] ? { deprecated: DEPRECATED[name] } : {}),
193
+ })),
194
+ };
195
+ }
196
+ schema.summary = { types: TYPES.length, resolved, unresolved: TYPES.length - resolved };
197
+
198
+ // ── 8. modes ─────────────────────────────────────────────────────────
199
+ const args = process.argv.slice(2);
200
+ const json = JSON.stringify(schema, null, 2) + "\n";
201
+
202
+ if (args.includes("--type")) {
203
+ const t = args[args.indexOf("--type") + 1];
204
+ const entry = schema.types[t];
205
+ if (!entry) { console.error(`unknown type "${t}"`); process.exit(1); }
206
+ console.log(JSON.stringify({ type: t, ...entry }, null, 2));
207
+ process.exit(0);
208
+ }
209
+
210
+ if (args.includes("--check")) {
211
+ const current = existsSync(OUT) ? readFileSync(OUT, "utf8") : "";
212
+ if (current !== json) {
213
+ console.error("schema.json is out of date — run: node scripts/generate-schema.mjs");
214
+ process.exit(1);
215
+ }
216
+ console.log(`schema.json current (${resolved}/${TYPES.length} types resolved)`);
217
+ process.exit(0);
218
+ }
219
+
220
+ if (args.includes("--stdout")) { process.stdout.write(json); process.exit(0); }
221
+
222
+ // The validator's vocabulary, emitted as a module so it ships in the bundle.
223
+ //
224
+ // The UNION of every type's parameters, not a per-type table. Per-type would
225
+ // be 20 kB in a zero-dependency bundle, and — worse — it would produce false
226
+ // positives: several mappers spread the whole element (`...el`) into their
227
+ // component, so they genuinely accept names no static scan can enumerate.
228
+ // Rejecting one of those would stop `preview` from rendering a page that
229
+ // works, which 1.2.7 established is the costlier direction to be wrong in.
230
+ //
231
+ // The union is used for NEAR-MISS detection only: `itms` is reported because
232
+ // it is one edit from `items`, while an unrecognised name with no close
233
+ // match is left alone. Per-type vocabularies are served on demand by
234
+ // `npx nodality schema <type>` — which is the point of Stage 2's property 2,
235
+ // schema on demand rather than schema in context.
236
+ {
237
+ const union = new Set();
238
+ for (const t of Object.values(schema.types)) for (const p of t.params) union.add(p.name);
239
+ const names = [...union].sort();
240
+ const mod = `// GENERATED by scripts/generate-schema.mjs — do not edit.
241
+ // Every parameter name any element type reads, recovered from source.
242
+ // Used for near-miss typo detection in validate-nodes.js. A drift test
243
+ // regenerates this and fails if it differs, so it cannot rot silently.
244
+ export const ELEMENT_PARAM_NAMES = ${JSON.stringify(names)};
245
+ `;
246
+ writeFileSync(join(ROOT, "lib", "element-params.generated.js"), mod);
247
+ }
248
+
249
+ writeFileSync(OUT, json);
250
+ console.log(`schema.json written: ${TYPES.length} types, ${resolved} resolved, ` +
251
+ `${TYPES.length - resolved} unresolved`);
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: nodality
3
- description: Build or edit a page with the Nodality library – declarative static UI from two arrays (elements + nodes), GPU raster effects, morph navigation graphs, prerendering, and agent surfaces. Use when the user asks to create or change a Nodality page, mentions (E, N), Des(), morph/raster/agent-surface nodes, works with imperative Nodality code (new Text(), new Link(), .render()), or asks to draft a landing page with Nodality.
3
+ description: Build or edit a page with the Nodality library – declarative static UI from two arrays (elements + nodes), GPU raster effects, morph navigation graphs, prerendering, and agent surfaces. Use when the user asks to create or change a Nodality page, mentions (E, N), Des(), morph/raster/agent-surface nodes, works with imperative Nodality code (new Text(), new Link(), .render()), asks to draft a landing page with Nodality, or asks to EDIT an already-rendered Nodality page (parse it back to data, change it, re-render).
4
4
  ---
5
5
 
6
6
  # Working with Nodality
@@ -45,6 +45,12 @@ drift. If the MCP is not configured, add it:
45
45
  1. **`list_ops` first.** Never guess an op name, a parameter, an easing
46
46
  name, a transition preset, or an element type. One call returns all
47
47
  of them as data.
48
+ 1b. **`get_schema` for the element you are about to write.** `list_ops`
49
+ gives the vocabulary of N; `get_schema` gives the vocabulary of E,
50
+ one type at a time – its parameters, recovered from the source the
51
+ components actually read. Ask for the type, not the whole schema:
52
+ that is the difference between paying for a schema once and paying
53
+ for it in every request.
48
54
  2. **Author the pair.** E top-down (page structure), then N (what
49
55
  happens to it). Small pages fit in one file.
50
56
  3. **`validate_nodes` before showing the user anything.** Pass both
@@ -62,6 +68,36 @@ Skipping step 3 is the classic failure: a misspelled op renders
62
68
  *nothing* rather than erroring, so the page looks plausible and is
63
69
  silently missing its effects.
64
70
 
71
+ ## Composites carry their content in a slot
72
+
73
+ A composite is not empty scaffolding – you give it content, and **which
74
+ key you use is part of the type**:
75
+
76
+ | slot | types |
77
+ |---|---|
78
+ | `items` | `cards`, `nav`, `sideNav`, `table`, `ulist` |
79
+ | `children` | `row`, `form`, `wrap` |
80
+
81
+ ```js
82
+ { type: "cards", items: [ // shorthand
83
+ { img: "a.jpg", title: "Alpha", link: "#a" },
84
+ [{ type: "h2", text: "Beta" }], // or nested specs
85
+ ]}
86
+ ```
87
+
88
+ `items` takes either shorthand entries or a nested list of element
89
+ specs, and one array may mix both. Use nested specs when the cards
90
+ differ from one another – that is the case where a data format wins by
91
+ the widest margin, and where writing it as code costs *more* than
92
+ hand-written JSX.
93
+
94
+ **Declaring content in the wrong slot renders the placeholders.** A
95
+ composite given nothing falls back to sample content, so
96
+ `{type:"table", children:[…]}` produces a plausible-looking table of
97
+ someone else's data rather than an error. `validate_nodes` reports this
98
+ as `WRONG_CONTENT_SLOT` and names the slot that carries content – which
99
+ is another reason step 3 is not optional. `get_schema` names it too.
100
+
65
101
  ## The four node families
66
102
 
67
103
  Told apart by the shape of `op`:
@@ -159,6 +195,40 @@ One naming caution: `Text` and `Image` collide with DOM constructor
159
195
  names. Always use the module import; on a page using the CDN globals,
160
196
  never assume `window.Text` / `window.Image` are still the DOM's own.
161
197
 
198
+ ## Editing a page that already exists
199
+
200
+ Editing is the common case in production, and it does not mean
201
+ regenerating. Read the page back to data, change the one thing, render
202
+ again:
203
+
204
+ ```js
205
+ import { parseHTML } from "nodality/parse";
206
+
207
+ const spec = parseHTML(html); // or the parse_html MCP tool
208
+ spec[0].items[1].title = "Gamma"; // change one card
209
+ new Des().nodes([]).add(spec).set({ mount: "#mount", annotate: true });
210
+ ```
211
+
212
+ **Render with `{annotate: true}` if the page will ever be edited.** It
213
+ writes each descriptor onto the node it produced, and that is what makes
214
+ the read exact – every type, every option, nothing inferred. It is
215
+ opt-in and off by default because it puts attributes in the output.
216
+
217
+ Without annotation only what the tag settles comes back: headings,
218
+ paragraphs, links, images, lists. Thirteen composite types render as a
219
+ bare `<div>` with nothing to tell them apart, so those are reported
220
+ **unrecovered rather than guessed** – a wrong guess would silently
221
+ become a different page. Use `parseReport` (or the `parse_html` tool)
222
+ rather than `parseHTML` when you need to know which you got:
223
+
224
+ ```js
225
+ const { ok, exact, spec, unrecovered, errors } = parseReport(html);
226
+ ```
227
+
228
+ `ok` is false if anything was unrecovered *or* the recovered spec fails
229
+ validation. Check it before re-rendering: a descriptor read out of a
230
+ document is untrusted input.
231
+
162
232
  ## Verifying your work
163
233
 
164
234
  - **Verify a morph by progress, not by DOM presence.** The destination