@atlashub/smartstack-cli 4.51.0 → 4.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/index.js +53 -1
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/skills/apex/references/core-seed-data.md +15 -0
  5. package/templates/skills/apex/references/error-classification.md +27 -3
  6. package/templates/skills/apex/references/post-checks.md +3 -1
  7. package/templates/skills/apex/steps/step-00-init.md +57 -0
  8. package/templates/skills/apex/steps/step-03-execute.md +33 -5
  9. package/templates/skills/apex/steps/step-03b-layer1-seed.md +18 -0
  10. package/templates/skills/apex/steps/step-03d-layer3-frontend.md +3 -0
  11. package/templates/skills/apex/steps/step-04-examine.md +35 -0
  12. package/templates/skills/business-analyse/references/canonical-json-formats.md +200 -0
  13. package/templates/skills/business-analyse/steps/step-03-specify.md +94 -20
  14. package/templates/skills/business-analyse-design/steps/step-01-screens.md +41 -4
  15. package/templates/skills/business-analyse-develop/references/init-resume-recovery.md +54 -0
  16. package/templates/skills/business-analyse-develop/steps/step-00-init.md +10 -3
  17. package/templates/skills/business-analyse-develop/steps/step-01-task.md +14 -2
  18. package/templates/skills/business-analyse-develop/steps/step-04-check.md +12 -2
  19. package/templates/skills/business-analyse-handoff/references/entity-canonicalization.md +158 -0
  20. package/templates/skills/business-analyse-handoff/steps/step-01-transform.md +26 -3
  21. package/templates/skills/business-analyse-handoff/steps/step-02-export.md +14 -0
  22. package/templates/skills/business-analyse-html/SKILL.md +4 -0
  23. package/templates/skills/business-analyse-html/references/data-build.md +24 -17
  24. package/templates/skills/business-analyse-html/references/data-mapping.md +79 -35
  25. package/templates/skills/business-analyse-html/references/output-modes.md +2 -1
  26. package/templates/skills/business-analyse-html/steps/step-01-collect.md +7 -2
  27. package/templates/skills/business-analyse-html/steps/step-02-build-data.md +155 -40
  28. package/templates/skills/business-analyse-html/steps/step-04-verify.md +22 -4
@@ -54,25 +54,56 @@ For each entity identified in step 02:
54
54
  {
55
55
  "name": "Employee",
56
56
  "description": "Représente un employé de l'entreprise",
57
+ "personRoleConfig": { "variant": "mandatory", "userFields": ["firstName", "lastName", "email"] },
57
58
  "attributes": [
58
- { "name": "code", "type": "string", "required": true, "description": "Identifiant unique" },
59
- { "name": "userId", "type": "guid", "required": true, "description": "Référence vers l'utilisateur" },
59
+ { "name": "code", "type": "string", "required": true, "description": "Identifiant unique auto-généré" },
60
+ { "name": "userId", "type": "string", "required": true, "description": "FK vers auth_Users (ASP.NET Identity — type string, NOT guid)" },
60
61
  { "name": "departmentId", "type": "guid", "required": true, "description": "Département d'affectation" },
61
62
  { "name": "hireDate", "type": "date", "required": true, "description": "Date d'embauche" },
62
63
  { "name": "position", "type": "string", "description": "Poste occupé" },
63
- { "name": "status", "type": "enum", "options": ["Active", "Inactive", "OnLeave", "Terminated"], "defaultValue": "Active", "description": "Statut de l'employé" },
64
- { "name": "salary", "type": "decimal", "validation": { "min": 0 }, "description": "Salaire mensuel" }
64
+ { "name": "status", "type": "enum", "options": ["Active", "Inactive", "OnLeave", "Terminated"], "defaultValue": "Active", "description": "Statut de l'employé" }
65
+ ],
66
+ "versionedAttributes": [
67
+ { "entity": "Salary", "attributes": ["grossAmount", "netAmount", "effectiveDate", "currency"], "reason": "Historique salarial versionné" }
65
68
  ],
66
69
  "estimatedVolume": { "monthly": 50, "total2y": 1200 },
67
70
  "searchableFields": ["code", "position", "status"],
68
71
  "defaultFilters": ["status"],
69
72
  "relationships": [
70
73
  { "target": "Department", "type": "ManyToOne", "description": "Appartient à un département" },
71
- { "target": "Contract", "type": "OneToMany", "description": "Possède plusieurs contrats" }
74
+ { "target": "Contract", "type": "OneToMany", "description": "Possède plusieurs contrats" },
75
+ { "target": "Salary", "type": "OneToMany", "description": "Historique de salaires versionnés" }
72
76
  ]
73
77
  }
