---
name: ba-audit-data-model
description: >
  Audits the conceptual data model (MCD) of a `.smartstack/ba/` module — attributes,
  FK integrity, cycles, naming, orphans, traceability, indexes, implicit FKs,
  unreferenced lookups, classification conventions, code-pattern numbering,
  Core-entity duplication, platform-capability steering, no file content in
  the MCD, derived declarations, and code-like classification — near-miss
  Code-pattern declarations + unclassified synonym attributes
  (Référence/Numéro/Matricule…) — plus, moved LEFT of the readiness GO: index
  integrity (every **Index** column exists, never TenantId in a unique — the
  scaffolder's own error, six phases earlier), complete attribute types (string
  maxLength, decimal precision/scale, enum values), no framework field listed,
  tenancy declared (**Portée** → tenantMode), and the model retaining what the
  BA no longer uses — dead columns (attribute grain) and leftover state values
  (value grain), and the business TEST DATASET `jeu-de-test.md` — present where
  the module carries business entities, coherent with the MCD, the rules, the
  actors and the cited modules' datasets, never on a reference table, every Flow
  status represented (DM-001..032). Reads the module's
  `entité.md` + `use-case.md` + `règles-métier.md` (+ `jeu-de-test.md`), writes a verdict to
  `<MODULE>/_audit/entité.md`. Run after `/ba-create-data-model` or as part of
  pre-dev readiness.
allowed-tools: [Read, Write, Glob, Grep, Bash]  # Bash: the audit-ba engine (deterministic mechanical rules)
---

# ba-audit-data-model — Data Model (MCD) audit

You audit the conceptual data model of a `.smartstack/ba/` module against the
rules below and write a verdict file. The rules are unchanged from the SmartStack
convention; only the I/O is file-based.

## Deterministic engine — how this audit runs

The MECHANICAL rules of this dimension are evaluated by the shared `audit-ba`
CLI (see `/ba-audit-run`) — **never by reading the corpus yourself, never by
spawning per-module subagents** (the 394M-token incident shape). Your only
job here is the judgment residue.

1. **Run the engine, scoped to this dimension**:

   ```bash
   npx --prefer-offline tsx skills/business-analyse/audit-run/cli/audit-ba/index.ts \
     --spec '{"baRoot":".smartstack/ba","scope":{"app":"<APP>","module":"<MODULE>"},"dimensions":["data-model"]}'
   ```

2. **Exit 3 = parsing suspect -> STOP.** A control counter disagrees with the
   parser (`report.parseControl.perDoc`): fix the doc's form or report the
   parser bug, then re-run. Never « complete by hand » — no green verdict may
   be born from a silent parser.
3. **No judgment rules in this dimension** — a single engine run suffices; the verdict is final.
4. **Chat summary** (3-6 lines, business terms): the PARSE TOTALS (say the
   counts — that is how a « 0 erreur » stays verifiable), err/warn counts,
   remaining judgments, and the fix skill each finding names.

The CLI writes the verdict to `.smartstack/ba/<APP>/<MODULE>/_audit/entité.md`
(existing format — anchor, `Verdict :` header, emoji sections; `0 err` =
pass for the downstream gate). The rule texts below remain the AUTHORITATIVE
spec — the CLI registry is drift-tested against them.

## Scope

- **Module scope** (default): the MCD is authoritative at the Module level, so a
  data-model audit always targets one module. Apply DM-001..023, DM-026, DM-027 to every entity
  declared in that module's `entité.md`. The verdict file lives at the audited
  module's `_audit/entité.md`.
- If asked to audit an application, run the module-scoped audit once per module
  under it (one verdict file each); don't merge them into a single report.

## Rules

### DM-001 — At least 1 entity exists per module
- **Severity**: err (if 0), ok (if >= 1)
- **ok**: the module declares ≥ 1 entity (state the count).
- **err**: the module has 0 entities — it cannot expose any data.
- Fix: `/ba-create-data-model`.

### DM-002 — All entities have at least 1 attribute
- **Severity**: err (if entities without attributes), ok (if all have)
- **err**: list the entities whose attribute table is empty.
- Fix: `/ba-create-data-model`.

### DM-003 — Attribute types are complete (string maxLength, decimal precision/scale, enum values)
- **Severity**: err (if any incomplete type), ok (if all complete)
- The whole contract of `create-data-model/levels/attributes.md` § Quality checks —
  « every string has a length, every decimal precision/scale, every enum
  non-empty values ». This rule used to implement the string third only.
- Check, for each STORED attribute (a `Calculé` member has NO column —
  `scaffold-entity` drops `formula` fields — and is exempt; it used to err here
  for nothing):
  - `string` → a maxLength > 0, in the type (`string(150)`) or the constraints
    (`maxLength 150`). Without it the column is `nvarchar(max)`.
  - `decimal` → `decimal(p, s)`. Without it no `.HasPrecision` is emitted and
    SQL Server falls back to `decimal(18,2)`: a declared `decimal(5,4)` (a rate)
    silently loses two decimals — the generator's own comment names the loss.
  - `enum` → at least one VALUE in Contraintes (`A/B/C`), constraint facets
    (`requis`, `unique`, `readonly`…) not counting as values.
- **err**: list the offending `entity.attr : <leg>` items.
- Fix: `/ba-create-data-model`.

### DM-004 — Relationship targets exist in the data model
- **Severity**: err (if broken FKs), ok (if all valid)
- Check: each relationship's target entity code must match an existing entity
  code in the MCD.
- **err**: list the broken references.
- Fix: `/ba-create-data-model`.

### DM-005 — No circular relationship chains
- **Severity**: warn (if cycles found), ok (if none)
- Check: follow the relationship graph A→B→C→…→A. Self-references are OK (skip
  them).
- **warn**: list the entities in each cycle.
- Fix: `/ba-create-data-model`.

### DM-006 — Entity names follow PascalCase convention
- **Severity**: warn (if violations), ok (if all comply)
- Pattern: `/^[A-Z][A-Za-z0-9]*$/` (starts with uppercase, no underscores/hyphens).
- **warn**: list the non-conforming entity names.
- Fix: `/ba-create-data-model`.

### DM-007 — Attribute names follow camelCase convention
- **Severity**: warn (if violations), ok (if all comply)
- Pattern: `/^[a-z][A-Za-z0-9]*$/` (starts with lowercase).
- **warn**: list the non-conforming attributes.
- Fix: `/ba-create-data-model`.

### DM-008 — All entities have a valid table prefix
- **Severity**: warn (if missing or invalid), ok (if all valid)
- Pattern: `/^[a-z]{2,5}_$/` (2-5 lowercase letters + underscore).
- **warn**: list the entities with a missing or invalid prefix.
- Fix: `/ba-create-data-model`.

### DM-009 — No unjustified isolated entities (no relationships at all)
- **Severity**: warn (if unjustified orphans found), ok (if none)
- Check: entity has no outgoing AND no incoming relationships AND classification
  is neither `lookup` NOR `technical` AND its `entité.md` heading block carries
  no `Isolation: by-design — <reason>` line with a non-empty reason.
- Rationale: most isolated entities are modeling mistakes, but two cases are
  legitimately standalone and exempt:
  - classification `lookup` (admin reference table, referenced by `code`/FK) and
    `technical` (outbox / integration journal / audit log — isolated by nature;
    NEVER a code allocator, see DM-017 check 5);
  - any entity carrying an `Isolation: by-design — <reason>` line (reason
    non-empty) — the override is then captured AND justified in the model,
    instead of being waved away in chat at audit time.
- An entity that is isolated, is neither `lookup`/`technical`, and has the marker
  but with an EMPTY reason → still **warn** (justify it or link it).
- **warn**: list the isolated entity names (note whether the marker is missing or empty).
- Fix: `/ba-create-data-model` — link it, reclassify it `technical`, or add an
  `Isolation: by-design — <reason>` line.

### DM-010 — Every entity referenced by a business rule exists in the MCD
- **Severity**: warn (if unknown entities referenced), ok (if all valid)
- Symmetric inverse of the legacy BR-005: at audit-rules time the data model is
  still BLOCKED, so the check has been pulled here where the entity catalogue
  is authoritative. Direction now: rule → entity ref → MCD lookup.
- Check: for each business rule expression, extract PascalCase tokens (skip
  primitives `DateTime`, `DateOnly`, `TimeOnly`, `String`, `Boolean`). Each
  remaining token must match an entity code or name in the current MCD.
