# `/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 ([NavRoute] → served at
                                   /api/{module}/{section}, 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 --no-restore` (screen acceptance
                                   tests run ONCE at the Phase 2b gate, NOT per
                                   subagent). 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               → seven ordered sub-phases (3.0 → 3.1 → 3a → 3b → 3c → 3.5 → 3d);
                                   3a fans out ONE subagent per feature/entity; 3.1 fans
                                   out ONE judgment subagent per module; the other
                                   sub-phases run on the orchestrator.
    ⚠️ CRITICAL — `projectPath` for EVERY frontend scaffold = the WEB APP directory
       (the React/Vite app: `<projectPath>/<webRoot>`, e.g. `…/web/<app>-web`),
       NEVER the project/repo root. Resolve it deterministically — the configured
       `webRoot`, or `findWebProjectFolder()` (lib/detector.ts) — and pass THAT.
       Frontend scaffolds write to `<projectPath>/src/…`; the repo root would make
       them write into the BACKEND's `src/`. The scaffolds now HARD-FAIL
       (`assertWebProjectRoot`) when handed a .NET/backend root → on that error,
       FIX the projectPath (point it at the web app) and re-run. NEVER `rm`/delete
       to "clean up wrong-location files".
    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
                          ONLY (idempotent; honors `// @customised` markers). It
                          no longer emits AppShell/Sidebar: the app chrome —
                          desktop header + sidebar AND the mobile "descente par
                          paliers" shell with its transverse bottom bar — is
                          rendered by @atlashub/smartstack around
                          `<DynamicRouter />`. Nothing to wrap, nothing to mount.
                          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.
                          scaffold-frontend-auth (development/frontend/auth) writes
                          src/business/auth/useAuth.ts (adapter over the package
                          useAuth adding the strip-leading-appCode permission
                          match) and src/components/auth/PermissionGuard.tsx.
                          Every generated page imports PermissionGuard, and
                          RowActionsMenu + the FormPage "Me" shortcut import the
                          useAuth adapter — WITHOUT this scaffold those are TS2307
                          dangling imports (nothing else emits the files).
                          NOTE its projectPath is the PROJECT ROOT (it derives
                          web/{appCode}-web itself from appCode), unlike the other
                          3.0 scaffolds which take the web root. MUST run before 3a.
    3.1 — UI-design pre-pass  Form-design + lifecycle judgment, written into the
                     pagespecs BEFORE the 3a fan-out (the 3a subagents have NO
                     Edit/Write, so the overlay must already exist when pages are
                     scaffolded). TWO INDEPENDENT LEGS with their OWN idempotency
                     keys — an existing overlay never blocks the lifecycle leg:
                     · form-design leg: SKIP every pagespec whose machine block
                       already contains an `"uiDesign"` key;
                     · lifecycle leg: SKIP every pagespec whose machine block
                       already contains a `"lifecycle"` key — it runs EVEN when
                       `"uiDesign"` exists (modules judged before the lifecycle
                       uplift get their phases on the next run).
                     IDEMPOTENT, never rewrite an existing judgment (regens
                     replay them via scaffold-component's `pickUiDesignOverlay` /
                     `resolveLifecycle`).
                     Orchestrator: list `<MODULE>/pagespecs/*.form.md`; FIRST run
                     the deterministic lifecycle backfill once per module —
                     `skills/business-analyse/create-prd/cli/derive-lifecycle`
                     (`mode:"derive"`, pagespecDir + moduleRoot): it writes the
                     blocks whose anchors are already authored (status enum +
                     workflow actions) and reports the `needs-judgment` remainder.
                     Then, for pagespecs still lacking a leg: ONE subagent PER
                     MODULE (all its form pagespecs together — they share the
                     entité.md context), loaded with
                     `ui-design/references/design-rubric.md`, the form pagespecs,
                     the module's `entité.md` AND `règles-métier.md` (the Flow
                     lines are the §6 status graph). **allowed-tools:
                     [Read, Glob, Grep, Bash]**; its ONLY allowed Bash invocation
                     is `skills/ui-design/cli/apply-form-directives` — one call
                     per pagespec, persisting the namespaced `uiDesign` overlay
                     (`currentUserFk` / an ordered `sections[]` / date
                     `control`+`dateBounds` / `fullWidth` / `editMode` /
                     `order`) and/or the FIRST-ORDER `lifecycle` block (rubric
                     §6 — the CLI writes it additively, never over an existing
                     block). Judgment discipline: when the pagespec carries
                     `sections[]` (first-order categories) or form `tabs[]`
                     (the BA `Onglet` groups), that grouping is an already-paid
                     human judgment — derive 1:1, NEVER invent a different
                     grouping over an authored one (the judgment shrinks to
                     intra-section order, date/Me/layout directives, missing
                     labels); a derive-lifecycle-written block is the same —
                     confirm/complete, never re-group; emit a directive ONLY
                     where it improves the page (the renderer's defaults +
                     odd-run balancing already handle the rest — see the rubric).
                     Return contract: `{ formsSeen, overlaysWritten,
                     lifecyclesWritten, skippedExisting, lifecycleSkipped,
                     failures: [{pagespec, reason}] }`.
                     NEVER-HALT: a subagent failure or an apply-form-directives /
                     derive-lifecycle error becomes a `ui-design.skipped` NOTE in
                     the final report — not a blocker, no retry loop; a form
                     without an overlay renders the plain deterministic output
                     (still balanced), a form without a lifecycle renders every
                     field (the legacy surface — safe).
                     Pipeline effect: writing either key CHANGES the pagespec →
                     `compute-page-diff` classifies the page as spec-drift → 3a
                     regenerates it. EXPECTED and LEGITIMATE (see gates.md):
                     the judgment is paid once, then replayed on every regen.
    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
                     emits a self-merged full catalogue [floor < existing < PRD,
                     siblings verbatim] and writes it atomically [tmp+rename], so
                     every observed file is complete and a later view's floor can
                     no longer revert an earlier view's PRD text; parallel
                     same-module runs remain FORBIDDEN — atomic rename fixes torn
                     writes, not lost updates: two concurrent read-merge-write
                     cycles can still drop each other's newly added entity).
                     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/{app}-{module}Registry.ts and
                     src/extensions/{app}-{module}Routes.ts per module (app-scoped naming,
                     lib/app-classification.extensionsModuleId).
                     The entities[] spec is built FROM THE PAGESPECS, deterministically —
                     pass each entity's pagespec views VERBATIM as `views`
                     (`["list","detail","form"]` — scaffold-routes expands the
                     `form` alias into create+edit itself); do NOT re-map to
                     routes-native views and NEVER rely on the schema default
                     (it has no `edit`, which is how every list page's
                     routes.{x}.edit(...) became TS2339). And
                     NEVER invent a route slug: for each entity of the module,
                     `section = pagespec.routeFamily ?? pagespec.section` and
                     `parentSection = pagespec.routeParent` (fields authored by
                     ba-create-prd; on the sub-view pattern — several list entities
                     under one menu section — every satellite pagespec carries them).
                     A slug decided here and recorded nowhere is exactly what
                     mis-routed every 360 related tab onto the porteur's pages
                     (AtlasHub, 27 hand-fixed fiches): if a satellite pagespec lacks
                     `routeFamily`, STOP and fix the pagespec (re-run /ba-create-prd
                     or stamp the field), do not improvise a slug.
                     UNIFIED FICHE (lib/edit-surface): an entity with BOTH detail
                     and form views edits IN PLACE on its DetailPage — the `.edit`
                     key mounts the DetailPage (sections opened on arrival), the
                     FormPage serves `.create` only. Pass `directEdit: true` on the
                     entity ONLY when the form pagespec (or its uiDesign overlay)
                     carries `editExperience`/`editMode: 'direct'` — the same
                     signal scaffold-component reads, so the two CLIs derive the
                     same surface and can never disagree on what `/edit` mounts.
                     CLOSING STEP — nav resources for every routeFamily (§24,
                     MANDATORY when any satellite pagespec exists). The keys
                     scaffold-routes just registered are only REACHABLE if
                     DynamicRouter can build their route: 4th-level segments
                     resolve from `section.resources` (the seeded
                     core.nav_Resources) or an IMPLICIT_SUFFIX — anything else
                     silently redirects to /applications (ProtectedCatchAll).
                     The BA menu deliberately declares NO resource for sibling
                     listings (ba-create-menu doctrine), so the seed must be
                     completed FROM THE PAGESPECS:
                       1. `npx --prefer-offline tsx skills/ba-develop/cli/derive-nav-resources/index.ts \
                            --spec '{"moduleRoot":".smartstack/ba/<APP>/<MODULE>"}'`
                          → one MenuResourceInput per distinct (routeParent,
                          routeFamily), with route + componentKey on the exact
                          scaffold-routes shape.
                       2. Merge `report.resources` into the Phase 0 core-seed
                          spec's `resources[]` and RE-INVOKE scaffold-core-seed —
                          an additive second pass; the state diff keeps it
                          idempotent (Phase 0 itself does NOT move: it still
                          runs first for nav/roles/permissions).
                     Gate: audit-dev-frontend DEV-UI-046 (err) — every registry
                     componentKey resolves to an implicit suffix or a seeded
                     nav node.
    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';`.
                     The aggregator ALSO emits src/extensions/moduleResources.generated.ts:
                     it SCANS src/i18n/locales/<locale>/*.json and registers EVERY bundle
                     (basename = namespace, including common; vitrine/login excluded — their
                     own generated files register them) through addClientResources — no
                     coupling to registry filenames — and imports it at the END of
                     componentRegistry.generated.ts, so it runs AFTER the SDK's i18next init
                     (main.tsx loads the aggregator after <SmartStackProvider>). This is what
                     stops business namespaces from rendering as raw keys: the SDK init
                     replaces the resource store, so a namespace only survives if registered
                     via addClientResources after it. Do NOT register module namespaces in
                     src/i18n/index.ts (it loads before the SDK and is clobbered — a
                     parallel i18next.init() there is DEV-UI-035, hard error). Business
                     pages on disk with ZERO locale bundle = hard error (exit 1), no longer
                     a silent warning.
    3.5 — Audit-apply  audit-dev-frontend --mode apply (DEV-UI-001..046; 028 = every business
                       i18n namespace with a locale bundle is registered in
                       moduleResources.generated.ts — re-run the aggregator if it flags one;
                       022/033 = no FK ever reaches the user as a raw Guid — form input,
                       list column, free-text FK filter, detail <dd>;
                       038 = module-wide i18n catalogue completeness — bare t() keys
                       resolve to STRINGS in all 4 locales (an object = label/children
                       collision), fr/en/it/de key sets identical per namespace;
                       heal = re-invoke scaffold-component for the offending entity;
                       036 = dashboards inherit the theme — no colour literal in a dashboard
                       page/primitive, no recharts outside ChartCard, and the theme carries
                       --dataviz-1..8 (else charts silently fall back off-brand))
                       THEN ui-polish --mode apply (CSS tokens, design rules).
                       Re-runs even when 3a-3c reported filesChanged: 0.
                       WRITE the report to _audit/dev-frontend-{module}.md (same
                       discipline as dev-api/dev-wire — the test-RH run left NO trace
                       of this step, which is how a BLOCKING 022 shipped unseen);
                       err findings BLOCK the phase gate.
    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 --no-restore
                                   --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 + check the permission floor per module (each section carries its 7 floor
rows in `*CorePermissionsSeedDataProvider.cs`; a 3-field pre-floor tuple or a
missing floor row → 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`)**: FIRST, ensure the project's `.gitignore` contains a
  `.smartstack/runs/` line (append if missing) — the run artefacts
  (`heal.log.json`, `blockers.json`) live under
  `.smartstack/runs/<APP>/<MODULE>/`, OUTSIDE the BA tree (see
  auto-healing.md; `.smartstack/ba/` is specification only). Then builds
  CoreSeedSpec from `index.md` (nav) + `<APP>/acteur.md`
  (roles) + `<MODULE>/rbac.md` (permissions), invokes `scaffold-core-seed` via Bash.
  **RESOURCE nodes seed too**: a section holding sibling resource nodes in the
  menu tree passes each one in the section's `resources[]`
  (`MenuResourceInput`, `level: 'resource'` — the CLI supports it; skipping
  them is how eight referential leaves shipped reachable only by typed URL).
  The menu tree usually declares NONE (ba-create-menu doctrine: sibling
  business listings are not resources) — that is expected here: the satellite
  resources are derived from the pagespecs' `routeFamily` and seeded by the
  Phase 3b CLOSING STEP (`derive-nav-resources` + additive scaffold-core-seed
  re-run), because the routeFamily values do not exist yet in Phase 0.
  Such a section ALSO needs its `section-home` screen (SCR-020) so the seeded
  root key `{app}.{module}.{section}` resolves a registered page — run-smoke's
  registry↔menu coverage fails otherwise.
  When a node's `index.md` anchor carries a `previousCodes=` attribute (written by
  `/ba-reconcile-menu` after a validated rename), transport it into the matching
  `navigation[].previousCodes` — it is the ONLY channel through which the prod
  reconciliation (derive-seed-delta) learns a rename instead of doing
  deactivate+insert. Permission `previousPaths` are derived automatically from
  the nav aliases when the CLI builds the state snapshot; only add explicit
  `previousPaths` for an ACTION segment rename.

  **RBAC transcription (v3.62 access lock + derived lookups + permission floor):**
  the matrices NEVER travel by hand. Run the deterministic transcription and
  paste its per-app fragments VERBATIM into the CoreSeedSpec slices:

  ```bash
  npx --prefer-offline tsx skills/business-analyse/create-rbac/cli/derive-rbac-grants/index.ts \
    --spec '{"baRoot":".smartstack/ba","mode":"derive"}'
  ```

  The envelope's `report.apps[]` carries, per application: `actors[]` (one
  role per actor — deterministic `slugifyRoleCode(label)` code, label as
  `name`, **Catégorie** already mapped to the RoleCategory enum via
  `role-taxonomy.md`), `permissions[]` (authored non-floor rows, producer-app
  attribution) and `rolePermissions[]` (human rows app-prefixed + derived
  `ba:rbac-derived-lookups` rows verbatim + the v3.62 grandfathered `.access`
  grants, attributed to the app that OWNS the actor's role — cross-app
  included). Rules the CLI already applies — do NOT re-apply them by hand:
  - **the FLOOR is NOT in the fragment** — `scaffold-core-seed` derives it
    from the nav tree it seeds (`lib/permission-actions.ts` FLOOR_BY_GRAIN);
    the `ba:rbac-floor` block of `rbac.md` is a human-review MIRROR, never an
    input.
  - a `needsResolution[]` entry (unknown actor, invalid path, role-code
    collision, matrix without acteur.md) is fixed in the BA docs and the CLI
    re-run — NEVER worked around by typing the row yourself; a non-empty
    `needsResolution` at the end of Phase 0 is a blocker.
  - after seeding, `"mode":"check"` + `"projectPath"` compares the state
    files to the fresh derivation in both directions — DEV-CORE-011. Its
    EXECUTOR is the Phase 0 gate itself (gates.md § Phase 0, check 3 runs the
    CLI); the `audit-dev-core` pack documents the rule but has no CLI and is
    not routed by any phase — never assume "the pack will catch it later".

  **Extension of a built-in platform app (the "extend `hr`" protocol):** when
  the BA app node's `## Contexte` carries the marker « Extension de
  l'application plateforme `<code>` » (written by `/ba-create-menu` § "extend,
  never duplicate"), the CoreSeedSpec slice MUST:
  - set `applications[].extendsBuiltinApp: { code, guid }` — code + GUID from
    `lib/platform-catalog.ts` (`getBuiltinApp(code).guid`; hr =
    `9cbeae29-772f-43b1-ac93-c56f2cb90921`); the slice `code` stays the
    PLATFORM code (`hr`) so permission paths root correctly;
  - emit NO `level: 'application'` navigation entry (the validator hard-rejects
    one in this mode) — only the client modules/sections/resources;
  - `build-spec.ts` handles both automatically when `MenuApplicationInput`
    carries `extendsBuiltin` — the generated providers then resolve the
    platform app by GUID (never by Code), so client modules attach to the
    existing `hr` row regardless of platform/client seed ordering, and the
    state snapshot never contains the platform app (derive-seed-delta cannot
    touch it). Roles/permissions/rolePermissions need no special handling.
- **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

### The backend is scaffolder-owned — confine Edit/Write to TODO bodies

The Phase 2a subagent has `Edit`/`Write` for ONE purpose only: the business-logic
pass — filling the `// TODO[BR-…]` / `// TODO[UC-…]` markers `scaffold-business`
left **inside `{Entity}Service` method bodies**, and adding the `RuleFor(...)`
validators those markers reference. **Everything structural is generated and
off-limits to hand-editing**: the controller (its routes, the paginated `GetAll`,
the `[HttpGet("lookup")]` endpoint), the DTOs, the Command/Query records, the
service class signature, and the `IExtensionsDbContext.Set<T>()` access pattern
all come 100% from `scaffold-controller` + `scaffold-business` and carry
`// @generated-by scaffold-controller`. NEVER rewrite the controller/service into
a different shape (e.g. a "pure MediatR + typed DbSet" rewrite) — that is the
exact drift that dropped `/lookup` and the paginated list across RH and broke
every lookup + table. `audit-dev-api DEV-API-016` BLOCKS the gate on that drift.
If a scaffolder cannot run for an entity, hard-fail the entity (skip its frontend
— coupling) and record the blocker; do not improvise the backend. See `SKILL.md`
§ ABSOLUTE RULE — the backend is scaffolder-owned.

### Lifecycle phase flags — the Create-honnête threading

When the entity's FORM pagespec carries a first-order `lifecycle` block
(canonical schema `lib/page-spec-lifecycle.ts` — written by create-prd, the
Phase 3.1 lifecycle leg or `derive-lifecycle`), set `phase: "<phaseKey>"` on the
matching derived fields of the `scaffold-entity`, `scaffold-business` AND
`scaffold-controller` specs — match by camelCase key against
`lifecycle.phases[].fields` (the OWNED lists ONLY: an entry that appears solely
in `requiredFields` gets NO phase — it stays a creation input, nullable in the
Create surface). Effect (all three generators enforce it): a phased field is
excluded from `Create{E}Dto`/`Command`, the Create validator, the dto→command
mapping and the entity factory, while `Update` keeps it — so a draft invoice can
never receive its `paymentDate` through Create, by construction. Fields NOT in
any phase are untouched (required + optional creation fields now BOTH ride
Create — the optional ones as nullable members). No pagespec lifecycle → no
`phase` flags → the legacy field lists, unchanged.

### Custom actions — DETERMINISTIC, never hand-derived

The custom-action arrays for `scaffold-controller` + `scaffold-business` are
produced by a CLI, NOT by reading the pagespecs and re-mapping fields yourself.
Hand-deriving them (the legacy prose) silently dropped actions — every generator
defaults `customActions` to `[]`, so an un-derived action vanished with **no
error**. That is the root cause of "page actions are not implemented". Run ONCE
per entity (or omit `entity` to derive the whole module at once):

```bash
npx --prefer-offline tsx skills/ba-develop/cli/derive-action-specs/index.ts \
  --spec '{"moduleRoot":"<.smartstack/ba/{appCode}/{moduleCode}>","entity":"<Entity>"}'
```

Take `report.entities[0]` and splice **verbatim** into the scaffolder specs —
do NOT transform any field:
- `entity.controller` → `scaffold-controller` `customActions`
- `entity.business`  → `scaffold-business`  `customActions`

The CLI reads every `<MODULE>/pagespecs/<Entity>.*.md`, validates each action
against `PageCustomActionSchema`, filters to `kind:"api"` non-CRUD, de-duplicates
by `(scope, endpoint, httpMethod)`, and applies the canonical projections in
`lib/page-spec-actions.ts` (controller `code` = the kebab `endpoint`; UPPER-case
`httpMethod`; `permissionAction` = last permission segment; business flattens
`workflowTransition` into `fromStatus[]` / `toStatus` / `flowParameters`). Those
projections are the single source of truth — the dev audits cross-check generated
code against the very same functions, so the wire can never drift.

> `kind:"navigate"` actions are surfaced in `report.entities[].navigate` for
> reference only — they have no backend/service/hook; scaffold-component renders
> them as inline `navigate(targetRoute)` buttons from the pageSpec (Phase 3a).

### Business rules — `scaffold-business.businessRules[]` (the leg that was missing)

The SAME un-derived-vanishes-with-no-error defect existed for the rules: every
generator defaults `businessRules` to `[]` and its validate only warns — a spec
without them emitted a basic-validation validator SILENTLY. Derive them
mechanically, never from memory:

1. The rule → entity binding is the pagespec channel:
   `create-prd/cli/derive-rule-links` (run at PRD time; re-run `--mode
   backfill` here if PRD-129 flagged missing links) filled each pagespec's
   `linkedBusinessRules[]`.
2. For EACH entity, `businessRules[]` = the module rules (parse of
   `règles-métier.md` — grammar in `lib/ba-rules-rows.ts`, docs at module AND
   section level) whose code appears in that entity's pagespecs'
   `linkedBusinessRules`, mapped FIELD-FOR-FIELD:

   | scaffold-business field | from règles-métier.md |
   |---|---|
   | `id` | the `BR-NNN` code |
   | `description` | `<title> — <Condition>` |
   | `expression` | `Expression` verbatim (omit when absent — the heuristic/TODO path takes over) |
   | `validExamples[]` | the `Cas valides` items |
   | `invalidExamples[]` | the `Cas invalides` items |
   | `severity` / `ruleType` | `Sévérité` / `Type` (transported — they ride the emitted `// BR-…` comment) |

3. The NET is deterministic either way: with the links transported, a rule you
   fail to derive here surfaces as **DEV-API-008 `untraced`** (no `// BR-…` in
   this module's files) and its missing test as **DEV-TEST-009** — the heal
   loop re-enters this derivation. An empty `businessRules[]` on an entity
   whose pagespecs LINK rules is never legitimate.

### Other per-entity inputs (still derived from the BA docs)

> **`applicationCode` — the `<App>` classification (REQUIRED on every backend +
> test spec)**. Pass `applicationCode` = the LOWERCASE BA `<APP>` folder name (the
> business application, e.g. `.smartstack/ba/CRM/…` → `"crm"`) to **scaffold-entity,
> scaffold-business, scaffold-controller, scaffold-tests**. It is the `<App>` folder +
> sub-namespace segment of every generated artifact (`…Domain/Entities/<App>/<Module>/`,
> `…Application/<App>/<Module>/…`, `…Api/Controllers/<App>/<Module>/`,
> `Tests/<App>/<Module>/…`). DISTINCT from `appCode`/`namespace`, which stay the .NET
> solution prefix (e.g. `"TestV2"`). The Zod schema REQUIRES it — a spec without it is
> rejected. (`scaffold-screen-controller` + `scaffold-tests-from-ac` already receive the
> business app as their `appCode`.)

> **`navRoute` — the controller's NavRoute key (REQUIRED on `scaffold-controller`)**.
> Pass `navRoute: "{module}.{section}"` — the kebab module + section codes joined by a
> dot (module `configuration`, section `types-clients` → `"configuration.types-clients"`).
> `scaffold-controller` emits it as `[NavRoute(...)]` (and NO `[Route]`); the platform
> serves the controller at `/api/{module}/{section}` (it REWRITES the route from
> `[NavRoute]` and discards any `[Route]`). Phase 3a passes the SAME `navRoute` to
> `scaffold-api-client` (per entity) and as each FK's `fkTo.navRoute`, so the frontend
> `API_PATH` + every lookup resolve to the identical backend path. NEVER emit the
> historical `/api/v1/integration/...` literal — the platform rewrote it away (404).

> **`permissionPrefix` — do NOT pass it (`scaffold-controller`)**. The CLI derives
> `{applicationCode}.{module}.{section}` and every permission constant lands as a
> 4-segment path (`{app}.{module}.{section}.{action}`) matching the grants
> scaffold-core-seed writes — the platform permission match is EXACT. Passing a
> hand-built prefix is the historical source of the "every role-based user 403s"
> bug (an app-less prefix can never be granted); if you ever must pass it, it has
> to start with `applicationCode.` — the Zod schema rejects anything else, and
> audit `DEV-API-021` cross-checks constants ↔ seeded grants after the fact.

1. **Derive `displayNameExpr` for `scaffold-business`**: when `entité.md` carries
   an `**Affichage** : <Attribut>` line, pass that PascalCase token (`Id` is
   the conscious GUID opt-out and passes through too); otherwise omit — the
   generator resolves the display family
   (`Name/Label/Code/Title/Libelle/Titre/Reference/Number/Numero`, SSOT
   `lib/display-field.ts`) then the projected identity (`FirstName + LastName`
   on `**Personne**` entities), and FAILS CLOSED when nothing resolves (no
   more silent first-string/GUID fallback — fix the BA: author the
   `**Affichage**` line, never invent a token here).
2. **Pass the entity's `classification` to `scaffold-business`**: the
   parenthesis of the `### ENT-NNN — <Name> (<classification>)` heading,
   verbatim (`lookup`, `agrégat racine`, `composant`, `technical`…). It has ONE
   effect — on a `lookup` the display cascade drops `Code`, because a reference
   value's LABEL is its identity. Measured: `Code` sits 3rd in the cascade and
   `Libelle` 5th, so 11 référentiel services shipped every combobox and every
   column rendering `A_FAIRE` instead of « À faire »; they were fixed by hand,
   the generator was not, and every regeneration brought the regression back.
   Omit the field only when the heading carries no parenthesis — never guess a
   classification here, an absent one keeps the historical cascade.
3. **Derive `fields[].source`** for `scaffold-business` from the
   `prd.entities.md` `Person:` / `Proj:` lines (cross-checked against
   `entité.md`'s `**Personne**` heading line + its `scope core` Relations):
   - Each `Proj: <E>.<Field> <= <Core>.<Prop> — nav <Nav>, FK <Fk>[, fallback <Local>]`
     line becomes `{ nav, target: <Core> when ≠ nav, property: <Prop>, fkField: <Fk>,
     fallbackLocal?: <Local> }` on the matching field entry.
   - **Pure projections** (no `fallback`, i.e. person-`mandatory` identity
     fields and Core-reference display fields) are **appended as NEW field
     entries** (type `string` unless the `Proj:` line says otherwise;
     `required` = FK required) — positioned immediately AFTER their `fkField`,
     mirroring the human reading order (the ListDto carries EVERY user field,
     so position is presentation, not survival — the historical first-5
     truncation window is gone from both strata).
   - **Person-`optional` projections** (with `fallback <Local>` where
     `<Local>` is the field itself) ANNOTATE the existing local attribute:
     set `source.fallbackLocal` to the field's own name — the column stays
     stored/writable, only the read path coalesces.
   - **Derived fields** (`**Dérivé**` in entité.md — lib/derived-field): pass
     them on `fields[].derived` to scaffold-business (which generates the
     nav/child projection) AND to scaffold-api-client (read-only member on
     List/Detail via lib/field-read-surface); pass them to scaffold-component
     with `isComputed: true` (display-only). They follow the same
     scaffold-controller exclusion as formula/source below.
   - Pure-projected fields must NOT be forwarded to `scaffold-controller`'s
     `fields[]` — same exclusion as `formula` fields, DERIVED fields and the
     dataScope OWNER
     column: none of them are Command arguments (the owner is server-resolved,
     anti-spoof). The controller now derives its Create mapping from the
     required subset (mirroring scaffold-business), but a projected/computed/
     owner field in `fields[]` still desyncs BOTH mappings from the DTOs
     scaffold-business actually emitted.
4. **Pass `screenFilters` to `scaffold-business` AND `scaffold-controller`**:
   the entity's LIST pagespec `filters[]`, verbatim as minimal `{field, control}`
   objects (omit when the entity has no list pagespec or no filters — default
   `[]` keeps legacy specs byte-identical). The SAME array goes to BOTH CLIs —
   the SSOT `lib/page-spec-filters.screenFilterParams()` derives one ordered
   param list from it: select/text → `string?`, boolean → `bool?`, date-range →
   `DateTime?` `{Field}From`/`{Field}To`; lookups and the global-search filter
   emit NOTHING (Guid FK channel / `Search`). scaffold-business appends the
   members to `Get{Entity}ListScreenQuery` AND to the integration
   `Get{Plural}Query`, and GENERATES one guarded `Where` per param in
   `GetAllAsync` (before `CountAsync`); scaffold-controller binds the same
   ordered params as `[FromQuery]` on `GetAll` and forwards them as NAMED args;
   `scaffold-screen-controller` reads the pagespec itself and exposes the same
   params on `GET /list`. Forgetting `screenFilters` on either backend CLI is a
   COMPILE error (fail-loud), healed by re-running it with the array —
   DEV-API-017's param leg checks both records.

5. **Scheduled runtime (derive-job-specs)** — once per module:
   ```bash
   npx --prefer-offline tsx skills/ba-develop/cli/derive-job-specs/index.ts \
     --spec '{"moduleRoot":".smartstack/ba/<APP>/<MODULE>","appCode":"<app>"}'
   ```
   Splice `report.jobs` as `scheduledJobs[]` into the OWNING entity's
   scaffold-business AND scaffold-controller specs (the UC's primary entity).
   scaffold-business emits the `Run{X}Async(DateOnly runDate, ct)` service
   method (runDate = the replay/testability seam) AND lands the
   `AddSmartStackRecurringJob` line in Program.cs' RECURRING-JOBS marker
   (never Hangfire's static API); scaffold-controller emits the manual
   `POST jobs/{slug}/run` trigger (permission `.execute`, `?date=` replay).
   A `ba.gap` warning (no emission entity) goes to the blockers — the
   idempotence contract has nowhere to write until the BA models it.
   Gate: `DEV-API-028` (triple presence per scheduled UC).

> **Dashboard defensive guard (Phase 2b)** — a `view: dashboard` pagespec
> WITHOUT `entity` (legacy PRD predating PRD-125) is SKIPPED with a blocker
> `prd.gap` (« dashboard has no host entity — bind it or re-model as a home
> view »). NEVER improvise a controller for it: an unbacked dashboard page is
> the DEV-API-010/008 class.

### 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 `failureKind: "custom-action-missing"`
  `{ phase: "api.customActions", entity, action_code, expected_verb,
  expected_path, controller_file }`. **Remediation is deterministic**: re-run
  `derive-action-specs` for the entity and re-invoke `scaffold-controller` /
  `scaffold-business` with the spliced arrays — never hand-patch the controller.

This is the structural insurance that the pagespec contract reached the wire.
Audit `DEV-API-010` runs the SAME check as a BLOCKING gate after Phase 2a (every
run) and post-hoc — see `references/gates.md`. With the derivation now done by a
CLI, an `err` here means the orchestrator skipped the splice, not that it mapped
a field wrong; the fix is always "re-derive + re-scaffold".

## 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, fkFilters }`.
   `fkFilters` = the entity's camelCase Guid FK columns (same detection as the
   FK-resolution rule of Phase 3a: type guid + `Id` suffix, excluding
   id/tenantId/createdBy/updatedBy — the SSOT is
   `lib/page-spec-related-tabs.fkFilterFields()`), in the SAME order as the
   fields passed to scaffold-business (the query args are positional). This is
   what lets a 360 related tab fetch `GET /list?{relationFk}={id}` on the
   screen stratum too.
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**.
      For the **list** screen (`GetForListScreenAsync`) the query record
      `Get{Entity}ListScreenQuery(Page, PageSize, Search, SortBy, SortDir, {Fk}…, {Filter}…)` is
      ALREADY emitted by scaffold-business (scaffolder-owned — do NOT redefine
      it; it carries one `Guid?` param per FK of the entity + one typed member
      per pagespec filter, from `screenFilters`). The method
      MUST paginate SERVER-side, exactly like `GetAllAsync`:
      build an `IQueryable`, apply the multi-field `Where(EF.Functions.Like(...))`
      on `query.Search`, apply **one explicit `Where` per `Guid?` FK param**
      (`if (query.{Fk} is not null) q = q.Where(x => x.{Fk} == query.{Fk});` —
      BEFORE `CountAsync`, so the 360 related tabs' filtered totals are right),
      apply **one predicate per pagespec-filter member** (select `string?` →
      equality on the stored column; text `string?` → `EF.Functions.Like`;
      boolean `bool?` → equality; date-range `DateTime?` From/To → inclusive
      bounds; a member serving a COMPUTED column — settlementStatus, origin… —
      has no stored column: implement the SAME derivation the projection uses,
      subquery/join, never a silent drop), compute the total with `CountAsync`,
      order via a
      whitelisted switch on `query.SortBy`/`query.SortDir` (default `CreatedAt`
      DESC), then `Skip((query.Page - 1) * query.PageSize).Take(query.PageSize)`
      → `PaginatedResult<{Entity}ListScreenDto>`. A bare `.ToListAsync()` with no
      `Skip`/`Take`/`CountAsync` makes the frontend paginate in the browser
      (everything past pageSize invisible) and is **BLOCKED by audit `DEV-API-017`**
      (whose filter-param leg also errs on a query record missing a filter member);
      a missing FK `Where` leg is **BLOCKED by audit `DEV-API-019`**.
      Columns carrying a `source` block project THROUGH the Core navigation
      (see `screen-controller/SKILL.md` § Core-projected columns): FK required
      → `x.{Nav}.{Prop}`; nullable + fallback → `(x.{Fk} != null ? x.{Nav}!.{Prop} : x.{Fallback})`;
      nullable, no fallback → guarded conditional projecting null. Coalesce
      nullable string projections with `?? string.Empty` (Screen DTO props are
      non-nullable with `""` defaults). Never add an EF `Include()` for a
      projection; never chain two Core navigations (`x.User.Department.Name`
      is ignored by the runtime — use `ICoreDataService` + post-query merge
      and flag `needsRefinement` if a spec genuinely demands it).
   c. Append a happy-path Fact to the existing `{Entity}ServiceTests`.
3. Run `dotnet build --no-restore`. On compile error: return a structured
   failure (`{ failureKind: "compile.*", ..., fingerprint }`) — the
   orchestrator's auto-healing protocol takes over. Do NOT run `dotnet test`
   here — the screen acceptance tests run ONCE at the Phase 2b gate after the
   whole fan-out (see `gates.md` "After Phase 2b"). Run per-subagent, the filter
   `Category=Acceptance&FullyQualifiedName~Screens` re-executes the ENTIRE screen
   suite for EVERY (section, entity) tuple = N redundant test-host + LocalDB /
   Respawn boots. A failure at the gate is attributed by test name → entity and
   healed there (`tests.acceptance.fail`).
4. 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/<appCode>/<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 backend precondition — front+back coupling

Front and back are a **coupled pair** generated from the same entity spec. The
frontend scaffolders (`scaffold-api-client` / `scaffold-component`) emit calls
against a FIXED contract — `GET /api/{module}/{section}/lookup` (the NavRoute-resolved
route) for every FK combobox and a paginated list read as `response.items`. If that contract has
no backend behind it, the page 404s the lookup and renders an empty table. The
recurring `effectifss`-class phantom service (a frontend CRUD client with no
controller) is exactly this failure.

So **before the orchestrator launches a Phase 3a subagent for an entity** (and
before the per-entity loop calls `scaffold-api-client`), it MUST confirm the
entity's backend was produced by the scaffolders and honors the contract:

1. The integration `src/<Ns>.Api/Controllers/<App>/<Module>/<EntityPlural>Controller.cs`
   exists AND carries `// @generated-by scaffold-controller`.
2. `audit-dev-api --rules DEV-API-016` reports **no `err`** for that entity
   (the `[HttpGet("lookup")]` endpoint + the paginated `GetAll` are present).

If EITHER fails — the entity's backend is missing, was deferred/hard-failed in
Phase 2a, or was hand-written off-contract — **SKIP the entity's frontend
entirely**. Do NOT scaffold an api-client or pages against a backend that does
not serve the contract (that is what manufactures phantom 404 services). Record
a `high` blocker (`wire.frontend-orphan`: "backend for <Entity> not generated —
frontend skipped to avoid a guaranteed-404 surface") and continue to the next
entity. The run never halts; the module is reported `failed` for that entity.

> Non-entity sections (a dashboard / board / calendar VIEW with no entity of its
> own) are a different case: they must NOT get an integration CRUD api-client at
> all — they bind to the **screen stratum** of their owning entity
> (`/api/screens/{plural}/dashboard`). `scaffold-api-client` + the orchestrator
> enforce this upstream so the phantom service is never emitted in the first
> place.

## 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):

   > **No kanban invocation exists.** The board is a viewMode of the LIST
   > page: `create-prd/cli/derive-kanban-spec` folded the SmartKanban screen
   > into the list pagespec (`kanban` block + `viewModes`), and the LIST
   > invocation carries it — pass the pageSpec VERBATIM (the standing rule)
   > and the board renders. A leftover `{Entity}.kanban.md` pagespec is the
   > pre-fold legacy shape: scaffold-component REFUSES `views: ['kanban']`
   > with the migration message — run derive-kanban-spec on the module and
   > delete the standalone file, never work around the refusal.

   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 — generator floor + PRD overrides**. scaffold-component
        ALWAYS emits a COMPLETE floor catalogue (every key its templates render,
        in all 4 locales: `breadcrumb.section`, `list.actionsColumn`,
        `list.edit`, `form.submitCreate`, `kanban.*`, `reconduction.*`, …) and
        layers `pageSpec.i18nKeys[locale]` on TOP as OVERRIDES — the PRD value
        wins where it supplies a key, the floor fills the rest, so NO rendered
        key is ever raw (this is what the `validate-page` rule `i18n-keys-resolve`
        now enforces against `src/i18n/locales/{locale}/{module}.json`). Phase 3a
        does NOT transform these — pass the pageSpec verbatim. A generic key
        (edit/delete/loading/breadcrumb/submit/…) needs nothing from the PRD.
        A BUSINESS label that shipped wrong (`"TypeAffaire"` instead of
        `"Types d'affaire"`) means `ba-create-prd` wrote a thin/incorrect
        `i18nKeys` override — fix it there, NOT in Phase 3a.
      - **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}"], entityViews, fields, projectPath, pageSpec, prdContext }`.
      - **Legacy mode** (no pagespec): `{ module, appCode, entity, section,
        views: ["{view}"], entityViews, fields, projectPath }` — derive `fields` from `entité.md`.
      - **`entityViews` — ALWAYS pass it**: the entity's FULL pagespec view set
        (e.g. `["list","detail","form"]`), while `views` stays the single view
        this invocation emits. It is the cross-view navigation gate: the list
        page only emits create/edit buttons and the row-open when a form/detail
        sibling actually exists — a list-only entity used to get
        `routes.{x}.edit(...)` navs against helpers scaffold-routes never
        declared (TS2339 on every such page). Omitting it falls back to `views`,
        which in per-single-view fan-out means a bare list with NO affordances.
      - **FK resolution** (powers `<EntityLookup>`) — **run the `derive-fk-specs`
        CLI, never hand-derive.** Hand-deriving `fkTo` from the Rel: lines (the
        legacy prose) silently dropped FKs — a field without `fkTo` fell back to
        a free-text Guid `<input>`, a raw-Guid list column and a Guid `<dd>` with
        no error (test-RH post-mortem: 2 FKs out of 3 derived on Projet). Run ONCE
        per entity (or omit `entity` to derive the whole module at once):

        ```bash
        npx --prefer-offline tsx skills/ba-develop/cli/derive-fk-specs/index.ts \
          --spec '{"moduleRoot":"<.smartstack/ba/{APP}/{MODULE}>","entity":"<Entity>"}'
        ```

        Take `report.entities[]` and splice each `fields[].fkTo` **verbatim** into
        the matching `fields[]` entry of `scaffold-component` (and keep it for
        `scaffold-api-client`) — do NOT transform any field. Use `required` for
        the field's required flag and `role` (when present) as the field label.
        The CLI already resolves: the target's `{app, module, section}` from its
        OWN pagespec (so `apiEndpoint`/`navRoute` can never drift from the
        backend's `[NavRoute]`), cross-app targets (`fkTo.app` drives the
        cross-app hook import `@/features/{app}/…`), core V1 targets via
        `lib/core-catalog.ts` (aliases included, `/api/core/{plural-kebab}/lookup`),
        and the **`TenantOrganisation` exception** (no `apiEndpoint` — scaffold-component
        wires the `/api/administration/users/organization-references` `selectItems`
        adapter deterministically; audit-dev-wire allow-lists that route). It NEVER
        emits the `/api/v1/integration/...` literal (platform-rewritten → 404).
        - `report.unresolved[]` non-empty is **BLOCKING** for those fields: fix the
          BA tree (create the target's screen/pagespec, or repair the Rel: line) —
          NEVER scaffold the field without `fkTo` and never hand-patch the page.
          scaffold-component's validate now hard-fails a FK-shaped field
          (`…Id`, string/guid) that has neither `fkTo` nor `options` nor an
          explicit `noLookup: true`, so a dropped derivation can no longer ship.
        - Audits DEV-UI-022 (form) / DEV-UI-033 (list + detail + filters) /
          DEV-WIRE-001 (route) surface any residual Guid leak as BLOCKING on the
          next pass.
      - **Reference FILTERS** (the list-side sibling — same class of bug, one
        surface over). A list filter's FK target must be carried BY THE FILTER
        (`pageSpec.filters[].fkTo`) and its `field` must be the FK PROPERTY
        (`clientId`, not `client`). The renderer no longer guesses: it reads
        `fkTo`, else the entity field of the same name — and `scaffold-component`
        REJECTS a `lookup` filter that resolves to neither. When the pagespec was
        written before this contract (filters named after the relation, no
        `fkTo`), backfill it ONCE per module — idempotent, `labelKey` untouched
        so authored translations survive:

        ```bash
        npx --prefer-offline tsx skills/ba-develop/cli/derive-filter-fks/index.ts \
          --spec '{"moduleRoot":"<.smartstack/ba/{APP}/{MODULE}>"}'
        ```

        It reuses `derive-fk-specs`' resolution, so cross-app / cross-module /
        core targets resolve identically. `report.unresolved[]` non-empty is
        **BLOCKING**: fix the BA tree (declare the relation in `entité.md`,
        create the target's pagespec) — never hand-author a filter's `fkTo`.
        Why it matters beyond the control: the filter `field` is ALSO the wire
        query param and the backend `[FromQuery] Guid?`, so a relation-named
        filter is dropped server-side too — it neither looks up nor filters.
        Authoring gate = PRD-113; page gate = DEV-UI-033 (`filter-input`).
      - **Coded entities (system-allocated Code)** — when the entity's
        `entité.md` block declares a `**Code pattern**` (the pagespec carries
        the `codedEntity` flag — boolean `true` or the enriched object
        `{label, supplied}` when the line authors those facets — reconciled
        DETERMINISTICALLY by `ba-develop/cli/derive-code-specs` — run it,
        never grep the line by hand; PRD-132 gates the parity), forward the
        flag VERBATIM (object included) on the scaffold-component spec
        (top-level `codedEntity`) and NEVER splice `code` into a form view's
        `fields[]` — at most `{"name":"code","readonly":true}` to display the
        allocated code on edit. scaffold-component's validate hard-fails an
        editable `code` on a coded entity; a rejection is **BLOCKING**: fix the
        pagespec (create-prd contract), never the generated page. The SAME flag
        is forwarded VERBATIM on the Phase 2a `scaffold-business` spec (top-level
        `codedEntity`, object included) — its validate rejects a `code` entry in
        `fields[]` (the Code never rides Create/Update DTOs; the backend was
        the unguarded third layer). Audits
        DEV-API-022 (backend seam) and DEV-UI-034 (frontend form) catch any
        residual drift 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.
      - **Lifecycle** (powers the smart create/edit split): the pagespec's
        first-order `lifecycle` block flows through `pageSpec` VERBATIM —
        scaffold-component compiles it itself (`resolveLifecycle` seeds the
        per-field `phase`/`requiredInPhase` pivots and synthesizes the status
        guards). Do NOT splice per-field lifecycle flags into the COMPONENT
        spec's `fields[]` — the block is the single source and hand-splicing
        would fork it. The BACKEND + api-client specs are the only place the
        per-field `phase` flag is set (see Phase 2a « Lifecycle phase flags »
        and step 2c below).
      - **Related-tabs data (powers the 360 detail view)**: when the DETAIL
        pageSpec carries `relatedTabs[]` (canonical schema
        `lib/page-spec-related-tabs.ts` — pass the pageSpec VERBATIM, the tabs
        themselves need NO transformation), derive the sibling input
        `relatedTabsData[]` DETERMINISTICALLY — run the CLI, never derive by
        hand (the hand-derivation era shipped 18/18 related tabs on the single
        `createdAt` fallback column):
        ```bash
        npx --prefer-offline tsx skills/ba-develop/cli/derive-related-tabs-data/index.ts \
          --spec '{"moduleRoot":".smartstack/ba/<APP>/<MODULE>","entity":"<Entity>"}'
        ```
        Splice `report.relatedTabsData` VERBATIM into the scaffold-component
        spec. The CLI follows each table/cards tab's target list pagespec
        (columns ≤5 with the relation FK EXCLUDED — in a vehicle's
        « Échéances » tab a `vehicleId` column would repeat the vehicle on
        every row —, per-locale labels from `i18nKeys`, `displayField`,
        `createPermission` verbatim from the target's `actions[]` create
        entry, `hasDetail`/`hasCreateForm` from the pagespec files on disk),
        with the entité.md attributes as the same-module fallback. Outcomes:
        - `success:false` (a target pagespec EXISTS but nothing derives) →
          heal the target's `columns[]`, never scaffold through;
        - `omitted[]` (cross-module PRD not on disk) → fail-open by design:
          the generator renders the single `createdAt` column, validate warns,
          DEV-UI-031 keeps it visible;
        - `summary` tabs need NO entry (count-only cartouche).
        A tab targeting ANOTHER APPLICATION resolves too (the CLI reads
        `<baRoot>/<APP>/<MODULE>` from the tab's `relatedApp`); it is `omitted[]`
        only when that module is genuinely absent from the BA tree.
      - **The cross-module tabs of that page are rendered under the tenant-catalogue
        guard.** Any `relatedTabs[]` entry whose `(relatedApp ?? own app,
        relatedModule)` leaves the page's own surface is emitted wrapped in
        `useModuleAvailability().hasModule(app, module)` — so a client that was never
        given the target module does not see the tab. That requires
        `src/components/ui/useModuleAvailability.ts`: run **scaffold-ui-primitives
        BEFORE scaffold-component** (it already is a prerequisite of this phase). On a
        project whose `@atlashub/smartstack` predates the hook, the primitive is emitted
        as a permissive stub and the CLI warns — record the warning, do not heal it by
        hand: the fix is `ss upgrade`, a USER decision.
      - 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.).
      NB: `Code` is editable here because TypeAffaire has NO `**Code pattern**`
      in entité.md — a user-typed referential code. For a coded entity the spec
      carries the `codedEntity` flag (boolean or enriched object, forwarded
      VERBATIM) and no editable `code` field (see the Coded-entities bullet
      above).
      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. **Run `derive-action-specs` for the entity** — the SAME CLI Phase 2a used,
   so the api/navigate split, the CRUD filter, the dedup and every field mapping
   come from ONE source. Splice `report.entities[0].apiClient` **verbatim** into
   `scaffold-api-client`'s `entities[].customActions` — do NOT re-map anything:

   ```bash
   npx --prefer-offline tsx skills/ba-develop/cli/derive-action-specs/index.ts \
     --spec '{"moduleRoot":"<.smartstack/ba/{appCode}/{moduleCode}>","entity":"<Entity>"}'
   ```

   The CLI already: DROPPED `kind:"navigate"` (no service/hook — the
   `POST /{id}/open` phantom-404 bug), dropped CRUD codes, de-duplicated by
   `(scope, endpoint, httpMethod)`, kebab-cased `code` for TS naming
   (hook = `use${PascalCase(code)}${Entity}`, service member = `toCamel(code)`),
   kept `endpoint` as the verbatim URL segment, and lower-cased `httpMethod`.
   So `code:'toggle-actif', endpoint:'activate'` round-trips to
   `useToggleActifEmploye()` calling `POST /api/.../{id}/activate` — the legacy
   `URL ≠ route` class of 404s cannot recur because both come from the projection.

   > When the controller route diverges from the pagespec (legacy backend
   > names), the pagespec already carries the right `endpoint` and the Phase 2a
   > gate verified the controller serves it. `derive-action-specs` reads that
   > same `endpoint` — there is nothing to look up on disk.

   **Lifecycle phase flags (api-client)** — same rule as Phase 2a's backend
   threading: when the entity's form pagespec carries `lifecycle`, set
   `phase: "<phaseKey>"` on the matching `entities[].fields` entries (OWNED
   `phases[].fields` only, camelCase match). `scaffold-api-client` then keeps
   the phased field out of `Create{E}Dto` (nullable optionals stay IN — the
   Create-honnête contract) while `Update{E}Dto` keeps everything.

   **Dialog lookup params (§28) — run it in `mode: "derive"`.** Pass
   `"mode":"derive"` in the spec: the CLI BACKFILLS each `type:lookup` payload
   parameter with its resolved `navRoute`/`apiEndpoint` **inside the pagespec**
   (same discipline as `derive-filter-fks` for reference filters; idempotent,
   an authored `apiEndpoint` is never touched). The pagespec write is a
   legitimate spec-drift — `compute-page-diff` sees it, so the page
   re-scaffolds on its own.
   Persisting is the POINT: scaffold-component falls back to a rebuilt
   `{action's module}/{english-plural}` when the param carries no route (9/9
   literals wrong on one project — wrong module, English plural instead of the
   menu slug, missing routeFamily segment, every dialog combobox on a 404). A
   value living only in the orchestrator's memory is regenerated wrong by ANY
   run outside this step — `/ui-design`, a manual regeneration, a heal loop
   re-entering Phase 3 alone.
   The report's `entities[].dialogLookupParams` mirrors the same resolution
   for the in-run splice. An `unresolved` entry is BLOCKING for that action:
   author the param's `apiEndpoint` in the pagespec (the target's REAL
   controller route — audit-dev-wire's object-literal check is the net behind
   it), never let the generator guess.
2. **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` AND `routeMode: 'integration'`, and STILL
     collect `screenColumns` from the pagespecs (same extraction as the
     **Present** branch below) — the pagespec columns shape the TS ListDto in
     BOTH route modes, and omitting them here used to drop the api-client onto
     its no-column fallback (historically a first-5 slice: every list with >5
     columns shipped an untypable DTO — client defect 2026-08-25 #5). The
     fallback now carries every field, but the pagespec columns remain the
     BETTER source (exact set + order the screen renders).
     **Set `navRoute: "{module}.{section}"` on each entity** (the
     SAME key Phase 2a's `[NavRoute]` carries). The api-client then emits the
     NavRoute-resolved path `/api/{module}/{section}` — the SAME route the platform
     serves for the integration controller (it rewrites the route from `[NavRoute]`
     and discards any `[Route]`). Zero drift possible: both sides derive it from the
     entity's navRoute via `buildNavApiPath` in `lib/url-conventions.ts`. `navRoute`
     defaults to `{module}.{section}` if omitted, but emit it explicitly. Pass
     `routeMode: 'integration'` EXPLICITLY rather than relying on the default — the
     same on-disk signal (presence of the screen controller) drives BOTH the backend
     phase and this flag, so the stratum is derived from ONE source and cannot
     diverge. Do NOT pass `apiBasePath`, `routeMode: 'direct'`, or the
     `/api/v1/integration/...` literal — all were removed/superseded (the free-form
     template on 2026-05-27, the `direct` mode on 2026-06-25, and the integration
     literal by NavRoute resolution) because they emitted URLs no backend 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.
   **In BOTH branches, collect `screenFilters`** from the SAME list pagespec
   (`pagespecsForEntity.find(p => p.view === 'list')?.filters ?? []`, minimal
   `{field, control}` objects — the same array Phase 2a passed to
   scaffold-business and scaffold-controller): the api-client appends one typed
   member per filter to the getAll/use{Plural} params in EVERY mode (SSOT
   `lib/page-spec-filters.ts` — the integration GetAll binds the same params),
   so the generated list page's debounced filter args type-check against the
   hook and reach the server on both strata.
   **In BOTH branches, pass the SAME `routeMode` to `scaffold-component`** —
   it no longer picks the list's data path (every list is server-driven:
   pagination, search, sort AND the filters, in both modes); the api-client's
   URL shape is what still branches on it. One on-disk signal (the screen
   controller) drives the backend phase, the api-client AND the component —
   the three cannot diverge.
3. **Pass `pageSpec.actions[]` UNCHANGED** to `scaffold-component` — pass the
   WHOLE pageSpec verbatim, never a subset. 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.

   Omitting the pageSpec (or stripping its `actions[]`) silently drops every
   custom button — the **frontend coverage gate** catches exactly this
   (`audit-dev-actions-alignment` → `ACTION-DRIFT-006`, BLOCKING; see
   `references/gates.md`).

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. `derive-action-specs` routes these to
  `report.entities[].navigate` and keeps them OUT of the `apiClient` array (via
  `splitActions` in `lib/page-spec-actions.ts`); `scaffold-api-client` 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.
- **Body contract (`payloadParameters` vs `payloadDto`)** — the frontend POSTs a body IFF
  the action declares `payloadParameters` (the fields the `<CustomActionDialog>` collects).
  A `payloadDto` WITHOUT `payloadParameters` = an **optional / server-defaulted** body:
  `derive-action-specs` sets the api-client `payloadType` to `null` (bodyless call + arity-0
  hook) and `scaffold-controller` binds it with
  `[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] <Dto>? dto = null` — the empty body
  is tolerated (no 415), not a build break. WITH `payloadParameters`, the binding follows the
  fields' requirements: all optional → the same `EmptyBodyBehavior.Allow` + `dto ?? new()`
  (the synthesized record defaults every member to `null`); ≥1 `required` field → a
  MANDATORY `[FromBody] <Dto> dto` (model binding 400s on a missing body — an empty body
  could never satisfy the requirement, and the record has no parameterless ctor). On
  `httpMethod: GET` the parameters ride the QUERY STRING instead (nullable `[FromQuery]`
  scalars mirrored into the service signature and the api-client `{ params }` — a GET has
  no body). **NEVER set `payloadType:null` by hand to make the
  frontend compile** — the projection already does the right thing; if a body is genuinely
  required, the pagespec MUST carry `payloadParameters` (audit `DEV-API-018` warns when it
  doesn't). Bulk actions post `{ ...payload, ids }` (the current DataTable selection).

### How `kind` is resolved

`derive-action-specs` (through `PageCustomActionSchema`) is the single resolver:
the pagespec carries `kind` explicitly, and the schema's `superRefine` enforces
the discriminator (a `navigate` action MUST carry `targetRoute`/`targetScreen`
and MUST NOT carry `endpoint`; an `api` action is the inverse). The orchestrator
never infers `kind` by hand — it splices what the CLI returns.

## 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`.

> **Unique indexes + decimal precision — carry EVERY BA declaration, FK and
> composite included**. `entité.md` `**Index**` declarations map to
> `scaffold-entity` through THREE channels — pick per shape, drop NONE:
>   - `(X) unique` on a business column → `unique: true` on that field;
>   - `(Fk) unique` on a RELATION's FK column (one Driver per User, one
>     LifecycleSummary per Vehicle) → `unique: true` on the `relations[]`
>     entry — the FK column is SYNTHESIZED, a field flag cannot reach it;
>   - any COMPOSITE — `(DrivingLicenceId, Kind, ThresholdDays) unique` — →
>     an `indexes[]` entry `{fields: [...], unique: true}` (non-unique
>     composites ride `indexes[]` too).
> Every unique emits tenant-aware (strict → composite `(TenantId, …)`;
> optional → the two filtered indexes; none → plain unique). Dropping a
> declaration ships a uniqueness BUSINESS RULE with no SQL safety net — the
> app-layer AnyAsync is racy AND blind to soft-deleted rows the query filter
> hides; on the incident project the 7 FK-bearing declared uniques (of 23)
> were ALL silently dropped. Gate: `audit-dev-api DEV-API-031` (err) compares
> declared vs emitted. When an
> attribute is declared `decimal(p,s)`, pass `precision`/`scale` — otherwise
> the column silently falls to the SQL Server default decimal(18,2).

> **Valeurs initiales — Phase 1 seeds the business-fixed rows (the l.14
> promise's execution step)**. For every entity whose `entité.md` block carries
> a `- **Valeurs initiales** : clé <Attribut> — …` table:
>   - build the `scaffold-seed` spec's `referenceData[]` entry VERBATIM from
>     the table — `{ entity, keyField: <Attribut>, types, rows: [...] }`, one
>     object per table row (columns = PascalCase properties),
>     `tenantMode: 'tenant'` unless the entity was scaffolded `tenantMode: none`;
>   - **pass `types` = the entity's `fields[]` type map you just handed
>     `scaffold-entity`** (property → C# type). It is a mechanical copy, and it
>     is what types the literals: `decimal`/`double` suffixes, `Guid`/date
>     `Parse(…)`, and an enum type name turning `"Insurance"` into
>     `AlertKind.Insurance`. Without it a fractional value is assumed
>     `decimal` and will not compile against a `double` column (validate warns);
>     `"cs:<expression>"` remains the escape hatch for the rest;
>   - invoke the CLI:
>     `npx --prefer-offline tsx skills/development/backend/seed-data/cli/scaffold-seed/index.ts --spec '{"module":"<mod>","appCode":"<Ns>","applicationCode":"<app>","projectPath":"<root>","referenceData":[...]}'`
>     → emits `Persistence/Seeding/Applications/{App}/Modules/{Module}/{Module}ReferenceDataSeedDataProvider.cs`
>     (idempotent upserts by the natural key, per tenant for tenant-scoped
>     entities, through a ctor-injected IExtensionsDbContext);
>   - the CLI LANDS the DI registration itself (`SEED-PROVIDERS-DI` marker
>     block, idempotent) — check the envelope warnings if it could not find a
>     DI host, because an unregistered provider never runs while DEV-API-030
>     still counts the entity as populatable.
>   An entity with fixed rows whose screens expose neither `create` nor
>   `delete` is DEFINITIVELY empty without this step — `audit-dev-api
>   DEV-API-030` (err) blocks the gate on any unpopulatable entity.

> **Jeu de test — Phase 1 seeds the business TEST DATASET for dev/test/qual
> (the second seed tier)**. When the module carries a `jeu-de-test.md`
> (optional — authored by `/ba-create-test-data`; grammar in
> `business-analyse/_workflow/doc-templates.md`):
>   - run the deterministic deriver (one line — never a multi-line `--spec`):
>     `npx --prefer-offline tsx skills/business-analyse/create-test-data/cli/derive-test-data/index.ts --spec '{"baRoot":"<baRoot>","app":"<APP>","module":"<MODULE>","mode":"derive"}'`
>     → `report.derived.sets[]` (blocks in dependency order, cells typed, every
>     FK cell resolved to a ROW REFERENCE `{ref, entity, keyField, key}`, a
>     `User` to `{actor, label}`, a `TenantOrganisation` to `{core, by, value}`
>     — never a Guid) and `report.derived.rank` (the module's rank among the
>     modules its dataset cites);
>   - `report.status: absent` → nothing to seed, say so in the phase summary
>     (never-halt); `totals.err > 0` → a `test-data.incoherent` blocker (high)
>     naming the file — or the OWNER module when a citation is unresolved —
>     and continue: the dataset is optional, the run never halts on it;
>   - pass `testData: report.derived.sets` and `testDataRank:
>     report.derived.rank` to the SAME `scaffold-seed` call as
>     `referenceData[]`, after merging each set's `types` with the entity's
>     `fields[]` type map for every attribute listed under `needsTypes` (the
>     enums — the deriver types the scalars, only Phase 1 knows the C# enum
>     name); `tenantMode` comes from the entity's `**Portée**`; DROP every
>     column of a lifecycle-PHASED attribute (pagespec `lifecycle` — the
>     scaffold-entity `phase` fields are never `Create(...)` parameters, the
>     capturing action sets them) — the seed emits ONE flat named-args
>     `Create(...)` per row, exactly the factory scaffold-entity generates
>     (required fields, `tenantId`, optional fields defaulting to null); a
>     factory that differs (older generation, hand-edited) is the
>     `seed.create-signature` heal row (auto-healing.md);
>   - the CLI emits `{Module}TestDataSeedDataProvider.cs`: guarded by
>     `IsDevelopment() || SmartStack:EnableDevSeeding` (the socle's own
>     dev-seeding switch — Development implicit, test sets the key, qual on
>     demand, never preprod/prod), `Order = 200 + rank` (after every module's
>     reference data and test users), idempotent upserts by key per tenant,
>     every FK resolved by key inside the tenant BEFORE `Create(...)`, a row
>     whose reference resolves to nothing SKIPPED and logged (retried at the
>     next startup — the socle seeds its demo Core rows after the client
>     providers), DI registration landed;
>   - the net: `audit-dev-data DEV-DAT-010` (err, After-Phase-1 gate) — every
>     block reached the provider, guarded, registered. This provider is NEVER
>     a feeding path for `DEV-API-030`: a row an environment needs at startup
>     belongs to `**Valeurs initiales**`, not here.

> **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.

> **Core-projected fields — end-to-end cascade**. When `entité.md` declares a
> `**Personne**` line (person extension of `auth_Users`) or a screen displays a
> field of a `scope core` reference (e.g. the customer organisation name):
>   - Phase 1: `scaffold-entity` emits the FK + navigation property for the
>     `scope core` relation (already shipped — nothing new to do).
>   - Phase 2a: `scaffold-business` receives `fields[].source` (derived from
>     the PRD `Person:`/`Proj:` lines — step 8 above) and projects through the
>     navigation in the read DTOs only; pure projections never reach
>     Create/Update Commands nor `scaffold-controller`.
>   - Phase 2b: screen DTOs project through the SAME navigations (per-subagent
>     contract step 2b). Audit DEV-API-015 (warn) checks the nav read landed.
>   - Phase 3: pass scaffold-api-client the **SAME enriched `fields[]`** Phase
>     2a gave scaffold-business — `formula`, `source`, `phase`, `isKey` and the
>     data-scope owner (`dataScopeOwner`) INCLUDED, never a re-derived subset.
>     The projected/computed members arrive as flat DTO members
>     (employeeDto.firstName) and `lib/field-read-surface` (shared by both
>     CLIs) puts them on the TS List/Detail interfaces and keeps them OFF
>     Create/Update — omitting them here is exactly the 34-missing-members
>     drift (`isOpen`, `isOverdue`, `email`… sent by the record, denied by the
>     interface, unrenderable "pas même à la main"). Person-`mandatory`
>     form pagespecs edit the FK via `<EntityLookup>` (`fkTo module "core"`),
>     never the identity fields.

> **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, TenantOrganisation, 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 — and `tenantMode` itself comes from the entity's
> `- **Portée**` in entité.md (`strict|optional|none`, entity-level else document-level,
> audited by DM-028); only when NEITHER says it does the scaffolder's `strict` default
> apply. Never invent one: that default decides the tenant-composite shape of every
> declared unique index. 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.

> **Data scopes (own/assigned) — end-to-end cascade**. When the module `rbac.md`
> matrix gives ANY actor a `read` whose **Portée** is `les siennes`/`own` or
> `attribuées`/`assigned` for an entity's section (Phase 0 already seeds the
> scoped `read` + the sibling `.read.all` bypass row from the same matrix):
>   - Phase 1 — COLUMN half: pass `dataScope: { mode: "own" | "assigned" |
>     "own-assigned" }` to `scaffold-entity` (mode = union of the scoped Portées
>     across actors: someone reads `les siennes` AND someone `attribuées` ⇒
>     `own-assigned`). It synthesizes the indexed ownership column(s)
>     (`OwnerUserId` set at Create, never updatable; `AssignedToUserId` nullable,
>     updatable) + the `IOwnedEntity`/`IAssignedEntity` markers.
>   - Phase 1 — POLICY half: invoke `cli/scaffold-data-scope` for the SAME
>     entities (`readPermission` = the section's app-qualified `{app}.{module}.
>     {section}.read`). It generates `{Entity}ScopePolicy.cs` and patches the
>     DbContext (`<<< DATA-SCOPE-FILTERS >>>`) + DI (`<<< DATA-SCOPE-POLICIES-DI >>>`)
>     markers. Without this half the matrix seeds `.read.all` but the lists
>     return EVERY row — the exact gap audit DEV-API-020 blocks on.
>   - Phase 2 — pass the SAME `dataScope` (and `tenantMode`) to `scaffold-business`:
>     it excludes the owner from the Create/Update DTOs+Commands+Validators and
>     emits `CreateAsync` resolving `ownerUserId` from `ICurrentUserAccessor`
>     (server-assigned, spoof-proof) + `tenantId` from `ICurrentTenantService`,
>     spliced positionally into the factory call. Reads need NO scope code: the
>     named "DataScope" EF filter applies to every query. NEVER emit
>     `[RequireDataScope(typeof(T), …)]` on an extension controller (Core-only
>     guard — throws at runtime); the `GET {id}` 404s out-of-scope rows via the
>     filter.
>   - Phase 2/4 — a scoped entity registered in global search carries the
>     mirrored `.RestrictTo(scope => scope.Has("{path}.read.all") ? null :
>     e => e.{Owner} == scope.UserId)` (scaffold-extension-search `rowScope`).
>   - `team`/`custom` Portées stay DESCRIPTIVE (no generated tier) — per
>     `/ba-create-rbac`; do not invent TVFs. At runtime the actor therefore
>     reads ALL rows — RBAC-010 (warn) is where that gap is said out loud;
>     surface it in the run report when the module carries such rows. See
>     `development/backend/data-layer/references/data-scopes.md`.

> **Coded entities (system-allocated codes) — end-to-end cascade**. When
> `entité.md` declares a SYSTEM-ALLOCATED business code (a numbering convention
> the user does not type — invoice number, mandate code; audit DM-017).
> The specs of BOTH halves are DERIVED, never hand-assembled:
>
> ```bash
> npx --prefer-offline tsx skills/ba-develop/cli/derive-code-specs/index.ts \
>   --spec '{"moduleRoot":".smartstack/ba/<APP>/<MODULE>"}'
> ```
>
> — `entities[].scaffoldEntityCoded` feeds scaffold-entity,
> `entities[].scaffoldCodedEntity` feeds scaffold-coded-entity, and the
> pagespec `codedEntity` flags are reconciled (PRD-132). A `blocked` entity
> (no mask / invalid format) is a BA fix in entité.md — never an improvised
> spec (DEV-API-034 errs on the hand-rolled result):
>   - Phase 1 — ENTITY half: pass `codedEntity: { codeKey: "{app}.{entity-kebab}",
>     format: "<the mask>" }` to `scaffold-entity` (Code column engine-assigned +
>     `ICodedEntity` + a UNIQUE index on Code per `tenantMode` — the DB safety
>     net under the allocation engine); the `Code` is NEVER a Create/Update
>     input — with ONE scaffolded exception: when the report says
>     `entities[].supplied: true` (the entité.md line declares « surchargeable
>     à la création »), Phase 2a forwards `codedEntity: { supplied: true }`
>     (the pagespec flag VERBATIM) on the `scaffold-business` AND
>     `scaffold-controller` specs — the generated Create command/DTO gain a
>     TERMINAL `string? Code = null`, the service validates it through the
>     socle's `ISuppliedCodeGuard` and applies it BEFORE SaveChanges (HasCode
>     idempotency), the integration controller forwards `Code: dto.Code` as a
>     NAMED argument. For a screen-driven controller, the hand-filled
>     dto→command mapping of the Create endpoint MUST transport
>     `Code: dto.Code` too (named — order-proof). Updates NEVER carry Code,
>     supplied or not. `scaffold-business` fails closed below socle 3.66.0
>     (the ISuppliedCodeGuard floor — `ss upgrade` or drop the facet). The `format` is the SAME mask as the descriptor's `defaultFormat`
>     (single source: the entité.md `**Code pattern**` line — pass it to BOTH
>     halves): it drives the `GetCodeInputs()` emission, without which every
>     derived-token format (`{ABBR:Champ:n}`, `{SLUG:Champ}`, …) fails at
>     allocation time.
>   - Phase 1 — DESCRIPTOR half: invoke `cli/scaffold-coded-entity` for the SAME
>     entities (label, `defaultFormat` mask from the BA numbering convention,
>     scope/reset/gapless). It generates `{Entity}CodeKeyDescriptor.cs` and patches
>     the `<<< CODED-ENTITY-KEYS-DI >>>` markers. No seed, no migration.
>     `collisionStrategy: "Suffix"` requires a `probeType` (the hand-written
>     `ICodeUniquenessProbe` — its scope is a business decision, no scaffolder
>     emits it; the DI line then becomes `AddSmartStackCodeKey<Descriptor, Probe>`)
>     — the CLI fails closed on a probe-less Suffix, mirroring the v3.67 engine
>     which refuses the allocation.
>     The format grammar is CLOSED and the CLI fails closed on it: `{YYYY} {YY}
>     {MM} {DD} {TENANT} {SEQ:n}` + derived `{FIELD|UPPER|LOWER|SLUG|INITIALS:Champ}`
>     / `{ABBR:Champ:n}`, `{SEQ}` required unless a derived token is present, reset
>     aligned with a date token, scope Tenant|Global only. If the BA authored an
>     invented token (a legacy `{NNNN}`), REWRITE it to `{SEQ:n}` in the spec and
>     fix `entité.md` — do NOT work around the CLI: the engine raises
>     `InvalidCodePatternException` at every insert.
>     NEVER scaffold a counter/sequence entity alongside it — the socle allocates
>     (`references/coded-entities.md`); a `{Entity}Sequence` in the MCD is a DM-017
>     error to send back, not a table to generate.
>   - Phase 2 — nothing: the Code arrives in read DTOs like any stored column; the
>     shared `CodedEntitySaveHandler` allocates at insert. The Phase 2 gate runs
>     **DEV-API-022** (BLOCKING err): every `**Code pattern**` entity of entité.md
>     must have all four legs — ICodedEntity, `AddSmartStackCodeKey` DI, the
>     DbContext ctor forwarding `IServiceProvider` (without it allocation is
>     SILENTLY skipped), unique Code index.

> **File attachments (documents) — end-to-end pattern**. When `entité.md`
> carries a file-METADATA entity (attachment semantics — `{Parent}Documents`,
> « pièces jointes », detected upstream by C-6/DM-019/CODE-006): the platform
> ALREADY ships the storage primitive (`IFileStorageService`, Scoped, Normal/
> Legal tiers, config in every generated appsettings) — NEVER rebuild storage,
> never a `binary` attribute (scaffold-entity fail-closes).
>   - Phase 1 — the metadata entity (`FileName`, `StoredFileName` opaque+unique,
>     `ContentType`, `FileSizeBytes`, parent FK) is a NORMAL `scaffold-entity`
>     run — nothing special.
>   - Phase 2/2b — the upload/download/delete endpoints are HAND-WRITTEN
>     conversational code (the controller scaffolders emit JSON `[FromBody]`
>     only): `IFormFile` + `[RequestSizeLimit]` + extension/size whitelist
>     validated IN the controller + `[RequirePermission]`; download
>     AUTHENTICATED and streamed via the tenant-filtered metadata query; never
>     route through the socle's anonymous `api/files/*`. Follow
>     `development/backend/data-layer/references/file-storage.md` verbatim.
>   - Phase 3 (frontend) — upload posts `FormData` through the package's `api`
>     (multipart interceptor); the dropzone + attachment list are LOCAL
>     components (the package exports none), mounted as the parent's
>     « Documents » related tab. NEVER a custom action with
>     `payloadParameters[].type: 'file'` (JSON pipeline — PRD-107 flags it).

> **Platform-seam registrations — Phase 2 closing step (deterministic)**. After
> the API layer of a module is scaffolded, run the seam CLIs so the module's
> entities are actually WIRED into the platform (a module without them ships
> invisible to search and unfiltered):
>   1. **Global search** — assemble the spec with
>      `scaffold-extension-search/build-spec.ts` (sections + permissions + list
>      screens + the entities' `dataScopes` so the `RestrictTo` row-rule is
>      DERIVED — search must never reveal a row the scoped list hides), then run
>      the CLI against the `<<< EXTENSION-SEARCH-DI >>>` markers.
>   2. **Data scopes / coded entities** — already invoked in Phase 1 (cascades
>      above — Phase 1 is the AUTHORITATIVE invocation; this closing step is
>      the idempotent catch-up on a re-run whose pre-entry diff skipped
>      Phase 1); verify the markers are filled before gating (**DEV-API-020**
>      data scopes, **DEV-API-022** coded entities — the former 020-only
>      citation was the wrong rule for the coded half).
>   3. **HR time-entry imputation (`scaffold-time-entry-refs`) — OPT-IN, never
>      automatic**: imputation is a business decision, not derivable from
>      pagespecs. Run it ONLY when the BA/PRD explicitly designates an entity as
>      an imputation dimension (e.g. « les heures s'imputent sur les projets ») —
>      then the module's projects/mandates become pickable in the core HR
>      time-entry form (tenant toggle `Hr / TimeEntryExternalRefsEnabled`).
>      Surface the decision in the final report when candidates exist but nothing
>      was registered.

> **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. **Enforced deterministically**: `scaffold-entity/validate.ts` rejects
> `domainPrefix` ∈ {`ext`,`extensions`,`core`} (fail-closed) — a reserved prefix
> hard-fails the spec, so this is no longer advisory-only prose. 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.
