@atlashub/smartstack-cli 3.20.0 → 3.22.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 (41) hide show
  1. package/dist/index.js +70 -6
  2. package/dist/index.js.map +1 -1
  3. package/dist/mcp-entry.mjs +69 -3
  4. package/dist/mcp-entry.mjs.map +1 -1
  5. package/package.json +1 -1
  6. package/templates/project/api.ts.template +8 -29
  7. package/templates/project/appsettings.json.template +1 -0
  8. package/templates/skills/application/references/application-roles-template.md +2 -2
  9. package/templates/skills/application/steps/step-05-frontend.md +40 -35
  10. package/templates/skills/application/templates-frontend.md +64 -36
  11. package/templates/skills/business-analyse/html/ba-interactive.html +642 -156
  12. package/templates/skills/business-analyse/html/src/scripts/01-data-init.js +11 -6
  13. package/templates/skills/business-analyse/html/src/scripts/02-navigation.js +209 -4
  14. package/templates/skills/business-analyse/html/src/scripts/04-render-modules.js +2 -8
  15. package/templates/skills/business-analyse/html/src/scripts/05-render-specs.js +95 -8
  16. package/templates/skills/business-analyse/html/src/scripts/07-render-handoff.js +3 -1
  17. package/templates/skills/business-analyse/html/src/scripts/08-editing.js +112 -22
  18. package/templates/skills/business-analyse/html/src/scripts/11-review-panel.js +7 -0
  19. package/templates/skills/business-analyse/html/src/styles/02-layout.css +1 -1
  20. package/templates/skills/business-analyse/html/src/styles/03-navigation.css +89 -31
  21. package/templates/skills/business-analyse/html/src/styles/05-modules.css +64 -0
  22. package/templates/skills/business-analyse/html/src/styles/06-wireframes.css +42 -0
  23. package/templates/skills/business-analyse/html/src/template.html +8 -76
  24. package/templates/skills/business-analyse/references/acceptance-criteria.md +169 -0
  25. package/templates/skills/business-analyse/references/deploy-data-build.md +13 -9
  26. package/templates/skills/business-analyse/references/handoff-file-templates.md +2 -1
  27. package/templates/skills/business-analyse/references/html-data-mapping.md +20 -28
  28. package/templates/skills/business-analyse/references/naming-conventions.md +245 -0
  29. package/templates/skills/business-analyse/references/validate-incremental-html.md +28 -5
  30. package/templates/skills/business-analyse/references/validation-checklist.md +31 -11
  31. package/templates/skills/business-analyse/references/wireframe-svg-style-guide.md +335 -0
  32. package/templates/skills/business-analyse/steps/step-03b-ui.md +59 -0
  33. package/templates/skills/business-analyse/steps/step-03c-compile.md +169 -2
  34. package/templates/skills/business-analyse/steps/step-03d-validate.md +217 -28
  35. package/templates/skills/business-analyse/steps/step-05a-handoff.md +189 -3
  36. package/templates/skills/business-analyse/steps/step-05b-deploy.md +55 -0
  37. package/templates/skills/ralph-loop/references/category-rules.md +5 -2
  38. package/templates/skills/ralph-loop/references/compact-loop.md +52 -1
  39. package/templates/skills/ralph-loop/references/core-seed-data.md +232 -21
  40. package/templates/skills/ralph-loop/steps/step-01-task.md +36 -4
  41. package/templates/skills/ralph-loop/steps/step-02-execute.md +81 -0
@@ -34,21 +34,50 @@ Validate the module specification for completeness and consistency, write to fea
34
34
 
35
35
  #### 9a. Completeness Checks
36
36
 
