@atlashub/smartstack-cli 4.75.0 → 4.79.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 (81) hide show
  1. package/dist/index.js +87 -41
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/project/claude-md/root.CLAUDE.md.template +1 -1
  5. package/templates/skills/ai-prompt/SKILL.md +64 -0
  6. package/templates/skills/ai-prompt/references/ai-agent-modes.md +89 -0
  7. package/templates/skills/ai-prompt/references/eval-framework.md +129 -0
  8. package/templates/skills/apex/SKILL.md +2 -2
  9. package/templates/skills/apex/references/checks/frontend-checks.sh +123 -11
  10. package/templates/skills/apex/references/checks/seed-checks.sh +81 -7
  11. package/templates/skills/apex/references/core-seed-data.md +27 -22
  12. package/templates/skills/apex/references/domain-events-pattern.md +45 -0
  13. package/templates/skills/apex/references/entity-hooks-pattern.md +68 -0
  14. package/templates/skills/apex/references/licensing-enforcement.md +52 -0
  15. package/templates/skills/apex/references/post-checks.md +18 -1
  16. package/templates/skills/apex/references/smartstack-api.md +116 -5
  17. package/templates/skills/apex/references/smartstack-frontend.md +1 -1
  18. package/templates/skills/apex/references/smartstack-layers.md +6 -6
  19. package/templates/skills/apex/steps/step-00-init.md +1 -1
  20. package/templates/skills/apex/steps/step-03b-layer1-seed.md +26 -0
  21. package/templates/skills/apex/steps/step-03d-layer3-frontend.md +124 -2
  22. package/templates/skills/apex/steps/step-04-examine.md +163 -0
  23. package/templates/skills/apex-verify/SKILL.md +110 -0
  24. package/templates/skills/apex-verify/references/audit-rules.md +50 -0
  25. package/templates/skills/apex-verify/steps/step-00-init.md +119 -0
  26. package/templates/skills/apex-verify/steps/step-01-nav-audit.md +96 -0
  27. package/templates/skills/apex-verify/steps/step-02-crud-audit.md +127 -0
  28. package/templates/skills/apex-verify/steps/step-03-perm-audit.md +119 -0
  29. package/templates/skills/apex-verify/steps/step-04-route-audit.md +98 -0
  30. package/templates/skills/apex-verify/steps/step-05-report.md +110 -0
  31. package/templates/skills/application/references/contexts-cheatsheet.md +86 -0
  32. package/templates/skills/application/references/extensions-system.md +158 -0
  33. package/templates/skills/application/references/frontend-route-naming.md +7 -5
  34. package/templates/skills/application/references/frontend-verification.md +7 -5
  35. package/templates/skills/application/references/provider-template.md +4 -2
  36. package/templates/skills/application/references/smartstack-provider.md +118 -0
  37. package/templates/skills/application/references/themes-db-driven.md +484 -0
  38. package/templates/skills/application/templates-frontend.md +2 -2
  39. package/templates/skills/application/templates-seed.md +4 -2
  40. package/templates/skills/audit-route/references/routing-pattern.md +3 -1
  41. package/templates/skills/business-analyse/SKILL.md +3 -3
  42. package/templates/skills/business-analyse/_shared.md +37 -0
  43. package/templates/skills/business-analyse/react/components.md +30 -28
  44. package/templates/skills/business-analyse/references/03-json-schemas.md +11 -3
  45. package/templates/skills/business-analyse/references/03-post-check-validation.md +64 -0
  46. package/templates/skills/business-analyse/references/canonical-json-formats.md +7 -3
  47. package/templates/skills/business-analyse/references/robustness-checks.md +1 -1
  48. package/templates/skills/business-analyse/references/validation-checklist.md +5 -5
  49. package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +15 -4
  50. package/templates/skills/business-analyse/steps/step-03-specify.md +162 -4
  51. package/templates/skills/business-analyse/steps/step-04-consolidate.md +211 -1
  52. package/templates/skills/business-analyse/templates-react.md +15 -15
  53. package/templates/skills/business-analyse-handoff/references/agent-handoff-transform-prompt.md +3 -0
  54. package/templates/skills/business-analyse-html/html/ba-interactive.html +198 -16
  55. package/templates/skills/business-analyse-html/html/src/scripts/01-data-init.js +64 -0
  56. package/templates/skills/business-analyse-html/html/src/scripts/05-render-specs.js +80 -11
  57. package/templates/skills/business-analyse-html/html/src/scripts/06-render-consolidation.js +2 -2
  58. package/templates/skills/business-analyse-html/html/src/scripts/06-render-mockups.js +6 -3
  59. package/templates/skills/business-analyse-html/html/src/scripts/12-render-diagrams.js +46 -0
  60. package/templates/skills/business-analyse-html/references/02-feature-data-building.md +4 -2
  61. package/templates/skills/business-analyse-html/references/data-build.md +2 -0
  62. package/templates/skills/business-analyse-html/references/data-mapping.md +88 -21
  63. package/templates/skills/business-analyse-html/steps/step-02-build-data.md +6 -0
  64. package/templates/skills/business-analyse-html/steps/step-04-verify.md +92 -3
  65. package/templates/skills/business-analyse-quick/SKILL.md +807 -0
  66. package/templates/skills/{sketch → business-analyse-quick}/references/domain-heuristics.md +59 -3
  67. package/templates/skills/business-analyse-quick/references/prd-schema.md +268 -0
  68. package/templates/skills/business-analyse-review/references/review-data-mapping.md +6 -0
  69. package/templates/skills/cli-app-sync/SKILL.md +105 -4
  70. package/templates/skills/cli-app-sync/references/comparison-map.md +13 -0
  71. package/templates/skills/cli-app-sync/references/diff-entities.md +162 -0
  72. package/templates/skills/dev-start/SKILL.md +7 -7
  73. package/templates/skills/documentation/templates.md +16 -16
  74. package/templates/skills/migrate/SKILL.md +312 -0
  75. package/templates/skills/migrate/references/v3.34-to-v3.46.md +289 -0
  76. package/templates/skills/sketch/SKILL.md +15 -153
  77. package/templates/skills/smoke-generation/SKILL.md +313 -0
  78. package/templates/skills/ui-components/SKILL.md +11 -1
  79. package/templates/skills/ui-components/patterns/data-table.md +1 -1
  80. package/templates/skills/ui-components/references/component-catalog.md +82 -0
  81. package/templates/skills/workflow/SKILL.md +70 -1