74
78
  ```
75
79
 
80
+ ### B-bis. SmartStack Entity Convention Guards (MANDATORY)
81
+
82
+ Before finalizing each entity, apply these rules:
83
+
84
+ **B-bis-1. Person Extension Pattern** (ref: `entity-architecture-decision.md` section 0)
85
+ IF the entity matches a person role (Employee, Customer, Manager, Consultant, etc.):
86
+ - **DO NOT** add firstName, lastName, email, phoneNumber as direct attributes
87
+ - **DO** add `userId` (type: `string`, FK to auth_Users — ASP.NET Identity uses string IDs)
88
+ - **DO** add `personRoleConfig` metadata with variant (mandatory/optional)
89
+ - Personal fields come from User, not from the domain entity
90
+
91
+ **B-bis-2. Versioned Sensitive Data**
92
+ IF the entity has attributes that change over time with audit requirements (salary, rate, grade):
93
+ - **DO NOT** put them directly on the entity as single fields
94
+ - **DO** extract into a versioned satellite table (e.g., Employee → Salary with effectiveDate)
95
+ - Mark with `"versionedAttributes"` in entity spec
96
+
97
+ **B-bis-3. SmartStack Socle Entities (NEVER redefine)**
98
+ - Users/Identity → `auth_Users` (managed by SmartStack Identity)
99
+ - Tenants → `tenant_Tenants` (managed by SmartStack Core)
100
+ - Departments → `ref_Departments` or `rh_Departments` (check if exists in target DB)
101
+
102
+ **B-bis-4. Foreign Key Conventions**
103
+ - All FK attributes MUST end with `Id` suffix (e.g., `departmentId`, `userId`)
104
+ - FK to Identity users MUST use type `string` (ASP.NET Identity convention, NOT guid)
105
+ - FK to domain entities MUST use type `guid`
106
+
76
107
  ### C. Business Rules
77
108
 
78
109
  For each entity/process, identify rules. **Each rule MUST specify `sectionCode`** matching a code from `anticipatedSections[]`:
@@ -111,23 +142,39 @@ Exemple de règle de calcul :
111
142
 
112
143
  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:
113
144
 
145
+ > **CANONICAL FORMAT (MANDATORY):** The usecases.json file MUST use these exact keys:
146
+ > - Root key: `"useCases"` (camelCase, NOT "usecases")
147
+ > - Actor field: `"primaryActor"` (NOT "actor")
148
+ > - Steps field: `"mainScenario"` (string[], NOT "steps" with objects)
149
+ > - Alternatives: `"alternativeScenarios"` (object[] with `{name, steps}`, NOT "alternative" as flat string)
150
+ > - This matches specification-schema.json. Deviation causes normalization overhead in 4+ downstream skills.
151
+
114
152
  ```json
115
153
  {
116
- "id": "UC-EMPLOYEES-001",
117
- "name": "Créer un employé",
118
- "sectionCode": "list",
119
- "actor": "Responsable RH",
120
- "preconditions": ["L'utilisateur a la permission HumanResources.Employees.Create"],
121
- "steps": [
122
- "L'utilisateur ouvre la page de création",
123
- "Il remplit les champs obligatoires (nom, département, date embauche)",
124
- "Il valide le formulaire",
125
- "Le système vérifie les règles métier (BR-VAL-EMPLOYEES-001)",
126
- "Le système crée l'employé et affiche la fiche"
127
- ],
128
- "alternative": "Si les données sont invalides, le système affiche les erreurs",
129
- "businessRules": ["BR-VAL-EMPLOYEES-001"],
130
- "result": "L'employé est créé avec le statut 'Actif'"
154
+ "useCases": [
155
+ {
156
+ "id": "UC-EMPLOYEES-001",
157
+ "name": "Créer un employé",
158
+ "sectionCode": "list",
159
+ "primaryActor": "Responsable RH",
160
+ "preconditions": ["L'utilisateur a la permission HumanResources.Employees.Create"],
161
+ "mainScenario": [
162
+ "L'utilisateur ouvre la page de création",
163
+ "Il remplit les champs obligatoires (nom, département, date embauche)",
164
+ "Il valide le formulaire",
165
+ "Le système vérifie les règles métier (BR-VAL-EMPLOYEES-001)",
166
+ "Le système crée l'employé et affiche la fiche"
167
+ ],
168
+ "alternativeScenarios": [
169
+ {
170
+ "name": "Données invalides",
171
+ "steps": ["Le système affiche les erreurs de validation", "L'utilisateur corrige et re-soumet"]
172
+ }
173
+ ],
174
+ "businessRules": ["BR-VAL-EMPLOYEES-001"],
175
+ "result": "L'employé est créé avec le statut 'Actif'"
176
+ }
177
+ ]
131
178
  }
