nodality 1.2.11 → 1.3.1

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 (282) hide show
  1. package/bin/mcp-server.mjs +45 -0
  2. package/dist/animator.cjs.js +1 -1
  3. package/dist/animator.cjs.js.LICENSE.txt +1 -1
  4. package/dist/animator.esm.js +1 -1
  5. package/dist/animator.esm.js.LICENSE.txt +1 -1
  6. package/dist/audionew.cjs.js +1 -1
  7. package/dist/audionew.cjs.js.LICENSE.txt +1 -1
  8. package/dist/audionew.esm.js +1 -1
  9. package/dist/audionew.esm.js.LICENSE.txt +1 -1
  10. package/dist/base.cjs.js +1 -1
  11. package/dist/base.cjs.js.LICENSE.txt +1 -1
  12. package/dist/base.esm.js +1 -1
  13. package/dist/base.esm.js.LICENSE.txt +1 -1
  14. package/dist/beta-desktop-bar.cjs.js +1 -1
  15. package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
  16. package/dist/beta-desktop-bar.esm.js +1 -1
  17. package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
  18. package/dist/beta-mobile-bar.cjs.js +1 -1
  19. package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
  20. package/dist/beta-mobile-bar.esm.js +1 -1
  21. package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
  22. package/dist/bundle.umd.js +1 -1
  23. package/dist/bundle.umd.js.LICENSE.txt +1 -1
  24. package/dist/button.cjs.js +1 -1
  25. package/dist/button.cjs.js.LICENSE.txt +1 -1
  26. package/dist/button.esm.js +1 -1
  27. package/dist/button.esm.js.LICENSE.txt +1 -1
  28. package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
  29. package/dist/card-getter.esm.js.LICENSE.txt +1 -1
  30. package/dist/center.cjs.js +1 -1
  31. package/dist/center.cjs.js.LICENSE.txt +1 -1
  32. package/dist/center.esm.js +1 -1
  33. package/dist/center.esm.js.LICENSE.txt +1 -1
  34. package/dist/checkbox.cjs.js +1 -1
  35. package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
  36. package/dist/checkbox.esm.js +1 -1
  37. package/dist/checkbox.esm.js.LICENSE.txt +1 -1
  38. package/dist/code.cjs.js +1 -1
  39. package/dist/code.cjs.js.LICENSE.txt +1 -1
  40. package/dist/code.esm.js +1 -1
  41. package/dist/code.esm.js.LICENSE.txt +1 -1
  42. package/dist/container.cjs.js +1 -1
  43. package/dist/container.cjs.js.LICENSE.txt +1 -1
  44. package/dist/container.esm.js +1 -1
  45. package/dist/container.esm.js.LICENSE.txt +1 -1
  46. package/dist/data-list.cjs.js.LICENSE.txt +1 -1
  47. package/dist/data-list.esm.js.LICENSE.txt +1 -1
  48. package/dist/designer.cjs.js +1 -1
  49. package/dist/designer.cjs.js.LICENSE.txt +1 -1
  50. package/dist/designer.esm.js +1 -1
  51. package/dist/designer.esm.js.LICENSE.txt +1 -1
  52. package/dist/element-mapper.cjs.js +1 -1
  53. package/dist/element-mapper.cjs.js.LICENSE.txt +1 -1
  54. package/dist/element-mapper.esm.js +1 -1
  55. package/dist/element-mapper.esm.js.LICENSE.txt +1 -1
  56. package/dist/finalresult.esm.js +1 -1
  57. package/dist/finalresult.esm.js.LICENSE.txt +1 -1
  58. package/dist/flex-card.cjs.js +1 -1
  59. package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
  60. package/dist/flex-card.esm.js +1 -1
  61. package/dist/flex-card.esm.js.LICENSE.txt +1 -1
  62. package/dist/flex-grid.cjs.js +1 -1
  63. package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
  64. package/dist/flex-grid.esm.js +1 -1
  65. package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
  66. package/dist/flex-row.cjs.js +1 -1
  67. package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
  68. package/dist/flex-row.esm.js +1 -1
  69. package/dist/flex-row.esm.js.LICENSE.txt +1 -1
  70. package/dist/floating-input.cjs.js +1 -1
  71. package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
  72. package/dist/floating-input.esm.js +1 -1
  73. package/dist/floating-input.esm.js.LICENSE.txt +1 -1
  74. package/dist/free.cjs.js +1 -1
  75. package/dist/free.cjs.js.LICENSE.txt +1 -1
  76. package/dist/free.esm.js +1 -1
  77. package/dist/free.esm.js.LICENSE.txt +1 -1
  78. package/dist/horizontal-scroller.cjs.js.LICENSE.txt +1 -1
  79. package/dist/horizontal-scroller.esm.js.LICENSE.txt +1 -1
  80. package/dist/image-picker.cjs.js +1 -1
  81. package/dist/image-picker.cjs.js.LICENSE.txt +1 -1
  82. package/dist/image-picker.esm.js +1 -1
  83. package/dist/image-picker.esm.js.LICENSE.txt +1 -1
  84. package/dist/image.cjs.js +1 -1
  85. package/dist/image.cjs.js.LICENSE.txt +1 -1
  86. package/dist/image.esm.js +1 -1
  87. package/dist/image.esm.js.LICENSE.txt +1 -1
  88. package/dist/index.cjs.js +1 -1
  89. package/dist/index.cjs.js.LICENSE.txt +1 -1
  90. package/dist/index.d.ts +3 -2
  91. package/dist/index.esm.js +1 -1
  92. package/dist/index.esm.js.LICENSE.txt +1 -1
  93. package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
  94. package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
  95. package/dist/link-getter.cjs.js +1 -1
  96. package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
  97. package/dist/link-getter.esm.js +1 -1
  98. package/dist/link-getter.esm.js.LICENSE.txt +1 -1
  99. package/dist/link.cjs.js +1 -1
  100. package/dist/link.cjs.js.LICENSE.txt +1 -1
  101. package/dist/link.esm.js +1 -1
  102. package/dist/link.esm.js.LICENSE.txt +1 -1
  103. package/dist/meta-adder.cjs.js +1 -1
  104. package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
  105. package/dist/meta-adder.esm.js +1 -1
  106. package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
  107. package/dist/modal-2025.cjs.js +1 -1
  108. package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
  109. package/dist/modal-2025.esm.js +1 -1
  110. package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
  111. package/dist/multiswitcher.cjs.js +1 -1
  112. package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
  113. package/dist/multiswitcher.esm.js +1 -1
  114. package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
  115. package/dist/new-nav-bar.cjs.js +1 -1
  116. package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
  117. package/dist/new-nav-bar.esm.js +1 -1
  118. package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
  119. package/dist/picker.cjs.js +1 -1
  120. package/dist/picker.cjs.js.LICENSE.txt +1 -1
  121. package/dist/picker.esm.js +1 -1
  122. package/dist/picker.esm.js.LICENSE.txt +1 -1
  123. package/dist/progress.cjs.js +1 -1
  124. package/dist/progress.cjs.js.LICENSE.txt +1 -1
  125. package/dist/progress.esm.js +1 -1
  126. package/dist/progress.esm.js.LICENSE.txt +1 -1
  127. package/dist/radio.cjs.js.LICENSE.txt +1 -1
  128. package/dist/radio.esm.js.LICENSE.txt +1 -1
  129. package/dist/range.cjs.js.LICENSE.txt +1 -1
  130. package/dist/range.esm.js.LICENSE.txt +1 -1
  131. package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
  132. package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
  133. package/dist/side-bar.cjs.js +1 -1
  134. package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
  135. package/dist/side-bar.esm.js +1 -1
  136. package/dist/side-bar.esm.js.LICENSE.txt +1 -1
  137. package/dist/side-nav-bar.cjs.js +1 -1
  138. package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
  139. package/dist/side-nav-bar.esm.js +1 -1
  140. package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
  141. package/dist/simple-bar.cjs.js +1 -1
  142. package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
  143. package/dist/simple-bar.esm.js +1 -1
  144. package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
  145. package/dist/slider-2025.cjs.js +1 -1
  146. package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
  147. package/dist/slider-2025.esm.js +1 -1
  148. package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
  149. package/dist/spacer.cjs.js +1 -1
  150. package/dist/spacer.cjs.js.LICENSE.txt +1 -1
  151. package/dist/spacer.esm.js +1 -1
  152. package/dist/spacer.esm.js.LICENSE.txt +1 -1
  153. package/dist/stack.cjs.js +1 -1
  154. package/dist/stack.cjs.js.LICENSE.txt +1 -1
  155. package/dist/stack.esm.js +1 -1
  156. package/dist/stack.esm.js.LICENSE.txt +1 -1
  157. package/dist/stacker.cjs.js.LICENSE.txt +1 -1
  158. package/dist/stacker.esm.js.LICENSE.txt +1 -1
  159. package/dist/table.cjs.js +1 -1
  160. package/dist/table.cjs.js.LICENSE.txt +1 -1
  161. package/dist/table.esm.js +1 -1
  162. package/dist/table.esm.js.LICENSE.txt +1 -1
  163. package/dist/text-field.cjs.js +1 -1
  164. package/dist/text-field.cjs.js.LICENSE.txt +1 -1
  165. package/dist/text-field.esm.js +1 -1
  166. package/dist/text-field.esm.js.LICENSE.txt +1 -1
  167. package/dist/text.cjs.js +1 -1
  168. package/dist/text.cjs.js.LICENSE.txt +1 -1
  169. package/dist/text.esm.js +1 -1
  170. package/dist/text.esm.js.LICENSE.txt +1 -1
  171. package/dist/theme.cjs.js.LICENSE.txt +1 -1
  172. package/dist/theme.esm.js.LICENSE.txt +1 -1
  173. package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
  174. package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
  175. package/dist/ulist.cjs.js +1 -1
  176. package/dist/ulist.cjs.js.LICENSE.txt +1 -1
  177. package/dist/ulist.esm.js +1 -1
  178. package/dist/ulist.esm.js.LICENSE.txt +1 -1
  179. package/dist/video.cjs.js +1 -1
  180. package/dist/video.cjs.js.LICENSE.txt +1 -1
  181. package/dist/video.esm.js +1 -1
  182. package/dist/video.esm.js.LICENSE.txt +1 -1
  183. package/dist/wrap.cjs.js +1 -1
  184. package/dist/wrap.cjs.js.LICENSE.txt +1 -1
  185. package/dist/wrap.esm.js +1 -1
  186. package/dist/wrap.esm.js.LICENSE.txt +1 -1
  187. package/dist/zoom-card.cjs.js +1 -1
  188. package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
  189. package/dist/zoom-card.esm.js +1 -1
  190. package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
  191. package/layout/animator.js +15 -2
  192. package/layout/audio.js +1 -1
  193. package/layout/audionew.js +1 -1
  194. package/layout/base.js +1 -1
  195. package/layout/beta-desktop-bar.js +1 -1
  196. package/layout/beta-mobile-bar.js +1 -1
  197. package/layout/button.js +1 -1
  198. package/layout/center.js +1 -1
  199. package/layout/checkbox.js +1 -1
  200. package/layout/circle.js +1 -1
  201. package/layout/code.js +1 -1
  202. package/layout/container.js +1 -1
  203. package/layout/dropdown-2025.js +10 -3
  204. package/layout/flex-card.js +1 -1
  205. package/layout/flex-grid.js +1 -1
  206. package/layout/flex-row.js +1 -1
  207. package/layout/form-components/custom.js +1 -1
  208. package/layout/form-components/data-list.js +1 -1
  209. package/layout/form-components/floating-input.js +23 -4
  210. package/layout/form-components/form-all.js +1 -1
  211. package/layout/form-components/form.js +1 -1
  212. package/layout/form-components/image-picker.js +5 -3
  213. package/layout/form-components/picker.js +1 -1
  214. package/layout/form-components/radio.js +1 -1
  215. package/layout/form-components/radiogroup.js +1 -1
  216. package/layout/form-components/range.js +1 -1
  217. package/layout/free.js +1 -1
  218. package/layout/grid-switcher.js +1 -1
  219. package/layout/grid.js +1 -1
  220. package/layout/horizontal-scroller.js +1 -1
  221. package/layout/image.js +15 -2
  222. package/layout/index.js +1 -1
  223. package/layout/link.js +1 -1
  224. package/layout/list.js +1 -1
  225. package/layout/meta-adder.js +1 -1
  226. package/layout/modal-2025.js +1 -1
  227. package/layout/morph.js +1 -1
  228. package/layout/multiswitcher.js +1 -1
  229. package/layout/nav-bar.js +1 -1
  230. package/layout/nav-factor/custom-div.js +1 -1
  231. package/layout/new-nav-bar.js +1 -1
  232. package/layout/polygon.js +1 -1
  233. package/layout/prerender-site.js +1 -1
  234. package/layout/prerender.js +1 -1
  235. package/layout/progress.js +1 -1
  236. package/layout/row.js +1 -1
  237. package/layout/scroll-video.js +1 -1
  238. package/layout/side-bar.js +11 -3
  239. package/layout/side-nav-bar.js +16 -3
  240. package/layout/simple-bar.js +1 -1
  241. package/layout/slider-2025.js +1 -1
  242. package/layout/spacer.js +1 -1
  243. package/layout/stack.js +1 -1
  244. package/layout/svg.js +1 -1
  245. package/layout/switcher.js +1 -1
  246. package/layout/table.js +1 -1
  247. package/layout/text-field.js +16 -1
  248. package/layout/text.js +28 -1
  249. package/layout/ulist.js +1 -1
  250. package/layout/video.js +1 -1
  251. package/layout/wrap.js +1 -1
  252. package/layout/zoom-card.js +1 -1
  253. package/lib/agent-surface.js +1 -1
  254. package/lib/card-getter.js +1 -1
  255. package/lib/check-page.js +348 -0
  256. package/lib/codegen.js +1 -1
  257. package/lib/data.js +1 -1
  258. package/lib/designer.js +64 -2
  259. package/lib/element-mapper.js +121 -23
  260. package/lib/element-params.generated.js +2 -2
  261. package/lib/keyframe-animation.js +1 -1
  262. package/lib/link-getter.js +1 -1
  263. package/lib/morph-node.js +8 -1
  264. package/lib/normalize-spec.js +134 -0
  265. package/lib/parse-html.js +1 -1
  266. package/lib/raster-inspect.js +1 -1
  267. package/lib/raster-ops.js +1 -1
  268. package/lib/raster-presets.js +1 -1
  269. package/lib/resolve-refs.js +121 -0
  270. package/lib/scroll-video.js +1 -1
  271. package/lib/seo.js +1 -1
  272. package/lib/stacker.js +1 -1
  273. package/lib/suggest.js +1 -1
  274. package/lib/theme.js +1 -1
  275. package/lib/transform-anim.js +1 -1
  276. package/lib/transition.js +1 -1
  277. package/lib/validate-nodes.js +111 -3
  278. package/lib/webmcp-adapter.js +1 -1
  279. package/package.json +3 -1
  280. package/schema.json +973 -12
  281. package/scripts/generate-schema.mjs +9 -1
  282. package/skills/nodality/SKILL.md +50 -0