@@ -244,6 +244,120 @@ This includes:
244
244
  - 5 contract verification checks (BR formulas, computed attributes, UC counts, permission counts, versioned attributes)
245
245
  - Blocking rule: 0 ERROR → PASS, ≥1 ERROR → BLOCK (user must fix before proceeding)
246
246
 
247
+ **BR-EXAMPLES-COMPLETE check — qualité test-ready (NEW):**
248
+ ```javascript
249
+ for (const module of completedModules) {
250
+ const rules = module.rules || [];
251
+ if (rules.length === 0) continue;
252
+
253
+ // 1. Couverture : toutes les règles doivent avoir des exemples
254
+ const withExamples = rules.filter(r => r.examples?.length > 0);
255
+ const exampleRate = withExamples.length / rules.length;
256
+
257
+ if (exampleRate < 0.4) {
258
+ ERROR(`Module ${module.code}: ${withExamples.length}/${rules.length} BRs ont des exemples (${Math.round(exampleRate*100)}% < 40% minimum) — BLOQUANT`);
259
+ } else if (exampleRate < 0.7) {
260
+ WARNING(`Module ${module.code}: ${withExamples.length}/${rules.length} BRs ont des exemples (${Math.round(exampleRate*100)}% — cible: 100%)`);
261
+ }
262
+
263
+ // 2. Format test-ready : exemples avec given/when/then
264
+ const testReady = rules.filter(r =>
265
+ r.examples?.some(e => e.given && e.when && e.then)
266
+ );
267
+ const proseOnly = rules.filter(r =>
268
+ r.examples?.length > 0 && !r.examples.some(e => e.given && e.when && e.then)
269
+ );
270
+
271
+ if (testReady.length < 4 && module.featureType !== 'portal') {
272
+ WARNING(`Module ${module.code}: ${testReady.length}/4 BRs au format test-ready {given, when, then}`);
273
+ }
274
+ if (proseOnly.length > 0) {
275
+ WARNING(`Module ${module.code}: ${proseOnly.length} BRs avec exemples en prose — convertir en format test-ready pour génération de tests`);
276
+ }
277
+
278
+ // 3. BR-CALC : formule + exemple chiffré obligatoires
279
+ const calcRules = rules.filter(r => r.category === 'calculation');
280
+ const calcWithoutFormula = calcRules.filter(r => !r.formula);
281
+ const calcWithoutNumericExample = calcRules.filter(r =>
282
+ !r.examples?.some(e =>
283
+ (e.scenario === 'calculation') ||
284
+ (e.then && Object.values(e.then).some(v => typeof v === 'number'))
285
+ )
286
+ );
287
+
288
+ if (calcWithoutFormula.length > 0) {
289
+ ERROR(`Module ${module.code}: ${calcWithoutFormula.length} BR-CALC sans champ formula`);
290
+ }
291
+ if (calcWithoutNumericExample.length > 0) {
292
+ WARNING(`Module ${module.code}: ${calcWithoutNumericExample.length} BR-CALC sans exemple chiffré — les tests de calcul nécessitent des valeurs numériques`);
293
+ }
294
+
295
+ // 4. Couverture scénarios : chaque règle devrait avoir happy_path + error
296
+ const withBothScenarios = rules.filter(r =>
297
+ r.examples?.some(e => e.scenario === 'happy_path' || e.then?.result === 'success') &&
298
+ r.examples?.some(e => e.scenario === 'error' || e.then?.result === 'error')
299
+ );
300
+ if (withBothScenarios.length < rules.length * 0.5 && module.featureType !== 'portal') {
301
+ WARNING(`Module ${module.code}: ${withBothScenarios.length}/${rules.length} BRs couvrent happy_path + error — cible: 100%`);
302
+ }
303
+ }
304
+ ```
305
+
306
+ ### 4b. Language Coherence Check (MANDATORY)
307
+
308
+ Verify that ALL generated content respects `{language}` from config:
309
+
310
+ ```javascript
311
+ const expectedLang = language; // from config.json
312
+
313
+ // Heuristics for language detection
314
+ const frIndicators = /\b(le|la|les|un|une|des|du|de|est|sont|doit|peut|dans|pour|avec|sur|par|cette|tout|qui)\b/i;
315
+ const enIndicators = /\b(the|a|an|is|are|must|can|in|for|with|on|by|this|all|who|should|when|each)\b/i;
316
+
317
+ function detectLang(text) {
318
+ if (!text || text.length < 10) return null;
319
+ const frScore = (text.match(frIndicators) || []).length;
320
+ const enScore = (text.match(enIndicators) || []).length;
321
+ if (frScore > enScore * 1.5) return 'fr';
322
+ if (enScore > frScore * 1.5) return 'en';
323
+ return null; // ambiguous
324
+ }
325
+
326
+ const langIssues = [];
327
+ for (const module of completedModules) {
328
+ // Sample entity descriptions
329
+ (module.entities || []).forEach(e => {
330
+ const detected = detectLang(e.description);
331
+ if (detected && detected !== expectedLang) {
332
+ langIssues.push({ module: module.code, type: 'entity', name: e.name, detected, text: e.description?.substring(0, 60) });
333
+ }
334
+ });
335
+ // Sample BR statements
336
+ (module.rules || []).forEach(r => {
337
+ const detected = detectLang(r.statement);
338
+ if (detected && detected !== expectedLang) {
339
+ langIssues.push({ module: module.code, type: 'rule', name: r.id, detected, text: r.statement?.substring(0, 60) });
340
+ }
341
+ });
342
+ // Sample UC names
343
+ (module.useCases || module.usecases || []).forEach(u => {
344
+ const detected = detectLang(u.name);
345
+ if (detected && detected !== expectedLang) {
346
+ langIssues.push({ module: module.code, type: 'useCase', name: u.id || u.name, detected, text: u.name });
347
+ }
348
+ });
349
+ }
350
+
351
+ if (langIssues.length > 0) {
352
+ WARNING(`${langIssues.length} content items detected in wrong language (expected: ${expectedLang})`);
353
+ Display(langIssues.slice(0, 10).map(i => ` - ${i.module}/${i.type} "${i.name}": detected ${i.detected}, text: "${i.text}"`).join('\n'));
354
+
355
+ if (langIssues.length > completedModules.length * 3) {
356
+ BLOCKING_ERROR(`Too many language violations (${langIssues.length}). Re-specify affected modules in ${expectedLang}.`);
357
+ }
358
+ }
359
+ ```
360
+
247
361
  ### 5. Data Model Consolidation