132
179
  ```
133
180
 
@@ -250,6 +297,33 @@ for (const mod of modules) {
250
297
  for (const a of (e.attributes || [])) {
251
298
  if (!a.type) warnings.push(mod.code + ": entity " + e.name + " attr '" + a.name + "' missing type");
252
299
  if (a.type === 'enum' && !a.defaultValue) errors.push(mod.code + ": enum attr '" + a.name + "' missing defaultValue");
300
+ // FK naming convention: must end with "Id"
301
+ if ((a.type === 'guid' || a.type === 'string') && a.foreignKey && !a.name.endsWith('Id'))
302
+ warnings.push(mod.code + ": entity " + e.name + " FK attr '" + a.name + "' should end with 'Id'");
303
+ }
304
+ // Person Extension Pattern check
305
+ const personFields = ['firstName', 'lastName', 'email', 'phoneNumber', 'phone'];
306
+ const foundPersonFields = (e.attributes || []).filter(a => personFields.includes(a.name));
307
+ if (foundPersonFields.length >= 3 && !e.personRoleConfig) {
308
+ errors.push(mod.code + ": entity '" + e.name + "' has " + foundPersonFields.length +
309
+ " person fields (" + foundPersonFields.map(f => f.name).join(", ") +
310
+ ") but no personRoleConfig — use Person Extension Pattern (entity-architecture-decision.md section 0). " +
311
+ "Person fields (firstName, lastName, email) belong on auth_Users, not on the domain entity.");
312
+ }
313
+ }
314
+
315
+ // Canonical usecases key check
316
+ const rawUCFile = READ(mod.dir + '/usecases.json');
317
+ if (rawUCFile && !rawUCFile.useCases && rawUCFile.usecases) {
318
+ errors.push(mod.code + ": usecases.json uses 'usecases' key instead of canonical 'useCases' — fix before proceeding");
319
+ }
320
+ // Check steps serialization format
321
+ for (const uc of ucs) {
322
+ if (uc.steps && Array.isArray(uc.steps) && uc.steps.length > 0 && typeof uc.steps[0] === 'object') {
323
+ errors.push(mod.code + ": UC '" + uc.id + "' uses steps[] with objects — must use mainScenario[] with strings");
324
+ }
325
+ if (uc.actor && !uc.primaryActor) {
326
+ warnings.push(mod.code + ": UC '" + uc.id + "' uses 'actor' instead of canonical 'primaryActor'");
253
327
  }
254
328
  }
255
329
 
@@ -149,28 +149,65 @@ Map entity attribute types to UI component types:
149
149
 
150
150
  For each module, write via ba-writer:
151
151
 
152
+ > **CANONICAL FORMAT (MANDATORY):** screens.json MUST use the `sections[]` wrapper format below.
153
+ > Keys MUST be `sectionCode` and `sectionLabel` (not `id`/`displayName`/`code`/`label`).
154
+ > Each section MUST have a `resources[]` array with typed resource objects (SmartTable, SmartForm, etc.).
155
+ > The flat `screens[]` format (one object per screen with componentType at top level) is **DEPRECATED**.
156
+ > All downstream consumers (business-analyse-html, business-analyse-handoff, business-analyse-develop) expect `sections[]`.
157
+
152
158
  ```json
153
159
  {
154
160
  "sections": [
155
161
  {
156
162
  "sectionCode": "list",
157
163
  "sectionLabel": "Liste des employes",
158
- "resources": [ /* SmartTable, filters */ ]
164
+ "resources": [
165
+ {
166
+ "code": "employees-grid",
167
+ "type": "SmartTable",
168
+ "label": "Grille des employes",
169
+ "columns": [ /* ... */ ],
170
+ "filters": [ /* ... */ ],
171
+ "actions": ["create", "export"],
172
+ "permission": "HumanResources.Employees.Read"
173
+ }
174
+ ]
159
175
  },
160
176
  {
161
- "sectionCode": "form",
177
+ "sectionCode": "detail",
162
178
  "sectionLabel": "Fiche employe",
163
- "resources": [ /* SmartForm with tabs */ ]
179
+ "resources": [
180
+ {
181
+ "code": "employee-form",
182
+ "type": "SmartForm",
183
+ "label": "Fiche employe",
184
+ "tabs": [ /* ... */ ],
185
+ "actions": ["save", "cancel"],
186
+ "permission": "HumanResources.Employees.Update"
187
+ }
188
+ ]
164
189
  },
165
190
  {
166
191
  "sectionCode": "dashboard",
167
192
  "sectionLabel": "Tableau de bord",
168
- "resources": [ /* SmartDashboard */ ]
193
+ "resources": [
194
+ {
195
+ "code": "employees-dashboard",
196
+ "type": "SmartDashboard",
197
+ "label": "Tableau de bord RH",
198
+ "kpis": [ /* ... */ ],
199
+ "charts": [ /* ... */ ],
200
+ "permission": "HumanResources.Employees.Read"
201
+ }
202
+ ]
169
203
  }
170
204
  ]
171
205
  }
