---
name: ba-create-prd
description: >
  Synthesises the upstream BA `.md` tree of ONE module (menu, actors, use cases,
  business rules, RBAC, data model, screens) into a Product Requirements Document
  on disk: `prd.md` (product framing: context, goals, non-goals, MoSCoW, technical
  constraints), three phase slices, per-screen `pagespecs/*.md` (each carrying a
  machine spec block), and `claude.md`. User stories and acceptance criteria are
  NOT in the PRD — they live under each UC in `<section>/use-case.md` as the
  test-scaffolder's input. The output drives development through `/ba-develop`.
  Run after the BA phases (and `/ba-audit-pre-dev` GO), before development.
allowed-tools: [Read, Write, Edit, Glob, Grep, Bash]  # Bash: derive-* CLI post-steps (rule-links, lifecycle, form-sections, uc-coverage)
---

# ba-create-prd — Product Requirements Document (per module)

You produce the **PRD of ONE module** by reading its already-written BA `.md`
docs and synthesising the dev-facing artifacts. You do **not** invent anything —
every PRD element traces to an upstream doc. If an upstream doc is missing or
thin, surface the gap (route back to the owning phase) rather than fabricating.

## File model — read the module subtree, write the PRD files

There is no database and no action blocks. State is the `.smartstack/ba/` tree.

1. **Read the module subtree** `.smartstack/ba/<APP>/<MODULE>/`:
   - `index.md` (+ the app and project `index.md`) → context + the **out-of-scope
     cascade** (project → app → module).
   - `<APP>/acteur.md` → actor roles (referenced from UC actors and RBAC).
   - `<MODULE>/use-case.md` (per section) → behaviour scope + each UC's
     `**Acceptance Criteria**` field (the test contract — not duplicated in the PRD).
   - `<MODULE>/règles-métier.md` → validation rules + valid/invalid examples
     (consumed by the entities slice as domain invariants and by the API slice
     as FluentValidation rules).
   - `<MODULE>/rbac.md` → permissions (API slice).
   - `<MODULE>/entité.md` → entities/attributes/relationships/computed (entities slice).
   - `<MODULE>/<section>/screen.md` → screens (frontend slice + pagespecs).
2. **Write** the PRD files (below) with the Write tool — one file at a time, no
   monolithic blob. The `pagespecs/<Entity>.<view>.md` fenced ```json blocks are
   the machine artifacts the dev pipeline reads directly — there is no separate
   cache to generate.

### Files written (per module, under `.smartstack/ba/<APP>/<MODULE>/`)

| File | Content | Source |
|------|---------|--------|
| `prd.md` | `## Context`, `## Goals`, `## Non-goals` (≥3), `## MoSCoW`, `## Technical constraints` | index out-of-scope + use cases + rules |
| `prd.entities.md` | `# Phase: Entities` — every entity, attributes, relationships, computed formulas, invariants, migrations (combined domain+data) as `- [ ]` bullets | `entité.md` + `règles-métier.md` |
| `prd.api.md` | `# Phase: API` — every permission → HTTP method+route, business handlers + validators. **Workflow transitions (SK-001):** for every entity with a Status field, include a `## Workflow: {Entity} Status` section listing state→state transitions with actor, action code, guard rules, and flow parameters. **Cross-module deps (SK-004/005):** when a BR references an entity from an undelivered module, annotate the handler with `crossModuleDeps: ["{moduleName}"]` and flag conditional matrices as `needsRefinement: true` with a note. | `rbac.md` + `règles-métier.md` + `<section>/use-case.md` |
| `prd.frontend.md` | `# Phase: Frontend` — every screen code → route + referenced entity | `screen.md` files |
| `pagespecs/<Entity>.<view>.md` | one per screen: a fenced ```json machine block (the pageSpec) + a human body | `screen.md` + `entité.md` + `rbac.md` |
| `claude.md` | the generated-project module CLAUDE.md (≤ ~50 lines) | template below |

The three `prd.*.md` slices are **self-contained and disjoint** (a dev subagent
loads exactly one). The `pagespecs/<Entity>.<view>.md` fenced ```json blocks ARE
the machine artifacts: `/ba-develop` reads them directly and feeds
each to `scaffold-component` — there is no separate `prd.json` to generate. All
PRD files are plain `.md`, the source of truth and the committed deliverable.

**Entities slice — relationships carry the FK contract.** In `prd.entities.md`,
EVERY relationship (any scope) MUST be written so the dev pipeline can build the
`scaffold-entity` `relations[]` deterministically — cardinality + target + FK
name + cascade + **scope**, e.g.
- `- [ ] Rel: Opportunity *→1 Contact — FK ContactId, scope same-module, onDelete restrict`
- `- [ ] Rel: Invoice *→1 Client — FK ClientId, scope cross-module, onDelete restrict`
- `- [ ] Rel: Employee *→1 Department — FK DepartmentId, scope core (ref_Departments), onDelete restrict`

Copy the scope + cascade from the `entité.md` **Relations** line (default
`restrict`). **A cross-table reference is ALWAYS a FK — cross-module / Core refs
are NO LONGER plain `Guid` bullets.** The ONLY plain-`Guid` `*Id` columns are the
identity/audit allowlist (`CreatedByUserId`, … — `lib/fk-allowlist.ts`). The
per-entity `TenantId` FK is emitted automatically from the entity's tenant mode —
no `Rel:` line needed. Phase 1 forwards every relationship (with its scope) to the
generator, which emits the real FK + `OnDelete` (cross-schema for `core`) — audit
DEV-DAT-008 (blocking) verifies the constraint actually landed.

**Person extension pass-through.** Every entity whose `entité.md` block carries
a `- **Personne**` line gets, in `prd.entities.md`, directly under its attribute
bullets:
- `- [ ] Person: Employee — mode mandatory — identity from auth_Users (FirstName, LastName, Email); do NOT create local identity columns`
- `- [ ] Person: Customer — mode optional — local identity (FirstName, LastName, Email) + nullable FK UserId fallback`
- `- [ ] Person: Interlocuteur — mode none — décision client : <raison verbatim>` (the
  explicit decorrelation override — propagate the reason VERBATIM so the trace
  survives into the PRD; a person-triggered entity with no `Person:` line at
  all is upstream DM-018c territory, never silently normalised here)

and the matching relation line is mandatory like any other rel (modes
`mandatory`/`optional` only — a `none` override has no User relation):
`- [ ] Rel: Employee *→1 User — FK UserId, scope core (auth_Users), onDelete restrict`.

**Core-projected fields (`Proj:` lines).** Every DTO field read from a Core
navigation (the person identity fields in `mandatory`/`optional` mode, and any
display field of a plain Core reference) gets one `Proj:` line:
- `- [ ] Proj: Employee.FirstName <= User.FirstName — nav User, FK UserId`
- `- [ ] Proj: Customer.Email <= User.Email — nav User, FK UserId, fallback Email`
- `- [ ] Proj: Order.CustomerCompanyName <= TenantOrganisation.Name — nav Customer, FK CustomerCompanyId`

Rules: one `Proj:` line per projected field; `fallback <Local>` present ⇔
person-`optional` (the local stored column read when the FK is null); the
entity left of `<=` on the right side must be a V1-whitelist Core entity;
every `Proj:` line must be backed by a `Rel: … scope core` line for the same
FK. Phase 2a derives `scaffold-business` `fields[].source` entries from these
lines — never from guesswork.

## `prd.md` — product framing

