---
phase: rules
kind: level
level: elaborate
---

# Level 2 — ELABORATE

> Turn a classified rule (Level 1) into a vibecoding-ready specification:
> a natural `Condition`, a pseudo-code `Expression`, an error code, and
> **at least one valid + one invalid example**. This is the most important
> level — the quality of the generated code and tests depends on it. Persist
> by re-Writing the scope's `règles-métier.md` (see `SKILL.md`).

## Anatomy of a complete rule

### Condition — natural language QUAND…ALORS / WHEN…THEN
Always `QUAND <trigger> ALORS <constraint>`. Unambiguous, no passive voice,
reference fields by name.

| Weak | Strong |
|------|--------|
| "Users are validated" | "QUAND un utilisateur est créé/modifié ALORS son email doit être unique parmi les non-supprimés" |
| "Status check" | "QUAND le statut d'une commande change ALORS le nouveau statut doit être un successeur valide" |
| "Auto compute" | "QUAND une ligne de facture est ajoutée ALORS sous-total = quantité × prix unitaire" |

### Expression — pseudo-code
Compact, deterministic, a developer can implement without guessing (SQL-like,
C#-like or math). It is a hint, not a contract.

```
// uniqueness
NOT EXISTS(SELECT 1 FROM users WHERE LOWER(email)=LOWER(@email) AND id!=@id AND deleted_at IS NULL)
// calculation
vatAmount = round(baseAmount * vatRate, 2)
// state transition
@to IN { draft:[submitted,cancelled], submitted:[shipped,cancelled], shipped:[delivered] }[@from]
// ownership
@user.id == @ticket.assigneeId OR 'manager' IN @user.roles
```

### Error code — mandatory format `{module}.{entity}.{cas}`
Lowercase dotted, >= 3 segments, the MODULE code FIRST (e.g.
`pipeline.discount.cap`, `parc.vehicule.immat-unique`). Non-negotiable for
enforcement rules: a code without its module prefix (`amount-positive`,
`discount.cap`, `USER_EMAIL_DUPLICATE`) collides the day a second module coins
the same case — error codes are the API's public error contract and land
verbatim in AC, generated tests and i18n keys. Audit gate: **BR-011** (err).

## Examples — the heart of the rule

Each rule MUST have at least **1 valid** and **1 invalid** example. Non-negotiable
(both the success and the failure path are needed to generate meaningful tests).
Write them as concrete one-liners under `## Cas valides` / `## Cas invalides`, in
Given/When/Then form when it helps:

```
- Email unique : aucun utilisateur 'alice@x.com' n'existe → POST /users {email:'alice@x.com'} → 201 créé.
```

### Two patterns: enforcement vs filter/visibility

**Enforcement rule** — declares an error code. The invariant rejects an action;
the invalid example names the error (e.g. "→ 409 `USER_EMAIL_DUPLICATE`").
Examples: uniqueness, illegal transition, ownership check.

**Filter / visibility rule** — **no error code** (leave it blank). The invariant
silently excludes records (repository WHERE clause, soft-delete guard,
anonymisation tombstone, tenant isolation). There is no user-facing error — the
record is simply absent. The invalid example describes the **observable absence**
("le contact n'apparaît PAS dans les résultats — filtré au niveau repository"),
never a fake error.

| Pattern | Rule error code | Invalid example |
|---------|:---------------:|-----------------|
| Enforcement | present | names the expected error |
| Filter/visibility | blank | observable absence, no error |

### Numbering rules — the extra spec

Before authoring one, the entity must PASS the code-worthiness decision test
(`create-data-model` levels/attributes.md § "When an entity deserves a code" —
referenced outside the UI: phone/email, outbound document, legal numbering,
long-lived case). A typed referential code (lookup) takes `validation` rules,
never `numbering`. And the rule is a **SPEC, not an implementation order**: it
is consumed as the entity's data-model `codePattern`; the socle's
`CodedEntitySaveHandler` allocates at insert — the generated app implements
NOTHING (no service, no counter, no uniqueness validator).

A `numbering` rule defines how an entity's business code/reference is
**generated** (not merely validated). Beyond the standard fields, capture in the
Condition + Expression:

- **Format** — literals + tokens from the socle's CLOSED vocabulary: `{YYYY}` /
  `{YY}` year, `{MM}` month, `{DD}` day, `{TENANT}`, `{SEQ:n}` zero-padded counter,
  plus the field-derived `{FIELD|UPPER|LOWER|SLUG|INITIALS:Champ}` and
  `{ABBR:Champ:n}` — e.g. `AFF-{YY}-{SEQ:4}`. `{SEQ}` is required unless the format
  derives from ≥1 field token. An invented token (`{NNNN}`, `{YEAR}`) is rejected
  by the engine at every insert — never coin one.
- **Sequence scope** — what the counter is partitioned by: `tenant` (default for
  tenant-isolated data) or `global` (platform-wide). Those two are the whole enum;
  a sub-count *inside a parent row* does not exist — express the parent in the
  format instead (`{ABBR:ClientName:3}-{SEQ:4}`). MUST be consistent with the
  entity's `tenantMode`.
- **Reset** — when the counter restarts: `none` / `yearly` / `monthly` / `daily`.
  A reset MUST align with a matching date token in the format (yearly ⇒ `{YY}`).
- **Gapless** — `true` (no gaps allowed — legal/accounting; needs transactional
  allocation) or `false` (gaps tolerated — faster, e.g. on rollback).
- **Immutable** — once assigned the code never changes (state it in the rule).

These map 1:1 to the data-model `codePattern` (`create-data-model`) and drive the
socle's code-generation engine, which allocates the number atomically (and
gaplessly) at insert and exposes the pattern in Administration → Configuration →
Code patterns. Never model a counter table for it. The valid examples MUST show the
sequence **progressing** and the **reset boundary**; an invalid example MUST show
**no duplicate** under concurrency / across tenants.

