---
name: step-03-specify
description: Sequential deep dive per module - entities, BRs, UCs, permissions
model: opus
next_step: steps/step-04-consolidate.md
---

# Step 03 — Specify (Deep Dive per Module)

## Objective

For each module (in dependency order), produce complete specifications: entities, business rules, use cases, and permissions. Interface specs (screens, wireframes, navigation) are handled by Phase 2 (`/business-analyse-design`).

## Context Recovery

Re-read session variables from `.business-analyse/config.json`:
```javascript
const config = readJSON('.business-analyse/config.json').currentFeature;
const { id: feature_id, docsDir: docs_dir, projectSlug, application: application_name,
        applicationCode, workflowMode: workflow_mode, language } = config;

// Path helpers (ba-006)
function toKebabCase(code) {
  return code.replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2').toLowerCase();
}
function resolveModuleDir(appCode, moduleCode) {
  return `${docs_dir}/${toKebabCase(appCode)}/${toKebabCase(moduleCode)}`;
}
```

> **Module directory (ba-006):** `mod.dir = resolveModuleDir(applicationCode, module.code)`
> Example: `docs/projet-rh/v1.0/human-resources/employees/`

## Prerequisites

- Step 02 (structure) completed
- Applications, modules, sections, dependencies identified

## Sequential Execution

**No team mode.** Each module receives 100% of LLM attention for maximum specification quality.

Process modules in topological order (dependencies first):
```
For module in topologicalSort(modules):
    specify(module)
```

## Per-Module Specification Loop

For each module, execute these sub-steps:

### A. Context Loading

Load via ba-reader:
- Module's `index.json` (if exists from previous run)
- Application `cadrage.json` for stakeholder context (including `_preAnalysis.domain_research`)
- Completed modules' summaries (compact, <100 lines each)

### A-bis. Entity Pattern Research (WebSearch — per module)

> **Before defining entities, validate design against industry best practices.**
> Uses domain_research from step-01 as baseline, then does targeted searches for this specific module.

```
IF cadrage._preAnalysis.domain_research exists:
  → Load as baseline (already researched in step-01)
ELSE:
  → Run a quick WebSearch: "{module_domain} entity data model best practices"

FOR each main entity in this module (max 2-3 searches):
  Query: "{entity_name} entity design pattern {domain}"
  (e.g., "invoice entity design ERP", "timesheet data model best practices")

  EXTRACT (ULTRATHINK — internal only):
  - Standard attributes for this entity type
  - Common relationships and cardinality
  - Versioned vs. inline data patterns (e.g., salary history)
  - Workflow states if applicable
  - Known anti-patterns to avoid

  CROSS-CHECK against SmartStack conventions (B-bis rules):
  - Would this entity trigger Person Extension Pattern?
  - Are there attributes that should be versioned (salary, rate)?
  - Does this entity already exist in the SmartStack socle DB?
```

> **CRITICAL:** This research informs entity design but does NOT override SmartStack conventions.
> If web research suggests putting firstName on Employee, the B-bis guard still blocks it
> because SmartStack uses auth_Users for personal data.

### B. Entities

For each entity identified in step 02:

```json
{
  "name": "Employee",
  "description": "Représente un employé de l'entreprise",
  "personRoleConfig": { "variant": "mandatory", "userFields": ["firstName", "lastName", "email"] },
  "attributes": [
    { "name": "code", "type": "string", "required": true, "description": "Identifiant unique auto-généré" },
    { "name": "userId", "type": "string", "required": true, "description": "FK vers auth_Users (ASP.NET Identity — type string, NOT guid)" },
    { "name": "departmentId", "type": "guid", "required": true, "description": "Département d'affectation" },
    { "name": "hireDate", "type": "date", "required": true, "description": "Date d'embauche" },
    { "name": "position", "type": "string", "description": "Poste occupé" },
    { "name": "status", "type": "enum", "options": ["Active", "Inactive", "OnLeave", "Terminated"], "defaultValue": "Active", "description": "Statut de l'employé" }
  ],
  "versionedAttributes": [
    { "entity": "Salary", "attributes": ["grossAmount", "netAmount", "effectiveDate", "currency"], "reason": "Historique salarial versionné" }
  ],
  "estimatedVolume": { "monthly": 50, "total2y": 1200 },
  "searchableFields": ["code", "position", "status"],
  "defaultFilters": ["status"],
  "relationships": [
    { "target": "Department", "type": "ManyToOne", "description": "Appartient à un département" },
    { "target": "Contract", "type": "OneToMany", "description": "Possède plusieurs contrats" },
    { "target": "Salary", "type": "OneToMany", "description": "Historique de salaires versionnés" }
  ]
}
```

