@adia-ai/a2ui-validator 0.5.3 → 0.5.5

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/CHANGELOG.md CHANGED
@@ -7,6 +7,70 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
7
7
 
8
8
  _No pending changes._
9
9
 
10
+ ## [0.5.5] - 2026-05-14
11
+
12
+ ### Changed — lockstep ride-along; no source changes
13
+
14
+ `version`: `0.5.4` → `0.5.5`. Source byte-identical to `0.5.4`.
15
+
16
+ Tracking the v0.5.5 cycle's detector-driven cleanup arcs (§176-§179) + exe.dev LLM Gateway migration (§181-§188) + FEEDBACK-08 closure (§184): see root `CHANGELOG.md` for the full narrative. This package carries no source changes this cycle.
17
+
18
+ ## [0.5.4] - 2026-05-14
19
+
20
+ ### Added — §165 validator check #19 `componentsHaveRenderableContent` — the 3rd safety layer (v0.5.4)
21
+
22
+ `packages/a2ui/validator/validator.js:516-616` — new validator check
23
+ firing on every `updateComponents` message tree. Closes the
24
+ "registered + valid + visually empty" gap diagnosed 2026-05-14:
25
+ pre-§165 the validator had 18 checks but ZERO coverage of components
26
+ that pass type registration + schema validation and still render
27
+ blank rectangles because leaf interactive primitives carry no
28
+ content-bearing props.
29
+
30
+ **The 3-layer safety net**:
31
+
32
+ Layer 1 — Type registration (renderer.js:141 resolveTag → element upgrade)
33
+ Layer 2 — Schema validation (18 checks)
34
+ Layer 3 — Visual integrity (this check, NEW)
35
+
36
+ Audits every component in the tree:
37
+
38
+ - **Interactive leaf types** (Button, Input/TextField/TextArea,
39
+ CheckBox/Check/Toggle/Switch/Radio, Select/ChoicePicker, Slider/
40
+ Range/Rating, Search/OtpInput/CalendarPicker/DateTimeInput/
41
+ ColorPicker/Upload) must have at least one of `label`, `placeholder`,
42
+ `text`, `aria-label`, `icon`, `textContent`, `value`, `alt`, `src`,
43
+ OR have non-empty `children[]`.
44
+ - **Text** must have `textContent` OR non-empty `children[]`.
45
+ - **Image** must have `src`.
46
+ - Container nodes (Card / Section / Header / Column / Row / etc.) are
47
+ not in scope — they pass on structural grounds.
48
+
49
+ **Broader than existing `interactiveHasLabel` check (#12)**: #12's
50
+ INTERACTIVE_TYPES set uses Check/Toggle but skips Input/CheckBox/Switch
51
+ (the registry-aliased forms the harvester emits) — that's why
52
+ auth-signin-card-password slipped through pre-§165. #19's set covers
53
+ both alias forms + 19 total interactive types + accepts icon-only Buttons
54
+ via `icon` OR `aria-label`.
55
+
56
+ Score weight: 6 (between `noBareDivs:7` and `flatAdjacency:5`). Per-violation
57
+ deduction. Doesn't flip overall validity unless 4+ leaf types are empty.
58
+
59
+ ### Added — `components-have-renderable-content.test.js` (+281 LOC, 20 tests, NEW)
60
+
61
+ `packages/a2ui/validator/components-have-renderable-content.test.js`
62
+ pins the §165 check across 8 describe groups:
63
+
64
+ - 19 standalone fixtures asserting check FAILS pre-content + PASSES
65
+ post-content for Input/CheckBox/Switch/Button/Text/Image
66
+ - Icon-only Button + aria-label-only Button pass
67
+ - Container nodes (Card/Column/Row/Section) untouched
68
+ - Full pre-§163 `auth-signin-card-password` tree FAILS with all 5
69
+ expected violations called out by id
70
+ - Post-§163 tree PASSES
71
+
72
+ ### Changed — `version`: `0.5.3` → `0.5.4`.
73
+
10
74
  ## [0.5.3] - 2026-05-14
11
75
 
12
76
  _No pending changes._
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adia-ai/a2ui-validator",
3
- "version": "0.5.3",
4
- "description": "AdiaUI A2UI validator JSON Schema structural validation plus catalog-aware semantic validation (component exists, props match YAML). Split out from the compose engine so non-compose tooling (tests, MCP validator tools, CI gates) can depend on validation without pulling the whole generator graph.",
3
+ "version": "0.5.5",
4
+ "description": "AdiaUI A2UI validator \u2014 JSON Schema structural validation plus catalog-aware semantic validation (component exists, props match YAML). Split out from the compose engine so non-compose tooling (tests, MCP validator tools, CI gates) can depend on validation without pulling the whole generator graph.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
7
7
  "exports": {
package/validator.js CHANGED
@@ -31,6 +31,7 @@ const WEIGHTS = {
31
31
  gridVsColumn: 3,
32
32
  landmarkStructure: 3,
33
33
  intentAlignment: 13,
34
+ componentsHaveRenderableContent: 6, // §165 (v0.5.4) — the 3rd safety layer
34
35
  };
35
36
 
36
37
  // Wiring check weights (scored separately, not affecting component score)
@@ -108,6 +109,7 @@ export function validateSchema(messages, options = {}) {
108
109
  checkTextContentSet(allComponents),
109
110
  checkIdUniqueness(allComponents),
110
111
  checkInteractiveHasLabel(allComponents),
112
+ checkComponentsHaveRenderableContent(allComponents),
111
113
  checkImagesHaveAlt(allComponents),
112
114
  checkHeadingHierarchy(allComponents),
113
115
  checkTabStructure(allComponents),
@@ -512,6 +514,108 @@ function checkInteractiveHasLabel(components) {
512
514
  };
513
515
  }
514
516
 
517
+ /**
518
+ * §165 (v0.5.4): componentsHaveRenderableContent — the 3rd safety layer.
519
+ *
520
+ * Catches the bug class diagnosed 2026-05-14 (commit `964885e70` postmortem):
521
+ * components that ARE registered (Layer 1 passes) AND structurally valid
522
+ * (Layer 2 passes) AND still render as blank rectangles because they have
523
+ * NO content-bearing props.
524
+ *
525
+ * Bug-class examples that this check catches:
526
+ * { id: "password", component: "Input" } ← no label/placeholder
527
+ * { id: "remember", component: "CheckBox", name: "remember" } ← no label
528
+ * { id: "btn-magic", component: "Button" } ← no text/icon
529
+ * { id: "title", component: "Text" } ← no textContent
530
+ * { id: "logo", component: "Image" } ← no src
531
+ *
532
+ * Broader scope than `interactiveHasLabel` (check #12):
533
+ * - covers Input/CheckBox/Switch (the registry-aliased forms missed by #12's
534
+ * `Check/Toggle` set)
535
+ * - accepts icon-only buttons (with `icon` OR `aria-label`)
536
+ * - accepts container nodes with children (Card/Section/Header pass on
537
+ * structural-not-leaf grounds; the check enforces content for LEAF
538
+ * interactive types)
539
+ *
540
+ * Score semantics: per-violation deduction. Each empty interactive primitive
541
+ * subtracts 1/total from the score. 0 violations = 1.0; all-empty = 0.
542
+ */
543
+ function checkComponentsHaveRenderableContent(components) {
544
+ // Interactive types: must have at least one content-bearing prop OR be
545
+ // non-leaf (has children that provide content).
546
+ const INTERACTIVE_LEAF_TYPES = new Set([
547
+ 'Button', 'Input', 'TextField', 'TextArea',
548
+ 'CheckBox', 'Check', 'Toggle', 'Switch', 'Radio',
549
+ 'Select', 'ChoicePicker', 'Slider', 'Range', 'Rating',
550
+ 'Search', 'OtpInput', 'CalendarPicker', 'DateTimeInput',
551
+ 'ColorPicker', 'Upload',
552
+ ]);
553
+ // Content-bearing props that satisfy the "renderable" requirement.
554
+ const CONTENT_PROPS = ['label', 'placeholder', 'text', 'aria-label',
555
+ 'icon', 'textContent', 'value', 'alt', 'src'];
556
+
557
+ const hasContent = (comp) => {
558
+ for (const k of CONTENT_PROPS) {
559
+ const v = comp[k];
560
+ if (v !== undefined && v !== null && v !== '') return true;
561
+ }
562
+ // Non-leaf interactives (e.g. CalendarPicker with named children) pass
563
+ if (Array.isArray(comp.children) && comp.children.length > 0) return true;
564
+ return false;
565
+ };
566
+
567
+ const violations = [];
568
+ let interactiveCount = 0;
569
+ for (const comp of components) {
570
+ if (!INTERACTIVE_LEAF_TYPES.has(comp.component)) continue;
571
+ interactiveCount++;
572
+ if (!hasContent(comp)) {
573
+ violations.push(`"${comp.id}" (${comp.component})`);
574
+ }
575
+ }
576
+
577
+ // Also check Text components — empty Text is a degenerate case worth surfacing.
578
+ // Text-specific: needs textContent OR children (Text + nested Strong etc.)
579
+ let textCount = 0;
580
+ let emptyTextCount = 0;
581
+ for (const comp of components) {
582
+ if (comp.component !== 'Text') continue;
583
+ textCount++;
584
+ const t = comp.textContent;
585
+ if ((t === undefined || t === null || t === '') &&
586
+ !(Array.isArray(comp.children) && comp.children.length > 0)) {
587
+ emptyTextCount++;
588
+ violations.push(`"${comp.id}" (Text: empty)`);
589
+ }
590
+ }
591
+
592
+ // Also check Image — needs src (catalog-mandatory)
593
+ let imageCount = 0;
594
+ let imageNoSrc = 0;
595
+ for (const comp of components) {
596
+ if (comp.component !== 'Image') continue;
597
+ imageCount++;
598
+ if (!comp.src) {
599
+ imageNoSrc++;
600
+ violations.push(`"${comp.id}" (Image: no src)`);
601
+ }
602
+ }
603
+
604
+ const total = interactiveCount + textCount + imageCount;
605
+ const passed = violations.length === 0;
606
+ return {
607
+ name: 'componentsHaveRenderableContent',
608
+ passed,
609
+ score: total === 0
610
+ ? 1
611
+ : (passed ? 1 : Math.max(0, 1 - violations.length / total)),
612
+ detail: passed
613
+ ? (total === 0 ? 'No interactive/Text/Image components to validate'
614
+ : 'All interactive/Text/Image components have renderable content')
615
+ : `Empty components (registered + valid but render blank): ${violations.join(', ')}`,
616
+ };
617
+ }
618
+
515
619
  /**
516
620
  * 13. imagesHaveAlt — Image components should have an alt property.
517
621
  */