172
206
  ```
173
207
 
208
+ > **Key naming:** Use `sectionCode` (not `id`, `code`). Use `sectionLabel` (not `displayName`, `label`).
209
+ > Resource `type` must be one of: `SmartTable`, `SmartForm`, `SmartDashboard`, `SmartKanban`, `SmartCard`, `SmartFilter`.
210
+
174
211
  Update `index.json` with screens.json hash and status.
175
212
 
176
213
  ## Validation
@@ -65,6 +65,60 @@ if (resumeValid) {
65
65
  }
66
66
  ```
67
67
 
68
+ ### Ralph State Recovery (context compression defense)
69
+
70
+ > **When context compression occurs mid-execution**, conversation variables are lost.
71
+ > `ralph-state.json` tracks the current position and is written before each major transition.
72
+ > This section reads it back to recover position.
73
+
74
+ ```javascript
75
+ // ALWAYS attempt to read ralph-state.json on resume — regardless of -r flag
76
+ // This file is written by: step-00 init, step-02 execute, compact-loop, module-transition
77
+ const statePath = '.ralph/ralph-state.json';
78
+ if (fileExists(statePath)) {
79
+ const state = readJSON(statePath);
80
+
81
+ console.log(`Ralph state recovered: step=${state.currentStep}, module=${state.currentModule}, ` +
82
+ `iteration=${state.iteration || '?'}, reason=${state.reason || 'normal'}`);
83
+
84
+ // Restore conversation variables from state
85
+ if (state.currentModule) {
86
+ {current_module} = state.currentModule;
87
+ }
88
+ if (state.iteration) {
89
+ {current_iteration} = state.iteration;
90
+ }
91
+ if (state.prdVersion) {
92
+ {prd_version} = state.prdVersion;
93
+ }
94
+
95
+ // Route to the correct step based on saved state
96
+ // This replaces the default "always go to step-01" behavior
97
+ const stepRouting = {
98
+ 'step-01-task': 'steps/step-01-task.md',
99
+ 'step-02-execute': 'steps/step-02-execute.md',
100
+ 'step-03-commit': 'steps/step-03-commit.md',
101
+ 'step-04-check': 'steps/step-04-check.md',
102
+ 'compact-loop': 'steps/step-04-check.md', // compact-loop re-enters via step-04
103
+ 'step-05-report': 'steps/step-05-report.md'
104
+ };
105
+
106
+ const targetStep = stepRouting[state.currentStep] || 'steps/step-01-task.md';
107
+
108
+ // Special case: module transition — go to step-01 to detect module-changed.json
109
+ if (state.reason === 'module-transition') {
110
+ console.log(`Module transition recovery: ${state.fromModule} → ${state.currentModule}`);
111
+ console.log('Routing to step-01-task.md to detect module-changed.json');
112
+ // targetStep stays 'steps/step-01-task.md'
113
+ }
114
+
115
+ console.log(`Recovery routing: → ${targetStep}`);
116
+ // The calling code (step-00) should load targetStep instead of the default step-01
117
+ }
118
+ ```
119
+
120
+ **IMPORTANT:** This recovery is triggered BEFORE the default "Proceed to step-01" routing in step-00-init.md. If ralph-state.json provides a different target step, that step should be loaded instead.
121
+
68
122
  ---
69
123
 
70
124
  ## Auto-Recovery: BA Artifacts Without PRD
@@ -76,10 +76,11 @@ If tests are slow (>30s in logs), display warning but **continue execution**:
76
76
 
77
77
  ## 4. Resume or Initialize
78
78
 
79
- See `references/init-resume-recovery.md` for complete Resume Mode and Auto-Recovery logic.
79
+ See `references/init-resume-recovery.md` for complete Resume Mode, Auto-Recovery, and Ralph State Recovery logic.
80
80
 
81
81
  **Quick:**
