baldart 4.76.1 → 4.78.0

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 (25) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +14 -13
  3. package/VERSION +1 -1
  4. package/framework/.claude/agents/REGISTRY.md +2 -1
  5. package/framework/.claude/agents/markup-fidelity-verifier.md +239 -0
  6. package/framework/.claude/agents/prd-card-writer.md +6 -1
  7. package/framework/.claude/agents/visual-fidelity-verifier.md +22 -0
  8. package/framework/.claude/skills/design-system-init/SKILL.md +16 -6
  9. package/framework/.claude/skills/design-system-init/scripts/compile-ds-cards.mjs +2 -1
  10. package/framework/.claude/skills/design-system-init/scripts/extract-manifest.mjs +17 -5
  11. package/framework/.claude/skills/design-system-init/scripts/extract-ts.mjs +302 -0
  12. package/framework/.claude/skills/design-system-init/scripts/render-manifest.mjs +26 -9
  13. package/framework/.claude/skills/design-system-init/scripts/serialize-spec.mjs +4 -2
  14. package/framework/.claude/skills/ds-edit/SKILL.md +2 -1
  15. package/framework/.claude/skills/ds-new/SKILL.md +2 -2
  16. package/framework/.claude/skills/e2e-review/SKILL.md +196 -27
  17. package/framework/.claude/skills/new/references/codex-gate.md +15 -1
  18. package/framework/.claude/skills/new/references/completeness.md +2 -0
  19. package/framework/.claude/skills/new/references/review-cycle.md +14 -1
  20. package/framework/agents/component-manifest-schema.md +24 -3
  21. package/framework/agents/design-system-protocol.md +79 -1
  22. package/framework/docs/COMPONENT-MANIFEST-LAYER.md +6 -3
  23. package/framework/templates/component-spec.template.md +3 -2
  24. package/framework/templates/overlays/e2e-review.fidelity-example.md +74 -0
  25. package/package.json +1 -1
@@ -208,16 +208,31 @@ available (probe `get_design_context` / `get_screenshot` tools at runtime):
208
208
  If the MCP tools error or time out, fall through silently to level (b) — do
209
209
  not surface the failure as a blocker.
210
210
 
211
- ### Level (b) — Local image
212
-
213
- If `card.links.design[]` contains a local file path (PNG / JPG / PDF) OR the
214
- card has a `mockups/` directory alongside its YAML:
215
-
216
- - Resolve the path. For PDFs, extract the first page as PNG (use `pdftoppm`
217
- or equivalent — degrade gracefully if unavailable).
218
- - Copy to `.baldart/e2e-review/<CARD-ID>/mockups/<route-slug>.local.png`.
211
+ ### Level (b) — Local image (PNG / JPG / PDF / **HTML**)
212
+
213
+ If `card.links.design[]` contains a local file path OR the card has a `mockups/`
214
+ directory alongside its YAML:
215
+
216
+ - **PNG / JPG**: copy to `.baldart/e2e-review/<CARD-ID>/mockups/<route-slug>.local.png`.
217
+ - **PDF**: extract the first page as PNG (`pdftoppm` or equivalent — degrade gracefully
218
+ if unavailable).
219
+ - **HTML** (`links.design` ends in `.html`): the mockup is markup, not an image. Two
220
+ things happen — (1) render it headless to a PNG for the visual pass: open `file://<abs
221
+ path>` with Playwright at 1440px and `page.screenshot({fullPage:true})` →
222
+ `.baldart/e2e-review/<CARD-ID>/mockups/<route-slug>.local.png` (degrade gracefully if
223
+ the render fails — the structural pass below still runs); (2) **retain the HTML path**
224
+ for the route-free structural pass (Phase 2.7).
219
225
  - Record `mockup_source.level = "local"` and the source path.
220
226
 
227
+ **Structural source (route-free ground truth).** Independently of the image, record a
228
+ `structural_source` for the route when the mockup is **code**:
229
+ - `links.design` is an `.html` file → `structural_source = { level: "html", mockup_path:
230
+ "<abs html path>" }`.
231
+ - `links.design_src` is set / `mockups/_src/` exists (finished Claude Design CODE, v4.75)
232
+ → `structural_source = { level: "design_src", design_src_dir: "<abs dir>" }`.
233
+ This feeds Phase 2.7 (`markup-fidelity-verifier`) and is what makes per-card fidelity
234
+ verifiable **without rendering the live route**.
235
+
221
236
  ### Level (c) — Compliance-only (registry)
222
237
 
223
238
  If no mockup image is available BUT `features.has_design_system: true`:
@@ -244,6 +259,59 @@ The cascade output is appended to the plan under `mockup_refs[]`.
244
259
 
245
260
  ---
246
261
 