- Skip when no business rules exist in scope (rules phase still EMPTY).
- **warn**: list the rule expressions that reference unknown entities.
- Fix: `/ba-create-data-model` (or fix the rule via `/ba-create-business-rules`).

### DM-011 — Every entity must be referenced by ≥1 use case, business rule, or resource
- **Severity**: warn (if untraced entities found), ok (if all entities traceable)
- Reciprocal direction of DM-010: prevents hallucinated entities (entities
  proposed by `/ba-create-data-model` from "domain knowledge" or web search,
  with no anchor in the upstream phases). Anti-hallucination guard.
- Check: for each entity `e` in the current MCD, scan upstream phases for ≥1
  verbatim reference. Tokens to match : `[e.name, e.code]` (NOT `e.tablePrefix`
  — collisions). Junctions inherit traceability from their relationships:
  if `e` has relationships whose target entity codes are themselves
  traced, `e` is traced by inheritance.
- Sources to scan, in order :
  - use case main flow + alternative flow steps + exception flow steps
    + preconditions + postconditions (PascalCase token match,
    case-insensitive)
  - business rule expression (PascalCase tokens, skip primitives — same
    skip-list as DM-010)
  - business rule resource code (string equality with `e.code` OR `e.name`)
  - Resource codes from the menu tree (esp. `SmartListView`)
- Dual parsing of the entity **Traçabilité** line :
  - If it carries structured references (`{ references: [...] }`) — use the array
    directly.
  - If it is prose — extract via regex `/(UC|BR|RES)-[A-Z0-9_-]+/g`.
- Skip when the module has no use cases, no rules and no resources (upstream
  phases empty — nothing to match against, default `ok`).
- **ok**: state the count of traced entities.
- **warn**: list the untraced entity names.
- Fix: `/ba-create-data-model`.
- **solution** (mandatory on warn): "For each listed entity, either add a
  verbatim reference in its **Traçabilité** line (e.g. `deduced from UC-XYZ
  step 3` or structured `{references: ['UC-XYZ', 'BR-NNN']}`), or remove
  the entity if it has no business justification in the upstream phases."

### DM-012 — A SANCTIONED reference code carries its unique index
- **Severity**: warn (if a lookup whose code DM-022 sanctions has no unique index on it), ok (if all lookups conform, no lookups in scope, or DM-022 already errs on them)
- A reference table is pointed at by a **Guid FK**, never by its `code` — so
  this rule is not about referential integrity, it is about the code the USER
  decided to keep. Where such a code exists, its uniqueness must be enforced at
  the DB level (a column-level UNIQUE MAY suffice, but composite tenant-scoped
  uniqueness needs an explicit index entry), otherwise the seed can produce
  duplicates.
- Check: for each entity with classification `lookup` AND an attribute
  whose name is "code" (case-insensitive):
  - The entity's index list MUST contain at least one entry whose
    fields begin with `"code"` AND that is `unique`.
  - The check accepts `{fields:["code"], unique:true}`, and a composite whose
    FIRST field is `code`. **Never list `TenantId` in an index**: the tenant
    discriminator is synthesised from the entity's tenancy — the scaffolder
    prefixes every declared unique with it, so a declared `(TenantId, Code)`
    used to emit `new { e.TenantId, e.TenantId, e.Code }` (CS0833, an
    uncompilable Configuration). DM-023 errs on that shape.
- Skip when the entity has no attribute named "code" (a reference table keyed
  by its label — the default state of the doctrine).
- **Skip when DM-022 errs on the entity** — a code with no `**Code décidé**`
  line, or an allocated one. Proposing `(code) unique` there would recommend
  HARDENING the very column being removed: two contradictory remedies on one
  line. Both rules read the same verdict (`lib/ba-referential-codes`
  `classifyReferentialCode`), so they cannot diverge.
- **warn**: list the lookup entities missing the unique index.
- Fix: `/ba-create-data-model`.
- **solution** (mandatory on warn): "Add `{fields:[\"code\"], unique:true}`
  to the index list of each listed lookup entity — never `TenantId`, the
  scaffolder adds the tenant leg itself."

### DM-013 — Every `*Id:guid` attribute is a declared FK relationship (or an allowlisted identity/audit column)
- **Severity**: err (if any implicit FK detected), ok (if all FK attributes are relationships)
- A cross-table reference is ALWAYS a foreign key — non-negotiable, whatever
  module the target lives in. This rule rejects the "implicit FK" anti-pattern: an
  attribute that LOOKS like a foreign key (`*Id:guid`) but is NOT a declared
  relationship, so EF Core generation would leave it a bare `Guid` with no
  constraint. (Replaces the old WARN that let a mere description "document" the
  reference away — documentation is not a constraint.)
- Check: for each entity, for each attribute where dataType is `guid` AND name
  ends with `"Id"` AND name is not `"id"`:
  - PASS if the entity's relationships contain a relationship whose foreign-key
    name (case-insensitive) matches the PascalCase form of the attribute name
    (e.g., `clientId` → `ClientId`), OR whose name matches the attribute name
    without the trailing `Id` (e.g., `clientId` ↔ `client`). The relationship's
    `scope` (`same-module` / `cross-module` / `core`) records HOW the FK is
    realized — but a relationship is REQUIRED regardless of scope.
  - PASS if the attribute is an identity/audit allowlist column (intentionally NOT
    a FK — `lib/fk-allowlist.ts`): `CreatedByUserId`, `UpdatedByUserId`,
    `ModifiedByUserId`, `DeletedByUserId`, `ChangedByUserId`, `ApprovedByUserId`,
    `AssignedToUserId`, `UserId`. **`TenantId` is NOT allowlisted** — but it is
    realized as a FK automatically from the entity's tenant mode, so it PASSES too.
  - Otherwise **err** — flag the `entity.attr` pair (a cross-table reference with
    no FK relationship).
- Skip when the attribute is the implicit primary key (name is `"id"`).
- **err**: list the offending `entity.attr` pairs.
- Fix: `/ba-create-data-model`.
- **solution** (mandatory on err): "For each listed `entity.attr`, add a `Relations`
  entry whose foreign-key name matches the attribute, with the right cardinality,
  cascade and `scope` (`same-module` / `cross-module` / `core` — for `core` name
  the physical table, e.g. `tenant_Tenants`). A documented description is NOT
  enough: every cross-table reference needs a real FK. Only the identity/audit
  allowlist may remain a plain `Guid`."

### DM-014 — Lookup entities are referenced by ≥1 other entity
- **Severity**: warn (if unreferenced lookup found), ok (if all lookups referenced)
- Reciprocal of DM-009 specifically for lookups. A lookup that no other
  entity references — neither via a relationship nor via an `*Id:guid`
  attribute — is dead code: no UC will ever read or write it. Distinct from
  DM-009 (which fires on isolated NON-lookup entities) and DM-011 (which
  flags hallucinated entities lacking an upstream UC/BR/RES anchor).
- Check: for each entity `L` with classification `lookup`:
  - Count incoming declared relationships : every entity `e` in the MCD
    such that one of `e`'s relationships targets `L.code` or `L.name`.
  - Count plain-guid FK attributes that look like FK to `L` : every entity
    `e` with an attribute `a` where `a` is `guid` AND its name is
    `${camelCase(L.name)}Id` (e.g., `BudgetType` → `budgetTypeId`).
  - If BOTH counts are 0 → WARN. List `L.name` (and `L.code`).
- Skip when the module has no lookup entities.
- **warn**: list the unreferenced lookup names.
- Fix: `/ba-create-data-model`.
- **solution** (mandatory on warn): "For each listed lookup, either declare
  a relationship from the entity that needs it (parent → lookup, N:1,
  cascade `restrict`), add a `${camelCase(name)}Id:guid` attribute on that
  entity, or remove the lookup via `/ba-create-data-model` if no entity
  needs it."

### DM-015 — Classification-based attribute conventions
- **Severity**: warn (if conventions violated), ok (if all entities conform)
- Each classification has a conventional minimum attribute set that downstream
  generation (Domain entity, EF Core mapping, seed data, list/form UI)
  expects. Missing fields don't block development but produce thin scaffolds
  that won't render correctly in the generated app.