82
- - If `-r` flag: restore state from .ralph/prd.json
82
+ - If `-r` flag: restore state from .ralph/prd.json + .ralph/ralph-state.json
83
+ - Else if `.ralph/ralph-state.json` exists: recover position (step, module, iteration) — see init-resume-recovery.md "Ralph State Recovery"
83
84
  - Else if BA artifacts exist: auto-recover PRDs via `ss business-analyse-handoff`
84
85
  - Else: fresh start
85
86
 
@@ -222,14 +223,20 @@ MCP: Ready | Branch: {branch} | PRD: v{prd_version}
222
223
 
223
224
  ### 7b. Initialize State File (context compression defense)
224
225
 
226
+ > **ralph-state.json is written here AND updated by step-02, compact-loop, and module-transition.**
227
+ > On resume or after context compression, `init-resume-recovery.md` reads it back to route
228
+ > to the correct step instead of always restarting from step-01.
229
+
225
230
  ```javascript
226
231
  writeJSON('.ralph/ralph-state.json', {
227
232
  currentStep: 'step-01-task',
228
233
  currentModule: {current_module},
229
234
  iteration: 1,
230
235
  prdVersion: {prd_version},
236
+ modulesQueuePath: fileExists('.ralph/modules-queue.json') ? '.ralph/modules-queue.json' : null,
231
237
  timestamp: new Date().toISOString()
232
238
  });
233
239
  ```
234
240
 
235
- **Proceed directly to step-01-task.md**
241
+ **Step routing:** If ralph-state recovery (section 4) provided a different target step, load THAT step.
242
+ Otherwise, proceed to step-01-task.md.
@@ -6,8 +6,20 @@ next_step: steps/step-02-execute.md
6
6
 
7
7
  # Step 1: Load Task
8
8
 
9
- > **STATE RECOVERY:** If you are unsure which step you are executing (e.g., after context compression),
10
- > read `.ralph/ralph-state.json` to recover your position.
9
+ > **STATE RECOVERY (executable):** If context was compressed and you are unsure of your position:
10
+ ```javascript
11
+ if (fileExists('.ralph/ralph-state.json')) {
12
+ const state = readJSON('.ralph/ralph-state.json');
13
+ if (state.currentStep && state.currentStep !== 'step-01-task') {
14
+ console.warn(`RECOVERY: ralph-state.json says currentStep="${state.currentStep}" but we are in step-01.`);
15
+ console.warn(`This may indicate context compression routed to the wrong step.`);
16
+ console.warn(`Current module: ${state.currentModule}, iteration: ${state.iteration}`);
17
+ }
18
+ // Restore module context
19
+ if (state.currentModule) { {current_module} = state.currentModule; }
20
+ if (state.iteration) { {current_iteration} = state.iteration; }
21
+ }
22
+ ```
11
23
 
12
24
  > **MODULE TRANSITION CHECK:** Before "Only Read Once" rule, check for module transition:
13
25
 
@@ -6,8 +6,18 @@ next_step: steps/step-05-report.md OR steps/step-01-task.md
6
6
 
7
7
  # Step 4: Check Completion
8
8
 
9
- > **STATE RECOVERY:** If you are unsure which step you are executing (e.g., after context compression),
10
- > read `.ralph/ralph-state.json` to recover your position.
9
+ > **STATE RECOVERY (executable):** If context was compressed:
10
+ ```javascript
11
+ if (fileExists('.ralph/ralph-state.json')) {
12
+ const state = readJSON('.ralph/ralph-state.json');
13
+ if (state.currentStep && state.currentStep !== 'step-04-check' && state.currentStep !== 'compact-loop') {
14
+ console.warn(`RECOVERY: ralph-state.json says currentStep="${state.currentStep}" but we are in step-04.`);
15
+ }
16
+ // Restore module context if lost
17
+ if (state.currentModule) { {current_module} = state.currentModule; }
18
+ if (state.iteration) { {current_iteration} = state.iteration; }
19
+ }
20
+ ```
11
21
 
12
22
  ## YOUR TASK:
13
23
 