248
362
 
249
363
  Generate global entity relationship diagram:
@@ -389,7 +503,98 @@ for (const step of flow.steps) {
389
503
 
390
504
  Store as `consolidation.mermaidDiagrams.sequences[{flowName}]` (string).
391
505
 
392
- #### 5b-iv. Storage
506
+ #### 5b-iv. Use Case Diagrams (per module)
507
+
508
+ For EACH module, generate a Mermaid use case diagram showing actors and their interactions:
509
+
510
+ ```javascript
511
+ const useCaseDiagrams = {};
512
+
513
+ for (const module of completedModules) {
514
+ const usecases = module.useCases || module.usecases || [];
515
+ if (usecases.length === 0) continue;
516
+
517
+ // Collect unique actors
518
+ const actors = [...new Set(usecases.map(uc => uc.primaryActor || uc.actor).filter(Boolean))];
519
+
520
+ // Build flowchart-based UC diagram (Mermaid does not have native usecase syntax)
521
+ let diagram = "flowchart LR\n";
522
+
523
+ // Add actors (left side)
524
+ actors.forEach((actor, i) => {
525
+ const actorId = 'actor_' + actor.replace(/[^a-zA-Z0-9]/g, '_');
526
+ diagram += ` ${actorId}[/"👤 ${actor}"\\]\n`;
527
+ diagram += ` style ${actorId} fill:#e1f5fe,stroke:#0288d1,color:#01579b\n`;
528
+ });
529
+
530
+ // Group UCs by section
531
+ const sections = [...new Set(usecases.map(uc => uc.sectionCode).filter(Boolean))];
532
+
533
+ sections.forEach(section => {
534
+ const sectionUCs = usecases.filter(uc => uc.sectionCode === section);
535
+ diagram += ` subgraph ${section}["${section}"]\n`;
536
+ sectionUCs.forEach(uc => {
537
+ const ucId = (uc.id || uc.name).replace(/[^a-zA-Z0-9]/g, '_');
538
+ diagram += ` ${ucId}(("${uc.name}"))\n`;
539
+ });
540
+ diagram += ` end\n`;
541
+ });
542
+
543
+ // UCs without section
544
+ const noSectionUCs = usecases.filter(uc => !uc.sectionCode);
545
+ noSectionUCs.forEach(uc => {
546
+ const ucId = (uc.id || uc.name).replace(/[^a-zA-Z0-9]/g, '_');
547
+ diagram += ` ${ucId}(("${uc.name}"))\n`;
548
+ });
549
+
550
+ // Connect actors to UCs
551
+ usecases.forEach(uc => {
552
+ const actor = uc.primaryActor || uc.actor;
553
+ if (!actor) return;
554
+ const actorId = 'actor_' + actor.replace(/[^a-zA-Z0-9]/g, '_');
555
+ const ucId = (uc.id || uc.name).replace(/[^a-zA-Z0-9]/g, '_');
556
+ diagram += ` ${actorId} --> ${ucId}\n`;
557
+ });
558
+
559
+ useCaseDiagrams[module.code] = diagram;
560
+ }
561
+ ```
562
+
563
+ Store as `consolidation.mermaidDiagrams.useCases` (object: moduleCode → diagram string).
564
+
565
+ #### 5b-v. MCD (Modèle Conceptuel de Données)
566
+
567
+ Generate a simplified conceptual data model showing ONLY entity names and named relationships with cardinalities. Unlike the ERD which shows all attributes, the MCD focuses on the conceptual structure.
568
+
569
+ ```javascript
570
+ let mcd = "erDiagram\n";
571
+
572
+ // Entities: name + PK only (no attributes)
573
+ for (const ent of globalEntities) {
574
+ mcd += ` ${ent.name} {\n`;
575
+ mcd += ` ${ent.pk || 'guid'} Id PK\n`;
576
+ mcd += ` }\n`;
577
+ }
578
+
579
+ // Named relationships with cardinalities
580
+ for (const rel of globalRelationships) {
581
+ const card = rel.cardinality === "1:N" ? "||--o{" :
582
+ rel.cardinality === "N:1" ? "}o--||" :
583
+ rel.cardinality === "N:M" ? "}o--o{" :
584
+ rel.cardinality === "1:1" ? "||--||" : "||--o{";
585
+
586
+ // Use description as relationship label, or derive from cardinality
587
+ const label = rel.description ||
588
+ (rel.cardinality === "1:N" ? "contient" :
589
+ rel.cardinality === "N:1" ? "appartient" : "associe");
590
+
591
+ mcd += ` ${rel.sourceEntity} ${card} ${rel.targetEntity} : "${label}"\n`;
592
+ }
593
+ ```
594
+
595
+ Store as `consolidation.mermaidDiagrams.mcd` (string).
596
+
597
+ #### 5b-vi. Storage
393
598
 
394
599
  Write to `consolidation.json`:
395
600
 
@@ -397,6 +602,11 @@ Write to `consolidation.json`:
397
602
  {
398
603
  "mermaidDiagrams": {
399
604
  "erd": "erDiagram\n Employee {\n guid id\n string code\n ...\n }\n ...",
605
+ "mcd": "erDiagram\n Employee {\n guid Id PK\n }\n Employee ||--o{ Contract : \"contient\"\n ...",
606
+ "useCases": {
607
+ "Employees": "flowchart LR\n actor_RH[/\"👤 RH Admin\"\\]\n ...",
608
+ "Absences": "flowchart LR\n ..."
609
+ },
400
610
  "stateMachines": {
401
611
  "Absence": "stateDiagram-v2\n [*] --> Draft\n Draft --> Submitted : submit\n ...",
402
612
  "Invoice": "stateDiagram-v2\n [*] --> Draft\n ..."
@@ -120,15 +120,15 @@ const docData: DocData = {
120
120
  ]
121
121
  };
122
122
 
123
- // Priority Badge Component
123
+ // Priority Badge Component (theme-aware via SmartStack status tokens)
124
124
  function PriorityBadge({ priority }: { priority: string }) {
125
125
  const colors = {
126
- Must: 'bg-red-500/10 text-red-600',
127
- Should: 'bg-yellow-500/10 text-yellow-600',
128
- Could: 'bg-blue-500/10 text-blue-600'
126
+ Must: 'bg-[var(--error-bg)] text-[var(--error-text)] border border-[var(--error-border)]',
127
+ Should: 'bg-[var(--warning-bg)] text-[var(--warning-text)] border border-[var(--warning-border)]',
128
+ Could: 'bg-[var(--info-bg)] text-[var(--info-text)] border border-[var(--info-border)]'
129
129
  };
130
130
  return (
131
- <span className={`px-2 py-0.5 rounded text-xs font-medium ${colors[priority as keyof typeof colors] || 'bg-gray-500/10 text-gray-600'}`}>
131
+ <span className={`px-2 py-0.5 rounded text-xs font-medium ${colors[priority as keyof typeof colors] || 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)]'}`}>
132
132
  {priority}