- Check by classification:
  - **lookup** : SHOULD declare `label:string` (OR `name:string`),
    `isActive:bool`. `sortOrder:int` is recommended (controls display order).
    **No `code`** — a reference value's label IS its identity and its natural
    key. Whether such a table carries a code at all is a USER decision, judged
    by **DM-022**, never prescribed here: this rule states a MISSING field, it
    never judges a presence. (Until 2026-09 this convention REQUIRED a unique
    `code` attribute and the mandatory `solution` below re-asked for it — three
    lines of skill that produced 11 `Code` columns on a single module. Do not
    restore it.)
  - **technical** : infrastructure — outbox, integration journal, audit-only log.
    **EXEMPT** from the lookup `label/isActive/sortOrder` set; SHOULD carry
    the `Isolation: by-design — <reason>` line (DM-009 exemption). A code
    **allocator is never one of these**: a counter/sequence entity (`nextValue`,
    `lastValue`, a `{tenantId, year}` key) duplicates the socle's engine — DM-017
    check 5 errors on it.
  - **component** : SHOULD have ≥1 FK linking it to a parent entity — either
    a relationship (any cardinality) OR a `*Id:guid` attribute pairable
    with DM-013. A component with neither is a misclassified
    lightweight-module candidate.
  - **lightweight-module** / **full-module** : SHOULD have a business
    identifier — a `name:string` / `label:string` attribute (the DEFAULT),
    OR a `code:string` attribute ONLY when the entity passes the
    code-worthiness decision test (`create-data-model` levels/attributes.md
    § "When an entity deserves a code" — referenced outside the UI), with a
    declared `codePattern` only for system-ALLOCATED codes. The list UI uses
    this as the row title. Never prescribe a `code` merely because a
    convention lists it — a superfluous allocated code is what DM-017
    check 1 flags.
- Skip entities already flagged by DM-002 (no attributes at all — fix that
  first, the convention check is meaningless on empty entities).
- For each violation, list `entity.code:missing-fields`
  (e.g., `BudgetType:label,isActive`).
- **warn**: list the entities with their missing convention fields.
- **Display corollary (the §26 predictor)** — when the arbitration concludes
  the absence of `code`/`name`/`label` is LEGITIMATE ("c'est voulu — identité
  projetée / désignée par ses dates"), the SAME conclusion must be persisted:
  the entity either carries a `**Personne**` line (projected identity resolves
  the display automatically) or an explicit `**Affichage** : <Attribut>` line
  (`: Id` = conscious GUID opt-out). An entity without a name-family field AND
  without either line is a **warn**: its `displayName` has nothing sanctioned
  to fall on, and `scaffold-business` now fails closed on it (no more silent
  first-string/GUID fallback — the phone-number-as-label incident).
- Fix: `/ba-create-data-model`.
- **solution** (mandatory on warn): "Add the missing convention fields per
  classification. Lookups : `label:string(150)`, `isActive:bool` (default true),
  `sortOrder:int` — NEVER a `code`: the label names the row, and a code on a
  reference table is a dated user decision (DM-022). Components : declare the
  FK to the parent (preferred) or convert to lightweight-module. Modules : add
  a `name` attribute as the business identifier ; a `code` ONLY when the entity
  passes the code-worthiness test (referenced outside the UI — phone/email,
  outbound document, legal numbering, long-lived case), with `codePattern` only
  when system-allocated. Entities legitimately without a name-family field : author
  `**Affichage** : <Attribut>` (or `: Id`) so the display decision is data,
  not luck."

### DM-016 — Computed attributes (`formula`) reference only same-entity properties
- **Severity**: err (if any formula references a foreign attribute), ok (if all conform or no formulas in scope)
- Computed attributes are translated downstream into a LINQ projection
  (`scaffold-repository`) running inside a single SQL query. Cross-entity
  formulas would require a JOIN that EF Core can't infer from the MCD alone
  and produce broken SQL at runtime — the `dev` cascade can't recover.
- Check: for each attribute `a` with a non-empty **Calculé** formula:
  - Tokenize the formula and extract every PascalCase identifier (regex
    `/\b[A-Z][A-Za-z0-9]*\b/g`), skipping C# primitives (`Math`, `Convert`,
    `String`, `DateTime`, `DateOnly`, `TimeOnly`, `Boolean`, `Int32`,
    `Decimal`, `Guid`).
  - Each remaining token MUST match (case-insensitive) the PascalCase form
    of one of the SAME entity's attributes capitalised — `currentBalance`
    → `CurrentBalance`.
  - Otherwise ERR — list `entity.attr→missing-token`.
- Also flag formulas exceeding 1000 characters (defensive: matches the DB
  column constraint).
- Skip attributes without a formula.
- **ok**: state the number of formulas verified.
- **err**: list each `entity.attr→token` whose formula crosses entities.
- Fix: `/ba-create-data-model`.
- **solution** (mandatory on err): "Cross-entity formulas are not supported.
  Either rewrite the formula using only attributes of the SAME entity (denormalize
  the foreign value into a local attribute updated by a business rule), or move
  the computation to the backend service layer (out of the MCD) and remove
  the formula from the attribute."

### DM-017 — Code patterns are engine-compatible, ruled, and never re-implemented
- **Severity**: err (unknown token / missing `{SEQ}` / re-modelled allocator), warn
  (incomplete or unmatched spec), ok (if all coherent or none)
- A `codePattern` is the data-model side of a `numbering` business rule. Without a
  matching rule the generated numbering has no spec (format / scope / reset /
  gapless) to follow; an inconsistent scope silently collides across tenants. And
  the format is not free text: the socle's code engine parses it with a CLOSED
  grammar and throws on every insert if a token is unknown.