```markdown
### BR-007 — Numéro de demande
- **Type** : numbering
- **Sévérité** : err
- **Portée** : RH / DEMANDES
- **Condition** : QUAND une demande est créée ALORS son numéro = `AFF-{YY}-{SEQ:4}`
  (séquence par tenant, remise à zéro chaque année, sans trou, immuable).
- **Expression** : `format = "AFF-{YY}-{SEQ:4}" ; scope = tenant ; reset = yearly ; gapless = true ; immutable = true`
- **Implémentation** : AUCUNE — alloué par le socle (`CodedEntitySaveHandler`) à l'insert ; cette règle est la SPEC du `**Code pattern**` de l'entité (ne rien coder : ni service, ni compteur, ni validateur d'unicité).
- **Code d'erreur** : `demande.reference.duplicate`
- **Cas valides** :
  - 1re demande 2026 (tenant A) → `AFF-26-0001` ; suivante → `AFF-26-0002`.
  - 1re demande 2027 → `AFF-27-0001` (reset annuel).
  - 1re demande tenant B en 2026 → `AFF-26-0001` (séquence isolée par tenant).
- **Cas invalides** :
  - Deux créations concurrentes → JAMAIS le même numéro (GARANTI par le socle — allocation atomique ; ne pas ré-implémenter de vérification d'unicité).
  - Numéro modifié après création → rejeté (immuable par construction — `Code` sans setter public).
- **Cas d'usage liés** : UC-RH-DEMANDES-001 (postcondition)
```

### Coverage — aim for 3–5 on rules with edge cases
1 valid happy path · 1 invalid clear failure · 1 invalid edge case · 1 valid edge
case on the right side. Pick 2–3 relevant edge cases per type:

| Type | Edge cases worth covering |
|------|---------------------------|
| `validation` | null, empty, max length, format mismatch, case-insensitive duplicate, whitespace-only |
| `calculation` | zero, negatives, rounding (2 vs 4 decimals), currency conversion, division by zero |
| `state-transition` | same-state (idempotent?), skipped state, concurrent transition, terminal state, re-entry |
| `ownership` | owner ok, manager bypass, deactivated owner, ownership transfer, service account |
| `constraint` | exactly at limit, just-above, just-below, zero, negative, null vs 0 |
| `derivation` | missing/empty inputs, recompute on update vs create |
| `workflow` | threshold exactly at boundary, multiple approvers, delegation, timeout |
| `integrity` | soft-deleted parent, cascade delete, circular/self FK, orphaned children |
| `cross-cutting` | system operation with no user, batch of N, async, retry |
| `compliance` | retention expiry, erasure conflict, cross-border transfer, retroactive change |
| `numbering` | first-of-period, sequence progression, per-tenant isolation, reset boundary (year rollover), concurrent allocation (no duplicate/gap if gapless) |

Application-level rules: cover inter-module scenarios (audit trail across modules
A→B). Module-level: show both authorized and unauthorized access patterns.

## Worked example — a fully elaborated rule (doc form)

```markdown
### BR-001 — Email unique
- **Type** : validation
- **Sévérité** : err
- **Portée** : HR / EMPLOYEES
- **Condition** : QUAND un utilisateur est créé/modifié ALORS son email ne doit
  matcher aucun email d'utilisateur non-supprimé (insensible à la casse).
- **Expression** : `NOT EXISTS(SELECT 1 FROM users WHERE LOWER(email)=LOWER(@email) AND id!=@id AND deleted_at IS NULL)`
- **Code d'erreur** : `USER_EMAIL_DUPLICATE`
- **Cas valides** :
  - Aucun 'alice@x.com' → créer {email:'alice@x.com'} → 201 créé.
  - Email d'un utilisateur soft-deleted réutilisable → 201 créé (tombstone).
- **Cas invalides** :
  - 'bob@x.com' existe → créer {email:'bob@x.com'} → 409 `USER_EMAIL_DUPLICATE`.
  - 'BOB@X.COM' → 409 `USER_EMAIL_DUPLICATE` (comparaison insensible à la casse).
- **Cas d'usage liés** : UC-HR-EMPLOYEES-DIRECTORY-001 (précondition)
```