133
133
  </span>
134
134
  );
@@ -186,7 +186,7 @@ export function {ModuleName}DocPage() {
186
186
  <span className="px-2 py-0.5 rounded bg-[var(--color-primary-600)]/10 text-[var(--color-primary-600)] text-xs font-medium">
187
187
  {docData.featureId}
188
188
  </span>
189
- <span className="px-2 py-0.5 rounded bg-green-500/10 text-green-600 text-xs font-medium">
189
+ <span className="px-2 py-0.5 rounded bg-[var(--success-bg)] text-[var(--success-text)] border border-[var(--success-border)] text-xs font-medium">
190
190
  v{docData.version}
191
191
  </span>
192
192
  </div>
@@ -264,7 +264,7 @@ export function {ModuleName}DocPage() {
264
264
  </div>
265
265
  <p className="text-sm text-[var(--text-secondary)] mb-2">{uc.description}</p>
266
266
  <div className="flex items-center gap-2 text-xs">
267
- <Shield className="w-3 h-3 text-amber-500" />
267
+ <Shield className="w-3 h-3 text-[var(--warning-text)]" />
268
268
  <code className="bg-[var(--bg-secondary)] px-2 py-0.5 rounded">{uc.permission}</code>
269
269
  </div>
270
270
  </div>
@@ -318,11 +318,11 @@ export function {ModuleName}DocPage() {
318
318
  ))}
319
319
  </div>
320
320
 
321
- {/* Permission Warning */}
322
- <div className="mt-4 p-4 rounded-lg bg-amber-500/10 border border-amber-500/20 flex items-start gap-3">
323
- <AlertTriangle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
321
+ {/* Permission Warning (theme-aware) */}
322
+ <div className="mt-4 p-4 rounded-lg bg-[var(--warning-bg)] border border-[var(--warning-border)] flex items-start gap-3">
323
+ <AlertTriangle className="w-5 h-5 text-[var(--warning-text)] flex-shrink-0 mt-0.5" />
324
324
  <div>
325
- <div className="font-medium text-amber-600">{t('warnings.permissionCheck')}</div>
325
+ <div className="font-medium text-[var(--warning-text)]">{t('warnings.permissionCheck')}</div>
326
326
  <p className="text-sm text-[var(--text-secondary)]">
327
327
  {t('warnings.permissionCheckDescription')}
328
328
  </p>
@@ -367,22 +367,22 @@ export function {ModuleName}DocPage() {
367
367
  </thead>
368
368
  <tbody>
369
369
  <tr>
370
- <td className="py-2 px-3"><span className="px-2 py-0.5 rounded bg-green-500/10 text-green-600">GET</span></td>
370
+ <td className="py-2 px-3"><span className="px-2 py-0.5 rounded bg-[var(--success-bg)] text-[var(--success-text)] border border-[var(--success-border)]">GET</span></td>
371
371
  <td className="py-2 px-3">/api/{'{module}'}</td>
372
372
  <td className="py-2 px-3 text-[var(--text-secondary)]">.read</td>
373
373
  </tr>
374
374
  <tr className="bg-[var(--bg-secondary)]/50">
375
- <td className="py-2 px-3"><span className="px-2 py-0.5 rounded bg-blue-500/10 text-blue-600">POST</span></td>
375
+ <td className="py-2 px-3"><span className="px-2 py-0.5 rounded bg-[var(--info-bg)] text-[var(--info-text)] border border-[var(--info-border)]">POST</span></td>
376
376
  <td className="py-2 px-3">/api/{'{module}'}</td>
377
377
  <td className="py-2 px-3 text-[var(--text-secondary)]">.create</td>
378
378
  </tr>
379
379
  <tr>
380
- <td className="py-2 px-3"><span className="px-2 py-0.5 rounded bg-yellow-500/10 text-yellow-600">PUT</span></td>
380
+ <td className="py-2 px-3"><span className="px-2 py-0.5 rounded bg-[var(--warning-bg)] text-[var(--warning-text)] border border-[var(--warning-border)]">PUT</span></td>
381
381
  <td className="py-2 px-3">/api/{'{module}'}/{'{id}'}</td>
382
382
  <td className="py-2 px-3 text-[var(--text-secondary)]">.update</td>
383
383
  </tr>
384
384
  <tr className="bg-[var(--bg-secondary)]/50">
385
- <td className="py-2 px-3"><span className="px-2 py-0.5 rounded bg-red-500/10 text-red-600">DELETE</span></td>
385
+ <td className="py-2 px-3"><span className="px-2 py-0.5 rounded bg-[var(--error-bg)] text-[var(--error-text)] border border-[var(--error-border)]">DELETE</span></td>
386
386
  <td className="py-2 px-3">/api/{'{module}'}/{'{id}'}</td>
387
387
  <td className="py-2 px-3 text-[var(--text-secondary)]">.delete</td>
388
388
  </tr>
@@ -47,6 +47,9 @@ When reading flat files, prefer canonical keys but fall back to alternatives:
47
47
  - `primaryActor`: `uc.primaryActor || uc.actor`
48
48
  - `mainScenario`: `uc.mainScenario || uc.steps` (if `steps[]` contains objects, extract `.action`)
49
49
  - `rules`: `data.rules || data.businessRules || []`
50
+ - `rules[].examples`: prefer `rule.examples[]` (`{input, expected}`); fallback: wrap `rule.example` string as `[{input: rule.example, expected: ""}]`
51
+ - `rules[].statement`: prefer `rule.statement`; fallback: `rule.description`
52
+ - `rules[].id`: prefer `rule.id`; fallback: `rule.code`
50
53
  - `screens`: `data.sections || data.screens` (if `screens[]` exists, treat each screen as a section with 1 resource)
51
54
 
52
55
  This safety net handles pre-4.52 BA outputs. It should become unnecessary once step-03-specify enforces canonical keys.
@@ -2646,6 +2646,70 @@ data.moduleSpecs = data.moduleSpecs || {};
2646
2646
  });
