baldart 4.3.0 → 4.5.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,36 @@ All notable changes to BALDART will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [4.5.0] - 2026-06-03
9
+
10
+ **`/new` Team Mode non revisiona più lo stesso diff fino a tre volte: la profondità della code-review ora scala col `review_profile` della card.** Lo Step D del team mode faceva passare ogni card "full" sotto code-review tre volte — `D.2` (code-reviewer di gruppo) → `D.4b` (`/codexreview` per-card, che rispawna code-reviewer + Codex + CoVe + FP-gate) → Final Review F.3 (Codex sull'intero batch) — e ogni card "light" due volte (D.2 + un D.4b che a `light` esegue solo code-reviewer + FP, puro doppione di D.2). Un nuovo step **D.1.5** calcola UNA volta il profilo effettivo per card (floor da `review_profile` + escalation-only dal detector Step-A sul diff reale) e partiziona il gruppo in `LIGHT_CARDS` / `FULL_CARDS`. **D.2** ora esegue il code-reviewer solo sull'union dei diff `LIGHT_CARDS` (skippato del tutto se non ce ne sono); **D.4b** gira `/codexreview` solo per i `FULL_CARDS`, mentre i `LIGHT_CARDS` lo saltano (la loro review adversarial è garantita dalla Final FULL gate, sempre on). Esito: ogni card riceve **una sola** code-review per-card/gruppo proporzionale al rischio + la Final batch gate — light: D.2 + Final; full: D.4b + Final (giù da 3). **D.3b Simplify** salta sulle card `review_profile == skip` (quality-only, niente diff sostanziale). Tutti gli skip sono **gate-reason enumerati e deterministici** (driven dal campo `review_profile` autorato in PRD), non skip "per time budget": una nuova clausola "REVIEW DEPTH SCALES WITH `review_profile`" in testa alla skill lo formalizza. Eliminato anche il polling `sleep N; echo "waiting..."` ad ogni barrier (i background agent re-invocano l'orchestratore automaticamente). **MINOR** (cambia il comportamento di review di `/new` team mode senza rimuovere agenti o rompere install; nessuna chiave `baldart.config.yml` nuova — si appoggia su `review_profile` già esistente nella card).
11
+
12
+ > **Why.** Il team mode parallelizza la parte economica (scrivere il codice) e serializzava la parte costosa (la coda di review): i coder partivano in parallelo, poi tutto convergeva in un imbuto sequenziale di 11 sotto-passi con la code-review ripetuta. La de-duplicazione si appoggia su due invarianti già presenti: (1) la **Final-review FULL gate** (dal v3.37.0) gira SEMPRE Codex (+ code-reviewer fallback) sull'intero diff di batch, senza riduzioni — quindi ogni riga di ogni card light riceve comunque Codex-adversarial prima del merge; (2) l'**escalation-only** del detector Step-A (`light`→`full`, mai l'inverso) garantisce che un path rischioso emerso a implementazione promuova comunque la card a `full`. Tradeoff accettato (scelta utente "aggressivo 3→2"): per i `FULL_CARDS` il rilevamento cross-card si sposta dal per-gruppo (D.2) al per-batch (Final F.3) — stessa sicurezza di merge-gate, feedback a fine batch invece che per-gruppo. `/codexreview` NON è toccato (il suo FP-gate dipende architetturalmente da `code-reviewer`, quindi la leva è l'invocazione condizionale di D.4b, non un flag interno). Il **percorso sequenziale è invariato** (non ha un code-reviewer di gruppo, quindi il suo `/codexreview` a Phase 3.7 resta l'unica code-review per-card). Allineato al principio "data-driven sopra threshold arbitrarie": nessuna soglia nuova, si onora un campo deterministico già esistente. AC-Closure (D.3a), D.4 QA e D.4a doc restano invariati.
13
+
14
+ ### Changed
15
+
16
+ - **`framework/.claude/skills/new/SKILL.md`** — (a) nuova clausola in testa "REVIEW DEPTH SCALES WITH `review_profile`" che enumera il profilo come gate-reason deterministico (distinto dallo skip "per time"); la clausola "NO PHASE SKIP" cita il profilo tra le ragioni valide e ammorbidisce "never reduce" in "never reduce for any model-invented reason". (b) Nuovo **D.1.5** (Effective per-card review profile) che calcola la partizione `LIGHT_CARDS`/`FULL_CARDS` una volta sola e la logga. (c) **D.2** code-reviewer scoped a `LIGHT_CARDS` (skip se vuoto); doc-reviewer invariato su tutto il gruppo; tradeoff cross-card→Final documentato inline. (d) **D.3b** salta su `review_profile == skip`. (e) **D.4b** condizionale: `/codexreview` solo per `FULL_CARDS` (profilo sempre `full`, niente recompute), `LIGHT_CARDS` skip con reason enumerata. (f) **Step D coverage assertion** accetta gli skip profilo-driven come entry valide documentate. (g) **Step C** direttiva anti-polling (no `sleep N; echo waiting`). (h) **D.3b Simplify** ora fa **fan-out in parallelo** su tutte le card eleggibili del gruppo (analisi read-only su diff file-disgiunti → niente più coda sequenziale per-card; i fix si applicano dopo, lint/tsc una volta sola); **D.3c E2E** valuta il gate per tutte le card insieme e parallelizza le eleggibili quando colpiscono route disgiunte (fallback sequenziale su contesa del dev-server). Percorso sequenziale, `codexreview.md`, AC-Closure D.3a (resta sequenziale per via dell'`AskUserQuestion`), D.4 QA, D.4a doc, Final Review F.3: non toccati.
17
+
18
+ > **Nota sullo scope dei tagli.** La de-dup si ferma qui di proposito. La **Final Review F.3** ri-revisiona l'intero batch (Codex + doc + perf + qa-sentinel suite completa) anche dopo il lavoro per-gruppo: è ridondante su batch a gruppo singolo MA è la rete di sicurezza incondizionata su cui ora si appoggiano le card light, e il framework ha **già tentato di ridurne lo scope in v3.35.0 e l'ha rollbackata in v3.37.0** dopo un incidente — quindi resta piena. Stessa logica per il doppio run della suite di test (D.4 per-gruppo + Final). Questi NON sono toccati.
19
+
20
+ ## [4.4.0] - 2026-06-03
21
+
22
+ **`/prd` mockup intake ora risolve le schermate di una feature in modo *strutturale* da un bundle Claude Design, invece di tirare a indovinare con lo scoring per keyword.** L'export di Claude Design non è controllabile dall'utente: si scarica solo il bundle intero del workspace multi-feature. Finora Step 1.6.4b gestiva il bundle `.zip` con un solo scoring di rilevanza su filename/`<title>` (pieno di `[CALIBRATION-NEEDED]`), e la sola modalità d'ingresso era l'archivio `.zip`. Ora un **passaggio strutturale** (Step b2) sfrutta i segnali nativi che il bundle già porta: **un canvas `*.html` per feature** (anchor sul titolo feature), **slot `id="s<N>"` = screen-ID** del § 3 del prompt di handoff (`s31` ↔ schermata `3.1`), e il manifest nativo **`.design-canvas.state.json`** (arricchimento label, mai autorità). Le schermate che vivono solo come artboard nel canvas (non esportate come PNG) vengono **renderizzate deterministicamente** via http-server + Playwright (nuova `canvas-intake-recipe.md`), risolvendo il CORS che blocca Babel su `file://`. Il trigger è allargato anche alle **cartelle già estratte** (non solo `.zip`). A valle, la routing Hybrid di Step 3 passa da fuzzy substring match a **exact join** su `slot_id`. Lo scoring per keyword resta come **fallback** (bundle generico o canvas ambiguo) e il **gate di conferma STOP umano è invariato**. **MINOR** (nuovo reference `canvas-intake-recipe.md` + nuovi campi di state-schema `mockups.archive.bundle_kind`/`canvas_ref`/`discarded[]`, `mockups.canvas_render`, `screens_in_scope[].slot_id`; trigger allargato; NON una chiave `baldart.config.yml` → nessuna cascade di schema-propagation).
23
+
24
+ > **Why.** Il mapping 1-a-1 tra schermate prodotte da Claude Design e schermate del PRD non va *imposto* nell'handoff (l'export è monolitico e fuori controllo): **esiste già** nella struttura del bundle, semplicemente il framework non la leggeva. Spostare il peso dall'handoff (dove non si può fare nulla) all'intake (dove i segnali sono tutti presenti) elimina l'archeologia manuale — CORS, http-server e Playwright re-improvvisati a ogni rientro — e la sostituisce con un resolver ripetibile. Contratto fail-safe in ogni direzione: firma assente / slot non-`s<N>` / manifest parziale / feature su 2 canvas / Playwright assente / porta occupata ricadono tutti sul comportamento attuale (scoring + STOP) — **il PRD non aborta mai sull'intake**. Allineato al principio "data-driven sopra threshold arbitrarie": lo scoring `[CALIBRATION-NEEDED]` diventa il ripiego, non la prima linea.
25
+
26
+ ### Added
27
+
28
+ - **`framework/.claude/skills/prd/references/canvas-intake-recipe.md`** — ricetta archiviata per renderizzare gli artboard canvas-only di un bundle Claude Design in PNG: preflight (no auto-install), http-server con port-loop 8080→8090, render loop Playwright su `[data-dc-section]`/`[data-dc-slot]` (attributi runtime, non statici), dedup vs pre-render del designer, tabella di degradazione. Invocata da Step 1.6.4b/A5, mai standalone.
29
+
30
+ ### Changed
31
+
32
+ - **`framework/.claude/skills/prd/references/discovery-phase.md`** — Step 1.6.4b: (a) Step a allargato per accettare una **directory** oltre al `.zip` (`mockups.archive.kind: zip | directory`, convergono su `root`); (b) nuovo **Step b2 — Claude-Design structural resolution** (A0 signature gate → A1 canvas anchor con `AskUserQuestion` su ambiguità → A2 estrazione slot `id="s<N>"` → screen-ID → A3 read manifest enrich-only → A4 sidecar jsx/PNG → A5 render via recipe → A6 discard-and-log) inserito tra enumerate e score; (c) Step c demoto a **fallback** esplicito; (d) Step d con variante tabella *screen-centric*; (e) Step e materializza anche jsx-deps + asset condivisi + PNG renderizzati e scrive `screens_in_scope[]` con `slot_id`/`screen_id`/`canvas_ref`. Appendice schema estesa coerentemente.
33
+ - **`framework/.claude/skills/prd/references/ui-design-phase.md`** — routing Hybrid (Step 2 della per-screen table): **exact-join-first** su `slot_id`/`screen_id` quando presenti (no STOP), fuzzy substring match conservato come ramo `else` con il suo STOP-and-ask invariato.
34
+ - **`framework/.claude/skills/prd/SKILL.md`** — HARD RULE 16: lead-in allargato a "`.zip` OPPURE cartella estratta di un bundle Claude Design (firma `.design-canvas.state.json` e/o `*.html` con slot `id="s<N>"`)" + frase su passaggio strutturale, scoring come fallback, STOP invariato, no-abort.
35
+ - **`framework/.claude/skills/prd/assets/state-template.md`** — `mockups.archive` estesa con `bundle_kind`/`canvas_ref`/`canvas_candidates[]`/`discarded[]`, nuovo blocco `mockups.canvas_render`, `kind` ora `zip | directory`; `screens_in_scope[]` con `slot_id`/`screen_id`/`canvas_ref`.
36
+ - **`framework/.claude/skills/prd/assets/claude-design-handoff-prompt.template.md`** — nudge minimo a fine § 3 (titola il canvas col nome feature; numera gli artboard coerenti con gli screen-ID): unica richiesta realistica dato che l'export non è controllabile.
37
+
8
38
  ## [4.3.0] - 2026-06-03
9
39
 
10
40
  **`/new` skips its pre-flight cross-card Codex check when `/prd` already audited the same batch jointly and nothing drifted since.** The `/prd` Step 6.6 holistic audit (4-agent team + Codex adversarial pass, 6.6d) already reviews the full card set for `files_likely_touched` conflicts, dependency shadows, implicit ordering, and shared-state mutation — the *exact* lens of `/new` Step 3d's cross-card check. Running Step 3d unconditionally re-paid that Codex call on every multi-card batch, even when implementing the freshly-audited PRD set with zero intervening change. Now the audit phase **stamps provenance** on each card (`metadata.holistic_audit`: `audited_at` + `audited_commit` + `audited_set`), and `/new` Step 3d evaluates a four-condition skip: provenance present (S1), batch jointly audited (S2), batch ⊆ audited set (S3), and no commit touched the batch's claimed paths since the audit baseline (S4). All four hold → SKIP (logged, non-decision); any fails → RUN as before. **MINOR** (new card-template field + new conditional capability; NOT a `baldart.config.yml` key — `metadata.holistic_audit` is a backlog-card field written by the audit phase).
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.3.0
1
+ 4.5.0
@@ -13,7 +13,9 @@ description: >
13
13
 
14
14
  > **SCOPE CLOSURE DISCIPLINE (BLOCKING)**: This orchestrator NEVER unilaterally defers an acceptance criterion. Silent deferral via `implementation_notes`, commit message, or final recap is a protocol violation per `framework/agents/workflows.md § Scope Closure Discipline`. Under context-window pressure, time pressure, or unforeseen complexity, the correct action is to STOP and route the decision back to the user via `AskUserQuestion` — Auto Mode's "bias toward proceeding" applies to routine decisions, NOT to scope reduction. **Phase 2.5b (AC-Closure Gate)** enforces this at the per-card level before commit and is non-skippable.
15
15
 
16
- > **NO PHASE SKIP FOR PERCEIVED TIME, EVER (BLOCKING)**: MANDATORY phases are non-negotiable in BOTH sequential mode and team mode. The phrases `time budget`, `context budget`, `context pressure as override`, `for time`, `to save tokens`, `to save iterations`, or any model-invented constraint are NOT valid skip reasons. The only valid skip reasons are those explicitly enumerated in each phase's Gate table (e.g. `features.has_e2e_review: false`, backend-only diff, card type `docs`/`chore`/`config`). Documenting skipped phases at the end of a batch as `"saltati per time budget"` is a protocol violation per the FEAT-0006 incident — treat any such phrase in your own output as a self-correction trigger: stop, surface the situation via `AskUserQuestion`, and resume only after explicit user direction. Auto Mode does NOT override this. Team-mode orchestrators (L0/L1) propagate every MANDATORY phase per-card and never aggregate, reduce, or pre-filter the per-card pipeline.
16
+ > **NO PHASE SKIP FOR PERCEIVED TIME, EVER (BLOCKING)**: MANDATORY phases are non-negotiable in BOTH sequential mode and team mode. The phrases `time budget`, `context budget`, `context pressure as override`, `for time`, `to save tokens`, `to save iterations`, or any model-invented constraint are NOT valid skip reasons. The only valid skip reasons are those explicitly enumerated in each phase's Gate table (e.g. `features.has_e2e_review: false`, backend-only diff, card type `docs`/`chore`/`config`, **and the card's `review_profile`-driven review depth — see next paragraph**). Documenting skipped phases at the end of a batch as `"saltati per time budget"` is a protocol violation per the FEAT-0006 incident — treat any such phrase in your own output as a self-correction trigger: stop, surface the situation via `AskUserQuestion`, and resume only after explicit user direction. Auto Mode does NOT override this. Team-mode orchestrators (L0/L1) propagate every MANDATORY phase per-card and never aggregate, reduce, or pre-filter the per-card pipeline **for any model-invented reason**.
17
+
18
+ > **REVIEW DEPTH SCALES WITH `review_profile` (deterministic — NOT a time/token skip)**: the review *depth* of a card MAY scale with its `review_profile` (the PRD-authored field, escalation-only via the Step-A detector — `light`→`full` never the reverse), exactly as the QA tier (D.4) and Codex `light`/`full` depth (D.4b / Phase 3.7 Step C) already do. This is an **enumerated, deterministic gate reason**, the polar opposite of a `time budget` skip: it is driven by a field a human authored at PRD time, not by the model's perception of how long the batch is taking. In **team mode** this means a `light`/`skip` (0-trigger) card receives its single per-card code review at the **D.2 group pass** and defers Codex-adversarial to the unconditional **Final-review FULL gate** (so D.4b is skipped for it), while a `full` card receives its per-card code review at the **D.4b `/codexreview`** pass (so the D.2 group code-reviewer does not re-review it). Every card still gets ≥1 per-card/group code review **plus** the cross-batch Final FULL gate — coverage is preserved, redundancy is removed. The **sequential** per-card pipeline is unchanged (it has no D.2 group code-reviewer, so its Phase 3.7 `/codexreview` stays the single per-card code review). Skipping a gate WITHOUT an enumerated `review_profile`/Gate-table reason remains a protocol violation.
17
19
 
18
20
  ## Project Context
19
21
 
@@ -2356,6 +2358,8 @@ Agent tool call:
2356
2358
 
2357
2359
  The orchestrator waits for ALL background agents in the group to complete. It will be notified automatically as each finishes (`run_in_background`).
2358
2360
 
2361
+ > **Do NOT poll with `sleep N; echo "waiting..."` loops.** Background agents (and background `Bash` commands) re-invoke the orchestrator automatically when they finish — issuing `sleep`/`echo` "waiting" turns burns orchestrator turns and tokens for zero progress. Simply end your turn after spawning the background work; you will be woken on completion. This applies to EVERY barrier in Step D (D.1 build, D.2 reviewers, D.3/D.3b agents, D.4 qa-sentinel, D.4a doc-reviewer, D.4b `/codexreview`), not just Step C.
2362
+
2359
2363
  For each completed agent:
2360
2364
  1. Read the completion report from the agent's output.
2361
2365
  2. Log to tracker: card ID, status, files changed, build/lint status.
@@ -2375,21 +2379,38 @@ After ALL agents in the group complete successfully:
2375
2379
 
2376
2380
  1. **D.1 — Build verification (group)** — Run `npm run build` in the worktree to verify combined changes compile. If build fails, identify which card's changes broke it (from `git diff` per card), spawn a targeted fix-coder for those files only.
2377
2381
 
2378
- 2. **D.2Combined static review (group)** Invoke **code-reviewer** + **doc-reviewer** IN PARALLEL (read-only) across ALL changes in the group. ONE combined pass catches cross-card issues. The doc-reviewer runs **read-only here** (it cannot write while code-reviewer reads the same diff in parallel — parallel-safety), MUST **attribute every doc finding to a specific card** (by file → ownership map), and applies the full Phase 3 mandate INCLUDING the spec/docs-drift→bug lens (since v3.35.0). D.4a consumes these per-card findings and dispatches the doc-reviewer (now alone, in write mode) to apply them — no second AUDIT spawn, but the FIXES are still owned by doc-reviewer.
2382
+ 1.5. **D.1.5Effective per-card review profile (compute ONCE; drives D.2 + D.4b)** For EACH card in the group, compute its **effective codex profile** with the SAME deterministic rule the sequential Phase 3.7 Step C uses, so the two paths never disagree:
2383
+ - **Floor**: read the card's `review_profile` field (`skip`/`light`/`balanced`/`deep`) per the QA Profile Selector (fallback-computed only for legacy cards lacking the field).
2384
+ - **Escalation-only (Step A detector)**: run the Phase 3.7 **Step A** high-risk detector (bash + grep) on THIS card's committed diff (`git diff "$TRUNK...HEAD" -- <card files from ownership map>`). A matched trigger promotes `light`→`full`; it can NEVER downgrade `full`→`light`. The card's `review_profile` is the floor; the detector is the safety net on top.
2385
+ - **Partition** the group into two lists and log them in the tracker under `## Team Mode`:
2386
+ - `LIGHT_CARDS` = cards whose effective profile is `light` or `skip` **AND** zero Step-A triggers.
2387
+ - `FULL_CARDS` = all others (effective profile `balanced`/`deep`, OR any Step-A trigger).
2388
+ - Log: `## D.1.5 Effective Profiles\n<CARD-ID>: profile=<floor> triggers=<n> → effective=<light|full> (<LIGHT_CARDS|FULL_CARDS>)` per card. This single computation is the SSOT for both D.2 (code-reviewer scoping) and D.4b (inclusion) — do NOT recompute it in either step.
2389
+
2390
+ 2. **D.2 — Combined static review (group)** — Two reviewers, **scoped by the D.1.5 partition** so each card gets exactly ONE per-card/group code review proportional to its risk (the other is the Final FULL gate):
2391
+ - **doc-reviewer — over the WHOLE group, always** (unchanged). It runs **read-only here** (it cannot write while code-reviewer reads in parallel — parallel-safety), MUST **attribute every doc finding to a specific card** (by file → ownership map), and applies the full Phase 3 mandate INCLUDING the spec/docs-drift→bug lens (since v3.35.0). D.4a consumes these per-card findings and dispatches the doc-reviewer (now alone, in write mode) to apply them — no second AUDIT spawn, but the FIXES are still owned by doc-reviewer.
2392
+ - **code-reviewer — scoped to the diff-union of `LIGHT_CARDS` ONLY** (run it in parallel with the doc-reviewer over those files). `LIGHT_CARDS` cards skip D.4b, so D.2 is their single per-card code review (the cross-batch Codex pass at Final covers them adversarially). **If `LIGHT_CARDS` is empty → do NOT spawn code-reviewer at D.2** (every card is `FULL_CARDS` and receives its deeper code review — `code-reviewer` + Codex + CoVe + FP — at D.4b). Log the deterministic gate reason: `D.2 code-reviewer: scope=LIGHT_CARDS [<ids>] | SKIPPED (no light cards — code-review owned by D.4b per-card + Final FULL gate)`.
2393
+ - **Tradeoff (documented, accepted — see top-of-file `REVIEW DEPTH SCALES` clause)**: for `FULL_CARDS`, cross-card detection moves from the per-group D.2 pass to the per-batch **Final-review FULL gate** (Codex over the entire batch diff, plus code-reviewer fallback) — same merge-gate safety, feedback arrives at batch-end rather than per-group. This is the intended de-duplication: `FULL_CARDS` are NOT re-reviewed by a group-level code-reviewer on top of their D.4b `/codexreview`.
2379
2394
 
2380
2395
  3. **D.3 — Apply fixes (group)** — If D.2 findings exist, spawn ONE fix-coder to apply all fixes in a single pass. Run build + lint after.
2381
2396
 
2382
2397
  3a. **D.3a — Phase 2.5b AC-Closure Gate (per-card, BLOCKING — non-skippable)** — For EACH card in the group, **sequentially**, invoke the full Phase 2.5b gate as documented in `### Phase 2.5b — AC-Closure Gate (BLOCKING — Scope Closure Discipline)`. This includes: build the AC Closure Ledger from the card YAML, run the rationalization scan, invoke `AskUserQuestion` one-per-deferred-AC, run the `implementation_notes` deferral audit, and persist the ledger in the tracker. Until EVERY card in the group exits PASS, do NOT proceed to D.3b. Cards exiting with `not_implemented` ACs that the user routes to "Implementa adesso" must finish their fix-coder loop and re-pass the gate before D.3b starts for the next card. Log under `## AC Closure Ledger — <CARD-ID>` per card.
2383
2398
 
2384
- 3b. **D.3b — Phase 2.55 Simplify (per-card)** — For EACH card in the group, invoke Phase 2.55 (Reuse / Quality / Efficiency parallel agents) on the per-card diff (`git diff --name-only` filtered by the card's File Ownership Map). Per-card (not group-aggregate) so findings are attributable. Apply fixes inline. After all cards: re-run `npm run lint` and `npx tsc --noEmit` on the worktree once.
2399
+ 3b. **D.3b — Phase 2.55 Simplify (per-card, FANNED OUT across the group)** — The Simplify agents are **read-only analysis on file-disjoint per-card diffs** (the orchestrator applies the fixes afterward), so there is NO reason to run them one card at a time. **Spawn the per-card Simplify analysis for ALL eligible cards in PARALLEL** — in a SINGLE message, fire each card's Phase 2.55 trio (Reuse / Quality / Efficiency) scoped to that card's diff (`git diff --name-only` filtered by the card's File Ownership Map). Per-card (not group-aggregate) so findings stay attributable. When all analyses return, **apply fixes per card** (file-disjoint no write conflict), then re-run `npm run lint` and `npx tsc --noEmit` on the worktree ONCE for the whole group. (Concurrency is capped by the platform; passing N cards is safe — excess agents queue.)
2400
+ - **Gate (enumerated, `review_profile`-driven)**: SKIP D.3b for a card whose `review_profile == skip` (trivial / non-implementable card — Simplify is quality-only, so there is no merge-gate coverage to lose, and there is no substantive diff to simplify). Log `simplify: SKIPPED (review_profile=skip)`. For `light`/`balanced`/`deep` cards D.3b runs unchanged. This is the ONLY enumerated skip — never skip D.3b "for time" on a `light`+ card. (Skipped cards are simply omitted from the parallel fan-out.)
2385
2401
 
2386
- 3c. **D.3c — Phase 2.6 E2E-Review (per-card)** — For EACH card in the group, honor the existing Gate table (skip when `features.has_e2e_review: false`, backend-only diff per the diff predicate documented in Phase 2.6, or card type in the Phase 2.6 skip set — `backend`/`api`/`db`/`infra`/`docs`/`chore`/`config`). For cards that pass the gate, invoke `/e2e-review` in programmatic mode with that card's payload. BLOCKING per-card: if `/e2e-review` returns `"blocked"` or `"error"`, surface to the user via the same `AskUserQuestion` documented in Phase 2.6 — do NOT proceed to D.4 with an unresolved card. Skips are logged with the documented gate reason, never with `"time budget"` or similar.
2402
+ 3c. **D.3c — Phase 2.6 E2E-Review (per-card)** — First, evaluate the existing Gate table for EVERY card at once (skip when `features.has_e2e_review: false`, backend-only diff per the diff predicate documented in Phase 2.6, or card type in the Phase 2.6 skip set — `backend`/`api`/`db`/`infra`/`docs`/`chore`/`config`). In practice most cards in a group skip this gate (backend/db/api), so the eligible set is usually 0–1. For the cards that PASS the gate, invoke `/e2e-review` in programmatic mode with that card's payload. Each `/e2e-review` keeps its own isolated state dir (`.baldart/e2e-review/<CARD-ID>/`), so multiple runs do not clobber each other's artifacts.
2403
+ - **Parallel-when-safe**: if **two or more** cards pass the gate, you MAY fan them out in parallel ONLY when they hit **disjoint routes/pages** — the genuinely shared resource is the worktree's single dev-server port, and concurrent Playwright sessions against one server are fine for disjoint routes. If the eligible cards touch overlapping routes (or `/e2e-review` spins its own server and a port clash is possible), run those **sequentially** to avoid a flaky cross-test. Default to parallel for the common disjoint case; fall back to sequential on any contention.
2404
+ - BLOCKING per-card: if `/e2e-review` returns `"blocked"` or `"error"`, surface to the user via the same `AskUserQuestion` documented in Phase 2.6 — do NOT proceed to D.4 with an unresolved card. Skips are logged with the documented gate reason, never with `"time budget"` or similar.
2387
2405
 
2388
2406
  4. **D.4 — QA gate (group)** — Read each card's `review_profile` field (fallback for legacy cards only: compute via the QA Profile Selector, whose criteria are the SSOT in `prd-card-writer.md § Rule C`). Select the HIGHEST profile across all cards in the group (e.g., if one card is BALANCED and another is DEEP, use DEEP) and pass it as the `QA profile` to **qa-sentinel** (invoked once for the group) using the same prompt contract as Phase 3.5 step 22 (`do NOT default to FULL`; balanced → SCOPED, deep → FULL). Note: a mixed group containing any DEEP card runs the group at FULL — the max-profile intentionally overrides the SCOPED tier the BALANCED cards would get individually, because the combined diff must be validated together.
2389
2407
 
2390
2408
  4a. **D.4a — Per-card doc gate (consumes D.2, since v3.35.0; doc-reviewer-applied since v3.40.0)** — Do NOT re-run the doc AUDIT (D.2 already produced per-card-attributed findings). If any card in the group has doc findings, invoke the **doc-reviewer once in write mode** over the group, passing the per-card-attributed findings from D.2, to APPLY all doc fixes in a single pass (it is now alone — code-reviewer is done — so the D.2 parallel-safety constraint no longer applies). Do **NOT** spawn a fix-coder for doc findings: `doc` is owned by `doc-reviewer` (see "Domain-Override Domains"); a code-oriented coder lacks the doc-invariant contract. The only exception is a doc-drift→bug finding rooted in CODE — that follows the D.4b code fix path. (Previously D.4a spawned a fix-coder per card; the audit was already collapsed to a single attributed D.2 pass. D.4b's per-card `/codexreview` also skips its doc-reviewer via the lean contract, so the doc AUDIT runs once per group while the doc FIXES run once per group via doc-reviewer.) **Telemetry** — append one row per applied doc finding to `## Fix Application Log`: `D.4a | doc | est_lines=<n> | decision=doc-reviewer | applied_by=doc-reviewer | card=<ID> | finding=<1-line>`. **Phase-8 producer (named counter)** — ALSO record per card the `doc_gaps: found=<N> fixed=<M>` line in that card's `## Completed Cards` entry (same named counter as sequential Phase 3 step 15), so Phase 8 reads `doc_gaps_found`/`doc_gaps_fixed` from a structured field, not a free-form row.
2391
2409
 
2392
- 4b. **D.4b — Pre-Merge Codex Review Gate (per-card, MANDATORY UNCONDITIONAL)** — Invoke `/codexreview` for EACH card in the group, one at a time, BEFORE any commit. This mirrors Phase 3.7 of the sequential path and is non-skippable regardless of file paths or perceived risk. Apply the same fix sub-loop as sequential Phase 3.7 Step C.4: if the consolidated report shows verified BLOCKER/HIGH findings, spawn a fix-coder and — **re-writing the consumed-once lean contract first** — re-invoke `/codexreview` via the Skill tool with `args: <CARD-ID>` (max 2 retries per card; a bare prose mention or a missing card-ID would let the retry review the wrong card). If still BLOCKER/HIGH after retries, ask the user before proceeding to D.5. The D.5 commits MUST NOT happen until every card in the group has a PASS verdict (or explicit user override via `AskUserQuestion`). Log results in the tracker under `## Pre-Merge Codex Review` per card. **Lean + profile (since v3.35.0)**: before each card's `/codexreview`, apply the same Review Profile Selector and write the same `/tmp/codexreview-lean-<CARD-ID>.json` contract as sequential Phase 3.7 Step C — `arch_baseline_path` pointing at `/tmp/arch-baseline-group-<FIRST-CARD-ID>.md`, `skip_doc_reviewer: true`, and `profile: light|full` derived from the card's `review_profile` (`skip`/`light` → `light`, `balanced`/`deep` → `full`; since v3.38.0 read from the card, not recomputed) — with the same **escalation-only** rule: a Step A high-risk trigger on the actual diff promotes `light` → `full`, never the reverse. LIGHT runs `code-reviewer` + FP-gate only — the per-card **Codex adversarial pass is dropped at `light` since v3.38.0** (prerogative of `full` + the final cross-check). The Review Profile Selector is **ACTIVE since v3.37.0** — same status as sequential Phase 3.7 Step C above: `light` is per-card early feedback, and the merge-gate safety is the post-batch **Final-review FULL gate** (a single FULL `/codexreview` over the entire batch diff, which team mode reaches via "Post-batch — same as sequential mode"). To make per-card always-full again, restore `profile = full`; the final full gate stays regardless.
2410
+ 4b. **D.4b — Pre-Merge Codex Review Gate (per-card, profile-gated via D.1.5)** — Invoke `/codexreview` for **EACH card in `FULL_CARDS`** (the partition computed at D.1.5), one at a time, BEFORE any commit. This mirrors Phase 3.7 of the sequential path.
2411
+ - **`LIGHT_CARDS` → SKIP D.4b** (enumerated `review_profile`-driven gate reason, NOT a time skip). A `light`/`skip` 0-trigger card already received its single per-card code review at the **D.2 group pass**, and its Codex-adversarial review is **guaranteed by the unconditional Final-review FULL gate** (Codex over the entire batch diff, post-batch). Running `/codexreview` here at `light` would only re-run `code-reviewer` + FP-gate (Codex is dropped at `light` since v3.38.0) — a pure duplicate of D.2. Log per card: `codex-review: SKIPPED (light, 0 triggers — D.2 group code-review covered + Codex adversarial guaranteed by Final FULL gate)`.
2412
+ - **`FULL_CARDS` → run `/codexreview` as today** (the per-card Codex adversarial + CoVe + FP-gate pass is exactly where the per-card code review for these cards lives — D.2 deliberately did NOT re-review them). The profile passed is always `full` (D.1.5 already resolved it; do NOT recompute). Keep the sequential one-at-a-time loop (avoid N concurrent `/codexreview`, each of which fans out multiple sub-agents → rate-limit risk).
2413
+ Apply the same fix sub-loop as sequential Phase 3.7 Step C.4: if the consolidated report shows verified BLOCKER/HIGH findings, spawn a fix-coder and — **re-writing the consumed-once lean contract first** — re-invoke `/codexreview` via the Skill tool with `args: <CARD-ID>` (max 2 retries per card; a bare prose mention or a missing card-ID would let the retry review the wrong card). If still BLOCKER/HIGH after retries, ask the user before proceeding to D.5. The D.5 commits MUST NOT happen until every card in the group has a PASS verdict (or explicit user override via `AskUserQuestion`). Log results in the tracker under `## Pre-Merge Codex Review` per card. **Lean + profile (since v3.35.0)**: before each card's `/codexreview`, apply the same Review Profile Selector and write the same `/tmp/codexreview-lean-<CARD-ID>.json` contract as sequential Phase 3.7 Step C — `arch_baseline_path` pointing at `/tmp/arch-baseline-group-<FIRST-CARD-ID>.md`, `skip_doc_reviewer: true`, and `profile: full` (every card that reaches D.4b is in `FULL_CARDS` per D.1.5 — `light`/`skip` 0-trigger cards were already partitioned out and skip D.4b entirely; do NOT recompute the profile here). `full` runs the standard pipeline (Codex adversarial + CoVe + FP-gate, minus the duplicate doc-reviewer). The merge-gate safety for the skipped `LIGHT_CARDS` is the post-batch **Final-review FULL gate** (a single FULL `/codexreview` over the entire batch diff, which team mode reaches via "Post-batch — same as sequential mode"). To make D.4b run for every card again (pre-v4.5.0 behavior), drop the D.1.5 partition and iterate over all group cards here at their per-card profile; the final full gate stays regardless.
2393
2414
 
2394
2415
  5. **D.5 — Commit** — One commit per card using explicit staging from the ownership map. **NO stash in worktrees** (stashes are globally shared via `refs/stash` — see Phase 4 WORKTREE COMMIT RULE):
2395
2416
  ```bash
@@ -2417,13 +2438,13 @@ After ALL agents in the group complete successfully:
2417
2438
  Before moving to Step E, the orchestrator MUST verify the tracker contains, for EACH card in the group, ALL of the following entries (or an explicit gate-skip log with the documented reason):
2418
2439
 
2419
2440
  - `## AC Closure Ledger — <CARD-ID>` with `ac-closure: implemented=N | user-approved deferrals=M | follow-up cards created=K`
2420
- - `simplify: <N fixes | clean — 0 fixes>` (per-card from D.3b)
2441
+ - `simplify: <N fixes | clean — 0 fixes | SKIPPED (review_profile=skip)>` (per-card from D.3b)
2421
2442
  - `e2e-review: <status>` (with documented gate-skip reason if SKIP)
2422
2443
  - `doc-review: <status>` (from D.4a)
2423
2444
  - `qa: <profile=... | verdict=...>` (group-level from D.4 — same value cited per card)
2424
- - `codex-review: <verdict>` (per-card from D.4b)
2445
+ - `codex-review: <verdict | SKIPPED (light, 0 triggers — D.2 group code-review covered + Final FULL gate)>` (per-card from D.4b — the SKIPPED form is valid ONLY for cards in `LIGHT_CARDS` per the D.1.5 partition)
2425
2446
 
2426
- A missing entry means a sub-step was skipped. If any entry is missing, do NOT proceed to Step E — return to the missing sub-step and execute it. Documenting "skipped per time budget" or similar is a protocol violation per the top-of-file rigidity clause.
2447
+ A missing entry means a sub-step was skipped. An entry whose value is a **`review_profile`-driven SKIP** with the documented enumerated reason above (D.3b `skip` profile, D.4b `LIGHT_CARDS`) is a VALID, present entry — not a violation. What remains forbidden: a missing entry, or a skip whose reason is `time budget` / `to save tokens` / any model-invented constraint. If any entry is genuinely missing, do NOT proceed to Step E — return to the missing sub-step and execute it. Documenting "skipped per time budget" or similar is a protocol violation per the top-of-file rigidity clause.
2427
2448
 
2428
2449
  #### Step E: Context purge + next group
2429
2450
 
@@ -103,15 +103,21 @@ message. You ask questions, wait for answers, and iterate.
103
103
  structure and `assets/card-template.yml` for child structure.
104
104
  16. **Mockup Intake (MANDATORY).** Subito dopo Kickoff (Step 1) e prima di entrare in
105
105
  Discovery (Step 2), esegui Step 1.6 — chiedi all'utente se ha mockup a disposizione.
106
- Se **sì**: chiedi formato (immagini in chat / path locali — singoli file **o
107
- archivi `.zip`** — / misti), **STOP**, attendi. Sull'arrivo:
106
+ Se **sì**: chiedi formato (immagini in chat / path locali — singoli file, **archivi
107
+ `.zip`**, **o una cartella già estratta di un bundle Claude Design** — / misti),
108
+ **STOP**, attendi. Sull'arrivo:
108
109
  - **chat-images** / file singoli in **local-paths**: copia in `${paths.prd_dir}/<slug>/mockups/`,
109
110
  passa subito all'analisi.
110
- - **`.zip` in local-paths** (es. export "Download as .zip" da Claude Design già
111
- sul disco): esegui Step 1.6.4b `unzip` in scratch dir sotto `mockups/_archive/`,
112
- enumera i candidati visivi, calcola uno score di rilevanza contro lo scope
113
- del Kickoff, presenta la selezione auto-proposta, **STOP**, attendi conferma,
111
+ - **`.zip` OPPURE cartella bundle Claude Design** (firma: `.design-canvas.state.json`
112
+ e/o più `*.html` con slot `id="s<N>"`; es. export "Download as .zip" o la cartella
113
+ scaricata dell'intero workspace): esegui Step 1.6.4b. Prima fa un **passaggio
114
+ strutturale** (canvas-anchor sul titolo feature estrazione slot `id="s<N>"` =
115
+ screen-ID → arricchimento dal manifest `.design-canvas.state.json` → render degli
116
+ artboard canvas-only via [canvas-intake-recipe.md](references/canvas-intake-recipe.md));
117
+ lo **scoring per keyword** resta solo come fallback (bundle generico o canvas
118
+ ambiguo). In entrambi i casi presenta la selezione, **STOP**, attendi conferma,
114
119
  poi materializza solo il sottoinsieme confermato in `${paths.prd_dir}/<slug>/mockups/`.
120
+ Il PRD non aborta mai sull'intake: ogni fallimento ricade sul fallback + STOP gate.
115
121
 
116
122
  Quindi analizza in dettaglio (vedi
117
123
  [discovery-phase.md](references/discovery-phase.md) § "Mockup analysis schema"), popola
@@ -46,6 +46,11 @@
46
46
  {{/if}}
47
47
 
48
48
  {{/each}}
49
+ > **Nota per l'export.** Dai al canvas di questa feature un titolo che contenga il
50
+ > nome feature («{{feature_title}}»), e numera gli artboard in modo coerente con gli
51
+ > ID schermata qui sopra (es. schermata 3.1 → artboard `s31`). Scaricherò il bundle
52
+ > completo del workspace: questo mi permette di ritrovare esattamente le TUE schermate
53
+ > ed evitare ambiguità con le altre feature.
49
54
 
50
55
  {{#if brand_voice}}
51
56
  ## 4. Brand & voice
@@ -88,12 +88,21 @@ mockups.format: pending # chat-images | local-paths | local-archive |
88
88
  mockups.original_paths: [] # user-provided paths, chat://image-N, or archive://<rel-path>
89
89
  mockups.canonical_paths: [] # mockups/<file> after copy
90
90
 
91
- mockups.archive: # populated only when format includes local-archive
92
- local_zip_path: pending # abs path of the .zip the user gave us
93
- kind: pending # zip
94
- extracted_dir: pending # abs path of _archive/extracted/
91
+ mockups.archive: # populated only when format includes local-archive OR a Claude-Design bundle directory
92
+ local_zip_path: pending # abs path of the .zip the user gave us (n/a when kind: directory)
93
+ kind: pending # zip | directory
94
+ extracted_dir: pending # abs path of _archive/extracted/ (zip) or the user dir (directory)
95
95
  truncated: false # true when candidate list capped at 200
96
96
  candidates: [] # see discovery-phase.md § 1.6.4b step b-c
97
+ bundle_kind: pending # zip | directory | claude-design | generic (set by step b2 A0)
98
+ canvas_ref: pending # chosen feature canvas *.html (claude-design only)
99
+ canvas_candidates: [] # [{ file, title, slot_count, score, anchor_confidence }]
100
+ discarded: [] # [{ path, reason }] reason: other-feature-canvas | support-asset | upload | unrelated
101
+
102
+ mockups.canvas_render: # populated only when step b2 A5 ran (claude-design + render)
103
+ served_port: pending # http-server port used for the render
104
+ rendered_slots: [] # [{ slot_id, png }]
105
+ render_status: pending # ok | partial | failed | skipped
97
106
 
98
107
  mockup_analysis.screens: [] # see discovery-phase.md § Mockup analysis schema
99
108
  mockup_analysis.user_flow: []
@@ -104,7 +113,8 @@ mockup_analysis.design_system_alignment:
104
113
  step_3_mode: pending # full | hybrid | skipped | mockup-only
105
114
  design.html_path: pending # path | n/a (mockup-only)
106
115
 
107
- screens_in_scope: [] # [{ name, covered_by_mockups: bool, generated_in_step_3b: bool }]
116
+ screens_in_scope: [] # [{ name, slot_id, screen_id, canvas_ref, covered_by_mockups: bool, generated_in_step_3b: bool }]
117
+ # slot_id/screen_id/canvas_ref set only for claude-design bundles (enable exact join in ui-design-phase Hybrid routing)
108
118
 
109
119
  ui_inventory: [] # populated by Step 3d
110
120
 
@@ -0,0 +1,113 @@
1
+ # Canvas Intake Recipe — rendering Claude-Design artboards to PNG
2
+
3
+ Deterministic recipe for capturing the screens of a Claude Design feature canvas
4
+ that exist **only** as artboards inside the canvas HTML (not exported as standalone
5
+ PNGs). Replaces the ad-hoc "spin up a server, debug CORS, script Playwright" dance
6
+ with a repeatable procedure.
7
+
8
+ ## When this runs
9
+
10
+ Invoked by [discovery-phase.md](discovery-phase.md) § "Step 1.6.4b → b2 → A5",
11
+ once per resolved `canvas_ref`, only for slots lacking a confident pre-render PNG.
12
+ **Never run standalone** — it assumes A0–A4 already chose the canvas and its slot
13
+ set. Outputs feed `mockups.canvas_render` in the state file.
14
+
15
+ ## Why a local HTTP server is mandatory
16
+
17
+ The canvas HTML loads its screens via Babel:
18
+ `<script type="text/babel" src="design-canvas.jsx">` (+ the feature's `*.jsx`
19
+ deps). Opening the file as `file://` makes the browser **block** those `src`
20
+ fetches under the CORS/`file:` origin policy → Babel never executes → the artboard
21
+ DOM is never created → `[data-dc-slot]` / `[data-dc-section]` do not exist. This is
22
+ exactly the `slots=0` failure seen when rendering from `file://`. Serving `root`
23
+ over `http://` lets the jsx load and Babel render.
24
+
25
+ > Slot attributes are **runtime-only**: the static HTML has `<DCSection id="s31">`
26
+ > JSX; the `data-dc-section` / `data-dc-slot` attributes only appear after Babel
27
+ > runs on the served page. Intake-time slot enumeration greps `id="s<N>"` (static);
28
+ > rendering selects `[data-dc-section]` / `[data-dc-slot]` (runtime).
29
+
30
+ ## Selectors
31
+
32
+ | Selector | Granularity | Source JSX |
33
+ |-----------------------|------------------------------|-------------------|
34
+ | `[data-dc-section="s3x"]` | one **screen** (3.x) | `<DCSection id>` |
35
+ | `[data-dc-slot="…"]` | one **artboard / variant** within a screen | `<DCArtboard id>` |
36
+
37
+ Capture at screen granularity (`[data-dc-section]`) by default; drop to
38
+ `[data-dc-slot]` only when a screen needs per-variant PNGs.
39
+
40
+ ## Preflight (degrade, never auto-install)
41
+
42
+ ```bash
43
+ npx playwright --version # must succeed
44
+ npx http-server --version # must succeed
45
+ ```
46
+
47
+ If either is missing: do **NOT** auto-install (this runs in consumer repos —
48
+ respect their toolchain). Emit a one-line hint
49
+ (`npm i -D playwright http-server && npx playwright install chromium`), set
50
+ `mockups.canvas_render.render_status: skipped`, and return control to A5, which
51
+ degrades to pre-render PNGs + `source: live-html`.
52
+
53
+ ## Serve
54
+
55
+ Pick a free port (loop 8080→8090, advancing on `EADDRINUSE`):
56
+
57
+ ```bash
58
+ npx http-server "$root" -p <PORT> -c-1 --silent # run in background; -c-1 disables caching
59
+ ```
60
+
61
+ Record `mockups.canvas_render.served_port: <PORT>`.
62
+
63
+ ## Render loop (Playwright)
64
+
65
+ ```js
66
+ // /tmp/canvas-shoot.mjs — node, run with the project's playwright
67
+ import { chromium } from 'playwright';
68
+ const PORT = process.env.PORT, CANVAS = process.env.CANVAS, OUT = process.env.OUT;
69
+ const SLOTS = JSON.parse(process.env.SLOTS); // [{ slot_id, screen_id }]
70
+ const browser = await chromium.launch();
71
+ const page = await browser.newPage({ deviceScaleFactor: 2 }); // ≥2x export
72
+ await page.goto(`http://127.0.0.1:${PORT}/${encodeURIComponent(CANVAS)}`);
73
+ await page.waitForSelector('[data-dc-section]', { timeout: 15000 }); // confirms Babel ran
74
+ await page.waitForLoadState('networkidle');
75
+ for (const { slot_id, screen_id } of SLOTS) {
76
+ const el = await page.$(`[data-dc-section="${slot_id}"]`) || await page.$(`[data-dc-slot="${slot_id}"]`);
77
+ if (!el) { console.log(`MISS ${slot_id}`); continue; }
78
+ await el.screenshot({ path: `${OUT}/${screen_id.replace('.', '-')}.png` }); // <slug>__3-1.png upstream
79
+ console.log(`OK ${slot_id} -> ${screen_id}`);
80
+ }
81
+ await browser.close();
82
+ ```
83
+
84
+ Name outputs `<slug>__<screen-id>.png` (e.g. `conferma-ordine__3-1.png`). Record
85
+ each in `mockups.canvas_render.rendered_slots[]`.
86
+
87
+ ## Teardown
88
+
89
+ Kill the background `http-server` (by PID). Set `render_status`:
90
+ - `ok` — every requested slot captured.
91
+ - `partial` — some slots captured, some `MISS` (record which).
92
+ - `failed` — `waitForSelector('[data-dc-section]')` timed out (Babel never ran:
93
+ offline CDN, broken jsx). Degrade as in preflight.
94
+
95
+ ## Dedup vs designer pre-renders
96
+
97
+ If a slot has BOTH a designer pre-render (A4) and a freshly rendered PNG, **prefer
98
+ the designer's pre-render** — keep our render only if it differs materially in
99
+ dimensions/content, and then name it `<screen-id>__rendered.png`. Never ship two
100
+ PNGs for the same screen state.
101
+
102
+ ## Failure → degradation summary
103
+
104
+ | Failure | Detection | Degradation |
105
+ |----------------------------------|------------------------------------|--------------------------------------------------|
106
+ | Playwright / http-server missing | preflight version check fails | `skipped` → pre-render + `live-html`, install hint |
107
+ | All ports busy | EADDRINUSE through 8090 | try an ephemeral port; else `skipped` |
108
+ | Babel never runs (CORS/offline) | `[data-dc-section]` wait times out | `failed` → pre-render + `live-html` |
109
+ | Some artboards missing | `MISS` in loop | `partial` — capture the rest, log the misses |
110
+
111
+ Global rule: **never abort the PRD**. Every failure here returns to A5 with a
112
+ degraded `render_status`; intake proceeds with whatever PNGs exist plus live-HTML
113
+ references.
@@ -242,37 +242,49 @@ When the user replies with the mockups:
242
242
  5. Update `mockups.status: provided` (or `partial` only if the user explicitly says
243
243
  "ho solo questi 2 ma servono anche altri").
244
244
 
245
- ### 1.6.4b — Archive ingestion & relevance selection (when `mockups.format` includes `local-archive`)
245
+ ### 1.6.4b — Archive ingestion & relevance selection (when `mockups.format` includes `local-archive`, OR the user gives a Claude-Design bundle directory)
246
246
 
247
247
  Triggered when the user hands a local `.zip` path (typically a Claude Design
248
- "Download as .zip" export already on disk). The flow scans every mockup inside,
249
- scores each against the feature scope from Kickoff, and asks the user to confirm
250
- the auto-selected subset before analysis.
248
+ "Download as .zip" export already on disk) **OR** a path to an already-extracted
249
+ **bundle directory** (the user downloaded the whole Claude Design workspace as a
250
+ folder). The flow resolves the subset of files belonging to THIS feature, then asks
251
+ the user to confirm before analysis.
251
252
 
252
- #### Step a Detect & unpack
253
+ Resolution is two-tier: a **structural pass** (step b2) that exploits Claude
254
+ Design's native bundle structure when present, with the **keyword scorer** (step c)
255
+ as the fallback. The STOP confirmation gate (step d) is identical in both modes.
253
256
 
254
- For each zip path provided:
257
+ #### Step a Detect & accept root
258
+
259
+ For each path provided:
255
260
 
256
261
  ```bash
257
- file "<user-zip-path>"
262
+ file "<user-path>" # only meaningful for files
258
263
  ```
259
264
 
260
- - If `file` does NOT report a Zip archive, treat the path as a regular file and
261
- go back to step 1.6.4 point 2's "Otherwise" branch — no special handling.
262
- - If it IS a zip, record in state:
265
+ - **Directory** (the path is a folder): skip unpacking. Record:
266
+ - `mockups.archive.kind: directory`
267
+ - `mockups.archive.extracted_dir: <the user dir>` (absolute)
268
+ - `mockups.archive.local_zip_path: n/a`
269
+ - **`.zip`** (`file` reports a Zip archive): record:
263
270
  - `mockups.archive.local_zip_path: <user-zip-path>` (absolute)
264
271
  - `mockups.archive.kind: zip`
265
- - Unpack into a scratch directory under the PRD's mockup folder:
272
+ - Unpack into a scratch directory under the PRD's mockup folder:
266
273
 
267
- ```bash
268
- mkdir -p "$WORKTREE_PATH/${paths.prd_dir}/<slug>/mockups/_archive"
269
- unzip -q "<user-zip-path>" \
270
- -d "$WORKTREE_PATH/${paths.prd_dir}/<slug>/mockups/_archive/extracted"
271
- ```
274
+ ```bash
275
+ mkdir -p "$WORKTREE_PATH/${paths.prd_dir}/<slug>/mockups/_archive"
276
+ unzip -q "<user-zip-path>" \
277
+ -d "$WORKTREE_PATH/${paths.prd_dir}/<slug>/mockups/_archive/extracted"
278
+ ```
279
+
280
+ - Record `mockups.archive.extracted_dir` (the `extracted/` path).
281
+ - If `unzip` fails (corrupted/encrypted): STOP, surface the exit code, ask the
282
+ user to verify the zip or provide an alternative.
283
+ - **Regular non-zip file**: treat as a single file — go back to step 1.6.4 point
284
+ 2's "Otherwise" branch, no special handling.
272
285
 
273
- - Record `mockups.archive.extracted_dir`.
274
- - If `unzip` fails (corrupted/encrypted): STOP, surface the exit code, ask the
275
- user to verify the zip or provide an alternative.
286
+ Both directory and zip branches converge on a single `root` =
287
+ `mockups.archive.extracted_dir`. Everything below operates on `root`.
276
288
 
277
289
  #### Step b — Enumerate candidates
278
290
 
@@ -309,8 +321,112 @@ top 200, recording `mockups.archive.truncated: true`.
309
321
 
310
322
  Record the full index in state under `mockups.archive.candidates[]`.
311
323
 
324
+ #### Step b2 — Claude-Design structural resolution (Feature-Subset Resolution)
325
+
326
+ A Claude Design export is **not** a flat pile of files: it carries native
327
+ structure that pins each feature's screens deterministically. When that structure
328
+ is present, resolve the subset *structurally* and skip the keyword scorer. When it
329
+ is absent (or the canvas anchor stays ambiguous), fall through to step c unchanged.
330
+
331
+ > **Global contract.** The structural resolver is an **accelerator**. Any failure
332
+ > at any step (A0–A6) falls through to the keyword scorer (step c) + the STOP gate
333
+ > (step d), which is already proven. **The PRD never aborts on intake.**
334
+
335
+ **A0 — Signature gate.** On `root`, compute:
336
+ - `has_manifest` — `[ -f "$root/.design-canvas.state.json" ]`.
337
+ - `has_canvas_jsx` — a `design-canvas.jsx` exists, or some `*.html` loads it via
338
+ `<script type="text/babel" src=…>` and references `data-dc-section`.
339
+ - `feature_html_count` — number of top-level `*.html` whose **source** contains an
340
+ `id="s<N>"` slot (see A2 — this is the static signal; `data-dc-slot` only exists
341
+ at runtime after Babel renders, so do NOT grep for it here).
342
+
343
+ Set `mockups.archive.bundle_kind`:
344
+ - `claude-design` iff `has_manifest` OR (`has_canvas_jsx` AND `feature_html_count ≥ 1`) → run A1–A6.
345
+ - otherwise `generic` → **skip to step c** (keyword scorer), unchanged.
346
+
347
+ **A1 — Canvas anchor (feature_title → ONE canvas `*.html`).**
348
+ - Candidate set = top-level `*.html` MINUS the denylist of component/system
349
+ canvases: `Design System.html`, `BottomNav*.html`, `*-bundle-src.html`,
350
+ `* · standalone.html`. **Do NOT pre-filter by slot presence** — feature canvases
351
+ in the wild use mixed slot schemes (some `id="s<N>"`, some semantic like
352
+ `id="supplier-edit-a-add"`), and an `id="s<N>"`-only allowlist would wrongly drop
353
+ a feature whose canvas doesn't use that numbering. Slot presence is a *confidence
354
+ signal* (below), not a candidacy gate.
355
+ - For each candidate read its `<title>` + filename, score against `feature_title`
356
+ + scope keywords (reuse step c's case-insensitive whole-word splitter): `<title>`
357
+ match `+50`, filename `+40`, `id="s<N>"` slot-range hint `+10` (boosts canvases
358
+ that expose clean screen-IDs, enabling A2's exact join). Record in
359
+ `mockups.archive.canvas_candidates[]`.
360
+ - **Resolution:**
361
+ - Exactly 1 candidate with a confident margin (top score ≥ 50 AND ≥ 2× the
362
+ runner-up) → `mockups.archive.canvas_ref = that file`, `anchor_confidence: high`.
363
+ - 0 clear, OR ≥ 2 within the 2× margin → **AMBIGUOUS**. Do NOT guess. Use
364
+ `AskUserQuestion` listing the candidate canvases (filename + `<title>` + slot
365
+ count): "Quale canvas è questa feature?". If the user says none/multiple →
366
+ fall through to step c over all candidates (`anchor_confidence: fallback-keyword`).
367
+ Multiple confirmed → A2 unions the slot sets and `canvas_ref` becomes a list.
368
+
369
+ **A2 — Slot extraction (canvas → screen IDs).** From `canvas_ref` source only.
370
+ Anchor on the **section** element that carries the screen title, NOT a loose
371
+ `title=` (sibling attributes like `subtitle="…"` contain the substring `title="`
372
+ and a greedy `id=…title=…` regex grabs the wrong one):
373
+ ```bash
374
+ # DCSection is the per-screen container; its `title` is the screen label.
375
+ grep -oE '<DCSection id="s[0-9]+[a-z]*" title="[^"]*"' "<canvas_ref>"
376
+ # <DCSection id="s31" title="3.1 · Wizard Step 3"
377
+ ```
378
+ → `{ slot_id, screen_id, title_raw }` per section. (If the bundle uses a different
379
+ section tag, anchor on whatever element holds the `id`+dotted-`title` pair.)
380
+ - Derive `screen_id` from the title's dotted prefix (e.g. `"3.1 · …"` → `3.1`).
381
+ Cross-check against the slot number (`s31` → `3.1`). On disagreement, **trust the
382
+ `title=` prefix** and log `slot_id_number_mismatch` (handles non-`s<N>`
383
+ numbering). If neither yields a dotted id, leave `screen_id: null` — that screen
384
+ joins downstream by the fuzzy fallback, never blocks.
385
+
386
+ **A3 — Manifest read (enrich, never gate).** If `has_manifest`, parse
387
+ `.design-canvas.state.json`. For each slot in A2:
388
+ - `sections["s31"].labels` → attach as candidate `states_visible[]` / variant labels.
389
+ - `sections["s31"].title` → prefer as the human label; else keep the A2 `title=`.
390
+ - Manifest is **partial and mixed-keyspace** (some keys are slot-ids, some are
391
+ semantic section names belonging to OTHER canvases). Keys not in this canvas's
392
+ slot set are ignored. A slot missing from the manifest is the **normal case**,
393
+ not an error. Set `screens[].source` = `manifest` (labelled) or `title` (not).
394
+
395
+ **A4 — Sidecar gather.**
396
+ - **JSX deps** — the `<script type="text/babel" src="X.jsx">` list inside
397
+ `canvas_ref` IS the authoritative dependency set (needed for A5 render). Record
398
+ for materialization in step e.
399
+ - **Pre-rendered PNGs** — scan `$root/screenshots/` (and any image dir) for files
400
+ whose name confidently matches a slot number / slug (`ap-31.png` → `s31`). On a
401
+ confident match set `screens[].prerender_png` + `source: prerender`. Naming is
402
+ ad-hoc (`modale__valido.png`) so matching is **best-effort only**; unmatched
403
+ images are parked, never force-assigned.
404
+
405
+ **A5 — Render canvas-only artboards.** Screens that exist only inside the canvas
406
+ (no confident `prerender_png`) must be rendered. Invoke
407
+ [canvas-intake-recipe.md](canvas-intake-recipe.md) (http-server + Playwright over
408
+ `[data-dc-section="s<N>"]`). Output `mockups/<slug>__<screen-id>.png`, record under
409
+ `mockups.canvas_render`. If render is impossible (Playwright/server unavailable),
410
+ **degrade**: keep pre-render PNGs, mark remaining slots `source: live-html`
411
+ referencing the canvas HTML directly, set `render_status: failed|skipped`, and
412
+ continue — never abort.
413
+
414
+ **A6 — Discard-and-log the rest.** Everything in `root` NOT in { `canvas_ref`, its
415
+ jsx deps, matched `screenshots/*`, shared assets (`*tokens*.css`, `*icons*.jsx`,
416
+ `*shell*.jsx`) } is out-of-feature: the other feature canvases, their jsx,
417
+ `frames/`, `uploads/`, `docs/`. Record in `mockups.archive.discarded[]` with a
418
+ one-word `reason`. Shown for transparency at the STOP gate (step d) but **not
419
+ copied**.
420
+
421
+ After A6, build the screen-centric selection and go to **step d** (its structural
422
+ variant). Skip step c entirely — it is the fallback path only.
423
+
312
424
  #### Step c — Score against feature scope
313
425
 
426
+ > **Fallback path.** Runs ONLY when step b2 set `bundle_kind: generic`, or the
427
+ > canvas anchor stayed unresolved (`anchor_confidence: fallback-keyword`). For a
428
+ > resolved Claude-Design bundle, skip directly to step d.
429
+
314
430
  Build the scope vector from the Kickoff state (Step 1):
315
431
  - `feature_title` (from `## Feature Description`).
316
432
  - `slug`.
@@ -340,8 +456,34 @@ Record per-candidate: `score`, `matched_keywords[]`, `selected: bool`.
340
456
 
341
457
  #### Step d — Confirmation gate (STOP)
342
458
 
343
- Present a compact table to the user — selected candidates first, then up to 10
344
- discarded with the highest scores, then a footer count for "altri N scartati":
459
+ Two table variants, same STOP/adjust/re-present loop:
460
+
461
+ **Structural variant** (step b2 resolved a `canvas_ref`). Present **screen-centric**
462
+ — one row per resolved screen, plus the discarded-canvas summary:
463
+
464
+ ```
465
+ Canvas individuato: «Approvazione Ordini.html» (feature «<feature_title>») — <K> schermate.
466
+
467
+ **Schermate risolte** (slot → screen-ID):
468
+ | # | Slot | Screen | Label | Sorgente |
469
+ |---|------|--------|------------------------------------|-----------|
470
+ | 1 | s31 | 3.1 | Wizard Step 3 · CTA approvazione | manifest |
471
+ | 2 | s32 | 3.2 | Elenco approver | rendered |
472
+
473
+
474
+ **Scartati** (altre feature / asset di supporto):
475
+ | File / dir | Motivo |
476
+ |-------------------------------|-----------------------|
477
+ | Budget Pianificazione.html | other-feature-canvas |
478
+ | frames/, uploads/ | unrelated / upload |
479
+
480
+ Procedo all'analisi delle <K> schermate? Oppure indicami modifiche
481
+ (es. "togli s37", "il canvas giusto è Budget Pianificazione").
482
+ ```
483
+
484
+ **Keyword-fallback variant** (step c ran). Present file-centric — selected
485
+ candidates first, then up to 10 discarded with the highest scores, then a footer
486
+ count for "altri N scartati":
345
487
 
346
488
  ```
347
489
  Bundle scaricato e analizzato: <N> file candidati, <K> selezionati come rilevanti.
@@ -366,22 +508,37 @@ Procedo all'analisi visiva dei <K> selezionati? Oppure indicami modifiche
366
508
  ```
367
509
 
368
510
  **STOP.** Wait for explicit confirmation or an adjustment list. On adjustment,
369
- recompute `mockups.archive.candidates[].selected`, then re-present and STOP again
370
- until the user confirms.
511
+ recompute the selection (`mockups.archive.candidates[].selected` in keyword mode,
512
+ the per-slot row set in structural mode), then re-present and STOP again until the
513
+ user confirms.
371
514
 
372
515
  #### Step e — Materialize the confirmed subset
373
516
 
374
517
  Copy ONLY the confirmed selection into `${paths.prd_dir}/<slug>/mockups/`,
375
- preserving relative directory structure under the extracted root so that
376
- relative asset references (CSS, images) keep working. On filename collision,
377
- use the `<name>-2.<ext>` rule from 1.6.4 point 2.
378
-
379
- Record `mockups.original_paths[]` as `archive://<relative-path-in-zip>` and
380
- `mockups.canonical_paths[]` as the new path under `mockups/`. The scratch
381
- `_archive/extracted/` directory is kept for the duration of the PRD session
382
- (may be needed to resolve `<link>` / `<img src>` references during analysis)
383
- and cleaned up at Step 7 (Commit & Merge) — add a `.gitignore` entry for
384
- `mockups/_archive/` if not already present.
518
+ preserving relative directory structure under `root` so that relative asset
519
+ references (CSS, images) keep working. On filename collision, use the
520
+ `<name>-2.<ext>` rule from 1.6.4 point 2.
521
+
522
+ **Structural mode also copies the render dependencies**, so the canvas stays
523
+ renderable/analyzable inside the PRD folder:
524
+ - `canvas_ref` itself + every jsx dep from A4 + shared assets (`*tokens*.css`,
525
+ `*icons*.jsx`, `*shell*.jsx`), preserving relative paths so `<script src>` /
526
+ `<link>` keep resolving.
527
+ - the rendered PNGs (`mockups.canvas_render.rendered_slots[].png`) and matched
528
+ pre-render PNGs.
529
+ - **Write `screens_in_scope[]`** here — one row per resolved screen with `name`,
530
+ `slot_id`, `screen_id`, `canvas_ref`. This is what lets the Step 3 (UI Design)
531
+ Hybrid routing do an **exact join** instead of a fuzzy substring match (see
532
+ [ui-design-phase.md](ui-design-phase.md) § "Build the per-screen routing table").
533
+
534
+ Record `mockups.original_paths[]` as `archive://<relative-path-in-zip>` (zip) or
535
+ `archive://<relative-path-in-root>` (directory) and
536
+ `mockups.canonical_paths[]` as the new path under `mockups/`. When `kind: zip`,
537
+ the scratch `_archive/extracted/` directory is kept for the duration of the PRD
538
+ session (may be needed to resolve `<link>` / `<img src>` references during
539
+ analysis) and cleaned up at Step 7 (Commit & Merge) — add a `.gitignore` entry for
540
+ `mockups/_archive/` if not already present. When `kind: directory`, `root` is the
541
+ user's own folder outside the worktree — never modify or clean it; only read.
385
542
 
386
543
  Then continue to Step 1.6.5 with the selected subset.
387
544
 
@@ -802,12 +959,23 @@ mockups:
802
959
  - <user-provided path, chat://image-N, or archive://<rel-path-in-zip>>
803
960
  canonical_paths:
804
961
  - mockups/<file>.png # only for local-paths and local-archive
805
- archive: # only when format includes local-archive
806
- local_zip_path: <abs path to the user-provided .zip>
807
- kind: zip
808
- extracted_dir: <abs path to _archive/extracted/>
962
+ archive: # only when format includes local-archive OR a Claude-Design bundle dir
963
+ local_zip_path: <abs path to the user-provided .zip> # n/a when kind: directory
964
+ kind: zip | directory
965
+ extracted_dir: <abs path to _archive/extracted/ (zip) or the user dir (directory)>
809
966
  truncated: false # true when candidate list capped at 200
810
- candidates: # full enumerated list (post-filter)
967
+ bundle_kind: zip | directory | claude-design | generic # set by step b2 A0
968
+ canvas_ref: <chosen feature canvas *.html> # claude-design only
969
+ canvas_candidates: # claude-design only
970
+ - file: <*.html basename>
971
+ title: <HTML <title>>
972
+ slot_count: <int> # count of id="s<N>" slots
973
+ score: <0-100>
974
+ anchor_confidence: high | user-resolved | fallback-keyword
975
+ discarded: # claude-design only — out-of-feature, NOT copied
976
+ - path: <rel path under root>
977
+ reason: other-feature-canvas | support-asset | upload | unrelated
978
+ candidates: # full enumerated list (post-filter) — keyword-fallback path
811
979
  - path: <rel path under extracted/>
812
980
  filename: <basename>
813
981
  ext: html | png | jpg | svg | …
@@ -817,10 +985,20 @@ mockups:
817
985
  score: <0-100>
818
986
  matched_keywords: [<keyword>, …]
819
987
  selected: <bool> # true = part of the confirmed subset
988
+ canvas_render: # only when step b2 A5 rendered canvas-only artboards
989
+ served_port: <int>
990
+ rendered_slots:
991
+ - slot_id: s31
992
+ png: mockups/<slug>__3-1.png
993
+ render_status: ok | partial | failed | skipped
820
994
  mockup_analysis:
821
995
  screens:
822
996
  - name: <inferred name, e.g. "Lista ordini">
997
+ slot_id: <s31, claude-design only — else omit>
998
+ screen_id: <"3.1" dotted, claude-design only — joins handoff prompt § 3 — else omit>
823
999
  mockup_ref: <canonical_path or chat://image-N>
1000
+ source: manifest | title | rendered | prerender # claude-design only — provenance of the label/image
1001
+ prerender_png: <mockups/<file>.png — only when a designer pre-render matched this slot>
824
1002
  purpose: <one line — what this screen lets the user do>
825
1003
  components:
826
1004
  - <component name from ${paths.design_system}/INDEX.md if matched, else free-form>
@@ -180,11 +180,19 @@ to the design system, approve, and inventory. Generate ONLY the screens not cove
180
180
  - ISA `NAVIGATION` and `DASHBOARD` touchpoints that imply landing screens.
181
181
  - The `mockup_analysis.screens[]` list itself (in case the mockups include
182
182
  screens the user stories didn't make explicit).
183
- 2. For each entry, set `covered_by_mockups: true` iff the screen matches a
184
- `mockup_analysis.screens[].name` (case-insensitive substring match is fine
185
- when ambiguous, ASK the user "questa schermata `<X>` corrisponde al mockup
186
- `<Y>`?" and STOP).
187
- 3. Persist `screens_in_scope[]` in the state file `## UI Design` section.
183
+ 2. For each entry, set `covered_by_mockups: true`. Match **exact-join-first**:
184
+ - **Exact join (Claude-Design bundles).** When the entry has a `slot_id` /
185
+ `screen_id` (populated by Step 1.6.4b structural resolution, see
186
+ [discovery-phase.md](discovery-phase.md) § "Step b2") AND a
187
+ `mockup_analysis.screens[]` entry shares the same `slot_id`/`screen_id`, join
188
+ them **deterministically** — no ambiguity, no STOP. This is the common case
189
+ for handoff bundles and skips the fuzzy guesswork entirely.
190
+ - **Fuzzy fallback.** Only when `slot_id` is absent (chat images, single files,
191
+ `generic` bundles) fall back to case-insensitive substring match on
192
+ `mockup_analysis.screens[].name` — and keep the STOP-and-ask on ambiguity:
193
+ "questa schermata `<X>` corrisponde al mockup `<Y>`?".
194
+ 3. Persist `screens_in_scope[]` (with `slot_id`/`screen_id`/`canvas_ref` when
195
+ present) in the state file `## UI Design` section.
188
196
 
189
197
  ### Per-screen execution
190
198
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.3.0",
3
+ "version": "4.5.0",
4
4
  "description": "Claude Agent Framework - Reusable framework for coordinating AI agents and humans in software projects",
5
5
  "bin": {
6
6
  "baldart": "./bin/baldart.js"