### B-bis. SmartStack Entity Convention Guards (MANDATORY)

Before finalizing each entity, apply these rules:

**B-bis-1. Person Extension Pattern** (ref: `entity-architecture-decision.md` section 0)
IF the entity matches a person role (Employee, Customer, Manager, Consultant, etc.):
- **DO NOT** add firstName, lastName, email, phoneNumber as direct attributes
- **DO** add `userId` (type: `string`, FK to auth_Users — ASP.NET Identity uses string IDs)
- **DO** add `personRoleConfig` metadata with variant (mandatory/optional)
- Personal fields come from User, not from the domain entity

**B-bis-2. Versioned Sensitive Data**
IF the entity has attributes that change over time with audit requirements (salary, rate, grade):
- **DO NOT** put them directly on the entity as single fields
- **DO** extract into a versioned satellite table (e.g., Employee → Salary with effectiveDate)
- Mark with `"versionedAttributes"` in entity spec

**B-bis-3. SmartStack Socle Entities (NEVER redefine)**
- Users/Identity → `auth_Users` (managed by SmartStack Identity)
- Tenants → `tenant_Tenants` (managed by SmartStack Core)
- Departments → `ref_Departments` or `rh_Departments` (check if exists in target DB)

**B-bis-4. Foreign Key Conventions**
- All FK attributes MUST end with `Id` suffix (e.g., `departmentId`, `userId`)
- FK to Identity users MUST use type `string` (ASP.NET Identity convention, NOT guid)
- FK to domain entities MUST use type `guid`

### C. Business Rules

For each entity/process, identify rules. **Each rule MUST specify `sectionCode`** matching a code from `anticipatedSections[]`:

```json
{
  "id": "BR-VAL-EMPLOYEES-001",
  "name": "Validation date embauche",
  "category": "validation",
  "sectionCode": "list",
  "statement": "La date d'embauche ne peut pas être dans le futur",
  "example": "Date embauche = 2027-01-01 → erreur car > date du jour",
  "entities": ["Employee"],
  "severity": "blocking"
}
```

Categories: `validation`, `calculation`, `workflow`, `security`, `data`, `notification`

> **OBLIGATOIRE** : Chaque attribut `enum` DOIT avoir un `defaultValue`. Chaque attribut numérique borné DOIT avoir `validation.min/max`. Les types userId (FK vers Identity) DOIVENT utiliser le type `string` (convention ASP.NET Identity).

> **OBLIGATOIRE** : Chaque attribut marqué "calculé" dans entities.json DOIT avoir une règle `category: "calculation"` correspondante avec le champ `formula`.

Exemple de règle de calcul :
```json
{
  "id": "BR-CALC-INV-003",
  "category": "calculation",
  "statement": "Le reste à payer = total - montant payé",
  "formula": "remainingAmount = total - paidAmount",
  "entities": ["Invoice"]
}
```

### D. Use Cases

For each stakeholder action. **Each use case MUST specify `sectionCode`** matching a code from `anticipatedSections[]` — this links the UC to the screen/page where it happens:

> **CANONICAL FORMAT (MANDATORY):** The usecases.json file MUST use these exact keys:
> - Root key: `"useCases"` (camelCase, NOT "usecases")
> - Actor field: `"primaryActor"` (NOT "actor")
> - Steps field: `"mainScenario"` (string[], NOT "steps" with objects)
> - Alternatives: `"alternativeScenarios"` (object[] with `{name, steps}`, NOT "alternative" as flat string)
> - This matches specification-schema.json. Deviation causes normalization overhead in 4+ downstream skills.

```json
{
  "useCases": [
    {
      "id": "UC-EMPLOYEES-001",
      "name": "Créer un employé",
      "sectionCode": "list",
      "primaryActor": "Responsable RH",
      "preconditions": ["L'utilisateur a la permission HumanResources.Employees.Create"],
      "mainScenario": [
        "L'utilisateur ouvre la page de création",
        "Il remplit les champs obligatoires (nom, département, date embauche)",
        "Il valide le formulaire",
        "Le système vérifie les règles métier (BR-VAL-EMPLOYEES-001)",
        "Le système crée l'employé et affiche la fiche"
      ],
      "alternativeScenarios": [
        {
          "name": "Données invalides",
          "steps": ["Le système affiche les erreurs de validation", "L'utilisateur corrige et re-soumet"]
        }
      ],
      "businessRules": ["BR-VAL-EMPLOYEES-001"],
      "result": "L'employé est créé avec le statut 'Actif'"
    }
  ]
}
```