package/lib/seo.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.11
2
+ * nodality v1.3.1
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/stacker.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.11
2
+ * nodality v1.3.1
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/suggest.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.11
2
+ * nodality v1.3.1
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/theme.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.11
2
+ * nodality v1.3.1
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.11
2
+ * nodality v1.3.1
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/transition.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.11
2
+ * nodality v1.3.1
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.11
2
+ * nodality v1.3.1
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -30,6 +30,8 @@ import {
30
30
  FRAMEWORK_DOC, EASING_NAMES,
31
31
  } from "./raster-ops.js";
32
32
  import { presetNames, presetInfo } from "./raster-presets.js";
33
+ import { collectRefs, isRef, resolveRefs } from "./resolve-refs.js";
34
+ import { normalizeSpec, aliasConflicts, ALIASES } from "./normalize-spec.js";
33
35
  import { ELEMENT_TYPES } from "./element-mapper.js";
34
36
  import { ELEMENT_PARAM_NAMES } from "./element-params.generated.js";
35
37
  import { didYouMean, suggest, levenshtein } from "./suggest.js";
@@ -161,7 +163,16 @@ export const CONTENT_SLOT = {
161
163
  row: "children", form: "children", wrap: "children",
162
164
  };
163
165
 
164
- export function validateNodes(nodes, elements) {
166
+ // Of the types that read `items`, which read the entries as ELEMENT SPECS
167
+ // rather than as data. `ulist` maps every object entry through the mapper;
168
+ // `cards`, `nav`, `sideNav` and `table` read theirs as data — nav entries are
169
+ // `{title, link}`, table entries are rows — and walking those as elements
170
+ // would report every correct page. Pinned against the mapper by a test rather
171
+ // than trusted, because a hand-kept table beside a mapper is exactly what
172
+ // drifted before 1.2.8.
173
+ export const SPEC_ITEMS = new Set(["ulist"]);
174
+
175
+ export function validateNodes(nodes, elements, defs) {
165
176
  const errors = [];
166
177
  const push = (code, path, got, suggestions, valid) => {
167
178
  errors.push({
@@ -219,6 +230,42 @@ export function validateNodes(nodes, elements) {
219
230
  // element whose `type` names nothing is not a silent no-op but a THROW
220
231
  // from the mapper at render time, which reaches a generator as a stack
221
232
  // trace rather than as a report it can repair from.
233
+ // Tier 3: a reference with no definition. Reported here as well as
234
+ // refused at render, because a report is repairable in one turn and a
235
+ // throw is not — the Stage 3 contract applied to the newest way of being
236
+ // wrong. `defs` is optional: without it references are left alone rather
237
+ // than all reported missing, since a caller may validate a page before
238
+ // deciding what to share.
239
+ if (defs && typeof defs === "object" && Array.isArray(elements)) {
240
+ const defined = Object.keys(defs);
241
+ for (const { name, path } of collectRefs(elements)) {
242
+ if (!Object.prototype.hasOwnProperty.call(defs, name)) {
243
+ push("DANGLING_REF", path, name, suggest(name, defined), defined);
244
+ }
245
+ }
246
+ }
247
+
248
+ // Walk the RESOLVED tree when definitions are available. A reference
249
+ // with keys beside it is a real element once merged, and only the merged
250
+ // form can be checked: `{ $ref: "nav", itms: [] }` is a typo in an
251
+ // override, and validating the unexpanded reference would miss it —
252
+ // a silent no-op inside the mechanism built to remove silent no-ops.
253
+ let toWalk = (defs && typeof defs === "object" && Array.isArray(elements))
254
+ ? resolveRefs(elements, defs)
255
+ : elements;
256
+
257
+ // An alias and its canonical name both present, disagreeing: one of them
258
+ // is going to be ignored. The author hears it here rather than finding it
259
+ // in the page.
260
+ for (const c of aliasConflicts(Array.isArray(elements) ? elements : [])) {
261
+ push("CONFLICTING_ALIAS", c.path, c.alias, [c.canonical],
262
+ [`"${c.alias}" is accepted as "${c.canonical}"; give one of them, not both`]);
263
+ }
264
+
265
+ // Walk the normalised tree, so `{type:"img", src:"..."}` is checked as the
266
+ // element it actually becomes rather than reported as an unknown parameter.
267
+ if (Array.isArray(toWalk)) toWalk = normalizeSpec(toWalk);
268
+
222
269
  if (idsKnown) {
223
270
  const walkEls = (list, at) => {
224
271
  if (!Array.isArray(list)) return;
@@ -239,6 +286,12 @@ export function validateNodes(nodes, elements) {
239
286
  push("BAD_ELEMENT", p, el, [], ["{ type: \"<element type>\", ... }"]);
240
287
  return;
241
288
  }
289
+ // A reference is not an element and has no `type` by design.
290
+ // Its validity is whether the name resolves, which the
291
+ // DANGLING_REF pass above decides; reporting MISSING_FIELD
292
+ // here rejected every correct `$ref` there is.
293
+ if (isRef(el)) return;
294
+
242
295
  if (typeof el.type !== "string") {
243
296
  push("MISSING_FIELD", `${p}.type`, el.type, [],
244
297
  ["an element type, e.g. \"h1\", \"wrap\", \"nav\""]);
@@ -252,6 +305,19 @@ export function validateNodes(nodes, elements) {
252
305
  // removed for `cards`, surviving one slot over. It is not a
253
306
  // typo, so near-miss detection cannot reach it.
254
307
  const wants = CONTENT_SLOT[el.type];
308
+ if (wants) {
309
+ // A content slot that is present but not a list. The
310
+ // mapper tests `Array.isArray(...) && length`, so
311
+ // `items: {…}` and `items: "x"` fall through to the
312
+ // placeholders — content declared, content ignored, no
313
+ // error. Same family as WRONG_CONTENT_SLOT: the key is
314
+ // right and the value cannot carry anything.
315
+ const given = el[wants];
316
+ if (given !== undefined && given !== null && !Array.isArray(given)) {
317
+ push("BAD_CONTENT_SHAPE", `${p}.${wants}`, given, [],
318
+ [`an array — "${el.type}" reads \`${wants}\` as a list`]);
319
+ }
320
+ }
255
321
  if (wants) {
256
322
  const other = wants === "items" ? "children" : "items";
257
323
  const filled = (v) => Array.isArray(v) ? v.length > 0 : v != null;
@@ -325,9 +391,51 @@ export function validateNodes(nodes, elements) {
325
391
  if (Array.isArray(entry)) walkEls(entry, `${p}.items[${k}]`);
326
392
  });
327
393
  }
394
+
395
+ // Phase S7: an object entry with no `type`, in a slot whose
396
+ // entries ARE elements. `{type:"ulist", items:[{text:"Fast"}]}`
397
+ // validated clean and then threw from deep in the mapper —
398
+ // `Unknown element type "undefined"` — which is the Stage 3
399
+ // contract exactly inverted: validate-then-render handed the
400
+ // agent a stack trace in the one place the tool exists to
401
+ // prevent it.
402
+ //
403
+ // Tier 7 measured what that cost. The throw names every valid
404
+ // type and not one word about WHICH entry, so it is the same
405
+ // string for every fault of this shape on any page; the weaker
406
+ // of the two models repaired none of them and returned a
407
+ // byte-identical spec. The suggestion below therefore hands
408
+ // back the replacement rather than describing it.
409
+ //
410
+ // A string entry is the shorthand and stays valid.
411
+ if (SPEC_ITEMS.has(el.type) && Array.isArray(el.items)) {
412
+ el.items.forEach((entry, k) => {
413
+ const at = `${p}.items[${k}]`;
414
+ if (typeof entry === "string" || typeof entry === "number") return;
415
+ if (Array.isArray(entry)) return;
416
+ if (!entry || typeof entry !== "object") {
417
+ push("BAD_ELEMENT", at, entry, [],
418
+ ["a string, or { type: \"<element type>\", ... }"]);
419
+ return;
420
+ }
421
+ if (isRef(entry)) return;
422
+ if (typeof entry.type !== "string") {
423
+ const text = ["text", "title", "label"]
424
+ .map((k2) => entry[k2]).find((v) => typeof v === "string");
425
+ push("MISSING_FIELD", `${at}.type`, entry.type,
426
+ text === undefined ? []
427
+ : [JSON.stringify(text),
428
+ `{ "type": "p", "text": ${JSON.stringify(text)} }`],
429
+ [`"${el.type}" reads each item as an element; a plain string is the shorthand`]);
430
+ } else if (!ELEMENT_TYPES.includes(entry.type)) {
431
+ push("UNKNOWN_ELEMENT_TYPE", `${at}.type`, entry.type,
432
+ suggest(entry.type, ELEMENT_TYPES), ELEMENT_TYPES);
433
+ }
434
+ });
435
+ }
328
436
  });
329
437
  };
330
- walkEls(elements, "elements");
438
+ walkEls(toWalk, "elements");
331
439
  }
332
440
 
333
441
  nodes.forEach((node, i) => {
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.11
2
+ * nodality v1.3.1
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodality",
3
- "version": "1.2.11",
3
+ "version": "1.3.1",
4
4
  "description": "A lightweight library for declarative UI elements.",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.esm.js",
@@ -33,6 +33,8 @@
33
33
  "./raster": "./lib/raster-ops.js",
34
34
  "./validate": "./lib/validate-nodes.js",
35
35
  "./parse": "./lib/parse-html.js",
36
+ "./refs": "./lib/resolve-refs.js",
37
+ "./normalize": "./lib/normalize-spec.js",
36
38
  "./inspect": "./lib/raster-inspect.js",
37
39
  "./examples/*": "./examples/*.js",
38
40
  "./transition": "./lib/transition.js",