@@ -0,0 +1,158 @@
1
+ # Entity Name Canonicalization
2
+
3
+ > **Used by:** step-01-transform (section 2: file mapping) and step-02-export (POST-CHECK)
4
+ > **Purpose:** Convert BA entity names (potentially French, with spaces/apostrophes/accents) into valid C# identifiers for file paths.
5
+
6
+ ---
7
+
8
+ ## Why This Exists
9
+
10
+ Business analysts write entity names in the user's language (e.g., French: "Type d'absence", "Employé", "Congé maladie"). These names are used as-is in `entities.json`. However, C# file paths and class names **MUST** be valid identifiers:
11
+ - No spaces
12
+ - No apostrophes
13
+ - No accents (diacritics)
14
+ - PascalCase convention
15
+
16
+ Without canonicalization, the handoff generates paths like `src/Domain/Entities/Type d'absence.cs` → build failure (CS1001).
17
+
18
+ ---
19
+
20
+ ## Canonicalization Rules
21
+
22
+ ### Step 1: Map to English PascalCase (PREFERRED)
23
+
24
+ If the BA provides an explicit `codeIdentifier` or `englishName` field on the entity, use it directly.
25
+
26
+ ### Step 2: Strip Diacritics
27
+
28
+ Remove accents from characters:
29
+
30
+ | Input | Output |
31
+ |-------|--------|
32
+ | é, è, ê, ë | e |
33
+ | à, â, ä | a |
34
+ | ù, û, ü | u |
35
+ | ô, ö | o |
36
+ | î, ï | i |
37
+ | ç | c |
38
+
39
+ Example: `Employé` → `Employe`, `Congé` → `Conge`
40
+
41
+ ### Step 3: Remove Apostrophes and Split on Spaces
42
+
43
+ Split the name on spaces, apostrophes, hyphens, and underscores:
44
+
45
+ | Input | Tokens |
46
+ |-------|--------|
47
+ | `Type d'absence` | `["Type", "d", "absence"]` |
48
+ | `Congé maladie` | `["Conge", "maladie"]` |
49
+ | `Mise à jour` | `["Mise", "a", "jour"]` |
50
+
51
+ ### Step 4: Remove Articles and Prepositions (1-2 letter tokens)
52
+
53
+ Remove French articles and prepositions that don't contribute to the identifier:
54
+
55
+ **Remove:** `d`, `de`, `du`, `l`, `la`, `le`, `les`, `un`, `une`, `des`, `a`, `au`, `aux`, `en`
56
+
57
+ | Input Tokens | Filtered |
58
+ |-------------|----------|
59
+ | `["Type", "d", "absence"]` | `["Type", "absence"]` |
60
+ | `["Mise", "a", "jour"]` | `["Mise", "jour"]` |
61
+
62
+ ### Step 5: PascalCase Assembly
63
+
64
+ Capitalize first letter of each remaining token, join without separator:
65
+
66
+ | Filtered Tokens | Result |
67
+ |----------------|--------|
68
+ | `["Type", "absence"]` | `TypeAbsence` |
69
+ | `["Conge", "maladie"]` | `CongeMaladie` |
70
+ | `["Mise", "jour"]` | `MiseJour` |
71
+ | `["Employe"]` | `Employe` |
72
+
73
+ ---
74
+
75
+ ## Canonicalization Function (Pseudocode)
76
+
77
+ ```javascript
78
+ function canonicalizeEntityName(baName) {
79
+ // Step 1: Use explicit code identifier if available
80
+ // (handled by caller — check entity.codeIdentifier || entity.englishName first)
81
+
82
+ // Step 2: Strip diacritics
83
+ let name = baName.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
84
+
85
+ // Step 3: Split on non-alphanumeric
86
+ let tokens = name.split(/[\s'\-_]+/).filter(t => t.length > 0);
87
+
88
+ // Step 4: Remove French articles/prepositions
89
+ const stopWords = new Set(['d', 'de', 'du', 'l', 'la', 'le', 'les', 'un', 'une', 'des', 'a', 'au', 'aux', 'en']);
90
+ tokens = tokens.filter(t => !stopWords.has(t.toLowerCase()));
91
+
92
+ // Step 5: PascalCase
93
+ return tokens.map(t => t.charAt(0).toUpperCase() + t.slice(1).toLowerCase()).join('');
94
+ }
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Validation: Is Valid C# Identifier?
100
+
101
+ After canonicalization, verify the result is a valid C# identifier:
102
+
103
+ ```javascript
104
+ function isValidCSharpIdentifier(name) {
105
+ // Must start with letter or underscore
106
+ // Must contain only letters, digits, underscores
107
+ // Must not be a C# keyword
108
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name)
109
+ && !CSHARP_KEYWORDS.has(name);
110
+ }
111
+
112
+ const CSHARP_KEYWORDS = new Set([
113
+ 'abstract', 'as', 'base', 'bool', 'break', 'byte', 'case', 'catch',
114
+ 'char', 'checked', 'class', 'const', 'continue', 'decimal', 'default',
115
+ 'delegate', 'do', 'double', 'else', 'enum', 'event', 'explicit',
116
+ 'extern', 'false', 'finally', 'fixed', 'float', 'for', 'foreach',
117
+ 'goto', 'if', 'implicit', 'in', 'int', 'interface', 'internal',
118
+ 'is', 'lock', 'long', 'namespace', 'new', 'null', 'object',
119
+ 'operator', 'out', 'override', 'params', 'private', 'protected',
120
+ 'public', 'readonly', 'ref', 'return', 'sbyte', 'sealed', 'short',
121
+ 'sizeof', 'stackalloc', 'static', 'string', 'struct', 'switch',
122
+ 'this', 'throw', 'true', 'try', 'typeof', 'uint', 'ulong',
123
+ 'unchecked', 'unsafe', 'ushort', 'using', 'virtual', 'void',
124
+ 'volatile', 'while'
125
+ ]);
126
+ ```
127
+
128
+ ---
129
+
130
+ ## Examples
131
+
132
+ | BA Entity Name (French) | Canonicalized | File Path |
133
+ |-------------------------|---------------|-----------|
134
+ | `Type d'absence` | `TypeAbsence` | `src/Domain/Entities/App/Module/TypeAbsence.cs` |
135
+ | `Employé` | `Employe` | `src/Domain/Entities/App/Module/Employe.cs` |
136
+ | `Congé maladie` | `CongeMaladie` | `src/Domain/Entities/App/Module/CongeMaladie.cs` |
137
+ | `Mise à jour` | `MiseJour` | `src/Domain/Entities/App/Module/MiseJour.cs` |
138
+ | `Département` | `Departement` | `src/Domain/Entities/App/Module/Departement.cs` |
139
+ | `Employee` | `Employee` | `src/Domain/Entities/App/Module/Employee.cs` (no change) |
140
+ | `AbsenceType` | `AbsenceType` | `src/Domain/Entities/App/Module/AbsenceType.cs` (no change) |
141
+
142
+ ---
143
+
144
+ ## Integration Points
145
+
146
+ 1. **step-01-transform.md**: Apply `canonicalizeEntityName()` to ALL entity names BEFORE generating `filesToCreate` paths
147
+ 2. **step-02-export.md**: POST-CHECK validates all paths in `filesToCreate` are valid C# identifiers
148
+ 3. **entity-domain-mapping.md**: `{EntityName}` in path templates refers to the CANONICALIZED name
149
+ 4. **handoff-file-templates.md**: All `{EntityName}` placeholders use canonicalized names
150
+
151
+ ## Anti-Patterns
152
+
153
+ | Anti-Pattern | Risk | Correct Approach |
154
+ |-------------|------|-----------------|
155
+ | Translate to English semantically | `MiseJour` → `Update`? No — loses domain meaning | Only strip illegal chars, keep semantics |
156
+ | Remove all short words | `TypeAbsence` missing `Type`? | Only remove articles/prepositions, not nouns |
157
+ | Lowercase everything | `typeabsence` is not PascalCase | PascalCase each token |
158
+ | Skip validation | `Task` is a valid name but also a C# type | Warn on namespace conflicts, don't block |
@@ -19,6 +19,7 @@ next_step: steps/step-02-export.md
19
19
  - **NEVER** invent entities/BRs not in module JSON files