262
+ ## Phase 2.7 — Structural Fidelity (route-free, runs BEFORE the browser)
263
+
264
+ **Goal**: catch layout/structure divergence from the mockup **without rendering the live
265
+ route** — the route-independent first line of defense. This is what would have caught
266
+ FEAT-0061 (a 2-column desktop mockup built as a single column) even though the
267
+ auth-gated route returned 500 in the harness. SSOT: `framework/agents/design-system-protocol.md` §
268
+ "Route-Independent Fidelity & Coverage Obligation" → 1. Structural Fidelity.
269
+
270
+ **When it runs**: for every route whose `structural_source` was recorded in Phase 2
271
+ (`level == "html"` or `"design_src"`). Skip the route here only when there is no
272
+ code-form mockup (image-only `figma`/PNG mockups have nothing to diff structurally —
273
+ they go through Phase 4 visual + the coverage obligation instead).
274
+
275
+ For each such route:
276
+
277
+ 1. **Resolve the implementation files** for the route from the diff / the card's File
278
+ Ownership Map (the `*.tsx` / `*.css` / `*.module.css` / `*.scss` / `*.svelte` / `*.vue`
279
+ the card touched under `${paths.app_dir}` / `${paths.components_root}`).
280
+ 2. **Invoke `markup-fidelity-verifier`** (input contract in
281
+ [`markup-fidelity-verifier.md`](../../agents/markup-fidelity-verifier.md)):
282
+
283
+ ```
284
+ subagent_type: "markup-fidelity-verifier"
285
+ mode: "bypassPermissions" // when called from /new
286
+ prompt: |
287
+ Compare the implementation's code structure against the mockup's code structure
288
+ and return JSON per your output schema.
289
+
290
+ Input payload:
291
+ {
292
+ "card_id": "FEAT-XXXX",
293
+ "route": "/settings",
294
+ "mockup_source": { ...the route's structural_source (html | design_src)... },
295
+ "implementation_files": [ ...abs paths the card touched for this route... ],
296
+ "tolerance": "<from config>",
297
+ "resolved_orphans": [ ...from the card's UI Element Inventory, as in Phase 4... ]
298
+ }
299
+ ```
300
+ 3. **Parse the JSON strictly** (same discipline as Phase 4). On malformed output log
301
+ `markup_verifier_protocol_violation` in `gaps[]` and treat the route's structural
302
+ findings as empty.
303
+ 4. **Aggregate** structural findings into `structural_findings[]` (tag each `source:
304
+ "structural"`). They flow into the Phase 5 gate under the canonical taxonomy — a
305
+ `layout-break` here is Critical and BLOCKS. Because this runs before Phase 3/4, a clear
306
+ structural divergence blocks **cheaply**, before the expensive browser + multimodal
307
+ passes.
308
+
309
+ This phase is **route-independent**: it never starts a dev server, never authenticates,
310
+ never needs data. It is the workhorse of per-card fidelity; the render passes (Phase 4)
311
+ are the confirmation.
312
+
313
+ ---
314
+
247
315
  ## Phase 3 — Functional E2E (browser-based)
248
316
 
249
317
  **Goal**: execute the Gherkin scenarios from Phase 1 in a real browser and
@@ -324,11 +392,72 @@ capture the result + screenshots per route.
324
392
  - `failure_reason` (when failed, the first assertion error + stack hint)
325
393
  - `screenshot_paths[]` (per route: the desktop `<route-slug>.png` AND the
326
394
  mobile `<route-slug>@mobile.png`)
395
+ - `render_health` (per route): the HTTP status of the route navigation and
396
+ whether the screenshot is a **degenerate render** — a non-2xx/error page, or
397
+ a blank/empty-state page. Capture the response status from the Playwright
398
+ navigation (`response.status()`); flag `degenerate: true` when status is not
399
+ 2xx OR the page has no meaningful content. This drives Phase 3.8.
327
400
  5. **Functional findings**: every `failed` or `flaky` scenario becomes a
328
401
  functional finding in the aggregate report (Phase 5). Severity:
329
402
  - Auth / payments / data-mutation scenario failed → `critical`
330
403
  - Display-only scenario failed → `major`
331
404
  - Flaky (passed on retry) → `minor` with `confidence: low`
405
+ - **A `skipped` scenario is NOT silently dropped when its route has a mockup
406
+ (`mockup_source.level != "skip"`)**: a skip means the fidelity check did not
407
+ run, which is a coverage concern, not a pass — it is carried into Phase 3.8
408
+ (state remediation) and, if still un-renderable, into the coverage obligation
409
+ (Phase 4 step 1c). Silently skipping a mockup'd route is the exact FEAT-0061
410
+ failure and is prohibited.
411
+
412
+ ---
413
+
414
+ ## Phase 3.8 — Comparable-render gate (route-independent lane + state remediation)
415
+
416
+ **Goal**: guarantee that the screenshot handed to Phase 4 is **comparable to the
417
+ mockup** — rendered route-independently where possible, and in the **data state the
418
+ mockup depicts**. The reviewer never accepts "I opened the page, it's empty, I skip".
419
+ SSOT: `design-system-protocol.md` § "Route-Independent Fidelity & Coverage Obligation"
420
+ → 2. Visual Fidelity + 3. State-aware Fidelity.
421
+
422
+ For each route with a mockup (`mockup_source.level != "skip"`):
423
+
424
+ 1. **Pick the render lane** (priority order):
425
+ - **(a) Isolated render** — when the route's primary surface is a **registry
426
+ primitive** (a component documented under `${paths.components_primitives}` with a
427
+ spec HEAD), render it in isolation via the `/ds-render` harness (`baldart render
428
+ build|shot`). This needs no route, no auth, no data — the route-independent visual
429
+ confirmation. Save to `.baldart/e2e-review/<CARD-ID>/screenshots/<route-slug>.iso.png`
430
+ and use it as the implementation screenshot for Phase 4. (Composed page surfaces are
431
+ NOT auto-rendered in isolation — that was refuted in v4.76; they use lane (b).)
432
+ - **(b) Live-route screenshot** from Phase 3, when the route rendered cleanly
433
+ (`render_health.degenerate != true`).
434
+ 2. **State remediation (the reach-state loop)** — when the chosen render is **degenerate**
435
+ (`render_health.degenerate == true`: non-2xx, empty-state, wrong tenant/store) and the
436
+ mockup depicts a populated state, do NOT accept it. Run a **bounded** loop (cap = 2):
437
+ - Read the consumer's `.baldart/overlays/e2e-review.md` **reach-state recipes** and run
438
+ the applicable ones in order: `seed_data` → `select_populated_context` (the multi-store
439
+ case: select a store/tenant that has data) → `goto_populated_entity`. Re-render, re-check
440
+ `render_health`.
441
+ - **Generic fallbacks when no recipe exists**: authenticate with the persona's real
442
+ creds (`test_plan.test_credentials`); enable MSW/API-mock of the data response if the
443
+ project supports it; apply a deterministic factory/snapshot seed; or heuristically
444
+ auto-seek a populated context via `webapp-testing` (enumerate an on-screen
445
+ store/tenant selector, pick the option that yields non-empty content).
446
+ - Stop as soon as a non-degenerate, populated render is produced.
447
+ 3. **Outcome**:
448
+ - A comparable render was produced → it is the implementation screenshot for Phase 4.
449
+ - Still degenerate after the loop **and** the mockup is **code-form** (a Phase 2.7
450
+ `structural_source` exists) → the structural pass already covered fidelity; record
451
+ `render_unavailable` in `gaps[]` and let Phase 4's visual step skip this route's
452
+ desktop diff (no false coverage-gap — structure was checked route-free).
453
+ - Still degenerate after the loop **and** the mockup is **image-only**
454
+ (`level ∈ {figma, local}` with no `structural_source`) → mark the route
455
+ `coverage_obligation_unmet = true` for Phase 4 step 1c (it will synthesize a
456
+ `coverage-gap`).
457
+
458
+ The orchestrator owns navigation/seeding here; `visual-fidelity-verifier` stays stateless
459
+ and only judges the frame it is handed (emitting `data-state-mismatch` if the state is
460
+ still wrong).
332
461
 