Filter-rule variant — `Code d'erreur` blank, invalid case = absence:

```markdown
### BR-014 — Contacts anonymisés exclus des recherches
- **Type** : constraint
- **Sévérité** : err
- **Portée** : CRM / CONTACTS
- **Condition** : QUAND une recherche de contacts s'exécute ALORS les contacts
  avec `anonymizedAt` non nul sont exclus.
- **Expression** : `WHERE contact.anonymizedAt IS NULL`
- **Code d'erreur** : —
- **Cas valides** : contact actif (anonymizedAt null) → présent dans la réponse.
- **Cas invalides** : contact anonymisé → ABSENT de la réponse (filtré au
  niveau repository, aucune erreur renvoyée).
- **Cas d'usage liés** : —
```

## Cadence — pin it ONCE, here, before drafting anything

The worklist is the rules of the pinned scope with **no `Condition`, or no
valid/invalid example**. It is DERIVED from the doc, never remembered — an
interruption loses nothing, the next run recomputes it.

<!-- detail-cadence:v1 — drift-tested against lib/detail-cadence.ts (edit ALL carriers or the suite fails) -->
| Cadence | What the model does | Where the human gate sits |
|---|---|---|
| **Pas à pas** | One item at a time: full draft, validation, write, next | One AskUserQuestion **per item** |
| **Par lot** | Draft every remaining item of the pinned scope internally, then present a compact recap + the open arbitrations | **One** AskUserQuestion for the whole batch, **one** write |
| **Enchaîné** | Draft and write the whole scope without stopping, then run the deterministic audit and publish its verdict + the arbitrations | None mid-run — the gate is the audit verdict + the arbitration list, after the fact |
<!-- /detail-cadence:v1 -->

**Default: `Par lot`** — which is what this level has always done; it is now
named, and the two others are reachable. Ask with ONE AskUserQuestion, then hold
it for the run.

Do NOT ask when: the worklist has **< 2 rules** (→ `Pas à pas`); the request
already names the cadence ("détaille tout" → `Enchaîné`, "une par une" →
`Pas à pas`, or `--cadence`); or you are a **subagent** / any context where
AskUserQuestion is forbidden (→ `Enchaîné`, silently).

Three rules hold in EVERY cadence: **ONE Write per batch** (the doc is
overwritten whole — an omitted rule is a deleted rule), **never cross the pinned
scope**, and **publish your arbitrations** (see below).

## Workflow
1. Pick the next rule with no examples (or no condition).
2. Fill `Condition`, `Expression`, `Code d'erreur` (blank for filter rules).
3. Write the valid example(s) first (the happy path), then the invalid one(s).
4. Review: could a developer with no context turn this into code AND a test? If
   not, add detail.
5. Then, per cadence:
   - **`Pas à pas`** — validate this rule with the user, re-Write the doc with
     the full set, move to the next rule.
   - **`Par lot`** — repeat 1-4 for all rules at the scope, present a compact
     recap (one line per rule: `BR-007 — Numéro de demande — error code
     `demandes.demande.numero-invalide` — 2 valid / 3 invalid`) then the
     arbitrations, then ONE AskUserQuestion (`Tout valider` / `Reprendre
     certaines règles` / `Repasser en pas à pas`; on *Reprendre*, re-draft and
     re-present ONLY the named rules, loop until validated), then ONE re-Write
     of the doc with the full set.
   - **`Enchaîné`** — repeat 1-4 for all rules, ONE re-Write, then run the
     deterministic audit and publish its verdict:
     ```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":["rules"]}'
     ```
     plus the arbitration list. No rule without a verbatim upstream trace, and
     never chain onto the next scope on your own.

## Arbitrations — ask, or say it out loud
You usually have enough context to draft. Ask (via AskUserQuestion, 2–4 closed
options) only for: a choice between two plausible error codes; a threshold
constant ("max discount: 50 / 100 / no limit"); or an edge-case decision
("soft-deleted users still block email reuse, or release it?").

In `Pas à pas` ask on the spot. In `Par lot` and `Enchaîné` you do not stop —
so each of those decisions becomes an **arbitration**: pick the most common
business pattern, and list it at the end by rule code with the open question
(`BR-007 — seuil de 50 retenu par défaut, à confirmer`). A decision taken in a
batch and never surfaced is the one failure mode a batch cadence has.

## Quality checks before writing
For every rule: `Condition` is `QUAND…ALORS`; `Expression` is non-empty; the
pattern is identified (enforcement → error code + invalid examples name it;
filter → error code blank + invalid examples describe absence); every example has
a concrete given/when/then; at least 1 valid + 1 invalid; a developer could code
it and test it.