20
20
  - **NEVER** load all module data in the main conversation (causes context overflow on 5+ modules)
21
21
  - **ALWAYS** generate API endpoints from use cases + entities (BA does not produce apiEndpoints)
22
+ - **ALWAYS** canonicalize entity names before generating file paths — see `references/entity-canonicalization.md`
22
23
 
23
24
  ## YOUR TASK
24
25
 
@@ -71,10 +72,19 @@ Transform module "{moduleCode}" for handoff.
71
72
  ## Instructions
72
73
  Read the following 5 files from {moduleDir}:
73
74
  1. entities.json → entities[]
74
- 2. usecases.json → useCases[]
75
- 3. rules.json → rules[]
75
+ 2. usecases.json → useCases[] (canonical key: "useCases", fallback: "usecases")
76
+ 3. rules.json → rules[] (canonical key: "rules", fallback: "businessRules")
76
77
  4. permissions.json → roles[], matrix[], permissionPaths[]
77
- 5. screens.json → screens[]
78
+ 5. screens.json → sections[] (canonical key: "sections", fallback: "screens")
79
+
80
+ ### Normalization Safety Net (BACKWARD COMPAT)
81
+ When reading flat files, prefer canonical keys but fall back to alternatives:
82
+ - useCases: `data.useCases || data.usecases || []`
83
+ - primaryActor: `uc.primaryActor || uc.actor`
84
+ - mainScenario: `uc.mainScenario || uc.steps` (if steps[] contains objects, extract `.action`)
85
+ - rules: `data.rules || data.businessRules || []`
86
+ - screens: `data.sections || data.screens` (if screens[] exists, treat each screen as a section with 1 resource)
87
+ This safety net handles pre-4.52 BA outputs. It should become unnecessary once step-03-specify enforces canonical keys.
78
88
 