- `## Context` recopies the upstream `**Sources**` citations (`SRC-NNN §n`)
  that ground the module's framing — copy the codes VERBATIM from the BA
  docs (Grep them across the tree; a code you cannot find upstream does not
  enter the PRD). Pagespecs carry none (they are derived artefacts).
- `## Non-goals` ≥ 3, each a **negated** statement. **Recopy every non-empty
  out-of-scope** from the project/app/module `index.md` cascade as a non-goal
  (the explicit empty marker `_Aucune exclusion connue à ce stade._` is a
  declaration, NOT an exclusion — never copy it as a non-goal)
  (paraphrase ok, stay faithful) — Claude Code cannot infer intent from omission.
- `## Goals` ≥ 2 action-verb statements describing what the module delivers.
- `## MoSCoW` lists Must / Should / Could / Won't priorities mined from the
  upstream UCs + index out-of-scope.
- `## Technical constraints` names the target stack constraints specific to this
  module (DB provider, integrations, performance budgets). Cross-module
  constraints belong in the project root `claude.md`, not here.

> **No `## User Stories` / `## Acceptance Criteria` sections.** Use cases own the
> behaviour (in each `<section>/use-case.md`) and **each UC carries its own
> `**Acceptance Criteria**` field** — that is the single source of truth the
> test-scaffolder consumes during `/ba-develop`. The PRD never paraphrases UCs
> into stories; it frames the product (context, goals, non-goals, scope priorities,
> technical constraints).

## `pagespecs/<Entity>.<view>.md` — the scaffold-component contract