### E. Permissions

Define the permission matrix:

```json
{
  "roles": ["RH Admin", "Manager", "Employee"],
  "matrix": [
    { "role": "RH Admin", "permissions": ["Read", "Create", "Update", "Delete", "Export"] },
    { "role": "Manager", "permissions": ["Read", "Export"] },
    { "role": "Employee", "permissions": ["Read"] }
  ],
  "permissionPaths": [
    "HumanResources.Employees.Read",
    "HumanResources.Employees.Create",
    "HumanResources.Employees.Update",
    "HumanResources.Employees.Delete",
    "HumanResources.Employees.Export"
  ]
}
```

**Auto-detection rules:**
- If the cadrage mentions "export", "Excel", "CSV", or "télécharger" → automatically add `.export` permission and an export use case (UC-{PREFIX}-EXPORT)
- If the cadrage mentions "import", "importer", "upload" → automatically add `.import` permission and an import use case (UC-{PREFIX}-IMPORT)

### F. Seed Data pour tables de référence

> **OBLIGATOIRE** : Chaque entité de type "table de référence" (lookup, étapes de pipeline, types) DOIT avoir ses valeurs initiales documentées.

```json
{
  "entity": "OpportunityStage",
  "type": "reference-table",
  "seedData": [
    { "name": "Qualification", "sortOrder": 1, "defaultProbability": 20, "isWon": false, "isLost": false },
    { "name": "Proposition", "sortOrder": 2, "defaultProbability": 50, "isWon": false, "isLost": false },
    { "name": "Négociation", "sortOrder": 3, "defaultProbability": 75, "isWon": false, "isLost": false },
    { "name": "Gagné", "sortOrder": 4, "defaultProbability": 100, "isWon": true, "isLost": false },
    { "name": "Perdu", "sortOrder": 5, "defaultProbability": 0, "isWon": false, "isLost": true }
  ]
}
```

### G. Interface Specs — Delegated to /business-analyse-design

> **Screen specifications are NOT produced in this step.**
> Interface design (SmartTable columns, SmartForm fields, wireframes, navigation) is handled by Phase 2: `/business-analyse-design`.
> This separation allows iterating on UI design without replaying the full analysis cycle.

### H. Write & Advance

After completing all sub-steps for a module:

1. **Validate against schema BEFORE writing** (uses schemas deployed in step-00):
   ```javascript
   ba-writer.validate("entities", entitiesData, docs_dir + "/schemas/entities.json")
   ba-writer.validate("rules", rulesData, docs_dir + "/schemas/rules.json")
   ba-writer.validate("usecases", usecasesData, docs_dir + "/schemas/usecases.json")
   ba-writer.validate("permissions", permissionsData, docs_dir + "/schemas/permissions.json")
   ```
   > If validation fails: fix the data and re-validate. Do NOT write invalid JSON.

2. Write via ba-writer:
   - `entities.json`
   - `rules.json`
   - `usecases.json`
   - `permissions.json`
   - Update `index.json` (hashes, summary counts)

3. Update module status to `specified`

3. Advance to next module in topological order

### I. Module Completion Check

After all modules are specified:
- Verify all modules have status `specified`
- Display summary: total entities, rules, UCs, permissions across all modules
- Remind user: "Run /business-analyse-design to design interfaces for these modules"
- Transition to step 04

## POST-CHECK (EXECUTABLE — BLOCKING)

Before advancing to step 04, run these validations for EACH module:

```javascript
const errors = [];
const warnings = [];

for (const mod of modules) {
  // mod.dir = resolveModuleDir(applicationCode, mod.code)
  // e.g., docs/projet-rh/v1.0/human-resources/employees/
  const ent = READ(mod.dir + '/entities.json')?.entities || [];
  const ucs = READ(mod.dir + '/usecases.json')?.useCases || [];
  const brs = READ(mod.dir + '/rules.json')?.rules || [];
  const perms = READ(mod.dir + '/permissions.json');
  const sections = mod.anticipatedSections || [];
  const sectionCodes = new Set(sections.map(s => s.code));

  // Mandatory checks (BLOCKING)
  if (ent.length === 0) errors.push(mod.code + ": 0 entities");
  if (ucs.length === 0) errors.push(mod.code + ": 0 use cases");
  if (brs.length === 0) errors.push(mod.code + ": 0 business rules");
  if (!perms?.permissionPaths?.length) errors.push(mod.code + ": no permissions");

  // sectionCode validation
  for (const uc of ucs) {
    if (!uc.sectionCode) errors.push(mod.code + ": UC '" + uc.id + "' missing sectionCode");
    else if (!sectionCodes.has(uc.sectionCode)) errors.push(mod.code + ": UC '" + uc.id + "' sectionCode '" + uc.sectionCode + "' not in anticipatedSections");
  }
  for (const br of brs) {
    if (!br.sectionCode) errors.push(mod.code + ": BR '" + br.id + "' missing sectionCode");
    else if (!sectionCodes.has(br.sectionCode)) errors.push(mod.code + ": BR '" + br.id + "' sectionCode '" + br.sectionCode + "' not in anticipatedSections");
  }

  // Entity attribute checks
  for (const e of ent) {
    for (const a of (e.attributes || [])) {
      if (!a.type) warnings.push(mod.code + ": entity " + e.name + " attr '" + a.name + "' missing type");
      if (a.type === 'enum' && !a.defaultValue) errors.push(mod.code + ": enum attr '" + a.name + "' missing defaultValue");
      // FK naming convention: must end with "Id"
      if ((a.type === 'guid' || a.type === 'string') && a.foreignKey && !a.name.endsWith('Id'))
        warnings.push(mod.code + ": entity " + e.name + " FK attr '" + a.name + "' should end with 'Id'");
    }
    // Person Extension Pattern check
    const personFields = ['firstName', 'lastName', 'email', 'phoneNumber', 'phone'];
    const foundPersonFields = (e.attributes || []).filter(a => personFields.includes(a.name));
    if (foundPersonFields.length >= 3 && !e.personRoleConfig) {
      errors.push(mod.code + ": entity '" + e.name + "' has " + foundPersonFields.length +
        " person fields (" + foundPersonFields.map(f => f.name).join(", ") +
        ") but no personRoleConfig — use Person Extension Pattern (entity-architecture-decision.md section 0). " +
        "Person fields (firstName, lastName, email) belong on auth_Users, not on the domain entity.");
    }
  }

  // Canonical usecases key check
  const rawUCFile = READ(mod.dir + '/usecases.json');
  if (rawUCFile && !rawUCFile.useCases && rawUCFile.usecases) {
    errors.push(mod.code + ": usecases.json uses 'usecases' key instead of canonical 'useCases' — fix before proceeding");
  }
  // Check steps serialization format
  for (const uc of ucs) {
    if (uc.steps && Array.isArray(uc.steps) && uc.steps.length > 0 && typeof uc.steps[0] === 'object') {
      errors.push(mod.code + ": UC '" + uc.id + "' uses steps[] with objects — must use mainScenario[] with strings");
    }
    if (uc.actor && !uc.primaryActor) {
      warnings.push(mod.code + ": UC '" + uc.id + "' uses 'actor' instead of canonical 'primaryActor'");
    }
  }

  // Cross-reference checks
  for (const uc of ucs) {
    for (const brRef of (uc.businessRules || [])) {
      if (!brs.find(br => br.id === brRef)) warnings.push(mod.code + ": UC '" + uc.id + "' references non-existent BR '" + brRef + "'");
    }
  }

  // Section permission checks
  for (const section of sections) {
    if (section.sectionType === 'view') {
      const viewPerms = (perms?.permissionPaths || []).filter(p => p.includes('.' + section.code + '.'));
      if (viewPerms.length > 0) errors.push(mod.code + ": view section '" + section.code + "' has " + viewPerms.length + " permissions (should inherit)");
    }
  }
}

if (errors.length > 0) {
  Display("POST-CHECK FAILED (" + errors.length + " errors):");
  errors.forEach(e => Display("  ✗ " + e));
  BLOCKING_ERROR("Fix all errors before advancing to step 04");
}
if (warnings.length > 0) {
  Display("POST-CHECK warnings (" + warnings.length + "):");
  warnings.forEach(w => Display("  ⚠ " + w));
}
Display("POST-CHECK PASS: " + modules.length + " modules validated");
```

## Transition

→ Step 04 (consolidate) — Cross-module validation