79
89
  Then build the handoff data following these rules:
80
90
 
@@ -96,6 +106,19 @@ Then build the handoff data following these rules:
96
106
  ### Section UC/BR Enrichment
97
107
  Using sectionCode from usecases.json and rules.json, link each UC and BR to its section.
98
108
 
109
+ ### Entity Name Canonicalization (MANDATORY)
110
+ Before generating any file paths, canonicalize ALL entity names from `entities.json`:
111
+ 1. Read `references/entity-canonicalization.md` for complete rules
112
+ 2. For each entity in `entities.json > entities[]`:
113
+ - If entity has `codeIdentifier` or `englishName` → use it directly
114
+ - Otherwise → apply canonicalization: strip diacritics, remove apostrophes/spaces, remove French articles (d, de, du, l, la, le, les, un, une, des, a, au, aux, en), PascalCase
115
+ 3. Use the CANONICALIZED name in ALL file path templates (`{EntityName}`, `{ServiceName}`, `{DtoName}`, etc.)
116
+ 4. Store the mapping `{ originalName: "Type d'absence", canonicalName: "TypeAbsence" }` in handoff metadata for traceability
117
+
118
+ **Example:** Entity `"Type d'absence"` → canonicalized to `"TypeAbsence"` → path `src/Domain/Entities/App/Module/TypeAbsence.cs`
119
+
120
+ **BLOCKING:** If ANY entity name after canonicalization is not a valid C# identifier (`/^[A-Za-z_][A-Za-z0-9_]*$/`), STOP and report the error.
121
+
99
122
  ### File Mapping (8 Categories)
100
123
  Read `references/handoff-file-templates.md` for complete JSON templates.
101
124
  All backend paths MUST include {ApplicationName}/ hierarchy.
@@ -111,6 +111,20 @@ for (const key of specKeys) {
111
111
  BLOCKING_ERROR(`Companion file empty or too small: ${companionPath} (${companionSize} bytes)`);
112
112
  }
113
113
  }
114
+
115
+ // Check 6: File paths must be valid C# identifiers (no spaces, apostrophes, accents)
116
+ // Reference: references/entity-canonicalization.md
117
+ for (const [cat, files] of Object.entries(ftc)) {
118
+ for (const file of (files || [])) {
119
+ const filePath = file.path || file;
120
+ // Extract filename without extension
121
+ const fileName = filePath.split('/').pop().replace(/\.(cs|tsx|ts|json)$/, '');
122
+ // Check for illegal characters in C# file names
123
+ if (/[\s'àâäéèêëïîôùûüçÀÂÄÉÈÊËÏÎÔÙÛÜÇ]/.test(fileName)) {
124
+ BLOCKING_ERROR(`${cat}: File path contains illegal characters for C# identifier: "${filePath}" — entity name must be canonicalized (see references/entity-canonicalization.md)`);
125
+ }
126
+ }
127
+ }
114
128
  ```
115
129
 
116
130
  Display verification table showing all 8 categories match between module JSON files and prd.json.
@@ -42,9 +42,13 @@ Generate the interactive HTML document of the business analysis from the JSON an
42
42
 
43
43
  - **NEVER** deploy an empty template — FEATURE_DATA must be injected
44
44
  - **moduleSpecs** MUST have ONE entry per module (empty = BROKEN)
45
+ - **moduleSpecs[].screens[]** MUST have populated resources when source screens.json has data (empty resources = NO HTML mockups)
45
46
  - **Scope keys** MUST be converted: `inScope` → `inscope`, `outOfScope` → `outofscope`
46
47
  - **Wireframe fields** MUST be renamed: `mockupFormat` → `format`, `mockup` → `content`
48
+ - **Input normalization** MUST handle both JSON schemas: Format A (`screens[]`, `useCases`, `mainScenario`) and Format B (`sections[]`, `usecases`, `steps[]` as objects)
49
+ - **NEVER** call `.join()` on arrays without checking element types — object elements produce `[object Object]`
47
50
  - **Final file** MUST be > 100KB
51
+ - **Final file** MUST NOT contain the string `[object Object]`
48
52
 
49
53
  ## References
50
54