For **every screen** in the module's `screen.md` files, write one pagespec file.
The machine block is fenced ```json (so the dev pipeline parses it natively — no
extra dependency); the human body explains it. Required fields,
all traced to the upstream docs (copy verbatim — never invent a code):

```json
{
  "screenCode": "SCR-CRM-PIPELINE-OPPORTUNITES-001",
  "appCode": "crm",
  "module": "pipeline",
  "section": "opportunites",
  "entity": "Opportunity",
  "view": "list",
  "permission": "pipeline.opportunites.read",
  "linkedUseCases": ["UC-CRM-PIPELINE-OPPORTUNITES-001"],
  "linkedBusinessRules": ["BR-001"],
  "screenType": "SmartListView",
  "columns": [{"key":"amount","labelKey":"list.columns.amount","formatHint":"currency","sortable":true,"filterable":false,"searchable":false,"isComputed":false}],
  "actions": [
    {"code":"create","scope":"header","labelKey":"list.create","permission":"pipeline.opportunites.create","variant":"primary"},
    {"code":"syncFromPce","kind":"api","scope":"header","endpoint":"sync-from-proconcept","httpMethod":"POST","labelKey":"list.actions.syncFromPce","permission":"pipeline.opportunites.execute","variant":"secondary","ucReference":"UC-CRM-PIPELINE-OPPORTUNITES-007"},
    {"code":"open","kind":"navigate","scope":"row","targetScreen":"SCR-CRM-PIPELINE-OPPORTUNITES-002","targetRoute":"routes.opportunites.detail(item.id)","labelKey":"list.actions.open","permission":"pipeline.opportunites.read"}
  ],
  "filters": [{"field":"stage","control":"select","labelKey":"list.filters.stage"},{"field":"clientId","control":"lookup","labelKey":"list.filters.client","fkTo":{"entity":"Client","app":"crm","module":"clients","navRoute":"clients.annuaire","apiEndpoint":"/api/clients/annuaire/lookup"}}],
  "i18nKeys": {"fr":{"list.title":"Opportunités"},"en":{"list.title":"Opportunities"},"it":{"list.title":"Opportunità"},"de":{"list.title":"Chancen"}},
  "needsRefinement": false,
  "refinementNotes": []
}
```

Rules (the dev pipeline `scaffold-component` consumes this verbatim):
- `screenCode` = the `SCR-…` heading from `screen.md`, verbatim.
- `appCode`/`module`/`section` = lowercase folder codes; `entity` = PascalCase
  from `entité.md`; `view` from the SmartComponent type (list/detail/form/
  dashboard/app-home/module-home/section-home). `SmartCard` and `SmartKanban`
  produce NO pagespec of their own — they fold into the section's LIST
  pagespec (`viewModes` + the `kanban` block; see the two representation
  rules below). A standalone `view: kanban` pagespec is the pre-fold legacy
  shape — PRD-135(f) flags it, scaffold-component refuses it.
- **Route identity (`routeFamily` + `routeParent`) — the sub-view pattern.**
  When SEVERAL entities carry list screens under ONE menu section (a "porteur"
  entity + satellites, e.g. section `list` hosting Project + ProjectWorkPackage
  + ProjectPhase), the URL/route family of each satellite is a DECISION that
  must be recorded HERE — it is the value scaffold-routes keys the module's
  `*Routes.ts` family with, and nothing downstream can re-derive it:
  - the section's PRIMARY entity keeps the section slug → omit both fields;
  - every satellite's pagespecs carry `"routeFamily": "<kebab-slug>"` +
    `"routeParent": "<section>"`. Default slug = kebab plural of the entity
    **stripped of the porteur-entity prefix** (`ProjectWorkPackage` under
    porteur `Project` → `work-packages`); you may shorten it further
    (`ProjectHistoryEntry` → `history`) — it is authored data, audited for
    uniqueness (per module) and kebab shape, never re-derived by generators.
  - Same `routeFamily` on EVERY view pagespec of the entity (list/detail/form).
  Downstream: Phase 3b builds the scaffold-routes spec from these fields
  (`section: routeFamily ?? section`, `parentSection: routeParent`), and
  related tabs targeting the satellite copy the value into
  `relatedRouteFamily` (see the Related-tabs propagation block). Omitting the
  fields on a sub-view satellite is what mis-routed every 360 tab onto the
  porteur (AtlasHub, 27 hand-fixed pages) — do not omit them.
- `permission` = `module.section[.resource].action` from `rbac.md` (3 segments
  for a section, 4 for a resource; no app prefix — added at controller boundary).
- `columns`/`actions`/`filters` mined from `screen.md` (1:1, drop nothing) and
  enriched from `entité.md` (formatHint from attribute type; isComputed from a
  non-empty `Calculé` formula → never editable).
- **Mobile / offline (`pwa`)** — when the screen carries a `- **Mobile**` bullet,
  the pagespec carries the top-level machine field
  `"pwa": { "support": "adapted"|"desktop-only", "offline": "read"|"write" }`
  (SSOT `lib/pwa-meta.ts`). Mapping: bullet `adapted` → `support: "adapted"`;
  `…, offline` → `"offline": "read"`; `…, offline-write` → `"offline": "write"`;
  absent bullet → OMIT the field entirely (the page stays desktop-only). Never
  author `support: "full"` (not generatable in v1 — scaffolders reject it). An
  entity with ANY `offline-write` screen must be scaffolded `versioned: true`
  in Phase 2 (scaffold-entity/scaffold-business — rowversion → real 409s) and
  Phase 3a passes `versioned: true` + `pwa` down to scaffold-api-client /
  scaffold-component, and `pwa`/`pwaByView` to scaffold-routes.
- **Coded entities (system-allocated Code)** — when the entity's `entité.md`
  block carries a `**Code pattern**` line, EVERY pagespec of that entity carries
  the top-level machine flag `codedEntity`: the boolean `true`, or — when the
  line authors the `libellé « … »` / `surchargeable à la création` facets —
  the enriched object (`{"codedEntity": {"label": {"fr": "Référence"},
  "supplied": true}}`, SSOT `lib/page-spec-coded-entity.ts`; derive-code-specs
  stamps the right form). A `label` facet also lands in the pagespec
  `i18nKeys` for the non-derivable locales (`list.columns.code` /
  `form.fields.code` / `detail.fields.code` in it/de — fr/en travel on the
  facet itself). The `code` appears in
  `columns[]` (list/detail — it IS the business key users see) but NEVER as an
  editable form field: omit it from the form's fields, or at most
  `{"name":"code","readonly":true}` to display the allocated code on edit —
  scaffold-component's validation REJECTS an editable one (the Code is
  engine-allocated at insert by `CodedEntitySaveHandler`). Entities WITHOUT a
  `**Code pattern**` (user-typed referential codes) are untouched: no flag, the
  `code` field stays editable.
  **Mandatory post-step** (same discipline as derive-rule-links): after the
  pagespecs are written, run the deterministic reconciliation —

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

  It adds every missing `codedEntity` flag (boolean or enriched object per
  the authored facets, MERGE-preserving — slots it does not own survive),
  removes stale ones (entité.md is the single source of truth) and emits both
  scaffolder halves for Phase 1. `/ba-audit-prd` PRD-132 re-runs it in
  `mode:"check"` — a hand-stamped flag is never the durable channel.
- **Satellites fed by a parent's action (`rowsCreatedBy`)** — when an entity's
  rows are NEVER created directly (no `create` action anywhere) but by an
  action of its aggregate root ("les lignes sont créées par l'action
  `immobilize` de la liste des véhicules"), record that as DATA, not prose:
  every pagespec of the entity carries the top-level machine field
  `"rowsCreatedBy": ["<ParentEntity>.<actionCode>", …]` (PascalCase parent +
  the parent pagespec's action `code`, e.g. `["Vehicle.immobilize",
  "Vehicle.returnToService"]`). Downstream: `audit-dev-api DEV-API-030` (err)
  flags an entity with no create endpoint, no seed provider AND no declared
  feeding path as UNPOPULATABLE — and verifies each `rowsCreatedBy` entry
  against the parent's own declared actions. Entities born with fixed rows use
  the entité.md `**Valeurs initiales**` table instead; entities with no API at
  all use `**API** : none`.
- **detail views additionally carry** `tabs[]` (the SmartForm's inner `Onglet`
  field groups: `{"key","labelKey","fields":[…]}`) and `relatedTabs[]` (the 360
  view — see the **Related-tabs propagation** block below). Both mined 1:1 from
  the screen, drop nothing. `sections[]` applies to detail views too — the
  groups render as titled cards on the read surface, while `tabs[]` stays
  rendered as panels (see the `sections[]` contract below).
- **form views additionally carry** `fields[]` — the editable field list mined
  1:1 from the screen's `Champs` (enriched from `entité.md`; computed fields
  excluded), shape mirroring `columns[]`:
  `{"key":"name","labelKey":"form.fields.name"}` (+ an optional `"control"`
  hint). This is the list `/ba-audit-prd` **PRD-106** enumerates to verify every
  field label is authored in all 4 locales — a form pagespec without `fields[]`
  is NO-GO. (Without it the generator falls back to the humanised PROPERTY NAME,
  identical in every locale — the "Name on a French form" bug.)
- **form views with inner `Onglet` groups additionally carry** the same
  `tabs[]` shape as detail views (`{"key","labelKey","fields":[…]}`, mined 1:1
  — the BA grouping is an already-paid human judgment, never drop it):
  `scaffold-component` seeds each listed field's form `section` from its tab,
  so the groups render as titled cards (a `/ui-design` `uiDesign` overlay still
  wins). Author `form.section.{key}` in all 4 locales, one per tab.
- **form AND detail views with `**Section « … »**` groups additionally carry**
  first-order `"sections": [...]` — mined **1:1 from the screen's Section
  bullets** (the BA grouping is an already-paid human judgment, never drop
  it), plus the page-level provenance `"sectionsOrigin": "authored"` (mined
  from BA bullets) vs `"derived"` (seeded from `tabs[]` or otherwise
  machine-derived) — the `/ui-design` precedence discipline protects
  `authored` (and absent — legacy fail-safe) and regroups `derived` freely
  (`lib/page-spec-sections.sectionsOriginOf`; derive-form-sections stamps its
  own backfills `derived`). Canonical machine schema:
  `lib/page-spec-sections.ts`. Example:
  ```json
  "sections": [
    {"key":"identity","labelKey":"form.section.identity","label":"Identité","fields":["userId","birthDate"]},
    {"key":"contract","labelKey":"form.section.contract","columns":2,"description":"…","fields":["hireDate","contractEndDate"]}
  ]
  ```
  Array order = on-screen card order; every `sections[].fields[]` entry
  references an existing pagespec field key (⊆ the pagespec's keys — no
  orphan; PRD-111 errs). `labelKey` is conventional `form.section.<camelKey>`
  — author `form.section.{key}` in **all 4 locales** of `i18nKeys`, one entry
  per section, same discipline as the tabs; the labels are SHARED by the form
  AND the sectioned detail render (no `detail.section.*`). Never author
  `sections[]` and `tabs[]` on the same view — one grouping form per screen,
  Section-inside-Tab nesting is not v1 (PRD-112 warns).
- The form's edit render is **read-first by default** (sections render as
  read-only cards with a per-section « Modifier » toggle); a form view may opt
  out with the top-level `"editExperience": "direct"` when the page is a pure
  input form.
- **form views of a screen carrying a `- **Cycle de vie**` bullet additionally
  carry** the first-order `"lifecycle"` block — mined **1:1 from the bullet**
  (canonical machine schema: `lib/page-spec-lifecycle.ts`). Example:
  ```json
  "lifecycle": {
    "statusField": "status",
    "phases": [
      {"key":"paiement","statuses":["PAYEE"],"capturedBy":"marquerPayee","fields":["paymentDate","paymentMethod"],"requiredFields":["paymentDate"]},
      {"key":"soumission","statuses":["SOUMISE","ENVOYEE","PAYEE"],"requiredFields":["dueDate"]}
    ]
  }
  ```
  Mining rules: `statut \`x\`` → `statusField` (camelCase — it MUST be one of
  the pagespec `fields[]`, `readonly`/`readonlyOn:"create"` is fine); each
  `phase « clé »` → one `phases[]` entry (statuses VERBATIM enum values from
  `entité.md`; `action :` → `capturedBy`); owned fields → `fields[]` of the
  phase; `(requis)` marks and `requis dès ce stade :` entries → the phase's
  `requiredFields[]`. Phase-owned fields STAY in the pagespec `fields[]` (their
  `form.fields.*` labels remain PRD-106-mandatory) and are **never
  `required: true`** in the derived scaffold specs (their column stays
  nullable; the obligation is status-guarded — PRD-120 errs otherwise). When a
  phase says `capturedBy`, auto-complete the named action's
  `payloadParameters`: one entry per owned field with `field` = `name` = the
  attribute (`type` from the entité.md attribute type mapping,
  `required: true` for the phase's `requiredFields`). The reserved phase key
  `creation` is never authored; `lifecycle` composes freely with `sections[]`
  (grouping stays orthogonal). Downstream compile: create form/DTO exclusion +
  status-gated edit + status-guarded validation, on ALL strata (PRD-120/121
  audit the contract; `cli/derive-lifecycle` is the deterministic engine and
  the backfill for existing PRDs).
- **Business-rule links (`linkedBusinessRules`)** — REQUIRED on every pagespec
  (an array; `[]` legal): the `BR-…` codes of `règles-métier.md` this screen's
  backend must enforce. This is the ONE field the DEV-API-008 gate reads — a
  rule listed nowhere ships unenforced with the gate green at count 0, so the
  transport is NEVER left to memory: after writing the pagespecs, run the
  deterministic backfill (mandatory post-step, same discipline as
  derive-form-sections / derive-lifecycle):

  ```bash
  npx --prefer-offline tsx skills/business-analyse/create-prd/cli/derive-rule-links/index.ts \
    --spec '{"baRoot":".smartstack/ba","app":"<APP>","module":"<MODULE>","mode":"backfill"}'
  ```

  It maps each err/warn rule to its section (rule-doc folder → linked-UC
  sections → `needs-judgment`, never a guess), adds the codes (authored links
  always win, nothing is ever removed), and reports exemptions by their real
  channel (`access` → RBAC, `numbering` → codePattern, `info`,
  `- **Enforcement** : plateforme|manuel`). `needs-judgment` entries are YOURS
  to resolve — author the link or the exemption. `/ba-audit-prd` PRD-129/130
  re-run it in `"mode":"check"`.
- **Use-case links (`linkedUseCases`)** — REQUIRED on every pagespec (an
  array; `[]` legal): transport the screen.md `- **Cas d'usage liés** :` codes
  VERBATIM onto the pagespec of each view, and each custom action's `UC:` tag
  as that action's `ucReference` (the schema REJECTS a custom kind:api action
  without it). This pair is what the coverage gate reads — `/ba-audit-prd`
  PRD-131 (err) verifies every user-goal UC of the module lands in at least
  one pagespec's `linkedUseCases[]` or an action's `ucReference` (scheduled
  UCs are covered by the scheduled surface; `subfunction`/`summary` levels are
  exempt). Verify with the deterministic counter:

  ```bash
  npx --prefer-offline tsx skills/business-analyse/create-screen/cli/derive-uc-coverage/index.ts \
    --spec '{"baRoot":".smartstack/ba","app":"<APP>","module":"<MODULE>"}'
  ```