- **Check**, for each entity carrying a `codePattern` (checks 1-4), then
  module-wide (checks 5-6):
  1. A `numbering` business rule for this entity exists — Grep the module's
     `règles-métier.md` for a `Type: numbering` rule naming the entity, OR a
     `BR-…` ref in the entity's **Traçabilité**. Missing → warn. The remedy is
     BIDIRECTIONAL: EITHER the entity passes the code-worthiness decision test
     (`create-data-model` levels/attributes.md § "When an entity deserves a
     code") → author the `numbering` rule; OR it does not → REMOVE the
     `**Code pattern**` line (and the `code` attribute unless a user-typed
     code is wanted) — a superfluous allocated code is a modelling error, not
     a missing rule.
  2. The pattern's **scope** is `tenant` or `global` (the whole `CodeScopeKind`
     enum — `parent:<Entity>` does not exist) and is consistent with `tenantMode`:
     a `tenant`-scoped pattern requires `tenantMode` ≠ `none`; a `global` pattern on
     `strict` tenant data must be explicitly intended. Unsupported scope → err;
     mismatch → warn.
  3. Every `{…}` group of the **format** is a known token — `{YYYY}` `{YY}` `{MM}`
     `{DD}` `{TENANT}` `{SEQ:n}` or a derived `{FIELD|UPPER|LOWER|SLUG|INITIALS:Champ}`
     / `{ABBR:Champ:n}` — and `{SEQ}`/`{SEQ:n}` is present unless ≥1 derived token
     is. An invented token (`{NNNN}`, `{YEAR}`, `{SEQUENCE}`) or a sequence-less,
     field-less format → **err**: the engine raises `InvalidCodePatternException`
     at the first create, so the feature ships broken.
  4. The **reset** period aligns with a date token in the format (yearly ⇒
     `{YY}` / `{YYYY}`, monthly ⇒ + `{MM}`, daily ⇒ + `{DD}`). Misaligned → warn
     (the engine rejects it at runtime too).
  5. **No allocator is re-modelled — MODULE-WIDE, whether or not any
     `codePattern` is declared.** Scan EVERY entity of the module (not just the
     coded ones — a counter entity modelled WITHOUT any declared pattern is the
     pure hand-rolled-generator shape, and used to pass this rule via the
     skip): an entity whose name ends in `Sequence` / `Séquence` / `Compteur` /
     `Counter` / `Numerotation`, or carrying a `nextValue`-like attribute. The
     socle allocates gaplessly at insert on `core.seq_Sequences` and surfaces
     the key in Administration → Configuration → Code patterns. Present →
     **err** (this is the duplication DM-019 / C-6 name the `code-generation`
     capability for).
  6. **Every `numbering` rule lands on a coded entity — MODULE-WIDE.** For each
     `Type: numbering` rule of the module's `règles-métier.md`, resolve the
     entity it targets (whole-token match of the Condition / title / Portée
     against the module's entities). Resolved entity WITHOUT a
     `**Code pattern**` line → **err** `rule:no-code-pattern`: the rule is
     INVISIBLE to the entire downstream chain (`ruleExemption` marks it
     `type-numbering` presuming the codePattern channel exists, DEV-API-022
     never arms, no scaffold spec is emitted — the dev agent improvises an
     allocator). Unresolvable rule → warn `rule:unmappable`.
  7. **Supplied + `{SEQ}` = the deferred-collision corridor — warn.** A pattern
     declaring `surchargeable à la création` on a SEQUENTIAL format: the
     engine's sequential allocator never consults the uniqueness probe before
     allocating, so a manually supplied code inside the sequence's FUTURE
     corridor only fails LATER, as a unique-index violation, when the sequence
     reaches it. The import/reprise case is legitimate (the index stays the
     last-resort net, as at the socle) — the remedy is SHAPE separation, never
     a ban: keep supplied codes out of the mask's shape (different prefix),
     reserve them to imports/reprises, or use a derived (no-`{SEQ}`) format.
- Skip checks 1-4 when the module has no entity with a code pattern; checks 5
  and 6 ALWAYS run (their signal is precisely the module where nothing declares
  one).
- **err**: list `entity:unknown-token:{TOKEN}` / `entity:no-seq-token` /
  `entity:unsupported-scope` / `allocator:remodelled` / `rule:no-code-pattern`.
- **warn**: list `entity:missing-numbering-rule` / `entity:scope-mismatch` /
  `entity:reset-misaligned` / `rule:unmappable` /
  `entity:supplied-sequential-corridor`.
- Fix: `/ba-create-data-model` (or add the rule via `/ba-create-business-rules`).
- **solution** (mandatory on err/warn): "Rewrite the format with the socle tokens
  only (`{SEQ:n}` required, plus `{YY}`/`{YYYY}`/`{MM}`/`{DD}`/`{TENANT}` and the
  `{ABBR:Champ:n}` family) — never invent one; use scope `tenant` or `global`
  only; DELETE any counter/sequence entity modelled for this pattern (the socle
  allocates — declaring the `**Code pattern**` is the whole job). A
  `numbering` rule without its `**Code pattern**` line: EITHER the entity passes
  the code-worthiness test → declare the `**Code pattern**` on the entity (the
  ONLY channel the dev chain reads — never implement the rule in code); OR it
  does not → delete the rule. A `**Code pattern**` without its rule: author the
  `numbering` rule (format + scope + reset + gapless) in `règles-métier.md` and
  reference it in the entity **Traçabilité** — or remove the unjustified
  pattern."
- Scope note: this rule only audits `**Code pattern**` lines that PARSE. A
  Code pattern MENTIONED in an unparsable shape (table cell, wrong casing) is
  **DM-021**'s near-miss check — a different axis (is every code-like signal
  classified?), not a 7th check here.

### DM-018 — No entity duplicates a SmartStack Core entity (catalogue collision + person/company linkage)
- **Severity**: err (duplication / redeclaration / unlinked person cluster on a triggered entity), warn (untraced `none` override, unlinked company cluster), ok
- Core ships inside the SmartStack NuGet (schema `core`) — it is invisible to any
  code scan, so this rule checks the MCD against the static Core catalogue below
  (drift-tested against `lib/core-catalog.ts`). Match = whole-token,
  accent/case/plural-insensitive comparison of the entity name against each
  catalogue name + alias (never substring — `UserStory` is not a hit).

<!-- core-catalog:v1 — drift-tested against lib/core-catalog.ts (edit BOTH or the suite fails) -->
| Core entity | Table | Tenant scope | Detect as duplicate (FR/EN aliases) |
|---|---|---|---|
| User | `core.auth_Users` | none | Utilisateur, Usager, AppUser, ApplicationUser |
| Role | `core.auth_Roles` | none | — |
| Tenant | `core.tenant_Tenants` | strict | Locataire |
| TenantOrganisation | `core.tenant_TenantOrganisations` | optional | Organisation, Organization, Société, Entreprise, Compagnie, Company |
| Department | `core.ref_Departments` | optional | Département |
| JobTitle | `core.ref_JobTitles` | optional | Fonction, Poste, JobFunction |
| Office | `core.ref_Offices` | optional | Bureau, Bureaux |
| Language | `core.loc_Languages` | none | Langue |
| Group | `core.auth_Groups` | none | Groupe |
<!-- /core-catalog:v1 -->

<!-- core-reserved:v1 — drift-tested against lib/core-catalog.ts (edit BOTH or the suite fails) -->
| Reserved Core name (aliases) | Not FK-able — use instead |
|---|---|
| Permission (Droit) | IPermissionService (permission resolution) |
| UserSession (Session) | security-internal — never modeled nor FK-ed |
| UserProfile (Profil) | ICoreDataService.GetUserBasicInfoAsync |
| UserPreference (Préférence) | ICoreDataService |
| Setting (Paramètre, Configuration) | platform settings (cfg_) — not a client entity |
| Notification | Core notifications feature (ntf_) |
| Ticket (SupportTicket) | Core support/ticketing feature (tkt_) |
| Workflow | Core workflow feature (wkf_) |
| EmailTemplate (ModèleEmail) | Core email templates (email_) |
| AuditLog (JournalAudit) | Core audit logs — read-only platform feature |
| License (Licence) | Core licensing (lic_) |
| Navigation (Menu) | INavigationService (menu / nav tree) |
<!-- /core-reserved:v1 -->

- Check (a) — **whitelist collision**: an entity whose name/alias matches a Core
  catalogue entry → **err**. The Core table pre-exists and may be pre-populated
  (`core.tenant_TenantOrganisations` is the shared organisation directory) — recreating it
  forks the data with no way to reconcile. e.g. "Organisation" / "Société" duplicates Core
  **TenantOrganisation**.
- Check (b) — **reserved collision**: name/alias matches the reserved
  (service-only) table → **err**: the concept is a Core platform feature; it is
  not FK-able and must not be a client table.
- Check (c) — **unlinked person cluster (fail-closed)**: the entity is
  person-triggered — its name matches a `PERSON_TRIGGERS` word
  (lib/core-catalog.ts, mirrored in create-data-model's person-triggers:v1
  block) OR its attribute table contains ≥2 of `email`, `firstName`,
  `lastName`, `displayName` (case-insensitive) — AND it has NO `- **Personne**`
  line AND no Relations entry `*→1 User — FK UserId, scope core` → **err**
  (a person directory fully decorrelated from `auth_Users` must be a
  DELIBERATE choice, never a silence — the annuaire incident). Gradations:
  an explicit `- **Personne** : none — décision client : <raison>` line = **ok**
  (the sanctioned override, traced); a bare `none` WITHOUT the client reason =
  **warn** (decision recorded but not traced). NOTE: DM-013 allowlists a bare
  `userId` attribute, so this case passes DM-013 silently — DM-018 catches it
  regardless.
- Check (d) — **mandatory person redeclares identity**: the `Personne` line says
  `mandatory` but the attribute table still declares `firstName`/`lastName`/
  `email`/`displayName` → **err** (identity comes from `auth_Users`; local
  copies drift and violate the zero-duplication contract).
- Check (e) — **company-identity cluster**: an entity not flagged by (a) whose
  attributes include ≥1 of `uid`, `ide`, `siret`, `siren`, `legalForm`,
  `companyName`, `raisonSociale`, `vatNumber` AND no Relations entry
  `*→1 TenantOrganisation — scope core` → **warn**: the concept overlaps Core **TenantOrganisation**
  (`core.tenant_TenantOrganisations`, the shared org directory) — reference it and keep only net-new
  fields.
