# `/ba-develop` — Phase-by-phase detail

> Loaded on demand when the orchestrator needs to expand a phase. The compact
> overview lives in `SKILL.md` § The phases.

## The six phases — full description

```
Phase 0 — Core Foundation Seed   → 5 IClientSeedDataProvider classes under
                                   Persistence/Seeding/Core/ (navigation, roles,
                                   permissions, role-permissions, dev test users).
                                   Deterministic — agent only invokes the CLI.
Phase 1 — Entities               → domain entities + invariants + EF configs
                                   + migrations + module-scoped reference data.
                                   Merges what was Domain + Data.
Phase 2a — API Integration       → integration controllers (/api/v1/integration/,
                                   Swagger group "integration"), DTOs,
                                   [RequirePermission], business handlers +
                                   Create/Update validators, THEN the business-
                                   logic pass — implement every rule / use-case
                                   the scaffolder left as a // TODO[BR-…] or
                                   NotImplementedException marker. CRUD + UC
                                   transitions for machine-to-machine consumers.
                                   Integration + Category=Business tests.
Phase 2b — API Screen-driven     → ONE subagent per (section, entity). Each
                                   reads <MODULE>/pagespecs/<Entity>.*.md,
                                   invokes scaffold-screen-controller, then runs
                                   `dotnet build` + targeted tests. Emits
                                   {EntityPlural}ScreenController.cs under
                                   Controllers/{Module}/Screens/ +
                                   {Entity}{View}ScreenDto.cs under
                                   DTOs/Screens/. Routes under
                                   /api/screens/{plural}/{action}, Swagger group
                                   "screens". Reuses Business methods from 2a —
                                   NEVER duplicates rules. Missing Business
                                   method → TODO[SCREEN-…] marker, caught by
                                   audit-dev-api (Wave D, DEV-API-012..014).
Phase 3 — Frontend               → six ordered sub-phases (3.0 → 3a → 3b → 3c → 3.5 → 3d);
                                   3a fans out ONE subagent per feature/entity; the
                                   other sub-phases run on the orchestrator.
    3.0 — Theme + layout + UI primitives
                          scaffold-theme writes src/index.css (CSS vars + design
                          tokens). Pass `useShoelace: true` ONLY when the target
                          project depends on `@shoelace-style/shoelace` — detect
                          by reading `<projectPath>/<webRoot>/package.json` and
                          checking `dependencies['@shoelace-style/shoelace']`.
                          When absent, omit the flag (default: false) — Vite
                          would otherwise fail at boot with
                          `Can't resolve '@shoelace-style/shoelace/dist/themes/light.css'`.
                          scaffold-layout writes src/components/ui/PageTemplate.tsx
                          + src/components/{AppShell,Sidebar}.tsx (idempotent;
                          honors `// @customised` markers). scaffold-ui-primitives
                          writes src/components/ui/{EntityLookup,DateInput,
                          EnumSelect,MultiSelect}.tsx + deep-merges
                          src/i18n/locales/{fr,en,it,de}/common.json with the
                          entityLookup/dateInput/enumSelect/multiSelect
                          namespaces. Theme-compliant (zero hardcoded Tailwind
                          colors; read --radius-input). MUST run before 3a.
                          scaffold-dashboard-primitives writes
                          src/components/dashboard/{KpiCard,ChartCard,ListWidget,
                          DashboardGrid,WidgetRenderer,types,useDatavizPalette}
                          (read --dataviz-*/--chart-*/--kpi-* from scaffold-theme;
                          ChartCard needs recharts) + the dashboard i18n namespace.
                          Run it here too so any `dashboard` view resolves its
                          imports. MUST run before 3a.
    3a — Pages       ONE subagent PER FEATURE/ENTITY — **allowed-tools: [Read, Glob, Grep, Bash]
                     only** (NO Edit, NO Write). The subagent MUST NOT hand-write .tsx
                     files; it invokes scaffold-component via Bash and validate-page
                     checks the `@generated-by scaffold-component` marker.
                     The orchestrator fans out over slices.frontend.entities: features
                     in DIFFERENT modules MAY run in parallel; features of the SAME
                     module run SERIALLY (they share one
                     src/i18n/locales/{locale}/{module}.json — scaffold-component
                     deep-merges it, safe serially but a race under parallel writes).
                     Each feature-subagent owns ONLY its entity's files; its only
                     allowed Bash invocations are scaffold-api-client (its entity,
                     once), scaffold-component (per single view) and validate-page
                     (per page). The per-page audit-or-regenerate loop is detailed
                     below in § Phase 3a per-entity loop.
    3b — Routes      scaffold-routes emits BOTH src/extensions/{module}Registry.ts and
                     src/extensions/{module}Routes.ts per module.
    3c — Aggregator  invoke `aggregate-component-registry --project-path "<web-root>"`.
                     Phantom imports / collisions = exit 1 (gate fails, no file written).
                     Patch src/main.tsx with `import './extensions/componentRegistry.generated';`.
    3.5 — Audit-apply  audit-dev-frontend --mode apply (DEV-UI-001..022)
                       THEN ui-polish --mode apply (CSS tokens, design rules).
                       Re-runs even when 3a-3c reported filesChanged: 0.
    3d — Build gate  npm run build (Vite resolves every `lazyWithRetry(() => import('@/…'))`)