- **Core-projected columns (`source`)** — when a screen displays a field read
  from a Core navigation (a `Proj:` line in `prd.entities.md`), the column
  carries the same contract verbatim:
  `{"key":"customerCompanyName","labelKey":"list.columns.customerCompanyName","sortable":true,"source":{"nav":"Customer","target":"TenantOrganisation","property":"Name","fkField":"CustomerCompanyId"}}`.
  `target` defaults to `nav` (set it when the nav is renamed — Customer →
  TenantOrganisation); `fkField` defaults to `{nav}Id`; add `"fallbackLocal":"Email"` for a
  person-`optional` overlay; `property` is a SINGLE identifier (never
  `"Department.Name"` — whitelist→whitelist navigation is ignored by the
  runtime). Keep `isComputed` false and omit `formatHint` for string
  projections. Phase 2b projects these through the nav (`x.Customer.Name`) —
  never invent a local column.
- **Person entities** — pagespecs of screens bound to a person entity carry an
  optional top-level machine field mirroring the `entité.md` `Personne` line:
  `"person": {"mode":"mandatory","identityFields":["FirstName","LastName","Email"]}`.
  On **form** views the editable field is the FK (`userId`) with
  `"fkTo": {"entity":"User","module":"core","apiEndpoint":"/api/core/users/lookup"}`
  — identity fields never appear as form fields in `mandatory` mode.