2647
2647
  });
2648
2648
 
2649
+ // Normalize permissions: convert any object format to "Role|Action" pipe-delimited strings
2650
+ // Handles 5 formats:
2651
+ // A: "Role|Action" strings (already correct)
2652
+ // B: {role, permissions: ["Read:all", ...]} with role-permission pairs
2653
+ // C: {role, permissions: ["Module.Entity.Read", ...]} with path-based permissions
2654
+ // D: {code, label, description} permission definitions without role assignment
2655
+ // E: {role: "", permissions: []} empty entries (skip)
2656
+ (data.modules || []).forEach(function(m) {
2657
+ var spec = data.moduleSpecs[m.code];
2658
+ if (!spec || !spec.permissions || spec.permissions.length === 0) return;
2659
+ // Skip if already normalized (first element is a pipe-delimited string)
2660
+ if (typeof spec.permissions[0] === 'string' && spec.permissions[0].indexOf('|') !== -1) return;
2661
+ if (typeof spec.permissions[0] !== 'object') return;
2662
+
2663
+ var actionMap = {
2664
+ 'read': 'Consulter', 'create': 'Créer', 'update': 'Modifier',
2665
+ 'delete': 'Supprimer', 'approve': 'Valider', 'validate': 'Valider',
2666
+ 'export': 'Exporter', 'admin': 'Administrer', 'import': 'Importer',
2667
+ 'viewsalary': 'Consulter', 'assignteam': 'Modifier', 'cancel': 'Supprimer',
2668
+ 'viewbalance': 'Consulter', 'invoicetime': 'Valider'
2669
+ };
2670
+ var normalized = [];
2671
+ var first = spec.permissions[0];
2672
+
2673
+ // Detect Format D: {code, label, description} — permission codes without roles
2674
+ if (first.code && !first.role && !first.permissions) {
2675
+ // Extract actions from permission codes, assign to stakeholder roles
2676
+ var roles = (data.cadrage.stakeholders || []).map(function(s) { return s.role; }).filter(Boolean);
2677
+ if (roles.length === 0) roles = ['Administrateur', 'Responsable', 'Contributeur', 'Lecteur'];
2678
+ spec.permissions.forEach(function(permDef) {
2679
+ var code = permDef.code || '';
2680
+ var raw = code.split('.').pop(); // "Module.Entity.Read" → "Read"
2681
+ var action = actionMap[raw.toLowerCase()] || raw;
2682
+ // Assign to first role (admin) by default — better than empty
2683
+ if (roles[0]) {
2684
+ var key = roles[0] + '|' + action;
2685
+ if (normalized.indexOf(key) === -1) normalized.push(key);
2686
+ }
2687
+ });
2688
+ } else {
2689
+ // Format B/C: {role, permissions: [...]}
2690
+ spec.permissions.forEach(function(entry) {
2691
+ var role = entry.role || entry.name || '';
2692
+ if (!role) return; // Skip Format E (empty role)
2693
+ var perms = entry.permissions || entry.actions || [];
2694
+ if (perms.length === 0 && entry.permissionPattern && entry.permissionPattern.endsWith('*')) {
2695
+ ['Consulter', 'Créer', 'Modifier', 'Supprimer', 'Valider', 'Exporter'].forEach(function(a) {
2696
+ normalized.push(role + '|' + a);
2697
+ });
2698
+ return;
2699
+ }
2700
+ perms.forEach(function(perm) {
2701
+ if (typeof perm !== 'string') return;
2702
+ var raw = perm.split(':')[0]; // "Read:all" → "Read"
2703
+ if (raw.indexOf('.') !== -1) raw = raw.split('.').pop(); // "Module.Entity.Read" → "Read"
2704
+ var action = actionMap[raw.toLowerCase()] || raw;
2705
+ var key = role + '|' + action;
2706
+ if (normalized.indexOf(key) === -1) normalized.push(key);
2707
+ });
2708
+ });
2709
+ }
2710
+ spec.permissions = normalized;
2711
+ });
2712
+
2649
2713
  // Detect if modules use section-level specs (hierarchical mode)