37
- | Section | Minimum | Status |
38
- |---------|---------|--------|
39
- | actors | 2 | PASS/FAIL |
40
- | useCases | 2 | PASS/FAIL |
41
- | functionalRequirements | 4 | PASS/FAIL |
42
- | permissionMatrix | 1 resource × 2 roles | PASS/FAIL |
43
- | entities | 1 | PASS/FAIL |
44
- | entitySchemaFormat | attributes[] not fields[] (BLOCKING) | PASS/FAIL |
45
- | wireframes | 1 per section (BLOCKING) | PASS/FAIL |
46
- | wireframeSchema | All required fields present (BLOCKING) | PASS/FAIL |
47
- | gherkinScenarios | 2 per UC | PASS/FAIL |
48
- | validations | 1 | PASS/FAIL |
49
- | messages | 4 | PASS/FAIL |
50
- | lifeCycles | 1 (if entity has status) | PASS/FAIL |
51
- | seedDataCore | 7 sections present | PASS/FAIL (BLOCKING) |
37
+ > **CRITICAL:** Checks MUST count ACTUAL elements in arrays, NOT declarative values.
38
+ > A check that reports PASS when the array is empty is a LIE and BLOCKS downstream quality.
39
+
40
+ | Section | Minimum | How to verify | Status |
41
+ |---------|---------|---------------|--------|
42
+ | actors | 2 | `specification.actors.length >= 2` | PASS/FAIL |
43
+ | useCases | 2 | `specification.useCases.length >= 2` | PASS/FAIL |
44
+ | functionalRequirements | 4 | `specification.functionalRequirements.length >= 4` | PASS/FAIL |
45
+ | permissionMatrix | 1 resource × 2 roles | `specification.permissionMatrix.permissions.length >= 1 && specification.permissionMatrix.roleAssignments.length >= 2` | PASS/FAIL |
46
+ | entities | 1 | `analysis.entities.length >= 1` | PASS/FAIL |
47
+ | entitySchemaFormat | attributes[] not fields[] (BLOCKING) | `analysis.entities.every(e => e.attributes?.length > 0)` | PASS/FAIL |
48
+ | entityAttributeTypes | ALL attributes have `type` field (BLOCKING) | `analysis.entities.every(e => e.attributes.every(a => a.type))` | PASS/FAIL |
49
+ | wireframes | 1 per section (BLOCKING) | `(specification.uiWireframes \|\| specification.wireframes \|\| []).length >= (specification.sections \|\| []).length` (count REAL elements, check BOTH key names) | PASS/FAIL |
50
+ | wireframeSchema | All required fields present (BLOCKING) | `(specification.uiWireframes \|\| specification.wireframes \|\| []).every(w => (w.screen \|\| w.title) && w.section && (w.mockup \|\| w.ascii \|\| w.content))` | PASS/FAIL |
51
+ | sections | 1 (BLOCKING) | `specification.sections.length >= 1` (EVERY module needs at least 1 section) | PASS/FAIL |
52
+ | gherkinScenarios | 1 array entry | `Array.isArray(specification.gherkinScenarios) && specification.gherkinScenarios.length >= 1` | PASS/FAIL |
53
+ | gherkinFormat | Array not object (BLOCKING) | `Array.isArray(specification.gherkinScenarios)` (NOT a single object) | PASS/FAIL |
54
+ | validations | 1 | `specification.validations.length >= 1` | PASS/FAIL |
55
+ | validationFormat | rules[] array (BLOCKING) | `specification.validations.every(v => Array.isArray(v.rules))` (NOT singular `rule`) | PASS/FAIL |
56
+ | messages | 4 | `specification.messages.length >= 4` | PASS/FAIL |
57
+ | messageFormat | `message` field present (BLOCKING) | `specification.messages.every(m => m.message)` | PASS/FAIL |
58
+ | lifeCycles | 1 (if entity has status) | `specification.lifeCycles.length >= 1` (if any entity has status/state field) | PASS/FAIL |
59
+ | seedDataCore | 7 arrays present with content | See detailed check below | PASS/FAIL (BLOCKING) |
60
+ | apiEndpoints | 1 | `specification.apiEndpoints.length >= 1` | PASS/FAIL |
61
+ | i18nKeys | present | `specification.i18nKeys !== undefined && specification.i18nKeys !== null` | PASS/FAIL |
62
+ | navigationIcons | non-null | `specification.seedDataCore.navigationModules.every(m => m.icon !== null)` | PASS/FAIL |
63
+
64
+ **seedDataCore detailed check (BLOCKING):**
65
+ ```javascript
66
+ const sdc = specification.seedDataCore;
67
+ const checks = [
68
+ { key: "navigationModules", actual: sdc.navigationModules?.length || 0, min: 1 },
69
+ { key: "navigationSections", actual: sdc.navigationSections?.length || 0, min: 1 }, // EVERY module needs ≥1 section
70
+ { key: "navigationResources", actual: sdc.navigationResources?.length || 0, min: 1 },
71
+ { key: "navigationTranslations", actual: sdc.navigationTranslations?.length || 0, min: 2 }, // min fr+en
72
+ { key: "permissions", actual: sdc.permissions?.length || 0, min: 1 },
73
+ { key: "rolePermissions", actual: sdc.rolePermissions?.length || 0, min: 1 },
74
+ { key: "permissionConstants", actual: sdc.permissionConstants?.length || 0, min: 1 }
75
+ ];
76
+ const failures = checks.filter(c => c.actual < c.min);
77
+ IF failures.length > 0:
78
+ BLOCKING ERROR: "seedDataCore incomplete — empty arrays: {failures.map(f => f.key).join(', ')}"
79
+ → Fix: Ensure specification.sections[] has ≥1 entry, then re-run 8f-bis transform
80
+ ```
52
81
 
53
82
  #### 9b. Consistency Checks
54
83
 
@@ -68,9 +97,74 @@ Validate the module specification for completeness and consistency, write to fea
68
97
  - Entity names PascalCase
69
98
  - Field names camelCase
70
99
  - Entity attribute format: `attributes[]` with {name, description}, NOT `fields[]` with {name, type} — entities must NOT have tableName or primaryKey
71
- - Wireframe structure: `screen` (not `name`), `componentMapping` is array of {wireframeElement, reactComponent} (not plain key-value object), `layout` is object with regions (not string)
100
+ - Wireframe structure: `screen` (not `name`/`title`), `mockup` (not `ascii`/`content`), `componentMapping` is array of {wireframeElement, reactComponent} (not plain key-value object), `layout` is object with regions (not string)
101
+ - Wireframe field naming: uses `screen` (not `title`), `mockup` (not `ascii`) — auto-fix if wrong (see 9c-fix below)
72
102
  - Permission paths dot-separated lowercase
73
103
 