- **FK fields (general)** — for a non-Core FK the form field carries
  `"fkTo": {"entity":"{Target}","module":"{module}","navRoute":"{targetModule}.{targetSection}","plural":"{TargetPlural}","apiEndpoint":"/api/{targetModule}/{targetSection}/lookup"}`
  (the NavRoute-resolved route the target's integration controller serves; `<EntityLookup>` reads it).
  Core targets use `/api/core/{plural-kebab}/lookup` as above. Always emit
  `apiEndpoint` — the frontend fallback derives the same integration URL but can
  drift on irregular plurals (`Category → Categories`) when it is omitted.
  **No permission field on `fkTo`** — the `/lookup` endpoint is gated
  server-side `[RequirePermission(x.lookup, x.read)]` (ANY), and the actor
  grants come from the machine-derived block of rbac.md
  (`derive-lookup-grants`) — nothing to author in the pagespec.
- **Reference filters (`filters[].fkTo`)** — a list filter that matches a FK is
  the SAME contract as a form FK field, on the filter entry:
  ```json
  {"field":"departmentId","control":"lookup","labelKey":"list.filters.department",
   "fkTo":{"entity":"Department","app":"hr","module":"rh","navRoute":"rh.departements","apiEndpoint":"/api/rh/departements/lookup"}}
  ```
  Two rules, both load-bearing:
  - **`field` is the FK PROPERTY** (`departmentId`), never the relation name
    (`department`) the screen bullet uses. That one string is simultaneously the
    filter state key, the DTO property, the `?…=` query param and the backend's
    `[FromQuery] Guid?`. A relation-named filter lines up with none of them: the
    control degrades to a free-text box over a Guid AND the param is dropped on
    the wire — a filter that neither looks up nor filters.
  - **`fkTo` is carried on the filter**, resolved exactly like a form field's
    (same block, same `apiEndpoint` rule). The renderer does NOT re-derive it
    from the entity's fields: it consumes what the pagespec declares, and
    `scaffold-component` REJECTS a `lookup` filter that resolves to nothing.
  `screen.md` keeps writing `type: "lookup"` + `entity: "Department"` — that is
  the BA's vocabulary; resolving it into `fkTo` is create-prd's job. For an
  already-written PRD, backfill both rules deterministically:
  `npx tsx skills/ba-develop/cli/derive-filter-fks/index.ts --spec '{"moduleRoot":".smartstack/ba/<APP>/<MODULE>"}'`
  (idempotent; `labelKey` is never renamed, so authored translations survive).
  Audited by PRD-113 (`err`) and, on the generated page, DEV-UI-033.
- **Column priority & visibility budget** — the list table shows AT MOST ~7
  columns by default; every other column stays in the ListDto and is reachable
  through the column picker ("Colonnes"). The pagespec DECIDES which ones show
  through `"priority"` per column — this is a rendering judgment, it never
  changes the DTO (columns stay mined 1:1, drop nothing):
  - `"always"` — pinned visible at every width, not even hidable through the
    picker. Reserve it for the row's identity: the natural key (`code`/number)
    plus the 1-2 values that name the row when the key is empty.
  - `"high"` — visible by default. Give it to the DECISIVE columns: the status
    badge, the primary date, the most-cited FK, 1-3 business amounts. Keep
    `always` + `high` **≤ 7 in total** (PRD-109 warns beyond).
  - `"medium"` / `"low"` — hidden by default, available in the picker. This is
    where audit/metadata columns go: `issuedBy`/`cancelledAt`-style trails,
    secondary references, projections only a specialist filters on.
  - **From 8 columns up, EVERY column must carry a priority** — with none
    authored the scaffolder falls back to declaration order (first 7 visible),
    a safety net, not a judgment (PRD-109 warns).
  Responsive rendering underneath is unchanged: `<ResponsiveDataTable>` keeps
  always-columns at every width and staggers the other default-visible ones
  `md → lg → xl`; override per column with `"minBreakpoint":"sm|md|lg|xl"`,
  and `"truncate":false` opts out of the ellipsis+tooltip.
- **Filter tiers (progressive disclosure)** — the filter toolbar shows the
  global search + **at most 3 primary filters**; every other filter renders
  behind the "Plus de filtres" toggle (badge = active count, active filters
  echoed as removable chips). Author `"tier"` per filter:
  - `"primary"` — the 2-3 cuts that cover most sessions: the workflow status,
    the most-cited FK (client, owner), the primary date-range OR a strong
    boolean predicate (overdue). **≤ 3 per list** (PRD-110 warns beyond).
  - `"advanced"` — everything else (exact number, currency, payment term,
    cancel reason…). Still declared 1:1 from `screen.md` (PRD-082 parity) —
    the tier is a RENDERING decision, never a silent drop.
  As soon as ONE filter declares a tier, the authored tiers win verbatim
  (unmarked → advanced). With no tiers at all the scaffolder falls back to a
  deterministic heuristic (first select/status, first FK lookup, first
  date-range) — a net, not a judgment: author the tiers on any list with ≥ 4
  filters (PRD-110 warns otherwise).
  **Never author a global-search filter** (`"field": "q"|"search"|…`): the
  toolbar always carries the global search box; the scaffolder fuses such a
  filter into it instead of rendering a dead duplicate input. If `screen.md`
  lists one, keep it in `filters[]` for PRD-082 parity — it simply renders as
  the search box.
- **KPI stat row (`stats[]`)** — the `- **Indicateurs** :` bullet of a
  SmartListView maps to `stats[]` on the LIST pagespec (canonical Zod shape:
  `PageStatMinSchema` in scaffold-component `types.ts`). Each entry:
  `{ "key", "labelKey": "stats.<camel>", "type": "kpi"|"counter",
  "filter"?: { "field", "value" }, "icon"?, "permission"? }`. Rules:
  - **≤ 4 stats** (one grid row — PRD-115 warns beyond) and every `labelKey`
    authored in `i18nKeys` for ALL 4 locales (PRD-115 errs otherwise).
  - A stat is a **count of the page's entity**. A scoped stat's
    `filter.field` MUST name a declared `filters[]` field of the same
    pagespec (that is the server-bound wire param); the value is the raw
    stored code (`"active"`), never a label. Anything else (foreign entity,
    sum/avg aggregation) is NOT wirable — the scaffolder renders it visibly
    unwired ("—") with a warning; move such figures to a SmartDashboard.
  - Rendering: a StatCard row ABOVE the FilterBar, one server count per stat
    (`use{Plural}({page:1,pageSize:1,…}) → totalCount`), permission-gated
    when `permission` is set.
- **Initial sort, density, business empty state** — three optional list
  fields (plan UI 2.4):
  - `"defaultSort": { "key", "direction" }` from the `- **Tri par défaut** :`
    bullet — `key` names a declared sortable column (PRD-118).
  - `"density": "compact"` — a PRD/ui-design judgment (never a BA bullet):
    author it on high-volume back-office lists (≥ 8 columns or the UCs show
    power-user scanning); default comfortable.
  - `"emptyState": { "titleKey": "empty.title", "descriptionKey":
    "empty.hint", "icon", "withCreate" }` from the `- **État vide** :` bullet —
    keys authored in the 4 locales (PRD-118); `withCreate` only when the
    entity has a form sibling (the CTA is permission-gated create).
- **Quick segments (`segments[]`)** — the `- **Segments** :` bullet of a
  SmartListView maps to `segments[]` on the LIST pagespec: each entry
  `{ "key", "labelKey": "segments.<camel>", "filter"?: { "field", "value" } }`.
  Rules: the FIRST segment is the unfiltered « Tous » default (PRD-116); a
  filtered segment's `filter.field` MUST be a declared `filters[]` field of
  the same pagespec (server-bound wire param) and the value is the raw stored
  code, never a label; every `labelKey` authored in the 4 locales. Rendering:
  a SegmentedControl row above the FilterBar — the active segment wins over
  the filter input on its field, pager reset on switch. 2-4 segments; beyond
  that it is a filter, not a cohort.
- **Cards representation (`viewModes`) — the SmartCard folding rule** — a
  `SmartCard` screen in a `*-list` section produces **NO pagespec of its own**:
  it folds into the section's LIST pagespec as `"viewModes": ["table","cards"]`
  (+ `"defaultViewMode": "cards"` when the SmartCard is the section's primary
  or only screen). Mirror of the kanban representation rule — a gallery is a
  second layout of the same list, never a second page/route. The generated
  list page then carries a Table ⇄ Cartes toggle (SegmentedControl); card
  anatomy derives deterministically from `columns[].priority` (title = first
  `always` column, badge = the status column, subtitle = next always/high,
  meta = up to 3 more default-visible columns) — no card-specific authoring
  in the pagespec (the `/ui-design` overlay refines it later if needed).
  Only wirable on a server-driven list (PRD-117 gates the shape).
  `viewModes` does NOT decide whether cards exist: every generated list falls
  back to them automatically on a container too narrow to give each column its
  minimum width. Authoring it declares that cards are a first-class reading of
  this section — it is what puts the manual Table ⇄ Cartes toggle on screen.
- **Kanban representation (`kanban`) — the SmartKanban folding rule** — a
  `SmartKanban` screen in a `*-list` section produces **NO pagespec of its
  own**: it folds into the section's LIST pagespec as the first-order
  `"kanban": { … }` block (SSOT `lib/page-spec-kanban.ts`) plus
  `"viewModes": [...,"kanban"]` (+ `"defaultViewMode": "kanban"` only when the
  section has no SmartListView). The board is a third layout of the same list
  — one route, one FilterBar, one URL state; the legacy separate
  `/…/list/kanban` page is retired. The block carries:
  - `statusField` + `columns[{key, labelKey, color?, initiallyHidden?}]` from
    the screen's `Champ statut`/`Colonnes` bullets — column keys are the
    status enum values VERBATIM (entité.md), `labelKey` =
    `kanban.columns.<key>` authored in the 4 locales;
  - `transitions[{from, to, rule}]` — the from→to edges PROJECTED from the
    module's workflow rules (`Type: workflow|state-transition`, `Flow`
    bullets). The Flow graph stays the transition SSOT; the block is its
    deterministic projection. The generated board disables drops outside the
    matrix AND scaffold-business compiles the SAME matrix into the `move`
    service guard. Absent = open matrix; `[]` = read-only board;
  - optionally `titleField`/`subtitleField`/`cardFields[≤4]` (card anatomy),
    `transitionErrorCode` (the workflow rule's `Code d'erreur`),
    `terminalColumns` (default: columns with no outgoing edge).
  Do NOT hand-author the block: after writing the pagespecs, run the
  deterministic backfill (mandatory post-step, same discipline as
  derive-lifecycle / derive-rule-links / derive-detail-summary):

  ```bash
  npx --prefer-offline tsx skills/business-analyse/create-prd/cli/derive-kanban-spec/index.ts \
    --spec '{"mode":"derive","pagespecDir":".smartstack/ba/<APP>/<MODULE>/pagespecs","moduleRoot":".smartstack/ba/<APP>/<MODULE>"}'
  ```

  It folds the section's SmartKanban from ALREADY-authored anchors only —
  columns re-anchored on the enum, transitions from the Flow rules (rules
  whose tokens map to nothing → `needs-judgment`, never a silently open
  matrix), i18n column labels seeded (fr copy, `needsTranslation` →
  `/ba-translate-prd`), and the canonical `move` action derived when the
  graph warrants DnD (permission = the page's root + `.update`, `ucReference`
  = the pagespec's first linked UC, `guardRules` = the contributing BR codes).
  It NEVER touches an existing `kanban` (`already`). `/ba-audit-prd` PRD-135
  re-runs it in `"mode":"check"` (legs a-h: columns ⊆ enum, Flow parity both
  ways, viewModes coherence, no standalone kanban pagespec, labels seeded).
- **Detail summary (`summary`)** — the optional `- **Résumé** :` bullet of a
  detail screen maps to `summary` on the DETAIL pagespec:
  `{ "titleField", "statusField", "fields": [≤4] }` — every key an ENTITY
  field name (camelCase, PRD-119). Rendering: a header band above the
  tabs/sections — big title value, status Badge, label/value meta pairs —
  through the same read-value machinery as the body (pills, FK labels,
  dates). Author it on 360 fiches where the identity + state must be visible
  from every tab; `/ui-design` refines via `uiDesign.detail.summary`. After
  writing the pagespecs, run the deterministic backfill (mandatory post-step,
  same discipline as derive-form-sections / derive-lifecycle /
  derive-rule-links):

  ```bash
  npx --prefer-offline tsx skills/business-analyse/create-prd/cli/derive-detail-summary/index.ts \
    --spec '{"mode":"derive","pagespecDir":".smartstack/ba/<APP>/<MODULE>/pagespecs","moduleRoot":".smartstack/ba/<APP>/<MODULE>"}'
  ```

  It derives the band from ALREADY-authored anchors only — titleField from
  the entité.md `**Affichage**` line (`: Id` = the conscious opt-out, nothing
  written), statusField from the entity's SINGLE state enum (ambiguous →
  slot omitted, `/ui-design` decides), ≤ 4 required stored meta fields — and
  NEVER touches an existing `summary`/`uiDesign.detail.summary`
  (`already`). `needs-judgment` entries are YOURS to resolve — author the
  `- **Résumé** :` bullet or the `**Affichage**` line. `/ba-audit-prd`
  PRD-134 re-runs it in `"mode":"check"` (a fiche with ≥ 4 rendered strip
  tabs and no band is a warn).
- **Custom actions propagation** — the `- **Actions personnalisées** :`
  sub-bullets of `screen.md` map 1:1 to `actions[]` entries with the FULL
  custom-action schema (`code`, `kind`, `scope`, `endpoint`, `httpMethod`,
  `payloadDto`, `payloadParameters`, `responseDto`, `targetScreen`, `targetRoute`,
  `labelKey`, `permission`, `variant`, `ucReference`, `guardRules`,
  `workflowTransition`, `crossModuleDeps`). The canonical Zod schema lives in
  `lib/page-spec-actions.ts` (`PageCustomActionSchema`) — `/ba-audit-prd`
  validates each entry against it. Defaults applied when missing:
  - `kind` = `"api"` (legacy fallback for standard CRUD codes).
  - `endpoint` = kebab-case of `code` (e.g. `syncFromPce` → `sync-from-pce`).
  - `httpMethod` = `"POST"`.
  - `payloadDto` / `responseDto` = `null` unless the linked UC main flow
    declares parameters or returns a typed payload.
  - `payloadParameters` = the input FIELDS the action collects before firing
    (each `{ name, type }`, `type` ∈ `text|textarea|number|date|lookup|file|select`).
    This — NOT `payloadDto` — is what makes `scaffold-component` render a
    `<CustomActionDialog>` and the frontend POST a body. So a `payloadDto` whose
    body is **user-entered** MUST also carry `payloadParameters`; a `payloadDto`
    with no `payloadParameters` = an **optional / server-defaulted** body (the
    controller binds it with `EmptyBodyBehavior.Allow`, no 415). `/ba-audit-prd`
    (via `DEV-API-018`) warns on the latter so the choice is deliberate.
  - `variant` = `"secondary"` (CRUD `create` keeps `"primary"`, `delete` keeps
    `"danger"`).
  - For `kind: "navigate"`, resolve `targetScreen: SCR-…` against the screen
    registry of the project to compute a `targetRoute` of shape
    `routes.<sectionFolder>.<view>(<arg>)` — view defaults to `detail(item.id)`
    for row scope and `list()` for header scope when the target is a
    `SmartListView`.
  - **Workflow detection** — when an action's linked `BR: BR-…` resolves to a
    rule whose `Type` field is `workflow` and whose flow lists a status transition,
    populate `workflowTransition: { fromStatus, toStatus, flowParameters }`.
    Same parsing rules as Phase 2 SK-001/002 (see `ba-develop/SKILL.md`).
    **EXACT shape — these are the silent killers `PageCustomActionSchema` rejects
    (and PRD-095 now blocks deterministically):**
    - `fromStatus` is an **array of strings** (`["EnAttente"]`), NEVER a bare
      string (`"EnAttente"`), and must be non-empty.
    - `flowParameters` is an **array of parameter names** (`["derogation"]`),
      NEVER an object (`{ derogation: ... }`); use `[]` when there are none.
    - `toStatus` is a single string.
    - For a non-workflow `kind:"api"` action (approve/recompute/export… with no
      status transition), **OMIT `workflowTransition` entirely** — do NOT write
      `workflowTransition: null` (the field is `.optional()`, not nullable, so
      `null` is rejected and the whole action is dropped).
  - **Cross-module dependency** — when the linked UC references an entity from
    another module not yet in this `prd.entities.md`, add the foreign module
    name to `crossModuleDeps: ["<module>"]` and set `needsRefinement: true`
    with a note. Phase 2 emits a `// BLOCKED[CROSS-MODULE: …]` TODO so the
    audit catches the deferral.
  - The `endpoint` written here is the EXACT string that scaffold-controller
    will emit in `[HttpVerb("<endpoint>")]` AND scaffold-api-client will emit
    in `apiClient.<verb>('/api/.../<endpoint>')`. Never derive separately
    downstream — frontend and backend stay aligned by construction.
- **Related-tabs propagation (the 360 view)** — the `- **Onglet lié « … »** :`
  bullets of a detail/edit `SmartForm` map **1:1, drop nothing** to a
  `"relatedTabs": [...]` array on the **detail** pagespec. The canonical Zod
  schema lives in `lib/page-spec-related-tabs.ts` (`PageRelatedTabSchema`) —
  `/ba-audit-prd` validates each entry against it (PRD-103..105). Mapping from
  the screen.md vocabulary:
  - `relationship` → `relationFk` and `screenTarget` → `targetScreen` (the
    schema also accepts the aliases, but author the canonical names).
  - `affichage` → `displayMode` ∈ `table|cards|summary` (default `table`;
    `report` is NOT valid in v1 — PRD-104 rejects it).
  - Resolve `relatedModule` + `relatedSection` from the target list screen's
    code chain (and `relatedPlural` from the related entity when its plural is
    irregular) — scaffold-component builds the hook import + navigation routes
    from these, no cross-pagespec read.
  - **`relatedRouteFamily`** = the target list pagespec's `routeFamily` (when
    that pagespec carries one — the sub-view pattern). This is the route family
    the tab NAVIGATES through (`routes.{camel}.create()/detail()`); without it
    the generator falls back to `relatedSection` — correct only when the target
    owns its menu section. Copy, never re-derive.
  - **`createPermission`** = the permission of the target list pagespec's
    `actions[]` entry `code:"create"`, verbatim — the SAME permission its own
    "Créer" button carries. Never recompose `{module}.{section}.create`: real
    shapes diverge (`portfolio.list.work-package.create`, 4 segments with a
    resource axis).
  - **`withCreate`** — author it EXPLICITLY: `false` when the related entity
    has no create form pagespec (`<RelatedEntity>.form.md` absent — audit
    journals, satellites created by business flows), `true` otherwise. There is
    no implicit `true` default any more; an explicit `true` with no resolvable
    create form is a generation error.
  - **`withRowOpen`** — `false` when no `<RelatedEntity>.detail.md` exists
    (rows stay inert instead of navigating to a page that resolves to the
    wrong surface).
  - `permission` copied verbatim when authored; omitted → the generator
    defaults to `{relatedModule}.{relatedSection}.read`. Related tabs render
    LISTS (table/cards/summary) — their permission stays `.read`, NEVER
    `.lookup` (`lookup` only opens id+name pairs; a tab's rows need the read
    surface).
  - When the related entity lives in ANOTHER module's PRD, set
    `"crossModule": true` (PRD-105 relaxes the same-PRD entity check).
  - When the related entity lives in another APPLICATION, set
    `"relatedApp": "<kebab app code>"` — the code of the application the entity
    belongs to, never the page's own. The generator keys the tab's hook and
    routes imports by it (`@/features/{relatedApp}/{relatedModule}/…`,
    `@/extensions/{relatedApp}-{relatedModule}Routes`) and defaults to the
    PAGE's application when it is absent, so an unsaid application produces
    paths that do not exist. `derive-related-tabs` seeds it from the
    `scope cross-module (APP/MOD)` the relation already carries — copy it,
    never guess. Omit it entirely for a target in this application: the field
    is only ever written when it says something. PRD-136 enforces it.
  - Any tab whose `(relatedApp ?? own app, relatedModule)` differs from the
    page's own is rendered under the TENANT-CATALOGUE guard
    (`useModuleAvailability().hasModule(app, module)`): where the target module
    was not delivered to a client, the tab does not appear at all. Nothing to
    author — it follows from the target. To force a tab to stay visible
    regardless (rare: a module always shipped alongside this one), author
    `"availabilityCheck": false`. DEV-UI-049 audits the generated page.
  - Example entry (sub-view target — note relatedSection is the MENU section
    while the navigation goes through relatedRouteFamily):
    `{"key":"lots-de-travail","displayMode":"table","relatedEntity":"ProjectWorkPackage","relationFk":"projectId","relatedModule":"portfolio","relatedSection":"list","relatedRouteFamily":"work-packages","targetScreen":"SCR-PROJECTS-PORTFOLIO-LIST-006","permission":"portfolio.list.read","createPermission":"portfolio.list.work-package.create","withCreate":true,"withRowOpen":true,"labelKey":"detail.related.lots-de-travail.label"}`
  - Example entry (target in ANOTHER application — relatedApp names where the
    entity lives; the generated page gates the tab on the tenant catalogue):
    `{"key":"factures","displayMode":"table","relatedEntity":"Invoice","relationFk":"clientId","relatedApp":"facturation","relatedModule":"factures","relatedSection":"factures","targetScreen":"SCR-FACTURATION-FACTURES-LIST-001","permission":"facturation.factures.read","labelKey":"detail.related.factures.label"}`
  - Example entry (normal one-entity section — no routing fields needed):
    `{"key":"invoices","displayMode":"table","relatedEntity":"Invoice","relationFk":"clientId","relatedModule":"crm","relatedSection":"invoices","targetScreen":"SCR-CRM-INVOICES-LIST-001","permission":"crm.invoices.read","withCreate":true,"labelKey":"detail.related.invoices.label"}`
  - i18n: author `detail.related.{key}.label` in ALL 4 locales (real words —
    the floor only humanises the key). The structural leaves
    (`detail.related.{key}.empty/loading/error/create/viewAll/count`) are
    floor-filled automatically.