- **err**/**warn**: list each `entity → core-entity (qualified table)` /
  `entity:cluster` pair.
- Fix: `/ba-create-data-model`.
- **solution** (mandatory on err/warn): "(a)/(b) remove the entity; reference the
  Core entity via a `Relations … scope core` FK (whitelist) or the named service
  (reserved), or rename + reduce it to an extension entity carrying the FK +
  net-new fields only. (c) decide the person mode: add the
  `- **Personne** : mandatory|optional — identité via auth_Users (…)` line + the
  `*→1 User — FK UserId, scope core` relation (mandatory ⇒ delete the local
  identity attributes) — or, when the client DELIBERATELY wants the directory
  decorrelated from the accounts, the explicit override
  `- **Personne** : none — décision client : <raison>`. (d) delete the redeclared identity attributes — the
  `Personne` field list already names them. (e) add
  `*→1 TenantOrganisation — FK OrganisationId, scope core (tenant_TenantOrganisations)` and remove any
  attribute that mirrors the registry data."

### DM-019 — No file content in the MCD; attachment entities follow the metadata pattern
- **Severity**: err (binary-content attribute), warn (attachment entity without
  the metadata shape), ok
- The platform ships transverse CAPABILITIES as services inside the NuGet —
  invisible to any code scan and carrying no entity name DM-018 could match.
  This rule checks the MCD against the static capability catalogue below
  (drift-tested against `lib/capability-catalog.ts`). Trigger match =
  whole-token OR trailing-word of a compound name (`InteractionDocuments` →
  `Documents` = hit; `Documentation` = single word, never a hit).

<!-- platform-capabilities:v1 — drift-tested against lib/capability-catalog.ts (edit BOTH or the suite fails) -->
| Capability | Entity/section/tab triggers (FR/EN) | Attribute triggers (name or type) | Socle provides | Use instead — canonical extension pattern | Reference |
|---|---|---|---|---|---|
| file-storage | Document, Attachment, PièceJointe, Pièce jointe, Pièces jointes, Fichier, File, GED, DMS, Justificatif, Annexe, Scan, Upload, Téléversement, Media, Média, Photo | binary, blob, varbinary, byte[], image, filestream, fileContent, fileData, contenu, contenuFichier | IFileStorageService (SmartStack.Application.Common.Interfaces) — Scoped via AddSmartStack, injectable from any extension handler/controller; StorageType Normal/Legal; Local + Azure Blob (config shipped in every generated appsettings) | Client METADATA entity in extensions.* (FileName, StoredFileName, ContentType, FileSizeBytes + parent FK) + IFileStorageService for the bytes + dedicated AUTHENTICATED upload/download endpoints. NEVER binary content in the DB, never raw disk I/O outside the service. | `development/backend/data-layer/references/file-storage.md` |
| global-search | GlobalSearch, RechercheGlobale, Recherche globale, SearchIndex, SearchEngine, Moteur de recherche, Moteurs de recherche, Index de recherche | — | AddExtensionSearch<ExtensionsDbContext> (socle search seam) — extension entities plug into the platform global search | Register searchable entities through the search seam (scaffold-extension-search) — never a client-built search index or engine. | `development/backend/data-layer/references/global-search.md` |
| time-entry-refs | Bookable, Imputable, TimeEntryRef, TimeEntryTarget | — | AddExtensionTimeEntryRefs<ExtensionsDbContext> — client entities become bookable targets of the platform HR time module | Register the entity via the time-entry-refs seam (scaffold-time-entry-refs) — never re-model time entries (PLATFORM_HR_ENTITIES / CODE-005 covers those names). | `development/backend/data-layer/references/time-entry-refs.md` |
| code-generation | Sequence, Séquence, Sequences, Compteur, Counter, Numerotation, Numérotation, Numbering, CodePattern, Code pattern, Allocator, Allocateur, NumberSequence, CodeSequence | nextValue, nextNumber, nextSeq, lastValue, lastNumber, prochainNumero, dernierNumero, compteur | ICodedEntity + ICodeKeyDescriptor registered via AddSmartStackCodeKey<T>() — the shared CodedEntitySaveHandler allocates the Code atomically at insert on core.seq_Sequences (UPDLOCK/SERIALIZABLE = gapless by default), scope Tenant/Global, reset None/Yearly/Monthly/Daily; the key surfaces in Administration → Configuration → Code patterns, where a CodePattern DB row only OVERRIDES the built-in default. No seed, no migration | Declare the `**Code pattern**` on the entity itself (format with {SEQ:n} + scope + reset + gapless) and register the key through scaffold-coded-entity — the socle allocates. NEVER model a counter/sequence/allocator entity, a nextValue column or a client numbering service: the gapless guarantee and the admin-side retuning are the platform's. | `development/backend/data-layer/references/coded-entities.md` |
| email-sending | EnvoiEmail, EmailSortant, OutgoingEmail, EmailQueue, Mailing | — | IEmailService (Scoped) + Core email templates (email_) | Send mail through IEmailService with Core email templates — never a client SMTP client, outbound-mail table or template store. | — |
<!-- /platform-capabilities:v1 -->

- **Which rule arms each row** (the table is shared; checking is not): the
  file-storage row → checks (a)/(b) below; the **code-generation row → DM-017
  checks 5+6** (re-modelled allocator err, numbering rule without its
  `**Code pattern**` err); global-search / time-entry-refs / email-sending →
  CODE-006 (`/ba-audit-cross-ref-code`, always-on catalogue match). No row is
  decorative — a trigger hit lands in exactly one rule's verdict.

- Check (a) — **binary content**: ANY entity (attachment-flavored or not) whose
  attribute table declares a type or name in the file-storage ATTRIBUTE triggers
  (`binary`, `blob`, `varbinary`, `byte[]`, `image` as a TYPE, `filestream`,
  `fileContent`, …) → **err**. File content is never a column — the bytes
  belong to `IFileStorageService` (downstream: PRD-053 errs, `scaffold-entity`
  throws on these types).
- Check (b) — **attachment entity without the metadata shape**: an entity whose
  name (whole-token or trailing-word) matches a file-storage ENTITY trigger but
  lacks the metadata trio (`StoredFileName`-like opaque key, `ContentType`,
  a size attribute) or a parent FK → **warn**: it is an attachment entity in
  name only; align it with the canonical shape (`FileName` string/256,
  `StoredFileName` string/500 unique, `ContentType` string/100,
  `FileSizeBytes` long + parent FK relation).
- The METADATA entity itself is **LEGITIMATE** — this rule steers its shape, it
  never asks to remove it (contrast DM-018, where a Core duplicate is deleted).
- **err**/**warn**: list each `entity.attribute → trigger` / `entity → missing
  shape element` pair.
- Fix: `/ba-create-data-model`.
- **solution** (mandatory on err/warn): "(a) replace the binary attribute with
  the metadata trio; the bytes go through `IFileStorageService` (pattern:
  `development/backend/data-layer/references/file-storage.md`). (b) complete the
  metadata shape + parent FK."

## Output

Write `_audit/entité.md` per the doc-templates skeleton:
- Header `# Audit entité — <APP> / <MODULE>` + `_<date> · Verdict : <emoji> N warn · M err · K ok_`.
- `## ✅ Conforme`, `## ⚠️ Avertissements`, `## ❌ Bloquants` sections; one bullet
  per finding. For `warn`/`err`: what's wrong (offending codes / `entity.attr`
  **bold**), why it matters, and a `→` fix naming `/ba-create-data-model`.
  When a rule defines a **solution**, fold it into the fix bullet.
- The rule codes (`DM-001`, …) stay **bold** so they remain greppable, but they
  are explained in business terms (no i18n label codes).