333
462
  ---
334
463
 
@@ -353,8 +482,19 @@ For each `route` in `plan.routes[]`:
353
482
  - When `mockup_source.level == "skip"`, skip the **desktop fidelity diff**
354
483
  (steps 2) for this route — but still run the **mobile responsive pass**
355
484
  (step 2b) if a `<route-slug>@mobile.png` screenshot was captured, since it
356
- needs no mockup. Skip the route entirely only when no screenshot exists
357
- (route unrenderable).
485
+ needs no mockup.
486
+ - **1c. Coverage obligation (un-runnable required check BLOCKS, never passes).**
487
+ When the route is marked `coverage_obligation_unmet = true` from Phase 3.8 (an
488
+ **image-only** mockup — `level ∈ {figma, local}` with no `structural_source` — that
489
+ could not be rendered in any lane and whose state could not be reached), **synthesize
490
+ a `coverage-gap` finding (Critical)** instead of silently skipping the route. SSOT:
491
+ `design-system-protocol.md` § "...→ 4. Coverage Obligation". **Guards — NEVER synthesize
492
+ `coverage-gap`** when `mockup_source.level` is `skip` / `harness-render` /
493
+ `compliance-only`, nor for the mobile pass (it runs at `level: skip`), nor when a
494
+ Phase 2.7 `structural_source` already covered fidelity route-free. A `coverage-gap`
495
+ is **not fixable by an agent** — it bypasses the Phase 5b self-heal loop and maps to
496
+ the `blocked` verdict (the route still will not render). This is the guard that turns
497
+ the FEAT-0061 "skip masked as pass" into a real block.
358
498
  - When `mockup_source.level == "harness-render"` (a render-harness PNG of an
359
499
  ISOLATED registry primitive, from `/ds-render` — Stage C of the closed loop),
360
500
  **NEVER run the fidelity diff (step 2)**: the image is a render of the
@@ -505,14 +645,20 @@ For each such `route`:
505
645
  the canonical severity taxonomy from `visual-fidelity-verifier.md`.
506
646
 
507
647
  1. **Compose** the `findings[]` array:
648
+ - **Structural findings** from Phase 2.7 (`source: "structural"`) — route-free
649
+ code-structure divergence (`markup-fidelity-verifier`).
508
650
  - Functional findings from Phase 3 (`source: "functional"`).
509
- - Visual findings from Phase 4 (`source: "visual"`).
651
+ - Visual findings from Phase 4 (`source: "visual"`) — including any
652
+ `data-state-mismatch` (the verifier saw an empty/wrong-state render) and any
653
+ orchestrator-synthesized `coverage-gap` (Phase 4 step 1c — a required check that
654
+ could not run at all).
510
655
  - Design-quality findings from Phase 4b (`source: "design-quality"`).
511
656
  - Each finding carries: `severity`, `category` (or `dimension` for
512
657
  design-quality), `source`, `route`, `description`, `expected`, `actual`,
513
- `fix_hint`, `evidence`, `ds_drift_code`, `confidence`. All three sources
658
+ `fix_hint`, `evidence`, `ds_drift_code`, `confidence`. All sources
514
659
  share the **same canonical severity taxonomy**, so the tolerance filter in
515
- step 2 applies to them uniformly — no per-source special-casing.
660
+ step 2 applies to them uniformly — no per-source special-casing. (`coverage-gap`
661
+ and `data-state-mismatch` are Critical and gate in every tolerance.)
516
662
  2. **Apply tolerance filter** based on
517
663
  `features.e2e_review.fidelity_tolerance`:
518
664
  - `strict` (DEFAULT): Critical + Major + Minor all gate. Additionally,
@@ -533,16 +679,25 @@ applies only when `verdict == "blocked"` AND `iteration < max`.
533
679
 
534
680
  For each iteration:
535
681
 
