@adia-ai/a2ui-validator 0.5.3 → 0.5.4

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,62 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
7
7
 
8
8
  _No pending changes._
9
9
 
10
+ ## [0.5.4] - 2026-05-14
11
+
12
+ ### Added — §165 validator check #19 `componentsHaveRenderableContent` — the 3rd safety layer (v0.5.4)
13
+
14
+ `packages/a2ui/validator/validator.js:516-616` — new validator check
15
+ firing on every `updateComponents` message tree. Closes the
16
+ "registered + valid + visually empty" gap diagnosed 2026-05-14:
17
+ pre-§165 the validator had 18 checks but ZERO coverage of components
18
+ that pass type registration + schema validation and still render
19
+ blank rectangles because leaf interactive primitives carry no
20
+ content-bearing props.
21
+
22
+ **The 3-layer safety net**:
23
+
24
+ Layer 1 — Type registration (renderer.js:141 resolveTag → element upgrade)
25
+ Layer 2 — Schema validation (18 checks)
26
+ Layer 3 — Visual integrity (this check, NEW)
27
+
28
+ Audits every component in the tree:
29
+
30
+ - **Interactive leaf types** (Button, Input/TextField/TextArea,
31
+ CheckBox/Check/Toggle/Switch/Radio, Select/ChoicePicker, Slider/
32
+ Range/Rating, Search/OtpInput/CalendarPicker/DateTimeInput/
33
+ ColorPicker/Upload) must have at least one of `label`, `placeholder`,
34
+ `text`, `aria-label`, `icon`, `textContent`, `value`, `alt`, `src`,
35
+ OR have non-empty `children[]`.
36
+ - **Text** must have `textContent` OR non-empty `children[]`.
37
+ - **Image** must have `src`.
38
+ - Container nodes (Card / Section / Header / Column / Row / etc.) are
39
+ not in scope — they pass on structural grounds.
40
+
41
+ **Broader than existing `interactiveHasLabel` check (#12)**: #12's
42
+ INTERACTIVE_TYPES set uses Check/Toggle but skips Input/CheckBox/Switch
43
+ (the registry-aliased forms the harvester emits) — that's why
44
+ auth-signin-card-password slipped through pre-§165. #19's set covers
45
+ both alias forms + 19 total interactive types + accepts icon-only Buttons
46
+ via `icon` OR `aria-label`.
47
+
48
+ Score weight: 6 (between `noBareDivs:7` and `flatAdjacency:5`). Per-violation
49
+ deduction. Doesn't flip overall validity unless 4+ leaf types are empty.
50
+
51
+ ### Added — `components-have-renderable-content.test.js` (+281 LOC, 20 tests, NEW)
52
+
53
+ `packages/a2ui/validator/components-have-renderable-content.test.js`
54
+ pins the §165 check across 8 describe groups:
55
+
56
+ - 19 standalone fixtures asserting check FAILS pre-content + PASSES
57
+ post-content for Input/CheckBox/Switch/Button/Text/Image
58
+ - Icon-only Button + aria-label-only Button pass
59
+ - Container nodes (Card/Column/Row/Section) untouched
60
+ - Full pre-§163 `auth-signin-card-password` tree FAILS with all 5
61
+ expected violations called out by id
62
+ - Post-§163 tree PASSES
63
+
64
+ ### Changed — `version`: `0.5.3` → `0.5.4`.
65
+
10
66
  ## [0.5.3] - 2026-05-14
11
67
 
12
68
  _No pending changes._
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/a2ui-validator",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
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.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
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
  */