- Re-Write the whole file each run (overwrite — it's a fresh verdict).

Then a 3–6 line chat summary in the user's language — business terms, not rule
codes. If any `err`, state clearly that the data model must be fixed before
moving on.

## Used by the readiness orchestrator

`/ba-audit-pre-dev` runs every dimension and aggregates the verdicts. When invoked
by it, still write `_audit/entité.md` as usual — the orchestrator reads these files.

### DM-020 — A `**Dérivé**` declaration resolves
- **Severity**: err, ok otherwise.
- Check, for every attribute carrying the `**Dérivé**` marker:
  - `nav <Nav>.<Prop>` → a Relation of the entity declares the `<Nav>` target
    (FK present);
  - `child <Collection> pick latest(<Date>)|open(<End>) select <Prop>` → a 1→*
    relation exists toward a child entity that carries `<Date>`/`<End>` and
    `<Prop>` as attributes;
  - the attribute is optional (a derived projection yields null) and NOT also
    `Calculé` (one derivation per member).
- The class it closes: a list column whose value has no FK on the entity (the
  vehicle's site lives in a dated child) shipped EMPTY on every row — the
  declaration makes the projection generatable (`scaffold-business
  projectField` via `lib/derived-field`).
- Fix: `/ba-create-data-model` — correct the Relations or the marker.

### DM-021 — Every code-like signal is classified (near-miss declarations + synonym attributes)
- **Severity**: err (unparsable `**Code pattern**` declaration — near-miss; or
  a code-like attribute co-signalled by a near-miss in the SAME entity block),
  warn (code-like attribute unclassified), ok otherwise.
- Two deterministic checks (engine legs: `findCodePatternNearMisses` in
  `lib/code-pattern-grammar` + `findCodeLikeAttributes` in `lib/ba-entities` —
  the same lexicon `derive-code-specs` feeds PRD-132 with):
  1. **Near-miss declaration** — a line of the module's `entité.md` MENTIONS a
     Code pattern in a declarative shape but does not parse: a table cell
     (`| Code pattern | … |` — the incident shape), a wrong-cased or unbolded
     bullet, or declarative prose carrying a `:`-value or an orphan mask. The
     whole chain (derive-code-specs, both scaffolder halves, DEV-API-022 /
     DEV-UI-034) derives NOTHING from such a line — mechanical green over a
     real declaration (the green-by-vacuity class) →
     err `entity:near-miss:<shape>`. A stated absence (« pas de Code
     pattern ») and bare prose mentions never match — precision over recall.
  2. **Code-like attribute unclassified** — an attribute named after the
     code-like lexicon (`code-like-lexicon:v1` in create-data-model
     levels/attributes.md: reference, ref, numero, number, num, matricule —
     FR/EN, accent-folded, whole-word, FK-shaped `…Id` names excluded), shaped
     like a business key (string + unique, not computed), carrying NO species
     classification: neither the `code` attribute of a `**Code pattern**`
     entity nor named by a `- **Code saisi** : <Attribut>` bullet (EN alias
     `**Typed code**`). In real vocabularies « Référence » IS the code — an
     undecided species ships as free text → warn
     `entity.attr:code-like-unclassified`, escalated to err
     `entity.attr:code-like-near-miss` when check 1 fired in the same block.
- This rule demands a CLASSIFICATION, it never forbids: a user-typed
  referential key (TypeAffaire, a department code) is legitimate — say it out
  loud with `**Code saisi**`.
- Section/resource-level entité.md docs are covered by the sibling legs
  (derive-code-specs → PRD-132, DEV-API-022, DEV-UI-034); this rule reads the
  module-level doc the corpus model loads.
- The class it closes: a client project authored its Code pattern declarations
  inside table cells — invisible to the whole chain, every gate green, the dev
  agent improvised a hand-rolled allocator (DEV-API-034's class); and
  business vocabularies where « Référence » is the code carried no signal at
  all.
- Fix: `/ba-create-data-model` — decide the species (attributes.md § the
  decision test): ALLOCATED → author the canonical
  `- **Code pattern** : `MASK` — …` bullet (never a table cell); TYPED → add
  `- **Code saisi** : <Attribut>`; neither → rename the attribute out of the
  code-like lexicon.

### DM-022 — Une valeur de référence ne porte pas de code (sauf décision utilisateur datée)
- **Severity**: err (code sur une table de référence sans ligne `**Code décidé**` ;
  `**Code pattern**` sur une table de référence ; quasi-manqué — une décision
  mentionnée sans être analysable), warn (décision dont plus aucune citation ne
  subsiste), ok otherwise.
- **La doctrine.** Une valeur de référence ne porte pas de code : son **libellé**
  est son identité et sa clé naturelle. **Seul l'UTILISATEUR peut décider qu'une
  table en porte un.** Cette décision, écrite dans `entité.md` et **datée**,
  **outrepasse la règle**.
- **DM-022 ne rediscute JAMAIS l'EXISTENCE du code d'une entité portant
  `**Code décidé**`** — pas de `warn` de consolation, pas d'« envisager de
  retirer ». Sans cette phrase, la passe d'audit suivante re-propose le retrait
  à chaque exécution. Une seule chose reste jugée sur une entité décidée : un
  code décidé est **SAISI, jamais ALLOUÉ** (check 2) — « Lookups, junctions and
  audit logs get no code pattern » (`create-data-model` levels/attributes.md)
  reste vrai et inchangé, et sans cette réserve écrire une ligne de décision
  suffirait à faire taire un vrai défaut.
- **Check**, pour chaque entité classée `lookup` :
  1. **Code sans décision** — un attribut nommé `code` (insensible à la casse)
     et **aucune** ligne `- **Code décidé** : <ce que le code nomme> — décision
     utilisateur du <AAAA-MM-JJ>` → **err**
     `entity.attr:code-without-decision`. L'évidence porte l'inventaire des
     citations (ci-dessous) : c'est ce qui casse si le code part.
  2. **Code ALLOUÉ sur une table de référence** — l'entité porte un
     `**Code pattern**` : le socle numérote un référentiel → **err**
     `entity:allocated-code-on-reference`. Cas que **rien** n'attrapait :
     DM-017 check 1 ne demande qu'une règle `numbering` et ne teste aucune
     classification, donc un lookup avec un Code pattern ET sa BR passait toutes
     les portes.
  3. **Quasi-manqué** — une ligne de `entité.md` MENTIONNE la décision dans une
     forme déclarative mais ne parse pas : cellule de tableau (la forme de
     l'incident `Code pattern`), puce mal casée ou non graissée, prose, ou —
     le plus probable — **puce bien formée sans date ISO réelle**
     (`missing-date`) → **err** `entity:decided-near-miss:<shape>`. Une
     « décision » sans date est une habitude, pas une décision ; et une ligne
     qui mentionne la décision sans être analysable n'existe pour aucun outil.
     Une absence énoncée (« pas de code décidé ») n'est jamais un quasi-manqué.
  4. **Décision devenue morte** — une ligne `**Code décidé**` valide dont plus
     **aucune** citation de ses codes ne subsiste dans le corpus (la BR qui la
     portait a été réécrite) → **warn** `entity:decided-code-uncited`. La
     décision reste souveraine : ce warn demande de la **revisiter**, jamais de
     la défaire. Ce check exige des valeurs de code CHERCHABLES : sur une table
     dont les `**Valeurs initiales**` n'ont pas de colonne `Code`, il n'y avait
     rien à chercher, et le warn se tait plutôt que de déguiser un inventaire
     vide en « la règle a été réécrite ».

- **Une liste de citations vide ne veut dire quelque chose que si quelque chose
  a été CHERCHÉ.** L'évidence porte donc toujours le compte —
  `N valeur(s) de code cherchée(s) dans les 4 sources, aucune citée` — et
  `N = 0` (pas de `**Valeurs initiales**`, ou pas de colonne `Code` dedans) ne
  se lit JAMAIS comme « rien ne dépend de ce code » : la BA n'a simplement pas
  de quoi le dire. La remédiation le traite comme tel — voir les deux cas où
  elle refuse d'écrire, ci-dessous.
- **Ce que le backfill REFUSE de faire**, au-delà d'un code cité : retirer un
  code sur une table sans valeur cherchable (`no-seeded-codes` — l'inventaire
  n'a rien conclu) ; démonter un `code` pris dans un index COMPOSITE ; laisser
  une ligne sans identifiant (aucun attribut de la famille libellé) ; et
  basculer la clé du semis sur un libellé qui **ne distingue pas les lignes**
  (`ambiguous-label-key`) — `scaffold-seed` refuse une clé naturelle en double,
  donc le backfill ne doit pas en fabriquer une.
- **L'inventaire des citations — QUATRE sources.** Pour chaque valeur de la
  colonne `code` des `**Valeurs initiales**` : les `règles-métier.md`, les
  **critères d'acceptation** des `use-case.md`, les **`prd*.md` / `pagespecs/`**,
  et les `**Valeurs initiales**` des AUTRES entités. Recherche en mot entier,
  casse et accents repliés, sur les valeurs de **code** uniquement — une citation
  de libellé ne lie pas le contrat. **Sans la 3ᵉ source l'inventaire
  sous-déclare** : sur un module réel, les motifs de retrait et de suspension
  n'étaient cités par aucune règle ni aucun critère, mais l'API les désignait par
  `ReasonCode`.
- **Le `warn` se tait quand le PRD n'est pas là.** `/ba-audit-data-model` tourne
  légitimement AVANT `/ba-create-prd` : sans `prd*.md` la 3ᵉ source est illisible,
  le check 4 est donc **muet** et le message `ok` porte la mention « inventaire
  PARTIEL ». Sans cette garde la règle crierait faux sur chaque module d'une
  phase BA normale.
- **Exemptions** : `technical` (déjà exempte de DM-015 — hors du branchement
  `lookup`).
- **Ce que la règle NE voit pas**, et qu'il faut lire tel quel : elle lit la BA,
  pas le C#. Une machine d'états qui compare un code sans qu'aucune BR ne le cite
  lui échappe (périmètre DEV-API). L'absence de citation n'est donc **pas** une
  preuve d'absence.
- **Moteur** : `lib/ba-referential-codes.ts` (`classifyReferentialCode` — la
  définition UNIQUE que DM-022, DM-012 et le backfill partagent) +
  `lib/code-pattern-grammar.ts` (`findDecidedCodeNearMisses`). L'inventaire
  complet, entité par entité avec ses citations verbatim, s'obtient par :

  ```bash
  npx --prefer-offline tsx skills/business-analyse/create-data-model/cli/derive-referential-codes/index.ts \
    --spec '{"baRoot":".smartstack/ba","app":"<APP>","module":"<MODULE>","mode":"check"}'
  ```

  La CLI sort 0 même en drift — le drift est une DONNÉE ; c'est CETTE règle qui
  sanctionne.
- Fix: `/ba-create-data-model`.
- **solution** (mandatory on err): "Trancher table par table, à partir de
  l'inventaire des citations. GARDER le code → écrire la puce datée
  `- **Code décidé** : <ce que le code nomme> — décision utilisateur du
  <AAAA-MM-JJ>` (jamais posée par un agent de sa propre initiative : elle
  transcrit une décision prise). LE RETIRER → `derive-referential-codes
  --mode backfill`, qui n'écrit que dans `entité.md` et s'arrête sur toute
  entité dont un code est cité. Un `**Code pattern**` sur une table de référence
  se retire toujours : un code décidé est saisi, jamais alloué."

### DM-023 — Every column an **Index** names exists (and never TenantId in a unique)
- **Severity**: err (phantom column, or TenantId inside a unique), warn (`(Code)
  unique` on a `**Code pattern**` entity), ok otherwise.
- The DECLARATION half of the index contract — `DEV-API-031` is the EMISSION
  half. `scaffold-entity/validate.ts` refuses a `**Index**` column that matches
  no known column, as a hard error: that check fires inside `/ba-develop`
  Phase 2, six phases AFTER the BA readiness GO let the typo through. This rule
  runs the same check at BA time — ONE definition, `lib/entity-columns`
  `knownIndexColumns`, imported by both sides so they cannot drift.
- A known column is: a declared attribute; a SYNTHESISED foreign key — the FK
  of an owning relation (`*→1` / `1→1`) written on the entity, or the FK a
  parent's `1→*` puts on this entity (the skeleton never lists FK columns in
  the table); `Code` on a `**Code pattern**` entity; the technical columns
  `TenantId` and `CreatedAt`.
- Check 2 — **`TenantId` inside a UNIQUE index → err.** The scaffolder prefixes
  every declared unique with the discriminator, so `(TenantId, Code) unique`
  emitted `new { e.TenantId, e.TenantId, e.Code }` — CS0833, « an anonymous type
  cannot have multiple properties with the same name », an uncompilable
  Configuration. The doctrine itself prescribed that shape (DM-012's former
  composite example, the doc-templates skeleton, the ba-entities fixture) — all
  corrected; `generate.ts` now dedups defensively as well. A NON-unique
  `(TenantId, Status)` is a legitimate composite and is not flagged.
- Check 3 — `(Code) unique` on a `**Code pattern**` entity → warn: redundant,
  the socle emits `(TenantId, Code) unique` for every coded entity itself.
- **err**: list `Entity : <index raw> — <reason>`; the offending column travels
  as a structured `anchors[]` entry (kind `attribute`) for the router.
- Fix: `/ba-create-data-model` — fix the `**Index**` declaration (or declare
  the attribute / the relation the column was meant to be).

### DM-027 — No framework field is listed in the attribute table
- **Severity**: warn, ok otherwise.
- `attributes.md` § Framework fields: `Id`, `CreatedAt`, `UpdatedAt`,
  `DeletedAt`, `TenantId` are implicit — the generated `ExtensionBaseEntity`
  and the entity's tenancy add them, and `scaffold-entity` DROPS them (warning +
  skip) if listed. The model then claims a column it does not own.
- Two classes, told apart because the remedy differs:
  - **dropped by the generator** — `CreatedAt`, `UpdatedAt`, `DeletedAt`,
    `TenantId`: remove the row;
  - **NOT provided by the generated base class** — `CreatedBy`, `UpdatedBy`,
    `Scope` (which the authoring doc USED TO call implicit): listed, they become
    real columns. Keep the row only if the model wants those columns.
- `Id` is exempt — the authoring contract says « Show only `Id Guid PK` in the
  table for readability ».
- **warn**: list `Entity.attr — <class>`.
- Fix: `/ba-create-data-model`.

### DM-026 — Every state value is still reachable (no leftover of a state change)
- **Severity**: warn, ok otherwise (incl. « inventaire PARTIEL » when neither a
  Flow rule nor a screen is in scope yet).
- The model retaining what the BA no longer uses, at VALUE grain: DM-011 is the
  entity grain, DM-025 the attribute grain. When a state machine changes (a
  status merged, renamed, retired), the enum in `entité.md` routinely keeps the
  old value: it is generated, seeded in the kanban's column palette, offered in
  filters — and nothing ever transitions to it. `mapFlowOntoEnum`
  (derive-kanban-spec) reports the INVERSE only (a transition toward a status
  the enum does not know), and only when a kanban exists.
- Check, for every state attribute (`stateAttrsOf` — name containing
  status/state/phase/step, or an enum whose values read as a lifecycle), each
  value of its enum (constraint facets excluded) must be reached by at least
  one of: a `- **Flow**` transition (`from` or `to`) of a module rule; a
  SmartKanban column key; a `**Cycle de vie**` phase of a screen; a screen
  `**Filtres**` entry; a verbatim whole-word mention in the module's use cases
  (title, flows, pre/postconditions, ACs) or rules (title, condition,
  expression, cases). Case- and accent-folded, like `mapFlowOntoEnum`.
- Silence: with NO Flow rule and NO screen in scope (rules are phase 4, screens
  phase 7) there is no state machine to compare against — `ok` with the
  « inventaire PARTIEL » note, never a warn on a corpus that has not reached the
  phase.
- **warn**: list `Entity.attr : v1, v2`.
- Fix: `/ba-create-data-model` — drop the leftover value, or
  `/ba-create-business-rules` — author the transition that reaches it.

### DM-028 — Every entity declares its **Portée** (tenancy)
- **Severity**: warn by default (err under `--strict`), err on an unreadable
  value, ok otherwise (incl. zero entities in scope).
- `attributes.md` § Quality checks has always demanded « `tenantMode` set », and
  audit-prd VIBE-002 « an unambiguous tenant mode » — but no grammar existed to
  SAY it: an unfalsifiable rule. `scaffold-entity` falls back to `strict`
  (`types.ts` Zod default), and that default decides: the `TenantId` column,
  `HasIndex(e => e.TenantId)`, the TENANT-COMPOSITE shape of every declared
  unique index and the coded `(TenantId, Code)` unique. A platform-wide
  catalogue silently becomes per-tenant unique; an `optional`-tenant table
  loses its two filtered indexes.
- Grammar: `- **Portée** : strict` (or `optional` / `none` — the CLOSED
  vocabulary of `ScaffoldEntityInputSchema.tenantMode`, verbatim; there is no
  fourth mode). On the entity, or ONCE at document level before the first
  `### ENT-` heading (inherited by every entity that carries none — the
  backfill of an existing corpus is one line per module).
- Check: every entity resolves a tenancy. A bullet whose value is not one of
  the three → **err** (`tenancyRaw` set, `tenancy` null — the near-miss shape,
  mirror of `**Code décidé**`). No bullet anywhere → **warn**, ONE finding per
  module listing the entities, framing the default as an inherited decision
  (« tenantMode retombe sur strict… »); **err under `--strict`**.
- Migration note: every existing client model lacks the bullet — a required
  bullet at `err` would flip every project's pre-dev GO to NO-GO on upgrade.
  Hence warn + the `--strict` ramp (the BR-011 precedent).
- Fix: `/ba-create-data-model` — add `- **Portée** : …` (document level for
  the common case, per entity to override).

### DM-025 — Every stored attribute is described somewhere (no dead column)
- **Severity**: warn, ok otherwise (incl. « inventaire PARTIEL » when no screen
  is in scope yet).
- The attribute-grain twin of DM-011 (entities) and DM-026 (state values). A
  column the BA no longer uses — a use case dropped, a screen redesigned — stays
  in `entité.md`, is generated, migrated and shipped, working perfectly and
  doing nothing. Nothing downstream ever fails on it.
- **Described** = cited by at least one of (engine: `attributeEvidence`):
  a whole-word, accent-folded mention (name or its PascalCase) in the module's
  use cases (title, flows, pre/postconditions, ACs), rules (title, condition,
  expression, cases, portée), the RAW `screen.md` docs, `rbac.md`, the module
  and section `index.md`; OR a structural reference — the FK column of a
  relation, an `**Index**` column, a PascalCase token of a sibling's `Calculé`
  formula, a member a `**Dérivé**` projects, a `**Valeurs initiales**` header
  cell, `**Affichage**`, `**Code saisi**`, a field a `**Code pattern**` mask
  derives from.
- **Exempt outright**, each closing a named false-positive class: `Id`,
  `TenantId`, the fk-allowlist audit columns; a `Calculé` member (no column);
  a `*Id:guid` (DM-013 owns FKs); a state attribute (`stateAttrsOf` — its
  VALUES travel, its name rarely; XD-001/002/003 and DM-026 own it); the `code`
  of a coded/decided-code entity (DM-017/021/022); the DM-015 convention set of
  a `lookup` (`label`/`name`/`isActive`/`sortOrder`/`code`) — **DM-015 demands
  those fields**, so proposing to delete them would be the DM-012↔DM-022
  arbitration failure; every attribute of a `technical` entity (DM-009 exempts
  them already).
- **Silence**: with no screen in scope (the data model is phase 6, screens
  phase 7 — right after `/ba-create-data-model` is the normal case) the richest
  source does not exist yet and most attributes would look dead → `ok` with
  the « inventaire PARTIEL » note (the DM-022 precedent).
- ONE finding per module, evidence capped at 20 (« … et N autres »). The
  residual false positive is linguistic — « le montant » in a UC while the
  attribute is `Amount` — and is irreducible mechanically: the absence of a
  citation is not a proof of absence, which is why this rule is warn, never err.
- **warn**: list `Entity.attr`.
- Fix: `/ba-create-data-model` — drop the column, or cite it where it is used
  (a screen field, a rule, an AC).

### DM-024 — A column the screens filter on has its index anticipated
- **Severity**: warn, ok otherwise (incl. « aucune SmartListView en portée »).
- The only index rule was DM-012 (the unique on a sanctioned reference code).
  The authoring contract (`attributes.md` § Indexes) asks for « composite for
  frequent filters » — and the screens declare their filters deterministically
  (`**Filtres**`). Nothing tied the two: a list filtering on `Status` over a
  growing table, with no index, is the classic scan.
- Deliberately TIGHT (precision over recall): SmartListView blocks only; a
  `**Filtres**` entry whose name is a single identifier token, carrying a
  widget hint (`(select)`, `(date-range)`, `(boolean)`) or naming a state
  attribute; resolving to a STORED (non-computed) attribute of the bound
  entity; that is neither an FK column nor a relation target (EF Core indexes
  FK columns by itself — `attributes.md`: « No need to index FK columns for
  performance ») nor the `code` of a coded entity (the socle emits its unique).
  Covered when a declared `**Index**` has it as LEADING column — a composite
  `(Status, CreatedAt)` covers a `Status` filter.
- Advice, never a blocker: no downstream consumer reads it, and an index per
  filtered column is itself an anti-pattern (write amplification). One finding
  per module, evidence `Entity.attr ← filtre de SCR-…`.
- Fix: `/ba-create-data-model` — add `(Field)` (or a composite starting with
  it) to `**Index**`.

### DM-029 — The business test dataset exists where the module carries business entities
- **Severity**: warn (no `jeu-de-test.md` while the module has business
  entities, or a business entity without its `JT-` block), ok otherwise —
  including a module made of reference tables only (« sans objet »).
- The dataset the clients forget. A module whose entities are business rows
  (not reference tables carrying `**Valeurs initiales**`) and that carries no
  `jeu-de-test.md` cannot be exercised once deployed: nothing populates it in
  test/qual, the acceptance tests have no fixture, and a BA simulator has
  nothing but noise to show. `jeu-de-test.md` is the SECOND seed tier — the
  setup rows are `**Valeurs initiales**`; these are 5-8 fictitious rows per
  business entity, seeded in dev/test/qual only (`SmartStack:EnableDevSeeding`).
- Grammar: `business-analyse/_workflow/doc-templates.md` § `jeu-de-test.md`
  (module level, `### JT-NNN — Entity (ENT-NNN)`, `- **Clé** : …`, one table).
- Check: business entity = classification not lookup AND no `**Valeurs
  initiales**`. Every such entity has a block whose heading names it.
- **warn** at most, deliberately — the dataset is OPTIONAL and every existing
  client tree lacks it; an err would flip every project's pre-dev GO.
- Fix: `/ba-create-test-data` on the module.

### DM-030 — The test dataset is coherent with the model, the actors and the cited modules
- **Severity**: warn (v1) when the deterministic checker reports any issue;
  ok otherwise; ok « non fourni » when the file is absent (DM-029's concern).
- Engine: `lib/ba-test-data-check` — the SAME function
  `create-test-data/cli/derive-test-data --mode check` runs (the CLI is the
  corrector's inventory; this rule is the verdict). It resolves every FK cell
  WHERE THE RELATION SAYS THE TARGET LIVES: `same-module` → this module's
  blocks or the target's Valeurs initiales; `cross-module (APP/MOD)` → that
  module's dataset (read full-tree, whatever the audit scope); `core` `User` →
  an actor of acteur.md; other Core → a literal resolved at seed time.
- Checks: column unknown to the MCD (neither attribute nor FK relation);
  `**Clé**` absent, not a column, not an attribute, empty or duplicated;
  required attribute without a column or with an empty cell; enum value not
  in the vocabulary (verbatim — the near-miss is suggested); `unique`
  attribute duplicated; integer / decimal / boolean / date cells that are none
  (a calendar date, not just the shape); `Code` column on a coded entity whose
  pattern is not `surchargeable à la création`; FK not resolved — the message
  names the OWNER module and what it must provide; actor unknown; dependency
  cycle between blocks (no insertion order) or between modules; a cited
  module with a different `Date de référence`. The CLI's volume advice
  (`row-count`, 5-8 rows recommended) is NOT an incoherence — reported by the
  CLI only, never by this rule.
- Evidence: up to 25 messages, each `JT-NNN — row N: …`. The finding's `file`
  is `<APP>/<MODULE>/jeu-de-test.md` (the remedy is authoring — DM-031/032
  cover their own issue classes, excluded here).
- Fix: `/ba-create-test-data` — inventory first with `--mode check`; an
  unresolved citation is the OWNER module's row to add.

### DM-031 — No test-data block on a reference table carrying Valeurs initiales
- **Severity**: warn when a `JT-` block names an entity whose entité.md block
  carries `**Valeurs initiales**`; ok otherwise (incl. absent file).
- A reference table's rows ARE its Valeurs initiales — the setup tier, every
  environment. A test-data block on it is a second truth: the two diverge,
  and the seed would upsert the same rows twice. Cite the initial values from
  the blocks that need them instead.
- Fix: `/ba-create-test-data` — remove the block.

### DM-032 — Every status a Flow rule reaches is carried by a row
- **Severity**: warn when a status cited by a `Flow` transition of the
  module's rules (`workflow` / `state-transition`) is an enum value of an
  entity that has a block, and no row of that block carries it; ok otherwise
  (incl. no Flow in scope, absent file).
- A dataset that never shows an « Archivé » client cannot exercise the
  archive transition — in the simulator, in the acceptance tests, in UAT.
- Fix: `/ba-create-test-data` — add the row (the `Note` column cites the BR).