536
- 1. **Route the gating findings to the right fixer by `source`**, then spawn the
537
- fixer(s). Design-quality fixes are UI-scoped (presentation / layout / styling
538
- / motion) they belong to `ui-expert`, the UI domain owner, NOT to `coder`.
539
- Functional fixes belong to `coder`. (Visual-fidelity findings follow their
540
- nature: token/spacing/typography fixes route to `ui-expert`; a missing or
541
- mis-wired element route to `coder`.) When both buckets are non-empty, run
542
- `coder` first (structure), then `ui-expert` (presentation), so the critic
543
- re-evaluates the final surface.
544
-
545
- - **`ui-expert`** — gating findings where `source == "design-quality"` (and
682
+ 0. **`coverage-gap` findings bypass the loop.** A `coverage-gap` is not fixable by any
683
+ agent (the route still will not render it is a harness/data limitation, not a code
684
+ defect). Do NOT spawn a fixer for it. It keeps the verdict `blocked` and flows straight
685
+ to the override path below (programmatic mode returns `blocked`; `/new` escalates it to
686
+ a follow-up card). If `coverage-gap` is the ONLY gating finding, skip the rest of this
687
+ loop entirely.
688
+
689
+ 1. **Route the remaining gating findings to the right fixer by `source`**, then spawn the
690
+ fixer(s). **Fidelity is UI-scoped**: `source == "structural"` (layout/grid/order —
691
+ `markup-fidelity-verifier`), `source == "design-quality"`, and UI-presentation
692
+ `source == "visual"` (token/spacing/typography/color, `data-state-mismatch` left to
693
+ state remediation) all belong to **`ui-expert`**, the UI domain owner, NOT `coder`.
694
+ `coder` gets only `source == "functional"` and genuine **wiring/data-binding** visual
695
+ findings (a missing element that is unwired, not mis-laid-out). When both buckets are
696
+ non-empty, run `coder` first (structure/wiring), then `ui-expert` (presentation), so the
697
+ critic re-evaluates the final surface. **On a pure-graphics card (`owner_agent:
698
+ ui-expert`), ALL fidelity fixes go to `ui-expert` — `coder` is not involved.**
699
+
700
+ - **`ui-expert`** — gating findings where `source in {structural, design-quality}` (and
546
701
  UI-presentation `source == "visual"` findings):
547
702
 
548
703
  ```
@@ -553,13 +708,16 @@ For each iteration:
553
708
 
554
709
  ## Gating findings
555
710
  [paste the JSON findings filtered to gating severity, source in
556
- {design-quality, visual}]
711
+ {structural, design-quality, visual}]
557
712
 
558
713
  ## Rules
559
714
  - File ownership: stay within the card's allowed file set (see
560
715
  /tmp/batch-tracker-<FIRST-CARD-ID>.md ## File Ownership Map).
561
716
  - One fix per finding. Reuse existing tokens / primitives before inventing
562
717
  new ones (cite ${paths.design_system}/INDEX.md + tokens-reference.md).
718
+ - For a `structural` `layout-break`, match the mockup's layout exactly (e.g.
719
+ restore the 2-column grid the mockup declares) — cite the mockup ref in the
720
+ finding's `expected`.
563
721
  - Run the Post-Intervention Coherence Check on any registry-touching fix.
564
722
  - After the fixes, re-run lint and typecheck. Both must pass.
565
723
  - Do NOT modify Playwright specs or this skill's state files.
@@ -594,8 +752,10 @@ For each iteration:
594
752
 
595
753
  Both fixers run in `bypassPermissions` mode when called by `/new`.
596
754
 
597
- 2. **Re-run Phase 3 (functional), Phase 4 (visual), and Phase 4b (quality)**
598
- with fresh screenshots. Increment `iteration`.
755
+ 2. **Re-run the affected verifiers**: Phase 2.7 (structural, route-free re-run when a
756
+ `structural` fix was applied), Phase 3 (functional), Phase 3.8 (re-render / state),
757
+ Phase 4 (visual), and Phase 4b (quality) with fresh screenshots. Increment `iteration`.
758
+ (A `coverage-gap`-only block does not enter this loop — see step 0.)
599
759
  3. **Re-aggregate and re-decide** (back to Phase 5 step 1).
600
760
  4. **STOP** when `verdict == "passed"` OR `iteration == max`.
601
761
 
@@ -698,8 +858,14 @@ and starts from Phase 3 (skipping plan extraction and mockup cascade).
698
858
  - **`/design-review` command** — when invoked from this skill with
699
859
  `mode: "programmatic"`, returns JSON instead of Markdown for gating.
700
860
  See [`framework/.claude/commands/design-review.md`](../../commands/design-review.md).
861
+ - **`markup-fidelity-verifier`** (Phase 2.7) — route-free structural twin of
862
+ `visual-fidelity-verifier`: compares the mockup's code structure (HTML / `design_src`)
863
+ against the implementation source, no render. See
864
+ [`markup-fidelity-verifier.md`](../../agents/markup-fidelity-verifier.md). Its fixes
865
+ route to `ui-expert`.
701
866
  - **`visual-fidelity-verifier`** (Phase 4) — judges fidelity to the mockup /
702
- spec. See [`visual-fidelity-verifier.md`](../../agents/visual-fidelity-verifier.md).
867
+ spec (pixels), and emits `data-state-mismatch` when the render is in the wrong data
868
+ state. See [`visual-fidelity-verifier.md`](../../agents/visual-fidelity-verifier.md).
703
869
  - **`ui-quality-critic`** (Phase 4b) — judges intrinsic design quality against
704
870
  the rubric, independent of any mockup. See
705
871
  [`ui-quality-critic.md`](../../agents/ui-quality-critic.md). It is the design
@@ -740,6 +906,9 @@ and starts from Phase 3 (skipping plan extraction and mockup cascade).
740
906
  | Quality loop never converges (design-quality findings persist) | Taste-leaning Minor findings, or `ui-expert` fixing one dimension while regressing another | Confirm the findings are gating (not low-confidence Minor auto-demoted in strict). If genuine and unresolved after max iterations, override with reason and open a `ui-expert` follow-up card — do NOT block indefinitely on subjective polish |
741
907
  | `quality_critic_protocol_violation` in gaps | `ui-quality-critic` returned prose mixed with JSON | Re-spawn the critic once; if persistent, surface to user (likely a model regression). Treat that route's quality findings as empty meanwhile |
742
908
  | `verifier_protocol_violation` in gaps | `visual-fidelity-verifier` returned prose mixed with JSON | Re-spawn the verifier once; if persistent, surface to user (likely a model regression) |
909
+ | `coverage-gap` (Critical) on a route | An image-only mockup route could not be rendered in any lane and its state could not be reached | Correct by design — the gate BLOCKS instead of silently passing. Add a reach-state recipe to `.baldart/overlays/e2e-review.md` (seed / select populated context), or provide an HTML/`design_src` mockup so the route-free structural pass covers it. Never override without fixing the lane |
910
+ | `data-state-mismatch` (Critical) on a route | Render opened an empty / wrong-tenant / error state; mockup depicts a populated state | The reach-state loop (Phase 3.8) should seed/select a populated context. If it has no recipe and cannot auto-seek, add `select_populated_context` / `seed_data` to the e2e-review overlay |
911
+ | `markup_verifier_protocol_violation` in gaps | `markup-fidelity-verifier` returned prose mixed with JSON | Re-spawn once; if persistent, surface to user. Treat that route's structural findings as empty meanwhile |
743
912
 
744
913
  ---
745
914
 
@@ -44,7 +44,21 @@ Log the detector output in the tracker for traceability. **This step never short
44
44
  > `baldart teardown-codex-broker`), with the `baldart doctor` reaper as the safety net. Per-card hygiene
45
45
  > is none.
46
46
 
47
- For EVERY card (no conditional skip the gate ALWAYS runs; only its DEPTH varies):
47
+ **Pure-graphics-card skip (narrow exceptionsince v4.78.0).** Skip the per-card
48
+ `/codexreview` gate entirely when the card is **pure-graphics**: `owner_agent: ui-expert`
49
+ **AND** `review_profile: light` **AND** `areas == [ui]` exactly (no `api`/`data`/`logic`/`auth`
50
+ area) **AND** Step A matched **zero** high-risk triggers on the actual diff. Rationale: a
51
+ zero-logic UI card's meaningful review is the **`/e2e-review` fidelity trio** —
52
+ `markup-fidelity-verifier` (structure) + `visual-fidelity-verifier` (pixels + a11y) +
53
+ `ui-quality-critic` (quality) — plus the mechanical build/lint/tsc gates; a general Codex
54
+ bug-sweep over presentational CSS/JSX is low-value noise (the user's "qa codex review non
55
+ serve" for graphics). **Safety net preserved:** the **Final-review FULL `/codexreview`**
56
+ (net #2 below) ALWAYS re-sweeps the ENTIRE batch diff before merge — so the graphics card's
57
+ code is still Codex-reviewed batch-wide, just not per-card. The Step-A zero-trigger condition
58
+ means any high-risk path on the diff cancels the skip (escalation-only invariant). Log
59
+ `codex: SKIPPED (pure-graphics — fidelity trio is the review; Final FULL gate re-sweeps)`.
60
+
61
+ For EVERY other card (no conditional skip — the gate ALWAYS runs; only its DEPTH varies):
48
62
 
49
63
  0. **Select the Codex review profile (deterministic — Review Profile Selector)** by mapping the
50
64
  card's review profile (read from the card's `review_profile` field per the QA Profile Selector
@@ -263,6 +263,8 @@ This gate enforces `framework/agents/workflows.md § Scope Closure Discipline` a
263
263
 
264
264
  Do NOT batch the question across multiple ACs — one `AskUserQuestion` per AC, so the user sees each AC's text in isolation. Issue the questions sequentially.
265
265
 
266
+ **Fidelity-AC carve-out (BLOCKING — the FEAT-0061 fix).** When the deferred AC is a **visual-fidelity AC** — its text matches `/visual|fidelit|mockup|layout|responsive|1:1|design (match|reference)/i` OR it asserts the screen matches a design reference (`links.design` / `links.design_src`) — option 2 **"Approva il deferral" is NOT offered**. A fidelity AC cannot be silently deferred or "covered by a downstream card": that is exactly the `FEAT-0061-08 → 04/10` cross-card deferral that shipped a 1-column page against a 2-column mockup. Offer only **1 (Implementa adesso — route the fix to `ui-expert`)**, **3 (Sposta su follow-up card — explicit, tracked, and the new card carries the fidelity AC + `links.design`, so it re-faces the `/e2e-review` gate)**, **4 (Ferma il batch)**. **When AUTONOMOUS:** take option 3 (follow-up card) — never approve the deferral. Note: with the route-free structural pass (`/e2e-review` Phase 2.7) a UI card now verifies its OWN surface without a live route, so a genuine *need* to defer fidelity should be rare; treat a fidelity deferral as a signal that the per-card gate was skipped, not as a routine outcome.
267
+
266
268
  5. **`implementation_notes` deferral audit (cheap text grep — also BLOCKING)** — before exiting the phase, scan the card YAML's `implementation_notes` field (post-edits, including any text added during this phase) for any of these phrases:
267
269
 
268
270
  - `deferred` / `deferral` / `defer to`
@@ -116,6 +116,8 @@ so it surfaces in telemetry.
116
116
 
117
117
  > **Trivial fast-lane gate**: re-confirm `IS_TRIVIAL` on the ACTUAL committed diff (§ "Trivial-card fast-lane" — all 3 conditions, now including the real non-source diff check). If trivial → **SKIP this phase** AND Phase 3.5 QA + Phase 3.7 Codex; instead run the **inline mechanical gates** (`markdownlint` on changed `.md`, `lint` on lintable changed files, `build` as a sanity check — no qa-sentinel, no test suite), then proceed to Phase 3 (doc review, which DOES run) and Phase 4 (commit). Log `simplify/qa/codex: SKIPPED (trivial — non-source diff)`. If the actual diff turned out to contain a source file → NOT trivial → run this phase and the normal review path. AC-Closure (Phase 2.5b) already ran and is never skipped.
118
118
 
119
+ > **Pure-graphics fast-lane gate (since v4.78.0)**: when the card is **pure-graphics** (`owner_agent: ui-expert` AND `review_profile: light` AND `areas == [ui]` exactly — no api/data/logic/auth — AND zero Step-A high-risk triggers on the diff), **SKIP this Simplify phase** (quality-only; `ui-expert` already builds reuse-first) and Phase 3.7 Codex (per `codex-gate.md` § "Pure-graphics-card skip"). QA (Phase 3.5) is already skipped at `light` — the mechanical lint/tsc/build from Phase 2 step 8 is the "qa = build" floor. **Phase 2.6 `/e2e-review` is NOT skipped** — the fidelity trio (`markup-fidelity-verifier` + `visual-fidelity-verifier` + `ui-quality-critic`) is the real review for a graphics card. Doc-review (Phase 3) still runs (registry/spec reconciliation). Log `simplify/codex: SKIPPED (pure-graphics — fidelity trio is the review)`.
120
+
119
121
  After completeness is verified, clean up the implementation before it reaches reviewers and E2E tests. This phase launches three parallel agents on the card's diff, then applies fixes directly.
120
122
 
121
123
  **Step-by-step**:
@@ -156,6 +158,17 @@ The skill replaces the previous advisory pair (legacy Phase 2.6 E2E testing
156
158
  and Phase 2.7 visual design review). It runs by default on every UI card,
157
159
  auto-skips on backend-only cards, and BLOCKS the commit on gating findings.
158
160
 
161
+ > **Route-independent, self-contained verification (since v4.78.0).** This card verifies
162
+ > its **OWN** UI surface — it does NOT defer fidelity to a downstream card. `/e2e-review`
163
+ > now runs a **route-free structural pass** (Phase 2.7, `markup-fidelity-verifier`: the
164
+ > mockup's HTML/`design_src` code vs the implementation source) before any browser, so a
165
+ > component card with no route of its own is still fully verifiable. Consequences for the
166
+ > gating table below: a fidelity AC closes **only** on a real `passed` (a comparison was
167
+ > performed) — a `skipped` does NOT close it, and a route that opened empty/degenerate
168
+ > surfaces as `blocked` via `data-state-mismatch` / `coverage-gap`, never a silent pass.
169
+ > A genuine cross-card fidelity deferral must go through the Phase 2.5b fidelity-AC
170
+ > carve-out (explicit follow-up card), never a free-text note. This is the FEAT-0061 fix.
171
+
159
172
  #### Gate (skip the whole phase when any of the below is true)
160
173
 
161
174
  | Condition | Action |
@@ -206,7 +219,7 @@ When the gate above passes, the orchestrator invokes `/e2e-review` in
206
219
  | `status` from `/e2e-review` | Orchestrator action |
207
220
  |----|----|
208
221
  | `"passed"` | Log result in tracker. Proceed to Phase 3 (doc review + code review). |
209
- | `"skipped"` | Log skip reason. Proceed to Phase 3. |
222
+ | `"skipped"` | Log skip reason. Proceed to Phase 3. **If the card has `links.design` / `links.design_src` yet the skill returned `skipped`, that is suspicious post-v4.78.0** (the route-free structural pass should have run): record `[E2E-FIDELITY-SKIP] <reason>` in `## Issues & Flags`, and do NOT mark any fidelity AC `[x]` on a skip — a fidelity AC closes only on a real `passed`. |
210
223
  | `"overridden"` | Log override + reason in `## Issues & Flags` as `[E2E-OVERRIDE] <reason>`. Proceed to Phase 3. |
211
224
  | `"blocked"` | **STOP the card**. Log findings in tracker + `## Issues & Flags` as `[E2E-BLOCKED] <count> gating findings (<categories>)`. Ask the user whether to (a) override with reason, (b) escalate (open follow-up card), or (c) abandon this card and continue the batch. Do NOT proceed to Phase 3 silently. **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE (SKILL.md): category=blocker; recommended=none safe (a blocked E2E is a real gating failure) → take (b) escalate to a follow-up card; NEVER take (a) override autonomously.** |
212
225
  | `"error"` | Log error in `## Issues & Flags`. Ask the user whether to retry, skip, or abandon. Do NOT proceed silently. **Retry re-entry (defined path, capped):** on "retry", re-enter at step 1 of this Invocation contract — reset the `.baldart/e2e-review/<CARD-ID>/` state dir, re-spawn `/e2e-review` with the SAME payload, and re-parse. **Cap: 2 error-retries per card** (central repair cap, consistent with the other loops). On the 2nd `error`, do NOT re-offer "retry" — re-invoke `AskUserQuestion` with only skip/abandon, so the loop cannot recur unbounded. **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE (SKILL.md): category=blocker; recommended=retry once (the capped re-entry above); if still `error` → skip the E2E for this card AND materialize a follow-up card to re-run it (NEVER abandon the card silently).** |
@@ -51,11 +51,12 @@ source: components/ui/Button.tsx # path relative to repo root
51
51
  source_sha: a1b2c3d # git blob sha of source at last generation (drift anchor)
52
52
  status: stable # stable | experimental | deprecated (default stable; `deprecated` is a hand override)
53
53
  category: primitive # primitive | composite | feature (heuristic on import fan-in; hand-overridable)
54
- props: # from the TS prop type / interface
54
+ props: # from the TS prop type / interface (TS-aware: resolves aliases / unions / intersections / Omit / Pick)
55
55
  variant: { type: "'primary'|'ghost'|'destructive'", required: false, default: "'primary'" }
56
56
  size: { type: "'sm'|'md'|'lg'", required: false, default: "'md'" }
57
57
  disabled: { type: boolean, required: false, default: false }
58
- variants: [primary, ghost, destructive] # enumerated literal union of the `variant` prop
58
+ variant_prop: variant # which prop IS the variant axis (ranked; '' if none). NOT hardcoded to `variant` — a Chip whose axis is `tone` reports `tone`.
59
+ variants: [primary, ghost, destructive] # enumerated literal union of the `variant_prop` type
59
60
  composes: [Slot, Spinner] # in-registry primitives imported by this component
60
61
  # === AGENTIC — curated by doc-reviewer; survives regeneration, never clobbered when non-empty ===
61
62
  purpose: "Primary action trigger; one primary per surface."
@@ -76,7 +77,27 @@ owner: doc-reviewer # curation owner (constant; documents
76
77
 
77
78
  | Origin | Fields | Rule |
78
79
  |---|---|---|
79
- | **Deterministic** (TS/git) | `name`, `source`, `source_sha`, `status` (default), `category` (heuristic), `props`, `variants`, `composes` | Always safe to **overwrite** on regeneration — they are derived. Extracted by a pure Node script (no agent), so the path is **portable to Codex**. |
80
+ | **Deterministic** (TS/git) | `name`, `source`, `source_sha`, `status` (default), `category` (heuristic), `props`, `variant_prop`, `variants`, `composes` | Always safe to **overwrite** on regeneration — they are derived. Extracted by a pure Node script (no agent), so the path is **portable to Codex**. |
81
+
82
+ **TS-aware extraction (since v4.77.0).** When `typescript` is resolvable from the
83
+ consumer root, the extractor parses the source syntactically (`ts.createSourceFile`
84
+ — no tsconfig/TypeChecker) and resolves type **aliases**, **unions**,
85
+ **intersections**, and **`Omit`/`Pick`** in the props type (`type V='a'|'b'; variant: V`
86
+ → `['a','b']`). Opaque / imported types (`ComponentPropsWithoutRef<'button'>`,
87
+ `HTMLAttributes`) stay **empty** — never a DOM-attribute dump. When typescript is
88
+ absent (Codex / greenfield / non-TS stack) or a file fails to parse, it falls back
89
+ to the zero-dep **regex** path for that file (today's behaviour, no regression).
90
+
91
+ **`variant_prop` (the variant axis).** A prop is a variant-axis *candidate* iff its
92
+ resolved type is a pure string-literal union; the winner is the first by the rank
93
+ `variant > tone > intent > kind > appearance > state > status > color > severity`,
94
+ else the sole candidate, else `''`. `variants` is the literal members of
95
+ `variant_prop`'s type. Consumers (render harness, `@dsCard` compiler) use
96
+ `variant_prop` as the axis key — so a `Chip` whose axis is `tone` mounts `tone`,
97
+ not a phantom `variant`. **Legacy HEADs** (pre-v4.77.0, no `variant_prop`) **default
98
+ the axis to `'variant'`** when `variants` is non-empty, so existing registries keep
99
+ rendering identically; a one-time re-extract backfills the field (see
100
+ `/design-system-init` UPGRADE mode).
80
101
  | **Agentic** (doc-reviewer) | `purpose`, `token_bindings`, `a11y`, `related`, `must_rules`, `canonical_for`, `use_when`, `selection_closed`, `last_verified`; plus `status: deprecated` as a judgment | **Never clobbered** when non-empty. Empty agentic fields = "not yet enriched", filled later by `doc-reviewer` / `ds-drift`, never auto-invented. |
81
102
 
82
103
  ### Selection-policy fields (the closed-set layer)
@@ -576,6 +576,84 @@ the most damaging case (a CSS-hidden heavy subtree) is invisible to a JS AST rul
576
576
  Rule 2 violations are HIGH; a viewport-scoped edit that mutates the other branch
577
577
  (rule 1) is MEDIUM.
578
578
 
579
+ ## Route-Independent Fidelity & Coverage Obligation (BLOCKING — SSOT)
580
+
581
+ This is the single definition of how UI fidelity is verified per-card. `/e2e-review`,
582
+ `/new` (implement + code-reviewer), `/ui-design`, and `/design-review` **cite** this
583
+ section; they do not redefine it. The driving lesson (FEAT-0061): a fidelity check that
584
+ depends on rendering a *live, data-bearing route* silently degrades to a pass when that
585
+ route cannot render (auth-gated, empty database, wrong tenant) — a **skip masked as a
586
+ pass**. The fix is to make verification **route-independent first** and to make an
587
+ **un-runnable required check block, never pass**.
588
+
589
+ ### 1. Structural Fidelity (route-free) — runs FIRST
590
+
591
+ When a card has a mockup expressed as code — `links.design` pointing to an **HTML**
592
+ file, or `links.design_src` / `mockups/_src/` (finished Claude Design CODE, v4.75) — the
593
+ **code-structural** comparison runs before any browser render, via the
594
+ `markup-fidelity-verifier` agent. It compares the mockup's markup/CSS structure (grid vs
595
+ flex, column count, element order, region presence, `@media` breakpoints) against the
596
+ implementation source. This is the analogue of **tree-edit-distance over the DOM** — the
597
+ *structural* dimension, complementary to the *perceptual* (pixel) dimension of
598
+ `visual-fidelity-verifier`. It needs no browser, no auth, no data, and it deterministically
599
+ catches the **2-column-mockup → 1-column-build** class of divergence. Its findings use the
600
+ canonical severity taxonomy (SSOT in `visual-fidelity-verifier.md`); a structural
601
+ `layout-break` is Critical. **Fixes route to `ui-expert`** (layout is the UI domain).
602
+
603
+ ### 2. Visual Fidelity — render as CONFIRMATION, route-independent where possible
604
+
605
+ The pixel diff (`visual-fidelity-verifier`) confirms the structural result on a rendered
606
+ image. The render source, in priority order: (a) **isolated render** of the surface via
607
+ the `/ds-render` harness when it is a registry primitive (no route/auth/data needed);
608
+ (b) the live-route screenshot when the route renders; (c) neither → a coverage obligation
609
+ (below). The live-route screenshot is **not** the only fidelity source.
610
+
611
+ ### 3. State-aware Fidelity — `data-state-mismatch` (never compare a degenerate render)
612
+
613
+ The render must reach the **data state the mockup depicts** (a populated table, a selected
614
+ store/tenant). An empty-state / placeholder / error / wrong-context render of a route that
615
+ HAS a mockup is a **`data-state-mismatch`** (Critical) — emitted by `visual-fidelity-verifier`,
616
+ which sees the emptiness — **not** a silent skip or pass. The orchestrator owns *reaching*
617
+ the depicted state via a bounded **reach-state loop**: project-specific recipes from the
618
+ consumer's `.baldart/overlays/e2e-review.md` (`seed_data`, `select_populated_context`,
619
+ `goto_populated_entity`), with generic fallbacks (real auth from the persona creds,
620
+ MSW/API-mock, deterministic factory/snapshot, heuristic context auto-seek). The framework
621
+ owns the contract + the loop; the overlay owns the project-specific *how*.
622
+
623
+ ### 4. Coverage Obligation — `coverage-gap` (un-runnable required check BLOCKS)
624
+
625
+ A required fidelity check that **could not execute at all** must block, never pass. The
626
+ orchestrator synthesizes a **`coverage-gap`** (Critical) finding when an **image-only**
627
+ mockup exists (`mockup_source.level ∈ {figma, local}`, i.e. no HTML/code to diff
628
+ structurally) AND neither an isolated render nor a route render produced a comparable
629
+ screenshot, AND the state could not be reached. **Guards (never fire `coverage-gap`):**
630
+ `mockup_source.level == skip` (no mockup → no obligation), `harness-render` (assertion-fitting
631
+ guard — these go to quality only), `compliance-only` (registry-based, no image target), and
632
+ the mobile responsive pass (runs at `level: skip` by construction). A `coverage-gap` is
633
+ **not fixable by an agent** (the route still will not render) — it **bypasses the self-heal
634
+ loop** and maps to the existing `blocked` verdict (the orchestrator's autonomous handling
635
+ escalates it to a follow-up card). No new verdict status is introduced.
636
+
637
+ ### 5. Graphics-only card → `ui-expert` + the fidelity trio IS the review
638
+
639
+ For a **pure-graphics card** (`owner_agent: ui-expert`, `review_profile: light`,
640
+ `areas == [ui]` exactly — no api/data/logic/auth), the implementer is `ui-expert` (not
641
+ `coder`), and the **fidelity trio is the review**: `markup-fidelity-verifier` (structure)
642
+ + `visual-fidelity-verifier` (pixels + a11y) + `ui-quality-critic` (intrinsic quality,
643
+ advisory), plus the mechanical build/lint/tsc gates. The deep `/codexreview` and
644
+ `simplify` passes are skipped — their UI-relevant coverage (a11y, token-bypass, DS-drift,
645
+ layout) is absorbed by the trio. All fidelity fixes (structural, presentational, quality)
646
+ route to `ui-expert`.
647
+
648
+ ### Gating philosophy (calibration)
649
+
650
+ Vision-model reliability research (*MLLM as a UI Judge*, 2025) shows multimodal judges are
651
+ reliable at **comparison with clear differences** (~93%) but weak at **absolute scoring**
652
+ (~35% exact, hedging toward low variance). Therefore the **hard gate is FIDELITY**
653
+ (comparison against a ground truth: structural + visual), and **intrinsic-quality absolute
654
+ scoring (`ui-quality-critic`) stays advisory / confidence-gated**, never a hard block. The
655
+ 2-col → 1-col case is a "clear difference" → maximum judge reliability.
656
+
579
657
  ## Post-Intervention Coherence Check (BLOCKING completion gate)