- `i18nKeys`: 4 locales (`fr/en/it/de`), parallel key sets — **all four locales
  AUTHORED with a real translation**. NEVER a `"[xx] <fr>"` placeholder. **You are
  the translator**: these are short UI strings (titles, columns, buttons, filters,
  widget labels) — translate the FR value into idiomatic `en`/`it`/`de` right here.
  There is NO downstream translation pass; `scaffold-component` copies your values
  verbatim into the shipped bundle, so a placeholder reaches the end user as a raw
  `[en] Opportunités` marker. Placeholders are now **rejected**: `/ba-audit-prd`
  **PRD-089 `err`** blocks GO on any value matching `[fr|en|it|de] …`, and
  **DEV-UI-029 `err`** blocks the generated app. Author the real words, or route to
  `/ba-translate-prd` if a batch of legacy pagespecs still carries placeholders.
  **`i18nKeys` is an OVERRIDE set, not the full catalogue.** `scaffold-component`
  emits a COMPLETE floor for every key its templates render — the generic
  structural keys (`breadcrumb.section`, `list.actionsColumn`, `list.edit`,
  `list.delete`, `list.search`, `list.loading`, `list.empty`, `list.error`,
  `detail.loading`, `detail.notFound`, `detail.edit`, `form.submitCreate`,
  `form.submitUpdate`, `form.save`, `form.cancel`, `form.required`, `kanban.*`,
  `reconduction.*`, `home.kpis`, `home.navigation`, …) are filled automatically
  in all 4 locales. Author here ONLY the keys that carry business wording; the
  PRD value wins over the floor. Do NOT enumerate the generic keys (the thin
  `{"list.title":"…"}` example above is the norm, not an omission).
  Business keys worth authoring, by view type:
  - **List**: `list.title`, `list.subtitle`, `list.create`,
    `list.columns.{field}`, `list.filters.{field}`
  - **Detail**: `detail.fields.{field}`, `detail.related.{key}.label` (one per
    related tab), `detail.tabs.{key}` (one per inner field tab)
  - **Form**: `form.createTitle`, `form.editTitle`, and — **MANDATORY, one per
    `fields[]` entry, all 4 locales** — `form.fields.{field}` (**PRD-106 `err`**
    blocks GO when one is missing: the generator's field-label floor merely
    humanises the PROPERTY NAME identically in every locale, so an unauthored
    label ships as an English "Name" on a French form — invisible to PRD-089,
    which only catches `[xx]` markers). Author `detail.fields.{field}` /
    `list.columns.{field}` with the same wording where those views exist.
    Optional: `form.placeholders.{field}` overrides a field's placeholder (FK
    lookup contextual search, textarea/plain input hint — a SIBLING branch of
    `form.fields`, NEVER `form.fields.{field}.placeholder`);
    `form.options.{field}.{value}` — one per enum option, all 4 locales
    (sibling of `form.fields`: an option label nested under the field-label
    leaf cannot ship); `form.section.{key}` names each `tabs[]`/`sections[]`
    group — shared by the form AND the sectioned detail.
    (Override `form.submitCreate`/`form.submitUpdate` only for non-default
    button copy.)
  - **Dashboard**: `dashboard.title`, `dashboard.subtitle`,
    `dashboard.widget.{key}` (one per widget)
  - **Home** (app-home/module-home/section-home): `home.title`, `home.subtitle`,
    `home.widgets.{key}` (per widget), `home.quicklinks.{key}` (per quickLink)
  - **Custom actions**: `list.actions.{code}` / `detail.actions.{code}` — one per
    `actions[]` entry whose `labelKey` uses that path. For an action carrying
    `payloadParameters[]`, param labels live in the SIBLING branches
    `{root}.actionParams.{code}.{param}` (recommended — the floor merely
    humanises the param name) and `{root}.actionParamOptions.{code}.{param}.{value}`
    for `type:select` options. NEVER `{labelKey}.params.{param}` — a key nested
    under the button-label leaf cannot coexist with the label in the JSON
    (legacy keys in that shape are remapped by `scaffold-component`).