2650
2714
  function hasHierarchicalSpecs(mod) {
2651
2715
  return (mod.anticipatedSections || []).some(function(s) {
@@ -3693,6 +3757,11 @@ function renderModuleSpecSection(mod) {
3693
3757
  }
3694
3758
 
3695
3759
  function renderUseCase(code, uc, index) {
3760
+ var preconditions = uc.preconditions || [];
3761
+ var postconditions = uc.postconditions || [];
3762
+ var errorScenarios = uc.errorScenarios || [];
3763
+ var description = uc.description || '';
3764
+
3696
3765
  return `
3697
3766
  <div class="uc-item">
3698
3767
  <div class="uc-header">
@@ -3703,8 +3772,12 @@ function renderUseCase(code, uc, index) {
3703
3772
  </div>
3704
3773
  </div>
3705
3774
  <div class="uc-actors"><div class="uc-actor">${escapeHtml(uc.actor)}</div></div>
3706
- ${uc.steps ? `<div class="uc-detail-label">Déroulement</div><div class="uc-detail">${escapeHtml(uc.steps).replace(/\n/g, '<br>')}</div>` : ''}
3707
- ${uc.alternative ? `<div class="uc-detail-label">En cas de problème</div><div class="uc-detail" style="color:var(--warning);">${escapeHtml(uc.alternative)}</div>` : ''}
3775
+ ${description ? `<div class="uc-detail-label">Description</div><div class="uc-detail" style="font-style:italic;color:var(--text-muted);">${escapeHtml(description)}</div>` : ''}
3776
+ ${preconditions.length > 0 ? `<div class="uc-detail-label">Préconditions</div><div class="uc-detail"><ul style="margin:0;padding-left:1.2rem;">${preconditions.map(p => '<li>' + escapeHtml(typeof p === 'string' ? p : p.description || '') + '</li>').join('')}</ul></div>` : ''}
3777
+ ${uc.steps ? `<div class="uc-detail-label">Déroulement principal</div><div class="uc-detail">${escapeHtml(uc.steps).replace(/\n/g, '<br>')}</div>` : ''}
3778
+ ${uc.alternative ? `<div class="uc-detail-label">Scénarios alternatifs</div><div class="uc-detail" style="color:var(--warning);">${escapeHtml(uc.alternative)}</div>` : ''}
3779
+ ${errorScenarios.length > 0 ? `<div class="uc-detail-label">Scénarios d'erreur</div><div class="uc-detail" style="color:var(--danger);"><ul style="margin:0;padding-left:1.2rem;">${errorScenarios.map(e => '<li><strong>' + escapeHtml(typeof e === 'string' ? e : e.name || '') + '</strong>' + (typeof e === 'object' && e.steps ? ' : ' + escapeHtml(Array.isArray(e.steps) ? e.steps.join(', ') : e.steps) : '') + '</li>').join('')}</ul></div>` : ''}
3780
+ ${postconditions.length > 0 ? `<div class="uc-detail-label">Postconditions</div><div class="uc-detail"><ul style="margin:0;padding-left:1.2rem;">${postconditions.map(p => '<li>' + escapeHtml(typeof p === 'string' ? p : p.description || '') + '</li>').join('')}</ul></div>` : ''}
3708
3781
  <div style="padding:0.5rem 0.75rem;border-top:1px solid var(--border);background:var(--bg-input);border-radius:0 0 8px 8px;">
3709
3782
  <label style="font-size:0.75rem;color:var(--text-muted);display:block;margin-bottom:0.25rem;">Commentaire / Feedback :</label>
3710
3783
  <textarea class="form-textarea" placeholder="Ajouter un commentaire sur ce cas d'utilisation..."
@@ -3797,15 +3870,16 @@ function renderEntity(code, ent, index) {
3797
3870
  </div>
3798
3871
  ${(ent.attributes || []).length > 0 ? `
3799
3872
  <table class="attr-table">
3800
- <thead><tr><th>Information</th><th>Description</th></tr></thead>
3873
+ <thead><tr><th>Information</th><th style="width:100px;">Type</th><th>Description</th></tr></thead>
3801
3874
  <tbody>
3802
- ${ent.attributes.map(a => `<tr><td style="font-weight:500;color:var(--text-bright);">${escapeHtml(a.name)}</td><td>${escapeHtml(a.description || '')}</td></tr>`).join('')}
3875
+ ${ent.attributes.map(a => `<tr><td style="font-weight:500;color:var(--text-bright);">${escapeHtml(a.name)}</td><td style="font-family:monospace;font-size:0.8rem;color:var(--accent);">${escapeHtml(a.type || 'string')}</td><td>${escapeHtml(a.description || '')}</td></tr>`).join('')}
3803
3876
  </tbody>
3804
3877
  </table>` : ''}
3805
3878
  <div style="padding:0.3rem 0.75rem;">
3806
3879
  <button class="add-btn" style="font-size:0.75rem;padding:0.4rem;" onclick="toggleForm('${attrFormId}')">+ Ajouter un attribut</button>
3807
3880
  <div class="inline-form" id="${attrFormId}">
3808
3881
  <div class="form-group"><label class="form-label">Nom de l'attribut</label><input type="text" class="form-input" id="attr-name-${code}-${index}" placeholder="Nom de l'attribut"></div>
3882
+ <div class="form-group"><label class="form-label">Type</label><input type="text" class="form-input" id="attr-type-${code}-${index}" placeholder="string, guid, int, decimal, date, enum, boolean" value="string"></div>
3809
3883
  <div class="form-group"><label class="form-label">Description (optionnel)</label><input type="text" class="form-input" id="attr-desc-${code}-${index}" placeholder="Description"></div>
3810
3884
  <div class="form-actions"><button class="btn" onclick="toggleForm('${attrFormId}')">Annuler</button><button class="btn btn-primary" onclick="addEntityAttribute('${code}',${index})">Ajouter</button></div>
3811
3885
  </div>
@@ -3830,7 +3904,7 @@ function addEntity(code) {
3830
3904
 
3831
3905
  const attrs = document.getElementById('ent-attrs-' + code).value.split('\n').filter(l => l.trim()).map(l => {
3832
3906
  const parts = l.split(' - ');
3833
- return { name: parts[0]?.trim() || l.trim(), description: parts.slice(1).join(' - ').trim() };
3907
+ return { name: parts[0]?.trim() || l.trim(), type: parts[1]?.trim() || 'string', description: parts.slice(2).join(' - ').trim() || parts.slice(1).join(' - ').trim() };
3834
3908
  });
3835
3909
  const rels = document.getElementById('ent-rels-' + code).value.split('\n').filter(l => l.trim());
3836
3910
 
@@ -3854,13 +3928,14 @@ function removeEntity(code, index) {
3854
3928
 
3855
3929
  function addEntityAttribute(code, entityIndex) {
3856
3930
  var attrName = document.getElementById('attr-name-' + code + '-' + entityIndex);
3931
+ var attrType = document.getElementById('attr-type-' + code + '-' + entityIndex);
3857
3932
  var attrDesc = document.getElementById('attr-desc-' + code + '-' + entityIndex);
3858
3933
  if (!attrName || !attrName.value.trim()) return;
3859
3934
  if (!data.moduleSpecs[code]?.entities?.[entityIndex]) return;
3860
3935
  if (!data.moduleSpecs[code].entities[entityIndex].attributes) {
3861
3936
  data.moduleSpecs[code].entities[entityIndex].attributes = [];
3862
3937
  }
3863
- data.moduleSpecs[code].entities[entityIndex].attributes.push({ name: attrName.value.trim(), description: (attrDesc?.value || '').trim() });
3938
+ data.moduleSpecs[code].entities[entityIndex].attributes.push({ name: attrName.value.trim(), type: (attrType?.value || 'string').trim(), description: (attrDesc?.value || '').trim() });
3864
3939
  renderAllModuleSpecs();
3865
3940
  autoSave();
3866
3941
  }
@@ -3971,13 +4046,68 @@ function toggleWireframeView(wireframeId, view) {
3971
4046
  }
3972
4047
  }
3973
4048
 
4049
+ // Normalize permissions array: handles 3 formats:
4050
+ // Format 1 (string): "Role|Action" → already correct
4051
+ // Format 2 (object): {role, permissions: ["Read:all", "Create:all", ...]}
4052
+ // Format 3 (object): {role, permissions: ["Module.Entity.Read", ...]}
4053
+ function normalizePermissions(permsArray) {
4054
+ if (!permsArray || permsArray.length === 0) return [];
4055
+ // If first element is a string with '|', already normalized
4056
+ if (typeof permsArray[0] === 'string' && permsArray[0].includes('|')) return permsArray;
4057
+ // If first element is a string without '|', return as-is (unknown format)
4058
+ if (typeof permsArray[0] === 'string') return permsArray;
4059
+ // Format 2/3: objects with {role, permissions[]}
4060
+ const actionMap = {
4061
+ 'read': 'Consulter', 'create': 'Créer', 'update': 'Modifier',
4062
+ 'delete': 'Supprimer', 'approve': 'Valider', 'validate': 'Valider',
4063
+ 'export': 'Exporter', 'admin': 'Administrer', 'import': 'Importer',
4064
+ 'viewsalary': 'Consulter', 'assignteam': 'Modifier', 'cancel': 'Supprimer'
4065
+ };
4066
+ const normalized = [];
4067
+ permsArray.forEach(function(entry) {
4068
+ if (typeof entry !== 'object' || !entry) return;
4069
+ var role = entry.role || entry.name || '';
4070
+ if (!role) return;
4071
+ var perms = entry.permissions || entry.actions || [];
4072
+ if (perms.length === 0 && entry.permissionPattern && entry.permissionPattern.endsWith('*')) {
4073
+ // Wildcard: expand to all standard actions
4074
+ ['Consulter', 'Créer', 'Modifier', 'Supprimer', 'Valider', 'Exporter'].forEach(function(a) {
4075
+ normalized.push(role + '|' + a);
4076
+ });
4077
+ return;
4078
+ }
4079
+ perms.forEach(function(perm) {
4080
+ var action = '';
4081
+ if (typeof perm === 'string') {
4082
+ // "Read:all" → extract "Read"
4083
+ var raw = perm.split(':')[0];
4084
+ // "Module.Entity.Read" → extract "Read"
4085
+ if (raw.includes('.')) raw = raw.split('.').pop();
4086
+ action = actionMap[raw.toLowerCase()] || raw;
4087
+ }
4088
+ if (action) {
4089
+ var key = role + '|' + action;
4090
+ if (normalized.indexOf(key) === -1) normalized.push(key);
4091
+ }
4092
+ });
4093
+ });
4094
+ return normalized;
4095
+ }
4096
+
3974
4097
  function getPermRoles() {
3975
- // Extract roles from actual permission data (handles English role names from JSON data)
4098
+ // Extract roles from actual permission data (handles multiple formats)
3976
4099
  const rolesFromPerms = [];
3977
4100
  const seen = new Set();
3978
4101
  data.modules.forEach(m => {
3979
- (data.moduleSpecs[m.code]?.permissions || []).forEach(p => {
3980
- const role = p.split('|')[0];
4102
+ var perms = data.moduleSpecs[m.code]?.permissions || [];
4103
+ // Handle object format directly for role extraction
4104
+ perms.forEach(p => {
4105
+ var role = '';
4106
+ if (typeof p === 'string') {
4107
+ role = p.split('|')[0];
4108
+ } else if (typeof p === 'object' && p) {
4109
+ role = p.role || p.name || '';
4110
+ }
3981
4111
  if (role && !seen.has(role)) { seen.add(role); rolesFromPerms.push(role); }
3982
4112
  });
3983
4113
  });
@@ -4000,7 +4130,9 @@ function renderPermissionGrid(code) {
4000
4130
  const baseRolesCount = roles.length - (data.customRoles || []).length;
4001
4131
  const baseActionsCount = 6;
4002
4132
 
4003
- const perms = data.moduleSpecs[code]?.permissions || [];
4133
+ // Normalize permissions to "Role|Action" format (handles object and string formats)
4134
+ const rawPerms = data.moduleSpecs[code]?.permissions || [];
4135
+ const perms = normalizePermissions(rawPerms);
4004
4136
 
4005
4137
  return `
4006
4138
  <table class="mock-table" style="background:var(--bg-card);border-radius:8px;overflow:hidden;">
@@ -4205,7 +4337,8 @@ function renderModuleMockups(code) {
4205
4337
  }) : [];
4206
4338
 
4207
4339
  // Priority 1: HTML mockups from screens[] specs (wireframes NOT shown when screens exist)
4208
- if (screens.length > 0) {
4340
+ var hasResources = screens.some(function(s) { return (s.resources || []).length > 0; });
4341
+ if (screens.length > 0 && hasResources) {
4209
4342
  var html = '';
4210
4343
  if (typeof renderScreenMockups === 'function') {
4211
4344
  html = renderScreenMockups(code);
@@ -4353,9 +4486,9 @@ function renderDataModel() {
4353
4486
  ${ent.description ? `<div class="dm-entity-desc">${escapeHtml(ent.description)}</div>` : ''}
4354
4487
  ${attrs.length > 0 ? `
4355
4488
  <table class="dm-attr-table">
4356
- <thead><tr><th>Champ</th><th>Description</th></tr></thead>
4489
+ <thead><tr><th>Champ</th><th style="width:100px;">Type</th><th>Description</th></tr></thead>
4357
4490
  <tbody>
4358
- ${attrs.map(a => `<tr><td class="dm-attr-name">${escapeHtml(a.name)}</td><td class="dm-attr-desc">${escapeHtml(a.description || '')}</td></tr>`).join('')}
4491
+ ${attrs.map(a => `<tr><td class="dm-attr-name">${escapeHtml(a.name)}</td><td style="font-family:monospace;font-size:0.8rem;color:var(--accent);">${escapeHtml(a.type || 'string')}</td><td class="dm-attr-desc">${escapeHtml(a.description || '')}</td></tr>`).join('')}
4359
4492
  </tbody>
4360
4493
  </table>` : ''}
4361
4494
  ${rels.length > 0 ? `
@@ -4495,13 +4628,16 @@ function renderScreenMockups(code) {
4495
4628
 
4496
4629
  return screens.map(function(screen, si) {
4497
4630
  var resources = screen.resources || [];
4631
+ var content = resources.length > 0
4632
+ ? resources.map(function(res, ri) {
4633
+ return renderResourceMockup(code, screen.sectionCode, res, ri);
4634
+ }).join('')
4635
+ : '<div class="card" style="padding:1.5rem;color:var(--text-muted);text-align:center;"><p>Maquette en attente de spécification pour cette section.</p></div>';
4498
4636
  return '<div class="screen-section" id="screen-' + code + '-' + screen.sectionCode + '" style="margin-bottom:2rem;">' +
4499
4637
  '<h3 style="color:var(--text-bright);font-size:1rem;margin-bottom:1rem;">' +
4500
4638
  '<span style="color:var(--accent);">&#9656;</span> ' + escapeHtml(screen.sectionLabel || screen.sectionCode) +
4501
4639
  '</h3>' +
4502
- resources.map(function(res, ri) {
4503
- return renderResourceMockup(code, screen.sectionCode, res, ri);
4504
- }).join('') +
4640
+ content +
4505
4641
  '</div>';
4506
4642
  }).join('');
4507
4643
  }
@@ -5856,6 +5992,24 @@ function renderMermaidDiagrams() {
5856
5992
  }
5857
5993
  }
5858
5994
 
5995
+ // 1b. MCD (Modèle Conceptuel de Données) — after ERD
5996
+ if (diagrams.mcd) {
5997
+ var erdContainer = document.getElementById('dataModelContainer');
5998
+ if (erdContainer) {
5999
+ var mcdDiv = document.createElement('div');
6000
+ mcdDiv.className = 'diagram-container diagram-mcd';
6001
+ mcdDiv.innerHTML =
6002
+ '<div class="diagram-section-header" style="font-size:0.95rem;font-weight:600;color:var(--text-bright);margin-bottom:0.75rem;">Mod\u00e8le Conceptuel de Donn\u00e9es (MCD)</div>' +
6003
+ '<div class="mermaid">' + escapeHtml(diagrams.mcd) + '</div>';
6004
+ var erdDiv = erdContainer.querySelector('.diagram-erd');
6005
+ if (erdDiv && erdDiv.nextSibling) {
6006
+ erdContainer.insertBefore(mcdDiv, erdDiv.nextSibling);
6007
+ } else {
6008
+ erdContainer.appendChild(mcdDiv);
6009
+ }
6010
+ }
6011
+ }
6012
+
5859
6013
  // 2. State machine diagrams — inject in module spec sections or consol-datamodel
5860
6014
  if (diagrams.stateMachines && Object.keys(diagrams.stateMachines).length > 0) {
5861
6015
  const smContainer = document.getElementById('dataModelContainer');
@@ -5878,6 +6032,34 @@ function renderMermaidDiagrams() {
5878
6032
  }
5879
6033
  }
5880
6034
 
6035
+ // 2b. Use Case diagrams — inject in each module's UC tab
6036
+ if (diagrams.useCases && Object.keys(diagrams.useCases).length > 0) {
6037
+ Object.entries(diagrams.useCases).forEach(function(entry) {
6038
+ var moduleCode = entry[0];
6039
+ var def = entry[1];
6040
+ var ucTab = document.getElementById('tab-' + moduleCode + '-uc');
6041
+ if (!ucTab) {
6042
+ // Try kebab-case version
6043
+ var kebab = moduleCode.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
6044
+ ucTab = document.getElementById('tab-' + kebab + '-uc');
6045
+ }
6046
+ if (ucTab) {
6047
+ var ucDiagramDiv = document.createElement('div');
6048
+ ucDiagramDiv.className = 'diagram-container diagram-usecase';
6049
+ ucDiagramDiv.innerHTML =
6050
+ '<div class="diagram-section-header" style="font-size:0.95rem;font-weight:600;color:var(--text-bright);margin-bottom:0.75rem;">Diagramme de cas d\'utilisation</div>' +
6051
+ '<div class="mermaid">' + escapeHtml(def) + '</div>';
6052
+ // Insert after the first paragraph (description text)
6053
+ var firstP = ucTab.querySelector('p');
6054
+ if (firstP && firstP.nextSibling) {
6055
+ ucTab.insertBefore(ucDiagramDiv, firstP.nextSibling);
6056
+ } else {
6057
+ ucTab.insertBefore(ucDiagramDiv, ucTab.firstChild);
6058
+ }
6059
+ }
6060
+ });
6061
+ }
6062
+
5881
6063
  // 3. Sequence diagrams in consol-flows
5882
6064
  if (diagrams.sequences && Object.keys(diagrams.sequences).length > 0) {
5883
6065
  var flowsContainer = document.getElementById('consolFlowsContainer');