@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 +64 -0
- package/package.json +2 -2
- package/validator.js +104 -0
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.
|
|
4
|
-
"description": "AdiaUI A2UI validator
|
|
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
|
*/
|