- For **home views** (`app-home`/`module-home`/`section-home`) AND the
  **dashboard view** (`SmartDashboard`), include a `widgets` array mined from
  `screen.md`'s `Widgets` (home views additionally include `quickLinks`):
  - `"widgets": [{"key":"total","label":"Clients","type":"kpi","entity":"Client",
    "aggregation":"count","col":3,"labelKey":"home.widgets.total"}]` — for a
    **dashboard** widget use `"labelKey":"dashboard.widget.{key}"`, any `type` in
    `kpi|counter|chart-line|chart-bar|chart-pie|list`, a `col` (1-12) and an
    optional `permission` (cross-module widgets).
  - `"quickLinks": [{"key":"to-list","label":"Annuaire clients","icon":"list",
    "screenTarget":"SCR-CRM-CLIENTS-LIST-001","labelKey":"home.quicklinks.to-list"}]`
    (home views only)
  `scaffold-component` renders dashboard widgets through `<WidgetRenderer>` / `<DashboardGrid>`
  and home widgets as KPI cards + navigation buttons. Omitting a dashboard's
  `widgets` produces an empty grid.
- Set `needsRefinement: true` + a note for any inferred field.
- Synthesise a `detail` view per `list`+`form` pair (`needsRefinement: true`,
  note "detail not captured by BA") — **EXCEPT that when the section's
  `SmartForm` (edit/detail) declares `Onglet lié` bullets or inner `Onglet`
  tabs, the detail pagespec MUST carry them (`relatedTabs[]` mapped per the
  propagation block above, `tabs[]` for the field groups). Dropping an
  authored tab here is exactly the silent 360-view loss PRD-103 blocks.**