Phase 4 — Acceptance Tests      → scaffold-tests-from-ac emits one [Fact] per AC
                                   declared under each UC in <section>/use-case.md
                                   (TODO[AC-NN] skeleton + Assert.Fail). The
                                   subagent then fills every TODO body using the
                                   AC text + the controller + DTO from Phases 2-3,
                                   commits, and runs `dotnet test --filter
                                   Category=Acceptance`. The gate enforces tests
                                   compile, all Facts pass, and ZERO `// TODO[AC-`
                                   markers remain (audit DEV-TEST-001 BLOCKING).
```

## Each phase is a subagent

### Pre-entry coverage checks (re-run only)

Before deciding to skip a phase on re-run, the orchestrator runs a fast
(<5 s) coverage check comparing the PRD/BA spec against what is on disk.
If items are missing → the phase MUST re-enter. Full protocol in
`references/gates.md` § "Pre-entry coverage checks (Phases 0-2)".

**Phase 0**: Grep `CoreNavigationSeedDataProvider.cs` for every BA module
code + count permissions per module. Missing module or < 2 permissions → re-enter.

**Phase 1**: Run `project-inventory --domains domain`. Compare entity names
and attributes against `entité.md`. Missing entity or attribute → re-enter.

**Phase 2a**: Run `project-inventory --domains api` (controller existence)
+ `audit-dev-api --rules DEV-API-010` (custom action coverage).
Missing controller or endpoint → re-enter.

**Phase 2b**: Run `audit-dev-api --rules DEV-API-012` (screen endpoint
coverage). Missing screen endpoint → re-enter.

**Phase 3**: always re-enters the pre-entry gate, but regenerates only the
**delta** — `compute-page-diff` (spec-drift) ∪ `validate-page` on the
`unchanged` set (disk-drift). Skip the per-entity fan-out only when BOTH are
empty. Full protocol in `references/gates.md` § "Phase 3 pre-entry".
**Phase 4**: always re-runs (tests are cheap, regressions are not).

- **Phase 0 (`core`)**: builds CoreSeedSpec from `index.md` (nav) + `<APP>/acteur.md`
  (roles) + `<MODULE>/rbac.md` (permissions), invokes `scaffold-core-seed` via Bash.
- **Phases 1-3**: reads its slice file — Phase 1 → `prd.entities.md`; Phase 2 →
  `prd.api.md`; Phase 3 → `prd.frontend.md`. Hand the subagent exactly that one
  file path so it sees only its slice.
- **Phase 4 (`acceptance`)**: reads EVERY `<section>/use-case.md` (the AC source
  of truth) + Phase 2 controllers + DTOs (to know what HTTP verb + path + DTO
  shape each AC asserts against). Writes only to `Tests/{Module}/Acceptance/*.cs`.

Each subagent has Read / Glob / Grep / Edit / Write / Bash against `projectPath`,
plus the appropriate scaffold-skill SKILL.md content loaded in context. It
returns `{ artifacts: [...], testsRun, testsPassed, compileOk }`. Phase 4 also
returns `{ factsEmitted, acsParsed }`.

You never expose a Phase 1+ subagent to a slice other than its own. If a subagent
needs cross-slice data (e.g. frontend needs entity field shape), it fetches that
slice **read-only** — but writes only to its own phase's files. Phase 4 is the
only phase that legitimately reads multiple phases' output — it is the
verification phase.

## Phase 2a (API Integration) — derive scaffolder inputs from pagespecs

Before calling `scaffold-business` / `scaffold-controller` for an entity:

1. **Read every pagespec** for the entity in `<MODULE>/pagespecs/<Entity>.*.md`
   and parse each fenced ```json block.
2. **Collect `actions[]`** across the parsed pagespecs.
3. **Filter** to `kind: "api"` AND `code ∉ STANDARD_CRUD_CODES` (the constant
   exported by `lib/page-spec-actions.ts`: `create`, `read`, `edit`, `update`,
   `delete`, `list`, `detail`).
4. **De-duplicate** by `(scope, endpoint, httpMethod)` — emit ONCE per route.
5. **Derive `ControllerCustomAction[]`** for `scaffold-controller`:
   - `code`: `pageAction.endpoint` (URL segment, kebab-case)
   - `scope`: `pageAction.scope`
   - `httpMethod`: `pageAction.httpMethod` (defaults POST)
   - `payloadDto`: `pageAction.payloadDto ?? null`
   - `responseDto`: `pageAction.responseDto ?? 'NoContent'`
   - `permissionAction`: `lastSegment(pageAction.permission)` (e.g.
     `referentiels.types-affaire.execute` → `execute`)
   - The C# method name is `PascalCase(endpoint)`, generated automatically.
6. **Derive `displayNameExpr` for `scaffold-business`**: when `entité.md` carries
   an `**Affichage** : Name` line, pass that PascalCase token; otherwise omit
   and let the generator fall back to `Name → Label → Code → Title`.
7. **Derive `BusinessCustomAction[]`** (same `code`, `scope`, `payloadDto`,
   `responseDto`) plus workflow metadata: `ucReference`, `fromStatus`,
   `toStatus`, `flowParameters`, `guardRules`, `crossModuleDeps`. Populate
   from `prd.api.md` "Workflow:" sections + `règles-métier.md` (workflow type)
   + `use-case.md` flows. Conforms to SK-001/002/004/005.

### Cross-check inside the Phase 2a gate

After `scaffold-controller` + `scaffold-business` finish for an entity, the
subagent MUST verify that every `kind: "api"` action landed on the controller
with the right route attribute. For each custom action of entity `<E>`:

- Read `src/<Ns>.Api/Controllers/<Module>/<E>sController.cs`
- Locate a method with `[Http<VERB>("<expectedRoute>")]` where
  `<expectedRoute>` is `expectedControllerRoute(scope, endpoint)` from
  `lib/page-spec-actions.ts` (row → `{id:guid}/<endpoint>`, bulk →
  `bulk/<endpoint>`, header → `<endpoint>`).
- If missing → fail the Phase 2a gate with `{ phase: "api.customActions",
  entity, action_code, expected_verb, expected_path, controller_file }`.

This is the structural insurance that the pagespec contract reached the wire.
Audit `DEV-API-010` performs the same check post-hoc.

## Phase 2b (API Screen-driven) — one subagent per (section, entity)

After Phase 2a passes, the orchestrator scans every pagespec under
`<MODULE>/pagespecs/<Entity>.*.md`, groups by **(section, entity)**, and each
unique tuple yields ONE subagent.

**Per-subagent contract** (read-only on Business / Domain, writes only under
`Controllers/{Module}/Screens/` + `DTOs/Screens/`):

1. Invoke `scaffold-screen-controller` once with the spec
   `{ section, entity, module, appCode, namespace, moduleDir, projectPath }`.
2. Capture the CLI's `todos[]`. Each entry is a `[SCREEN-<code>]` marker
   (typically a missing `I{Entity}Service` method, e.g. `GetForListScreenAsync`).
   For each missing method:
   a. Add the signature to `I{Entity}Service`.
   b. Implement it in `{Entity}Service` (Phase 2a's class), mapping through
      Linq to the new `{Entity}{View}ScreenDto`. Reuse the SAME query
      primitives Phase 2a's CRUD uses — **never duplicate rules across strata**.
   c. Append a happy-path Fact to the existing `{Entity}ServiceTests`.
3. Run `dotnet build`. On compile error: return a structured failure
   (`{ failureKind: "compile.*", ..., fingerprint }`) — the orchestrator's
   auto-healing protocol takes over.
4. Run `dotnet test --filter "Category=Acceptance&FullyQualifiedName~Screens"`.
   On test failure: return `{ failureKind: "tests.acceptance.fail", ... }`.
5. Stage and commit ONLY this subagent's files (regex of allowed paths is
   `Controllers/{Module}/Screens/{EntityPlural}ScreenController.cs` +
   `DTOs/Screens/{Entity}*.cs` + the modified service files).

**Parallelism rules**:
- Different (section, entity) tuples MAY run in parallel.
- Subagents that share an entity but differ on section MUST run serially.
- Failure of one subagent **defers its own item** (logs a blocker, best-effort
  skip/stub); the others' commits remain and the run continues.

## Phase 3a.0 — Cross-module FK pre-flight (Bug 6)

Run ONCE per module before the per-entity loop starts. Detects every FK target
in another module and scaffolds it upstream so the per-entity agents can
`import { useTypeAuditLookup } from '@/features/referentiels/typeAudit/...'`
without resolving against an empty file.

Why this gate exists: AFFAIRES/Demande pagespec declared FK fields targeting
`TypeAudit`, `TypeTravaux`, `Client` (all in `referentiels`). The orchestrator
ran Phase 3a on `affaires/demande` only — the upstream `referentiels` module
was never re-scaffolded, the generated `Demande*Page.tsx` imported hooks that
didn't exist on disk, and TS2305 « no exported member » fired 5 times per page.

**Procedure**:

1. **Collect FK targets**. For every entity in the current module:
   - Read `<MODULE>/entité.md` → parse `**Relations**` lines (e.g.
     `Demande *→1 TypeAudit (FK TypeAuditId, restrict)`) → extract the FK
     target entity name.
   - Also scan `<MODULE>/pagespecs/<Entity>.<view>.md` for FK fields whose
     `fkTo` block points at another module.
   - Resolve the target module by reading `.smartstack/ba/<APP>/<targetModule>/entité.md`
     and matching the entity name. If the target lives in the SAME module, skip —
     it's covered by the per-entity loop. If the target lives in **another module**
     of the SAME app, add to `upstreamDeps[]`. Cross-app FKs are out of scope
     (and would trigger a separate audit).

2. **Check on disk**. For each `(targetModule, targetEntity)` in `upstreamDeps`:
   - `targetLower = camelCase(targetEntity)` (e.g. `TypeAudit → typeAudit`)
   - Expected hook path:
     `src/web/<appCode>-web/src/features/<targetModule>/<targetLower>/hooks/use<targetEntity>.ts`
   - If the file exists AND exports `use<targetEntity>Lookup`, mark as `present`.
   - Otherwise, mark as `missing` — needs upstream scaffolding.

3. **Auto-scaffold missing upstream**. For each `missing` entry:
   - Invoke `scaffold-api-client` with a minimal spec on the target entity
     (parse its `entité.md` for fields). The lookup hook + service is the
     primary deliverable; the rest is incidental but harmless.
   - Invoke `scaffold-component` if any page on the current module navigates
     to the upstream entity (e.g. via `routes.<targetSection>.detail(...)`).
   - Invoke `scaffold-routes` to wire the upstream URL helpers.
   - Persist the decision in `<MODULE>/_audit/cross-module-deps.md` for traceability.

4. **Heal vs defer**. If auto-scaffolding fails (target entity missing from BA,
   ambiguous match, etc.), emit failure kind `fk.cross-module-missing` → the
   auto-healing protocol retries with a wider context window. If retry budget
   exhausts, **defer** (blocker `fk.cross-module-missing`, high): best-effort
   skip/guard the unresolved import so the rest of the page compiles;
   `userAction: run /ba-develop on {targetModule} first then retry`. Continue.

**Why this is upstream-not-per-entity**: the FK deps are KNOWN before the
per-entity loop starts. Running them in parallel with the loop creates a race
where Demande's agent imports `useTypeAudit` while another agent is still
generating it — silent partial scaffolding. The pre-flight is sequential by
design: scaffold all upstream first, then fan out.

## Phase 3a per-entity loop — audit-or-regenerate

Each per-feature subagent runs this loop for ITS entity:

0. **Detect sub-resource nesting** (read `entité.md` ONCE before invoking
   any scaffolder). When the entity is a sub-resource — its `**Relations**`
   line carries a `*→1 <Parent> (FK <ParentId> ...)` to a same-module entity
   AND the controller URL nests under that parent — populate the
   `parentPath` + `parentIdParam` fields BEFORE invoking `scaffold-api-client`
   AND `scaffold-routes`:

   - `parentPath`: URL template containing the literal `{parentId}` placeholder,
     matching what the backend controller emits. Read the controller file
     `src/<Ns>.Api/Controllers/<Module>/<E>sController.cs` and look at the
     class-level `[Route("…")]` attribute. Examples:
     ```
     [Route("api/gaf/referentiels/sites/{siteId:guid}/rues")]
     → parentPath: "/api/gaf/referentiels/sites/{parentId}/rues"

     [Route("api/gaf/referentiels/types-affaire/{typeId:guid}/qualificatifs/audit")]
     → parentPath: "/api/gaf/referentiels/types-affaire/{parentId}/qualificatifs/audit"
     ```
     Replace the backend's `{xxxId:guid}` constraint with the literal token
     `{parentId}` — the api-client's URL builder substitutes it at runtime
     using the `parentIdParam` value as the variable name in emitted JS.

   - `parentIdParam`: a domain-readable camelCase identifier used in the
     generated TS signatures. Derive from the parent entity name:
     `Site → siteId`, `TypeAffaire → typeAffaireId`. NEVER pass the generic
     `parentId` literal — generated code reads better as
     `useRues(siteId: string)` than `useRues(parentId: string)`.

   - `parentSection` (for `scaffold-routes`): the parent's section, kebab-case,
     read from the parent entity's `index.md` (e.g. `sites`, `types-affaire`).
     Drives the 4-segment componentKey + nested URL + nested page path.

   For flat (non-nested) entities, leave all three undefined — the CLIs emit
   their legacy flat URLs and skip the parent-arg plumbing.

   **REFERENTIELS sub-resources detected at this step**:
   - Rue (parent Site, FK SiteId) → `parentPath: /api/gaf/referentiels/sites/{parentId}/rues`, `parentIdParam: siteId`, `parentSection: sites`
   - QualificatifTypeAudit (parent TypeAffaire, FK TypeAffaireId) → `parentPath: /api/gaf/referentiels/types-affaire/{parentId}/qualificatifs/audit`, `parentIdParam: typeAffaireId`, `parentSection: types-affaire`
   - QualificatifTypeTravaux (parent TypeAffaire) → same as above, suffix `/qualificatifs/travaux`

   Sub-resource detection is incompatible with `useScreens: true` — the screen
   stratum has no nested URL convention. If the spec would set both, the
   api-client throws at runtime. Reject the combination upstream by setting
   `useScreens: false` for sub-resources (the screen stratum will be invoked
   only on top-level entities).

1. If `src/features/{module}/{entityLower}/` does not exist, invoke
   `scaffold-api-client --spec '<single entity>'`. The `<single entity>` JSON
   MUST follow the strata detection contract spelled out below — the subagent
   stats `Controllers/{Module}/Screens/{EntityPlural}ScreenController.cs` and
   sets `useScreens` + `screenColumns` accordingly. When sub-resource detection
   in step 0 set `parentPath` / `parentIdParam`, include them in the spec — the
   CLI emits a parameterised `API_PATH = (siteId: string) =>` instead of the
   flat constant, and every method/hook takes `siteId` as its first argument.
2. For each view in entity.views (order: list → detail → form → dashboard):
   a. `target = src/pages/{appLower}/{module}/{section}/{Entity}{View}Page.tsx`
   b. **Skip rule** (diff-aware on re-runs — F4.1):
      - On a re-run, the orchestrator's Phase 3 pre-entry gate already computed
        the regenerate set = spec-diff (`compute-page-diff`) ∪ disk-drift
        (`validate-page` on the `unchanged` set). A view whose page key
        (`{Entity}.{view}`) is **not** in that set — i.e. its pagespec is
        unchanged AND its `.tsx` is valid on disk — was filtered out upstream
        and never reaches this loop. This is the perf win: an unchanged module
        re-runs in ~15 s, not ~3 min. On a first run (no `.run-snapshot.json`)
        the set is "all views" → regenerate everything.
      - A view that IS in the regenerate set is NEVER view-skipped: a present
        pagespec carries data the validator can't see (i18n catalogue, column
        hints, action permissions), so always re-invoke `scaffold-component`.
      - No pagespec, target exists → run `validate-page`. Success → continue.
   c. Build the spec for `scaffold-component`:
      - Read `<MODULE>/pagespecs/{Entity}.{view}.md`, extract the fenced JSON
        → `pageSpec`. Match by file name (`entity === Entity, view === currentView`).
      - **i18n labels — pagespec is the source of truth**. The pageSpec's
        `i18nKeys` field carries the business-translated labels per locale
        (`{ fr: {...}, en: {...}, it: {...}, de: {...} }`). scaffold-component
        reads `pageSpec.i18nKeys[locale]` directly when present and falls back
        to `humanize(entity.name)` only when the pagespec is absent. Phase 3a
        does NOT transform these — pass the pageSpec verbatim. If a page
        shipped with raw `humanize(name)` outputs (`"TypeAffaire"` instead of
        `"Types d'affaire"`), the root cause is upstream: `ba-create-prd`
        wrote a thin `i18nKeys` block. Fix it there, NOT in Phase 3a — the
        derivation pipeline ALREADY threads the labels through correctly when
        they exist in the pagespec.
      - **labelKey case (Fix #9, 2026-05-27)** — column / filter `labelKey`s
        can ship from the PRD in PascalCase (`list.columns.Code`,
        `list.filters.EstActif`). scaffold-component now normalises the leaf
        to camelCase symmetrically with the JSON catalogue via
        `normalizeI18nKey()` — both the JSON `list.columns.code` and the TSX
        `t('contact.list.columns.code')` end up matching. No Phase 3a action
        needed; this is purely defense in depth in the CLI. Action labels
        (`list.create`, `list.toggle-actif`) stay verbatim because their
        parent is not a field-parent. Idempotent: camelCase leaves are
        unaffected.
      - **Enriched mode** (pagespec found): `{ module, appCode, entity, section,
        views: ["{view}"], fields, projectPath, pageSpec, prdContext }`.
      - **Legacy mode** (no pagespec): `{ module, appCode, entity, section,
        views: ["{view}"], fields, projectPath }` — derive `fields` from `entité.md`.
      - **FK resolution** (powers `<EntityLookup>`): for each field whose type
        ∈ {guid, uuid} AND name ends with `Id`, look up the matching
        `Rel: ... *→1 {Target} (FK {ThisFieldName} ...)` line in the entity's
        `entité.md`. If found, enrich with `fkTo: { entity, module }`. If the
        FK is cross-module (bare Guid + "FK cross-module vers {APP}/{MODULE}.{Target}"
        description), parse and set `module` accordingly. Otherwise warn — audit
        DEV-UI-022 will surface it as BLOCKING on the next pass.
      - **Enum resolution** (powers `<EnumSelect>` / `<MultiSelect>`): for each
        non-FK field whose `entité.md` attribute declares a fixed value set (an
        enumeration — e.g. `statut : enum(Open, InProgress, Closed)`, a `[lookup]`
        with inline values, or a "valeurs possibles : …" note), enrich the field
        with `options: [{ value, label }, …]` (value = the persisted code, label =
        the display text; use the pagespec i18n when available, else the BA label).
        If the attribute is multi-valued (a collection / "plusieurs" / `*` set —
        e.g. `compétences : enum[]`), also set `multiple: true`. scaffold-component
        then emits `<EnumSelect>` (single) or `<MultiSelect>` (multi) instead of a
        free-text `<input>`. When unresolved, the field falls back to `<input>` and
        audit DEV-UI-023 surfaces it as a WARNING (non-blocking) on the next pass.
      - **Date controls** (powers `<DateInput>`): no extra resolution — a field
        whose type ∈ {date, datetime} is rendered as the theme-compliant
        `<DateInput>` automatically by scaffold-component (never a native
        `<input type="date">`). DateInput / EnumSelect / MultiSelect all ship via
        scaffold-ui-primitives (run before 3a) and read `--radius-input`, so the
        project's "square vs rounded" choice (scaffold-theme `radiusControl`) applies.
      - Invoke `scaffold-component --spec '<JSON>'`.

      **Concrete example — enriched spec for a form view:**
      ```bash
      npx --prefer-offline tsx skills/development/frontend/component/cli/scaffold-component/index.ts --spec '{
        "module": "referentiels",
        "appCode": "myapp",
        "entity": "TypeAffaire",
        "section": "types-affaire",
        "views": ["form"],
        "fields": [
          {"name": "Code", "type": "string", "required": true},
          {"name": "Libelle", "type": "string", "required": true},
          {"name": "EstActif", "type": "boolean", "required": true}
        ],
        "projectPath": "/path/to/web/myapp-web",
        "pageSpec": { <the entire fenced JSON from pagespecs/TypeAffaire.form.md> }
      }'
      ```
      The `pageSpec` value is the ENTIRE parsed JSON block from the pagespec file
      (including `i18nKeys`, `columns`, `actions`, `fields`, `permission`, etc.).
      Copy it verbatim — do NOT extract or transform individual fields.

   d. Run `validate-page` on the freshly-written file.
   e. Success → continue. Failed (missing hook) → re-invoke `scaffold-api-client`
      first (e.g. with `hasDashboard: true` if `useDashboard{Entity}` is missing).
   f. Re-invoke `scaffold-component` with `priorErrors[]` from validate-page
      violations + run `validate-page` once more. Keep `pageSpec` on retries.
   g. Still failed → return a structured failure to the orchestrator
      (`failureKind: "validate-page.persistent"`); the auto-healing protocol
      takes over (typical fix: re-invoke `scaffold-api-client` for the entity,
      then retry this page).
3. After all pages succeed, aggregate any pageSpec entries where
   `needsRefinement === true` into a `refinementSummary[]` — non-blocking but
   surfaced in the run summary.
4. **After ALL entities succeed and the Phase 3 build gate is green** (F4.1),
   write the run baseline so the next re-run diffs against it:
   ```bash
   npx --prefer-offline tsx skills/ba-develop/cli/update-snapshot/index.ts \
     --spec '{"moduleRoot":"<.smartstack/ba/{appCode}/{moduleCode}>"}'
   ```
   Snapshot ONLY on success — a failed run must not poison the baseline. This
   single write makes the NEXT `compute-page-diff` see today's pages as
   `unchanged`, so an untouched module skips Phase 3a entirely.

The eight validate-page rules: imports-resolve, pagetemplate-wrapping,
useparams-null-check, permissionguard-on-mutations, no-local-usepermissions,
i18n-keys-resolve, hook-imports-exist, permission-keys-no-appcode (see
`frontend-component` SKILL.md).

## Phase 3a — derive api-client + component specs

Each per-entity feature subagent constructs its scaffolder inputs:

1. **Re-read pagespecs** (never re-derive — they are the source of truth).
2. **Filter customActions** down to API-bound, non-CRUD actions:
   - DROP every action with `kind: "navigate"` (those become inline
     `navigate(targetRoute)` buttons in scaffold-component; scaffold-api-client
     emits NOTHING for them — the historical `POST /{id}/open` phantom
     endpoint class of 404s is the bug this filter prevents).
   - DROP every action whose `code` is in `STANDARD_CRUD_CODES`.
3. **De-duplicate** by `(scope, endpoint, httpMethod)`.
4. **Derive `ApiCustomAction[]`** for `scaffold-api-client` (2026-05-27 contract):
   - `code`: `pageAction.code` — the **business identifier** (camelCase / kebab).
     Drives TS naming: service member = `toCamel(code)`, hook =
     `use${PascalCase(code)}${Entity}`. NEVER use `endpoint` for naming;
     `endpoint` is for URLs only.
   - `endpoint`: `pageAction.endpoint` (when present and ≠ `code`) — the
     **URL segment**, kebab-case, MUST equal the backend route attribute. When
     omitted, the api-client falls back to `code` as the URL segment.
     This decoupling fixes the historical
     `code: 'toggle-actif' → URL /toggle-actif vs controller [HttpPost("activate")]`
     class of 404s: BA pagespecs keep the business name, controllers keep their
     route name, the CLI emits both correctly.
   - `kind`: pass `'api'` explicitly (or omit; defaults to `'api'`). Navigate
     actions never reach this list — they were filtered out in step 2.
   - `scope`, `payloadType`, `responseType` (default `'void'`), `httpMethod`
     (lowercase).

   **Example — REFERENTIELS Employe**:
   ```jsonc
   // pagespec.actions[]
   { "code": "toggle-actif", "endpoint": "activate", "scope": "row", "kind": "api", "httpMethod": "POST" }
   // → ApiCustomAction[]
   { "code": "toggle-actif", "endpoint": "activate", "scope": "row", "kind": "api", "httpMethod": "post", "responseType": "void" }
   // → TS: useToggleActifEmploye() calls POST /api/.../{id}/activate
   ```

   **When the controller route diverges from the pagespec code** (legacy
   backend names): populate `endpoint` by reading
   `src/<Ns>.Api/Controllers/<Module>/<E>sController.cs` and matching the
   `[HttpPost("…")]` attribute next to the handler whose name PascalCases the
   business action. The Phase 2a gate's cross-check (see § Phase 2a) already
   verifies the controller carries the expected route — that's where the
   `endpoint` value comes from.
5. **Detect screen-driven mode** (Wave F2) — applies ONLY to top-level entities.
   When the per-entity loop's step 0 detected a sub-resource (parentPath set),
   skip this step entirely and pass `useScreens: false` — the screen stratum
   has no nested URL convention, and the api-client rejects the combination
   at runtime. Otherwise, stat
   `src/<Ns>.Api/Controllers/<Module>/Screens/{EntityPlural}ScreenController.cs`:
   - **Absent** → `useScreens: false`, omit `screenColumns`. The api-client
     emits `/api/v1/integration/{plural-kebab}` paths — the SAME route the
     integration controller emits at the class level. Zero drift possible:
     both sides import `buildIntegrationRoute` from `lib/url-conventions.ts`.
     Do NOT pass `apiBasePath` — that free-form template was removed from the
     api-client schema on 2026-05-27 (Wave 1 of the URL alignment) because it
     was the source of orphan `/api/{appCode}/{module}/{section}/...` URLs
     that no backend scaffolder ever served.
   - **Present** → `useScreens: true` AND collect `screenColumns` from pagespecs:
     ```
     screenColumns: {
       list:   pagespecsForEntity.find(p => p.view === 'list')?.columns ?? [],
       detail: pagespecsForEntity.find(p => p.view === 'detail')?.columns ?? [],
     }
     ```
     Pass each column **verbatim** as `{ key, formatHint }`. The api-client's
     `tsTypeForColumn` mirrors scaffold-screen-controller's `dotnetTypeFor`
     byte-for-byte — that's what makes the wire match.
6. **Pass `pageSpec.actions[]` UNCHANGED** to `scaffold-component`. The
   component generator branches on `kind`:
   - `kind: "api"` → imports `use<…>${Entity}` hook + declares mutation + emits
     button click handler calling `mutation.mutateAsync(...)`.
   - `kind: "navigate"` → emits `navigate(<targetRoute>)` directly; no hook,
     no mutation; synchronous handler.

The derivation is mechanical — the subagent does not "invent" code names or
routes. It transcribes `pageSpec.endpoint` verbatim into each scaffolder.

**Why detect-on-disk rather than passing a flag**: a single module can be
mid-migration (some entities have screen controllers, others don't). Resolving
per entity keeps the orchestrator decoupled from migration state.

## Custom-action propagation

The pagespec `actions[]` array carries every non-CRUD button. Canonical Zod
schema: `templates/skills/lib/page-spec-actions.ts` (`PageCustomActionSchema`).
Each entry has a `kind` discriminator:

- `kind: "api"` → backend endpoint + service method + React hook + button.
  The `code` is the **business identifier** (drives TS naming). The `endpoint`
  is the **URL segment** used verbatim by the controller's
  `[HttpVerb("<endpoint>")]` attribute AND the api-client's
  `apiClient.<verb>('/api/.../<endpoint>')` call. When `code === endpoint`
  (the common case), `endpoint` MAY be omitted; the api-client falls back to
  `code`. When they differ (legacy backend routes like `toggle-actif → activate`,
  `sync-from-pce → sync-from-proconcept`), both MUST be provided.
- `kind: "navigate"` → ONLY a button calling `navigate(targetRoute)`. No
  endpoint, no hook, no axios call. Phase 3a's derivation step 2 filters these
  out BEFORE the customActions array reaches `scaffold-api-client`; the CLI
  also self-defends with `(entity.customActions ?? []).filter(a => a.kind !== 'navigate')`
  at the start of its generation loop — both layers prevent the 2026-05-27
  REFERENTIELS post-mortem class of phantom `POST /{id}/open` endpoints.

### How Phase 3a populates `kind`

When constructing the `ApiCustomAction[]` array:

1. Read `pageAction.kind` from the pagespec. When present, pass it through verbatim.
2. When ABSENT (legacy pagespecs that predate the kind discriminator), infer:
   - `targetScreen` OR `targetRoute` is set → `kind: 'navigate'` (and the action
     is then filtered out before reaching the api-client).
   - Otherwise → `kind: 'api'` (default).
3. Never invent a navigate action without `targetRoute`/`targetScreen` — the
   pagespec schema enforces the discriminator (see
   `PageCustomActionSchema.superRefine`).

## Domain notes

> **Test generation is mandatory.** Every phase 1+ invokes `scaffold-tests`
> after its scaffolds. The gate cannot be passed with `testsRun == 0`; Golden
> Rule #8. Phase 0 is exempt (Core providers are deterministic).

> **Frontend aggregator — deterministic CLI**. Sub-phase 3c calls
> `aggregate-component-registry`; the Frontend subagent must NOT hand-write
> `componentRegistry.generated.ts`.

> **Computed attributes — end-to-end cascade**. When the BA `data-model`
> declares an attribute with non-null `formula`:
>   - Phase 1: `scaffold-entity` SKIPS the field on Domain + EF Config.
>   - Phase 2: `scaffold-business` includes it in ListDto/DetailDto, EXCLUDES
>     from Create/Update Dtos+Validators, injects formula into LINQ projections
>     (one SQL query, no N+1).
>   - Phase 3: `scaffold-component` excludes from Form, renders read-only on
>     List + Detail. Audit DEV-UI-014 rejects stub helpers.

> **Relationships — ALL references become real FK constraints; `core` is gated by the V1 whitelist**. When
> `entité.md` declares a `**Relations**` line (e.g.
> `Employee *→1 Department — FK DepartmentId, scope core`), Phase 1 MUST forward
> it to `scaffold-entity` as a `relations[]` entry — cardinality to `type`
> (`many-to-one`/`one-to-one`/`one-to-many`), BA cascade to `onDelete`
> (`restrict`/`cascade`/`set-null`/`no-action`), and the scope to `targetScope`
> (`same-module` | `cross-module` | `core`). For `core`, the target MUST be one
> of the V1 whitelist entities — **User, Role, Tenant, Company, Department,
> JobTitle, Office, Language, Group**. Anything else (sessions, tokens, navigation
> rows, AI/Workflow/Support internals, …) is rejected at validation; use
> `ICoreDataService` instead or open an issue per
> `docs/extensions/whitelist-evolution.md`. The generator emits a REAL FK with
> navigation property for `same-module` AND for `core` whitelist (resolved against
> the base class `SmartStackExtensionDbContext`), and a no-navigation typed FK for
> `cross-module` — never a bare `Guid`, never a locally-duplicated principal stub.
> The per-entity `TenantId` FK is emitted automatically from `tenantMode` against
> the base-class `Tenant` DbSet. The ONLY non-FK `*Id` columns are the
> identity/audit allowlist (`CreatedByUserId`, … — `lib/fk-allowlist.ts`;
> **`TenantId` is NOT exempt**). Audit DEV-DAT-008 is BLOCKING.

> **Table prefix — derive `domainPrefix` from `entité.md`, never `ext`**. Each
> entity in `entité.md` carries a `**Préfixe table**` (`aff_`, `cli_`, `ref_`…).
> Phase 1 maps it to the `scaffold-entity` `domainPrefix` by stripping the
> trailing underscore (`aff_` → `aff`), so the table is `{domainPrefix}_{Plural}`
> in PascalCase (`aff_Demandes`) — aligned with SmartStack.app (`support_Tickets`,
> `ai_ApiKeys`). NEVER pass `ext`: that is the *migration* prefix of the
> `extensions` schema (`efcore/migration-name`), not a business-domain table
> prefix. The schema (`schemaTarget: "extensions"`) is the isolation boundary;
> the per-entity `domainPrefix` is the functional domain. Omit `pluralName` to
> let the generator pluralize consistently — do NOT pass the singular as
> `pluralName` (that produced the `ext_Demande` vs `ext_DossierValidations` mix).

> **Columns are PascalCase — `scaffold-entity` emits NO `HasColumnName`**. EF
> Core's default mapping (property → column) is PascalCase, exactly like
> SmartStack.app (`FirstName`, `CreatedAt`, `ClientId`). Phase 1 must not expect
> or inject snake_case column names anywhere.