104
+ #### 9c-fix. Auto-Fix Wireframe Field Names (MANDATORY before writing)
105
+
106
+ > **CRITICAL:** The agent may use non-canonical field names (`title`, `ascii`, `name`, `content`).
107
+ > These MUST be normalized to canonical names before writing to feature.json.
108
+
109
+ ```javascript
110
+ // AUTO-FIX: Normalize wireframe field names before writing
111
+ const wireframes = specification.uiWireframes || specification.wireframes || [];
112
+ for (const wf of wireframes) {
113
+ if (wf.title && !wf.screen) { wf.screen = wf.title; delete wf.title; }
114
+ if (wf.ascii && !wf.mockup) { wf.mockup = wf.ascii; delete wf.ascii; }
115
+ if (wf.content && !wf.mockup) { wf.mockup = wf.content; delete wf.content; }
116
+ if (wf.name && !wf.screen) { wf.screen = wf.name; delete wf.name; }
117
+ }
118
+ // Store normalized wireframes back under canonical key
119
+ specification.uiWireframes = wireframes;
120
+ ```
121
+
122
+ #### 9c-fix-entities. Auto-Fix Entity Attribute Types (MANDATORY before writing)
123
+
124
+ > **DEFENSE-IN-DEPTH:** step-03c POST-CHECK should catch attributes without `type`,
125
+ > but if any slip through to step-03d, auto-fix them here before writing to feature.json.
126
+ > Uses the same type inference algorithm as step-03c POST-CHECK.
127
+
128
+ ```javascript
129
+ // AUTO-FIX: Ensure all entity attributes have structured type
130
+ const entities = analysis.entities || [];
131
+ let entityAutoFixCount = 0;
132
+ for (const entity of entities) {
133
+ for (const attr of entity.attributes || []) {
134
+ if (!attr.type) {
135
+ // Infer type from attribute name pattern
136
+ if (attr.validation?.match(/max\s*\d+/i) || attr.name.match(/name|title|code|description|label|email|phone|address/i)) {
137
+ attr.type = "string";
138
+ const maxMatch = attr.validation?.match(/max\s*(\d+)/i);
139
+ if (maxMatch) attr.maxLength = parseInt(maxMatch[1]);
140
+ } else if (attr.name.match(/id$/i)) {
141
+ attr.type = "Guid";
142
+ } else if (attr.name.match(/date|At$/i)) {
143
+ attr.type = "DateTime";
144
+ } else if (attr.name.match(/is[A-Z]|has[A-Z]|active|enabled/)) {
145
+ attr.type = "bool";
146
+ } else if (attr.name.match(/amount|salary|rate|price|total/i)) {
147
+ attr.type = "decimal";
148
+ } else if (attr.name.match(/count|number|order|sort|index/i)) {
149
+ attr.type = "int";
150
+ } else {
151
+ attr.type = "string"; // safe default
152
+ }
153
+ // Normalize free-text maxLength
154
+ if (typeof attr.validation === 'string' && !attr.maxLength) {
155
+ const m = attr.validation.match(/max\s*(\d+)/i);
156
+ if (m) attr.maxLength = parseInt(m[1]);
157
+ }
158
+ if (attr.required === undefined) attr.required = true;
159
+ entityAutoFixCount++;
160
+ }
161
+ }
162
+ }
163
+ if (entityAutoFixCount > 0) {
164
+ console.warn(`DEFENSE-IN-DEPTH: auto-fixed ${entityAutoFixCount} attributes without type in step-03d`);
165
+ }
166
+ ```
167
+
74
168
  #### 9d. Decision
75
169
 
76
170
  IF validation PASS:
@@ -136,10 +230,10 @@ ba-writer.enrichSection({
136
230
 
137
231
  **Execute the comprehensive validation checklist:**
138
232
 
139
- Run the 27-check validation process across 10 categories:
233
+ Run the 29-check validation process across 10 categories:
140
234
  - Data Model (4 checks) | Business Rules (3 checks) | Use Cases & FRs (4 checks)
141
235
  - Permissions (3 checks) | UI & Navigation (4 checks) | I18N & Messages (3 checks)
142
- - Seed Data (2 checks) | API Endpoints (2 checks) | Validations (1 check) | Gherkin (1 check)
236
+ - Seed Data (2 checks) | API Endpoints (2 checks) | Validations (1 check) | Gherkin (3 checks)
143
237
 
144
238
  ```javascript
145
239
  const blockingFailures = checks.filter(c => c.blocking && c.status === "FAIL");
@@ -214,18 +308,13 @@ Display comprehensive summary:
214
308
 
215
309
  → Validation: {PASS/FAIL}
216
310
  ═══════════════════════════════════════════════════════════
217
- ```
218
311
 
219
- Ask via AskUserQuestion:
312
+ {IF validation PASS}:
313
+ → Module validé automatiquement
314
+ → Passage automatique au module suivant (section 12)
220
315
 
221
- ```
222
- question: "Le module {currentModule} est-il correctement spécifié ?"
223
- header: "Module"
224
- options:
225
- - label: "Validé"
226
- description: "Passer au module suivant"
227
- - label: "Réviser"
228
- description: "Modifier des éléments du module"
316
+ {IF validation FAIL}:
317
+ Afficher les erreurs et offrir les options de correction (section 9d)
229
318
  ```
230
319
 
231
320
  ---
@@ -278,6 +367,106 @@ ba-writer.updateStatus({module_feature_id}, "specified")
278
367
  ba-writer.updateModuleStatus({feature_id}, {currentModule.code}, "specified")
279
368
  ```
280
369
 
370
+ #### 11-POST-CHECK: Verify Written Data (BLOCKING)
371
+
372
+ > **CRITICAL — Data loss prevention.** After writing, READ BACK the module feature.json and verify the following arrays are **non-empty**:
373
+
374
+ ```javascript
375
+ // READ BACK the written feature.json
376
+ const written = ba-reader.read({module_feature_id});
377
+
378
+ // BLOCKING checks — if ANY fails, the write was incomplete
379
+ const checks = [
380
+ { key: "specification.actors", actual: written.specification?.actors?.length, min: 2 },
381
+ { key: "specification.useCases", actual: written.specification?.useCases?.length, min: 2 },
382
+ { key: "specification.wireframes", actual: (written.specification?.uiWireframes?.length || written.specification?.wireframes?.length || 0), min: 1 },
383
+ { key: "specification.sections", actual: written.specification?.sections?.length, min: 1 },
384
+ { key: "specification.seedDataCore", actual: Object.keys(written.specification?.seedDataCore || {}).length, min: 7 },
385
+ { key: "specification.lifeCycles", actual: written.specification?.lifeCycles?.length, min: 0 },
386
+ { key: "specification.gherkinScenarios",actual: written.specification?.gherkinScenarios?.length,min: 1 },
387
+ { key: "specification.apiEndpoints", actual: written.specification?.apiEndpoints?.length, min: 1 }
388
+ ];
389
+
390
+ const failures = checks.filter(c => (c.actual || 0) < c.min);
391
+
392
+ IF failures.length > 0:
393
+ BLOCKING ERROR: "Feature.json write INCOMPLETE — missing data in: {failures.map(f => f.key).join(', ')}"
394
+ → Re-execute section 11 write with ALL specification data
395
+ → DO NOT proceed to next module until ALL checks pass
396
+
397
+ // SPECIAL CHECK: wireframes content verification (check BOTH key names)
398
+ const wireframes = written.specification?.uiWireframes || written.specification?.wireframes || [];
399
+ IF wireframes.length > 0:
400
+ const emptyMockups = wireframes.filter(wf => !wf.mockup && !wf.ascii && !wf.content);
401
+ IF emptyMockups.length > 0:
402
+ WARNING: "{emptyMockups.length} wireframes have empty mockup content — verify step-03b data"
403
+
404
+ // SPECIAL CHECK: wireframes >= sections
405
+ const sectionCount = written.specification?.sections?.length || 0;
406
+ IF wireframes.length < sectionCount:
407
+ BLOCKING ERROR: "{wireframes.length} wireframes < {sectionCount} sections — wireframes MISSING for some sections"
408
+ → Re-read wireframes from conversation context and re-write to feature.json
409
+ ```
410
+
411
+ #### 11-POST-CHECK-BASH: Acceptance Criteria Verification (BLOCKING)
412
+
413
+ > **CRITICAL:** The pseudocode checks above are interpreted by the model — they can be "passed" incorrectly.
414
+ > This bash check reads the REAL file on disk and provides an objective verification.
415
+ > See [references/acceptance-criteria.md](../references/acceptance-criteria.md) for the full acceptance criteria definition.
416
+
417
+ ```bash
418
+ MODULE_JSON="{module_feature_json_path}"
419
+ node -e "
420
+ const fs = require('fs');
421
+ const data = JSON.parse(fs.readFileSync(process.argv[1], 'utf-8'));
422
+ const spec = data.specification || {};
423
+ const analysis = data.analysis || {};
424
+ const wf = spec.uiWireframes || spec.wireframes || [];
425
+ const sections = spec.sections || [];
426
+ const checks = [
427
+ ['entities >= 1', (analysis.entities||[]).length, 1],
428
+ ['useCases >= 2', (spec.useCases||[]).length, 2],
429
+ ['FRs >= 4', (spec.functionalRequirements||[]).length,4],
430
+ ['wireframes >= 1', wf.length, 1],
431
+ ['wireframes >= sections', wf.length, sections.length],
432
+ ['sections >= 1', sections.length, 1],
433
+ ['seedDataCore 7 arrays', Object.keys(spec.seedDataCore||{}).filter(k=>(spec.seedDataCore||{})[k]&&(spec.seedDataCore||{})[k].length>0).length, 7],
434
+ ['gherkin is array', Array.isArray(spec.gherkinScenarios)?1:0, 1],
435
+ ['apiEndpoints >= 1', (spec.apiEndpoints||[]).length, 1],
436
+ ['messages >= 4', (spec.messages||[]).length, 4],
437
+ ['validations >= 1', (spec.validations||[]).length, 1]
438
+ ];
439
+ const fails = checks.filter(c => c[1] < c[2]);
440
+ fails.forEach(f => console.error('FAIL: ' + f[0] + ' = ' + f[1] + ' (min: ' + f[2] + ')'));
441
+ // Check wireframe content
442
+ const emptyWf = wf.filter(w => !w.mockup && !w.ascii && !w.content);
443
+ if (emptyWf.length > 0) { fails.push(['wireframe content', 0, 1]); console.error('FAIL: ' + emptyWf.length + ' wireframes have EMPTY content'); }
444
+ // Check entity attribute types
445
+ const badAttrs = (analysis.entities||[]).flatMap(e => (e.attributes||[]).filter(a => !a.type).map(a => e.name+'.'+a.name));
446
+ if (badAttrs.length > 0) { fails.push(['attr.type', 0, 1]); console.error('FAIL: attributes without type: ' + badAttrs.join(', ')); }
447
+ // AC-15: Validation rules format
448
+ const badRules = (spec.validations||[]).filter(v => v.rules && !Array.isArray(v.rules));
449
+ if (badRules.length > 0) { fails.push(['AC-15: rules not array', badRules.length, 0]); console.error('FAIL: AC-15: ' + badRules.length + ' validations have rules as string'); }
450
+ // AC-16: Messages must have message field
451
+ const noMsg = (spec.messages||[]).filter(m => !m.message);
452
+ if (noMsg.length > 0) { fails.push(['AC-16: message missing', noMsg.length, 0]); console.error('FAIL: AC-16: ' + noMsg.length + ' messages missing message field'); }
453
+ // AC-17: Gherkin content structure
454
+ if (Array.isArray(spec.gherkinScenarios)) { const badG = spec.gherkinScenarios.filter(g => !g.feature || !Array.isArray(g.scenarios)); if (badG.length > 0) { fails.push(['AC-17: gherkin content', badG.length, 0]); console.error('FAIL: AC-17: ' + badG.length + ' gherkin entries invalid'); } }
455
+ if (fails.length > 0) { console.error('BLOCKING: ' + fails.length + ' acceptance criteria failed'); process.exit(1); }
456
+ console.log('PASS: All acceptance criteria met');
457
+ " "$MODULE_JSON"
458
+ ```
459
+
460
+ IF this check FAILS:
461
+ - Identify which criteria failed from the output
462
+ - Fix the corresponding data (re-read from conversation context, re-generate, or re-run the step that produces it)
463
+ - Re-write to feature.json
464
+ - Re-run the POST-CHECK until PASS
465
+
466
+ > **WHY:** Step-03b now writes wireframes intermediately (since fix), but step-03d section 11 does the FULL write.
467
+ > This bash POST-CHECK is the ultimate safety net — it reads the REAL file and verifies ALL acceptance criteria.
468
+ > The pseudocode checks (section 9a) catch issues early; this bash check catches anything the model missed.
469
+
281
470
  ---
282
471
 
283
472
  ### 11-bis. Deploy Incremental Interactive HTML (MANDATORY)
@@ -194,16 +194,66 @@ See [references/handoff-file-templates.md](../references/handoff-file-templates.
194
194
  |----------|--------|-----------|
195
195
  | **4.1 Domain** | `analysis.entities[]` | Entities, ValueObjects, Enums, Exceptions |
196
196
  | **4.2 Application** | `analysis.useCases[]` | Services, DTOs, Validators |
197
- | **4.3 Infrastructure** | `analysis.entities[]` | EF Configurations, DbSet, DI |
197
+ | **4.3 Infrastructure** | `analysis.entities[]` | EF Configurations, DbSet, DI. **DEPENDENCY:** Each EF config task MUST `dependsOn` its corresponding domain entity task. |
198
198
  | **4.4 API** | `specification.apiEndpoints[]` | Controllers with `{ContextShort}` mapping |
199
199
  | **4.5 Frontend** | `specification.uiWireframes[]` | Pages, Components, Hooks + wireframe traceability |
200
200
  | **4.6 SeedData** | `specification.seedDataCore` | 5 CORE + business + IClientSeedDataProvider |
201
201
  | **4.7 Tests** | All layers | Unit, Integration, Security tests |
202
202
 
203
+ #### Route Convention (CRITICAL)
204
+
205
+ > **MANDATORY:** All NavigationModule/Section routes MUST use kebab-case transformation.
206
+ > See [references/naming-conventions.md](../references/naming-conventions.md) for complete guide.
207
+
208
+ **Problem:**
209
+ - Module codes in feature.json are **PascalCase** (e.g., `"HumanResources"`)
210
+ - Routes in seed data MUST be **kebab-case** (e.g., `"/business/human-resources"`)
211
+ - Failing to transform causes 404 when clicking menu items
212
+
213
+ **Solution:**
214
+
215
+ When generating `seedDataCore` for handoff, transform PascalCase codes to kebab-case routes:
216
+
217
+ ```javascript
218
+ // Transform module codes to kebab-case routes
219
+ function toKebabCase(code) {
220
+ return code
221
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
222
+ .toLowerCase();
223
+ }
224
+
225
+ // Example usage in seedDataCore generation
226
+ const moduleCode = "TimeManagement"; // PascalCase (C#)
227
+ const appCode = "HumanResources"; // PascalCase (C#)
228
+ const contextCode = "business"; // lowercase
229
+
230
+ const route = `/${contextCode}/${toKebabCase(appCode)}/${toKebabCase(moduleCode)}`;
231
+ // Result: "/business/human-resources/time-management"
232
+ ```
233
+
234
+ **Include in task instructions:**
235
+
236
+ For NavigationSeedData tasks in `handoff.filesToCreate`, add:
237
+
238
+ ```json
239
+ {
240
+ "path": "Infrastructure/Persistence/Seeding/Data/{Module}/NavigationSeedData.cs",
241
+ "type": "SeedData",
242
+ "category": "core",
243
+ "instructions": "Generate NavigationModule seed data. CRITICAL: Use ToKebabCase() helper for Route property. Example: Route = ToKebabCase($\"/{context}/{app}/{module}\"). See core-seed-data.md template."
244
+ }
245
+ ```
246
+
247
+ **Validation:**
248
+ - ralph-loop POST-CHECK validates routes after generation (see step-02-execute.md)
249
+ - MCP `validate_frontend_routes` detects case mismatches
250
+ - Convention documented in naming-conventions.md
251
+
203
252
  **Critical rules:**
204
253
  - All backend paths include `{ContextPascal}/{ApplicationName}/` hierarchy
205
254
  - Frontend pages MUST have `linkedWireframes[]` + `wireframeAcceptanceCriteria`
206
255
  - SeedData: 5 CORE entries ALWAYS + IClientSeedDataProvider for client projects
256
+ - **Acceptance Criteria Mapping:** Each task's `acceptanceCriteria` MUST be derived from its own `linkedFRs[]` entries (lookup FR → `acceptanceCriteria`). NEVER map by sequential FR index — use the task's explicit linkedFRs to resolve the correct criteria.
207
257
  - Path convention: `Persistence/Seeding/Data/` (NEVER `Data/SeedData/`)
208
258
 
209
259
  ---
@@ -228,6 +278,52 @@ For each endpoint: operation, method, route, linkedUC, linkedFR, permissions, re
228
278
 
229
279
  Total endpoints = count of specification.apiEndpoints[] across all modules.
230
280
 
281
+ ### 6b. Frontend Task Splitting (MANDATORY)
282
+
283
+ > **CRITICAL:** Frontend MUST be split into discrete tasks, NOT bundled as a single mega-task.
284
+ > A single mega-task covering 5-8 files prevents granular error recovery in ralph-loop.
285
+
286
+ For each module, generate these SEPARATE frontend file entries (not one monolithic entry):
287
+
288
+ ```json
289
+ "frontend": [
290
+ { "path": "src/pages/{Mod}/{ListPage}Page.tsx", "type": "Page", "linkedWireframes": ["{module}-list"], "module": "{moduleCode}" },
291
+ { "path": "src/pages/{Mod}/{DetailPage}Page.tsx", "type": "Page", "linkedWireframes": ["{module}-detail"], "module": "{moduleCode}" },
292
+ { "path": "src/hooks/use{Module}.ts", "type": "Hook", "module": "{moduleCode}" },
293
+ { "path": "src/i18n/{module}.json", "type": "I18n", "module": "{moduleCode}" }
294
+ ]
295
+ ```
296
+
297
+ Additional per-section pages (dashboard, import, etc.) are separate entries.
298
+
299
+ **Route wiring task (MANDATORY — separate entry):**
300
+ ```json
301
+ { "path": "src/App.tsx", "type": "RouteWiring", "module": "{moduleCode}", "description": "Wire {module} routes into App.tsx (detect pattern: contextRoutes array OR JSX Route wrappers). BLOCKING: pages without route wiring = blank pages." }
302
+ ```
303
+
304
+ ### 6c. Cross-Module PRD (MANDATORY for multi-module features)
305
+
306
+ > **CRITICAL:** Cross-module integration tasks MUST exist in a dedicated PRD file.
307
+ > Without this, ralph-loop will NOT execute E2E tests or FK validation.
308
+
309
+ Generate `.ralph/prd-CrossModule.json` with:
310
+ ```json
311
+ {
312
+ "$version": "3.0.0",
313
+ "implementation": {
314
+ "filesToCreate": {
315
+ "tests": [
316
+ { "path": "src/Tests/Integration/CrossModule/ForeignKeyValidationTests.cs", "type": "IntegrationTests", "module": "CrossModule" },
317
+ { "path": "src/Tests/Integration/CrossModule/PermissionMatrixTests.cs", "type": "SecurityTests", "module": "CrossModule" },
318
+ { "path": "src/Tests/Integration/CrossModule/E2EFlowTests.cs", "type": "E2ETests", "module": "CrossModule" }
319
+ ]
320
+ }
321
+ }
322
+ }
323
+ ```
324
+
325
+ Add to progress.txt after all module tasks.
326
+
231
327
  ---
232
328
 
233
329
  ### 7. Write Handoff to Feature.json
@@ -312,12 +408,48 @@ Total endpoints = count of specification.apiEndpoints[] across all modules.
312
408
  ```
313
409
  // Derive seedDataCore from modules[], applicationRoles[], and coverageMatrix[]
314
410
 
411
+ // Helper: PascalCase → "Human Readable" (e.g., "HumanResources" → "Human Resources")
412
+ function toHumanReadable(pascalCase) {
413
+ return pascalCase
414
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
415
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
416
+ }
417
+
418
+ const contextCode = master.metadata.context || "business";
419
+ const appCode = master.metadata.application;
420
+ const appLabel = toHumanReadable(appCode);
421
+ const appDesc = master.cadrage?.problem?.split('.')[0] || `Gestion ${appLabel}`;
422
+ const lang = master.metadata.language || "fr";
423
+
315
424
  const seedDataCore = {
425
+ // Application-level navigation (MANDATORY — one entry per application)
426
+ navigationApplications: [{
427
+ code: appCode.toLowerCase(),
428
+ name: appCode,
429
+ labels: {
430
+ fr: lang === "fr" ? (master.cadrage?.applicationName || appLabel) : appLabel,
431
+ en: lang === "en" ? (master.cadrage?.applicationName || appLabel) : appLabel,
432
+ it: appLabel,
433
+ de: appLabel
434
+ },
435
+ description: {
436
+ fr: lang === "fr" ? appDesc : `Gestion ${appLabel}`,
437
+ en: lang === "en" ? appDesc : `${appLabel} management`,
438
+ it: `Gestione ${appLabel}`,
439
+ de: `${appLabel} Verwaltung`
440
+ },
441
+ icon: inferIconFromApplication(master) || "layout-grid",
442
+ iconType: "lucide",
443
+ context: contextCode,
444
+ route: `/${contextCode}/${toKebabCase(appCode)}`,
445
+ displayOrder: 1
446
+ }],
447
+
316
448
  navigationModules: master.modules.map((m, i) => ({
317
449
  code: m.code,
318
450
  label: m.name || m.code,
319
451
  description: m.description,
320
- icon: null, // set by ralph-loop
452
+ icon: inferIconFromModule(m) || "folder", // MUST be non-null — use sensible default
321
453
  iconType: "lucide",
322
454
  route: `/business/${master.metadata.application.toLowerCase()}/${m.code.toLowerCase()}`,
323
455
  displayOrder: (i + 1) * 10
@@ -386,6 +518,49 @@ const seedDataCore = {
386
518
  )
387
519
  };
388
520
 
521
+ // Icon inference for APPLICATION level (NEVER leave as null)
522
+ function inferIconFromApplication(master) {
523
+ const appLC = (master.metadata.application || '').toLowerCase();
524
+ const iconMap = {
525
+ 'humanresource': 'users', 'rh': 'users', 'hr': 'users', 'personnel': 'users',
526
+ 'sales': 'shopping-cart', 'vente': 'shopping-cart', 'commerce': 'shopping-cart',
527
+ 'finance': 'wallet', 'comptabilite': 'calculator', 'accounting': 'calculator',
528
+ 'inventory': 'warehouse', 'stock': 'package', 'logistic': 'truck',
529
+ 'crm': 'building-2', 'customer': 'building', 'client': 'building-2',
530
+ 'project': 'folder-kanban', 'task': 'check-square', 'gestion': 'layout-grid',
531
+ 'admin': 'shield', 'support': 'headphones', 'maintenance': 'wrench',
532
+ 'production': 'factory', 'quality': 'badge-check', 'document': 'file-text',
533
+ 'fleet': 'car', 'vehicle': 'car', 'transport': 'truck',
534
+ 'education': 'graduation-cap', 'formation': 'graduation-cap', 'training': 'graduation-cap'
535
+ };
536
+ for (const [keyword, icon] of Object.entries(iconMap)) {
537
+ if (appLC.includes(keyword)) return icon;
538
+ }
539
+ return "layout-grid"; // fallback — NEVER null
540
+ }
541
+
542
+ // Icon inference from module context (NEVER leave as null)
543
+ function inferIconFromModule(module) {
544
+ const codeLC = module.code.toLowerCase();
545
+ const iconMap = {
546
+ 'employee': 'users', 'staff': 'users', 'personnel': 'users', 'user': 'users',
547
+ 'project': 'folder-kanban', 'task': 'check-square', 'work': 'briefcase',
548
+ 'time': 'clock', 'schedule': 'calendar', 'planning': 'calendar-days',
549
+ 'absence': 'calendar-off', 'leave': 'calendar-off', 'vacation': 'palm-tree',
550
+ 'finance': 'wallet', 'billing': 'receipt', 'invoice': 'file-text',
551
+ 'inventory': 'warehouse', 'stock': 'package', 'product': 'shopping-bag',
552
+ 'customer': 'building', 'client': 'building-2', 'contact': 'contact',
553
+ 'report': 'bar-chart', 'dashboard': 'layout-dashboard', 'analytics': 'trending-up',
554
+ 'document': 'file-text', 'notification': 'bell', 'setting': 'settings',
555
+ 'order': 'shopping-cart', 'vehicle': 'car', 'maintenance': 'wrench',
556
+ 'audit': 'shield-check', 'workflow': 'git-branch', 'approval': 'check-circle'
557
+ };
558
+ for (const [keyword, icon] of Object.entries(iconMap)) {
559
+ if (codeLC.includes(keyword)) return icon;
560
+ }
561
+ return "folder"; // fallback — NEVER null
562
+ }
563
+
389
564
  ba-writer.enrichSection({
390
565
  featureId: {feature_id},
391
566
  section: "seedDataCore",
@@ -393,12 +568,23 @@ ba-writer.enrichSection({
393
568
  })
394
569
  ```
395
570
 
396
- **POST-CHECK (non-blocking):**
571
+ **POST-CHECK (BLOCKING for icons/application, warning for counts):**
397
572
  ```
573
+ IF !seedDataCore.navigationApplications || seedDataCore.navigationApplications.length === 0:
574
+ BLOCKING ERROR: "navigationApplications is empty — Application navigation will NOT be seeded. Menu will show Context → (nothing) → Modules."
575
+ IF seedDataCore.navigationApplications[0].icon === null || seedDataCore.navigationApplications[0].icon === undefined:
576
+ BLOCKING ERROR: "Application icon is null — ralph-loop will render empty navigation"
577
+ IF !seedDataCore.navigationApplications[0].labels?.fr || !seedDataCore.navigationApplications[0].labels?.en:
578
+ BLOCKING ERROR: "Application labels missing fr/en — translations will fail"
398
579
  IF seedDataCore.navigationModules.length !== master.modules.length:
399
580
  WARNING: seedDataCore has ${seedDataCore.navigationModules.length} nav modules but ${master.modules.length} modules exist
400
581
  IF seedDataCore.permissions.length === 0:
401
582
  WARNING: seedDataCore has 0 permissions — applicationRoles may be missing
583
+ IF seedDataCore.navigationModules.some(m => !m.icon || m.icon === null):
584
+ BLOCKING ERROR: "Navigation modules with null icons detected — ralph-loop will render empty navigation"
585
+ → Fix: Re-run inferIconFromModule() for modules with null icons
586
+ IF seedDataCore.navigationSections.length === 0:
587
+ WARNING: "0 navigation sections — every module should have at least 1 section (e.g., 'list')"
402
588
  ```
403
589
 
404
590
  #### 7c. Master Handoff (after ALL modules written + seedDataCore generated)
@@ -238,6 +238,34 @@ See [references/deploy-data-build.md](../references/deploy-data-build.md) for:
238
238
 
239
239
  ---
240
240
 
241
+ ### 7-bis. Generate SVG Wireframes (Parallel Enrichment)
242
+
243
+ > **After EMBEDDED_ARTIFACTS is built (sections 5-7) but BEFORE writing the HTML (section 8).**
244
+ > This step enriches each ASCII wireframe with a professional SVG version for dual-view rendering.
245
+
246
+ See [references/wireframe-svg-style-guide.md](../references/wireframe-svg-style-guide.md) for the complete SVG style specification, prompt template, and orchestration process.
247
+
248
+ **Process summary:**
249
+
250
+ 1. **Read** `references/wireframe-svg-style-guide.md` to get the prompt template
251
+ 2. **Collect** all wireframes across ALL modules in `EMBEDDED_ARTIFACTS.wireframes` where `content` exists and `svgContent` is null
252
+ 3. **Spawn parallel Task(sonnet) agents** — ONE per wireframe, ALL in a single message
253
+ 4. **Collect and validate** results: strip markdown fences if present, verify SVG starts with `<svg` and contains `</svg>`
254
+ 5. **Inject** valid SVGs into `EMBEDDED_ARTIFACTS.wireframes[moduleCode][index].svgContent`
255
+ 6. **Display summary:**
256
+ ```
257
+ SVG wireframe generation:
258
+ Total wireframes: {total}
259
+ SVG generated: {successCount}/{total}
260
+ Fallback (ASCII only): {failCount}/{total}
261
+ ```
262
+
263
+ > **CRITICAL:** This step is NEVER blocking. If all SVG generations fail, deployment
264
+ > continues with ASCII-only wireframes. The HTML renderer checks for `svgContent` before
265
+ > showing the SVG view. SVG is an enhancement, not a requirement.
266
+
267
+ ---
268
+
241
269
  ### 8. Write and Verify
242
270
 
243
271
  1. Write the populated HTML to the output directory: `docs/business/{app}/business-analyse/v{version}/ba-interactive.html`
@@ -357,6 +385,33 @@ After writing the HTML file, verify:
357
385
  BLOCKING_ERROR("Module mismatch: expected [${expectedModules}] but wireframes has [${wireframeModules}]")
358
386
  ```
359
387
 
388
+ 9. **MODULE DATA COMPLETENESS** — every module in FEATURE_DATA.modules[] must carry ALL fields from feature.json
389
+ ```
390
+ FOR each htmlModule in FEATURE_DATA.modules[]:
391
+ masterModule = master.modules.find(m => m.code === htmlModule.code)
392
+
393
+ // name MUST be display name, NOT code
394
+ IF htmlModule.name === htmlModule.code AND masterModule.name !== masterModule.code:
395
+ BLOCKING_ERROR("Module '${htmlModule.code}' has name === code. Expected: '${masterModule.name}'")
396
+
397
+ // anticipatedSections MUST be present (drives Structure tab + navigation tree)
398
+ IF masterModule.anticipatedSections?.length > 0 AND (!htmlModule.anticipatedSections OR htmlModule.anticipatedSections.length === 0):
399
+ BLOCKING_ERROR("Module '${htmlModule.code}' lost anticipatedSections: master has ${masterModule.anticipatedSections.length}, HTML has 0")
400
+ → FIX: Copy anticipatedSections from master.modules[].anticipatedSections
401
+
402
+ // dependencies/dependents MUST be present (drives dependency graph)
403
+ IF masterModule.dependencies?.length > 0 AND (!htmlModule.dependencies OR htmlModule.dependencies.length === 0):
404
+ BLOCKING_ERROR("Module '${htmlModule.code}' lost dependencies array")
405
+ ```
406
+
407
+ Display verification table:
408
+ ```
409
+ POST-CHECK: Module data completeness
410
+ | Module | name | sections | deps | wireframes | specs | Match |
411
+ |----------------|---------|----------|------|------------|-------|-------|
412
+ | {code} | {name} | {n} | {n} | {n} | OK | OK/FAIL |
413
+ ```
414
+
360
415
  **IF ANY CHECK FAILS → DO NOT PROCEED. Fix the data mapping and regenerate.**
361
416
 
362
417
  ---
@@ -304,7 +304,9 @@ fi
304
304
  **Execution sequence (IN ORDER):**
305
305
  1. `mcp__smartstack__scaffold_api_client` → API client + types + React Query hook
306
306
  2. `mcp__smartstack__scaffold_routes` (with `outputFormat: "clientRoutes"`) → route registry + route fragments
307
- 3. **Wire routes to App.tsx** insert `<Route>` entries inside correct Layout wrapper (BOTH standard AND tenant-prefixed `/t/:slug/...` blocks)
307
+ 3. **Wire routes to App.tsx (detect pattern: `contextRoutes` array OR JSX `<Route>`):**
308
+ - **Pattern A** (`contextRoutes: ContextRouteExtensions` in App.tsx): add to `contextRoutes.{context}[]` with RELATIVE paths → auto-injected into both standard + tenant trees
309
+ - **Pattern B** (JSX `<Route>` in App.tsx): insert `<Route>` entries inside correct Layout wrapper (BOTH standard AND tenant-prefixed `/t/:slug/...` blocks)
308
310
  4. Create pages using SmartStack components
309
311
  5. Create preferences hook: `use{Module}Preferences.ts`
310
312
  6. Generate i18n (4 languages: fr, en, it, de)
@@ -348,7 +350,8 @@ import axios from 'axios' → use @/services/api/apiClient
348
350
  Only fr/en translations → MUST have 4 languages
349
351
  src/pages/{Module}/ → MUST be src/pages/{Context}/{App}/{Module}/
350
352
  Routes generated but NOT added to App.tsx → MUST wire routes to App.tsx after scaffold_routes
351
- Routes in standard block only → MUST also add to /t/:slug/ tenant block
353
+ Routes in standard block only → MUST also add to /t/:slug/ tenant block (Pattern B only; Pattern A auto-handles)
354
+ Adding routes to clientRoutes[] instead of contextRoutes.{context}[] → MUST use contextRoutes for business/platform/personal routes
352
355
  '00000000-0000-0000-0000-000000000000' → use dynamic ID from auth context or route params
353
356
  const api = axios.create(...) → use @/services/api/apiClient (single instance)
354
357
  useXxx with raw axios inside hooks → hooks MUST use the shared apiClient from @/services/api