## Single-pagespec entry point — ONE screen, after the fact

The mirror of `/ba-create-business-rules` § Single-UC entry point. When the
request names ONE screen code of a module that **already has a PRD**
(« write the pagespec for SCR-CRM-PIPELINE-OPPORTUNITES-004 », or `/ba-change`
kind=screen / kind=entity routing here), work on **that pagespec only**:

1. Read the screen block in its `screen.md` (Grep the code), the entity in
   `entité.md`, the row(s) of `rbac.md` its permission comes from, and the
   sibling pagespecs of the same entity (their `routeFamily` / `routeParent`
   if any — a satellite's views share them).
2. Write **one** `pagespecs/<Entity>.<view>.md` per the contract above:
   `screenCode` / `permission` / `entity` verbatim, `columns` / `actions` /
   `filters` (or `fields`) mined 1:1 from the screen bullets, `linkedUseCases`
   from `- **Cas d'usage liés**`, `routeFamily` + `routeParent` when the entity
   is a satellite of the section. A SmartKanban / SmartCard writes **no** file
   — it folds into the section's LIST pagespec (`derive-kanban-spec --mode
   derive` / `viewModes`).
3. Add the screen's line to `prd.frontend.md` (and the handler bullet to
   `prd.api.md` for a `kind: api` custom action). **Do not** regenerate
   `prd.md`, the slices or any sibling pagespec — a re-Write of an existing
   pagespec drops the `uiDesign` overlay and the `lifecycle` block `/ui-design`
   wrote into it.
4. Run the derivations on the new file: `derive-rule-links --mode backfill`,
   then for a form `derive-lifecycle --mode derive` + `derive-form-sections`,
   for a detail `derive-detail-summary --mode derive` +
   `derive-related-tabs --mode validate` with `pagespecs: true`; a declared
   `routeFamily` → `ba-develop/cli/derive-nav-resources` (collisions are
   blocking).
5. Hand back to `/ba-audit-prd` on the module (the new page is audited with
   its siblings, score ≥ 80), then `/ba-develop <APP>/<MODULE>` without
   `--force` — Phase 2b re-enters on DEV-API-012, Phase 3 regenerates the
   added page only.

## `claude.md` — the module's generated-project CLAUDE.md

```markdown
# {ModuleLabel} — CLAUDE.md
## Stack
- .NET 10 + EF Core 10 · React 18 + TS strict · Shoelace · Zustand · React Router v6 · PostgreSQL/SQL Server
## Architecture
- Backend Clean Architecture (Domain/Application/Infrastructure/Api); Frontend 5-layer (pages/business/services/stores/components). Mutations via business hooks.
## Conventions
- Entities under Domain/Entities/<ModuleCode>/; snake_case columns, schema {module}; permissions {app}.{module}.{section}.{action} via [RequirePermission]; tests colocated.
## Module-specific decisions
<copy the module's technical constraints>
## Non-goals
<copy prd.md non-goals so every dev session sees them>
```

Fill from the module's docs; keep it to one screen.

## Self-check before finishing

- ≥ 3 negated non-goals, including every upstream out-of-scope.
- `## Goals` / `## MoSCoW` / `## Technical constraints` populated (no User Stories
  / Acceptance Criteria sections — those live in `<section>/use-case.md` under
  each UC).
- Every entity from `entité.md` appears in `prd.entities.md`.
- Every `Personne` line from `entité.md` is surfaced as a `Person:` bullet in
  `prd.entities.md` (+ its `Rel: … *→1 User — FK UserId, scope core` line), and
  every Core-projected DTO field has its `Proj:` line backed by a matching
  `Rel: … scope core` line.
- Every screen from `screen.md` has a `pagespecs/*.md` (+ synthesised detail) and
  each pagespec's fenced ```json block parses and copies its `screenCode`,
  `permission`, `entity` and upstream codes verbatim; every column displaying a
  Core-projected field carries its `source` block (consistent with the `Proj:`
  line).
- Each slice non-empty, self-contained, disjoint.

## Hand off

Acknowledge in one line, then: the PRD must pass `/ba-audit-prd` (dev-ready score
≥ 80) before development. On a GO, develop with `/ba-develop`.

> Note: regeneration preserves the upstream UC + AC ids (they live in
> `use-case.md`, not in the PRD). The PRD itself has no story/AC ids to renumber.
> Don't invent features absent from the upstream BA — surface the gap and route
> back to the owning phase.

> **Unified fiche (development contract)** — an entity whose pagespecs declare
> BOTH a `form` and a `detail` view (and no `editExperience: 'direct'`) is
> developed as ONE fiche: the detail page edits in place (read-first sections),
> `/edit` routes to it, and the form page serves creation only. Author
> `sections[]`/`lifecycle` on the FORM pagespec as usual — the generators
> resolve them for the fiche too. PRD-114 (a mutable list declares its
> form/detail siblings) is what makes the pair exist; a form WITHOUT a detail
> sibling keeps the legacy full-page edit (consider authoring the detail).

> **Dashboards bind their host entity (PRD-125)** — a `view: dashboard`
> pagespec without `entity` ships a page with nothing to call (the dashboard
> endpoint is generated per HOST entity). Bind the dominant entity of the
> section; a genuinely entity-less KPI surface becomes a home view
> (`section-home` widgets) marked `needsRefinement`, never a dashboard
> pagespec.