580
658
 
581
659
  The BLOCKING pre-work above enforces *reads* before any UI change. This
@@ -600,7 +678,7 @@ and the advisory check 6 (mirror, non-blocking):
600
678
  2. **Modified primitive ⇒ updated spec**: did the change alter a primitive's
601
679
  props / variants / accessibility behavior? If yes, the corresponding
602
680
  `components/<Name>.md` MUST be updated to match — including its **frontmatter
603
- HEAD** (deterministic fields `props`/`variants`/`composes`/`source_sha`
681
+ HEAD** (deterministic fields `props`/`variant_prop`/`variants`/`composes`/`source_sha`
604
682
  regenerated from source; agentic fields re-verified). Stale spec =
605
683
  `DS_COMPONENT_STALE` finding, task blocked. (A prose-only spec lacking a HEAD
606
684
  is NOT stale merely for that — see `agents/component-manifest-schema.md`
@@ -40,7 +40,7 @@ BALDART wraps the **standard** (DTCG) and owns only a thin generator — the sam
40
40
  |---|---|---|
41
41
  | HEAD schema | `framework/agents/component-manifest-schema.md` | SSOT for the frontmatter fields + read/regeneration/leniency contract |
42
42
  | Gate + cascade | `framework/agents/design-system-protocol.md` | BLOCKING reads, drift codes, token SSOT inversion, Authority Matrix |
43
- | Deterministic extractor | `framework/.claude/skills/design-system-init/scripts/extract-manifest.mjs` (`extractFile`) + `extract-one.mjs` (single-component wrapper) | zero-dep Node; name/source/source_sha/props/variants/composes (Codex-portable) |
43
+ | Deterministic extractor | `framework/.claude/skills/design-system-init/scripts/extract-manifest.mjs` (`extractFile`) + `extract-one.mjs` (single-component wrapper) + `extract-ts.mjs` (optional TS-aware resolver) | Node; name/source/source_sha/props/variant_prop/variants/composes. **TS-aware** when `typescript` resolves from the consumer root (aliases/unions/intersections/Omit/Pick); **zero-dep regex fallback** otherwise (Codex-portable) |
44
44
  | Spec template | `framework/templates/component-spec.template.md` | CANONICAL prose-body template (SSOT); serializer fills it on creation via `--prose-template` (default) |
45
45
  | Bootstrap/upgrade | `framework/.claude/skills/design-system-init/SKILL.md` | greenfield + upgrade modes |
46
46
  | Token emitters | `src/utils/token-emitters/{index,ts,css}.js` | DTCG → stack-native output (REGISTRY pattern) |
@@ -72,8 +72,11 @@ has_design_system: true (no separate flag — the manifest rides on it)
72
72
  ## 5. Invariants (do not break)
73
73
 
74
74
  - **Codex portability**: the READ path and the deterministic half (extractor,
75
- `baldart tokens build`) run with **no subagent**. Only agentic enrichment needs
76
- Claude; under Codex it degrades to empty/TODO fields never a hard failure.
75
+ `baldart tokens build`) run with **no subagent**. The TS-aware resolver
76
+ (`extract-ts.mjs`) is **optional** it uses the consumer's own `typescript` when
77
+ resolvable and falls back to the **zero-dep regex** path per file otherwise, so
78
+ Codex / non-TS stacks are unaffected. Only agentic enrichment needs Claude; under
79
+ Codex it degrades to empty/TODO fields — never a hard failure.
77
80
  - **No twin**: ONE artifact per component (the spec, evolved) and ONE token SSOT
78
81
  (`.tokens.json`); `tokens-reference.md` and `tokens.ts`/CSS are **generated
79
82
  views/outputs**, never hand-authored sources. The INDEX is a generated router.
@@ -24,8 +24,9 @@ source: {{file_path}}
24
24
  source_sha: {{source_sha}}
25
25
  status: {{status}} # stable | experimental | deprecated (default stable)
26
26
  category: {{category}} # primitive | composite | feature
27
- props: {{props_yaml}} # name → { type, required, default } ({} if unresolved)
28
- variants: {{variants_yaml}} # [] if none / unresolved
27
+ props: {{props_yaml}} # name → { type, required, default } ({} if unresolved; TS-aware: resolves aliases/unions/intersections/Omit/Pick)
28
+ variant_prop: {{variant_prop}} # which prop is the variant axis (ranked; '' if none / unresolved; legacy HEADs default to 'variant')
29
+ variants: {{variants_yaml}} # [] if none / unresolved (literal members of variant_prop's type)
29
30
  composes: {{composes_yaml}} # in-registry primitives this imports
30
31
  # AGENTIC fields (curated by doc-reviewer — preserved across regeneration):
31
32
  purpose: "{{purpose}}"