@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
@@ -105,6 +105,70 @@ data.moduleSpecs = data.moduleSpecs || {};
105
105
  });
106
106
  });
107
107
 
108
+ // Normalize permissions: convert any object format to "Role|Action" pipe-delimited strings
109
+ // Handles 5 formats:
110
+ // A: "Role|Action" strings (already correct)
111
+ // B: {role, permissions: ["Read:all", ...]} with role-permission pairs
112
+ // C: {role, permissions: ["Module.Entity.Read", ...]} with path-based permissions
113
+ // D: {code, label, description} permission definitions without role assignment
114
+ // E: {role: "", permissions: []} empty entries (skip)
115
+ (data.modules || []).forEach(function(m) {
116
+ var spec = data.moduleSpecs[m.code];
117
+ if (!spec || !spec.permissions || spec.permissions.length === 0) return;
118
+ // Skip if already normalized (first element is a pipe-delimited string)
119
+ if (typeof spec.permissions[0] === 'string' && spec.permissions[0].indexOf('|') !== -1) return;
120
+ if (typeof spec.permissions[0] !== 'object') return;
121
+
122
+ var actionMap = {
123
+ 'read': 'Consulter', 'create': 'Créer', 'update': 'Modifier',
124
+ 'delete': 'Supprimer', 'approve': 'Valider', 'validate': 'Valider',
125
+ 'export': 'Exporter', 'admin': 'Administrer', 'import': 'Importer',
126
+ 'viewsalary': 'Consulter', 'assignteam': 'Modifier', 'cancel': 'Supprimer',
127
+ 'viewbalance': 'Consulter', 'invoicetime': 'Valider'
128
+ };
129
+ var normalized = [];
130
+ var first = spec.permissions[0];
131
+
132
+ // Detect Format D: {code, label, description} — permission codes without roles
133
+ if (first.code && !first.role && !first.permissions) {
134
+ // Extract actions from permission codes, assign to stakeholder roles
135
+ var roles = (data.cadrage.stakeholders || []).map(function(s) { return s.role; }).filter(Boolean);
136
+ if (roles.length === 0) roles = ['Administrateur', 'Responsable', 'Contributeur', 'Lecteur'];
137
+ spec.permissions.forEach(function(permDef) {
138
+ var code = permDef.code || '';
139
+ var raw = code.split('.').pop(); // "Module.Entity.Read" → "Read"
140
+ var action = actionMap[raw.toLowerCase()] || raw;
141
+ // Assign to first role (admin) by default — better than empty
142
+ if (roles[0]) {
143
+ var key = roles[0] + '|' + action;
144
+ if (normalized.indexOf(key) === -1) normalized.push(key);
145
+ }
146
+ });
147
+ } else {
148
+ // Format B/C: {role, permissions: [...]}
149
+ spec.permissions.forEach(function(entry) {
150
+ var role = entry.role || entry.name || '';
151
+ if (!role) return; // Skip Format E (empty role)
152
+ var perms = entry.permissions || entry.actions || [];
153
+ if (perms.length === 0 && entry.permissionPattern && entry.permissionPattern.endsWith('*')) {
154
+ ['Consulter', 'Créer', 'Modifier', 'Supprimer', 'Valider', 'Exporter'].forEach(function(a) {
155
+ normalized.push(role + '|' + a);
156
+ });
157
+ return;
158
+ }
159
+ perms.forEach(function(perm) {
160
+ if (typeof perm !== 'string') return;
161
+ var raw = perm.split(':')[0]; // "Read:all" → "Read"
162
+ if (raw.indexOf('.') !== -1) raw = raw.split('.').pop(); // "Module.Entity.Read" → "Read"
163
+ var action = actionMap[raw.toLowerCase()] || raw;
164
+ var key = role + '|' + action;
165
+ if (normalized.indexOf(key) === -1) normalized.push(key);
166
+ });
167
+ });
168
+ }
169
+ spec.permissions = normalized;
170
+ });
171
+
108
172
  // Detect if modules use section-level specs (hierarchical mode)
109
173
  function hasHierarchicalSpecs(mod) {
110
174
  return (mod.anticipatedSections || []).some(function(s) {
@@ -210,6 +210,11 @@ function renderModuleSpecSection(mod) {
210
210
  }
211
211
 
212
212
  function renderUseCase(code, uc, index) {
213
+ var preconditions = uc.preconditions || [];
214
+ var postconditions = uc.postconditions || [];
215
+ var errorScenarios = uc.errorScenarios || [];
216
+ var description = uc.description || '';
217
+
213
218
  return `
214
219
  <div class="uc-item">
215
220
  <div class="uc-header">
@@ -220,8 +225,12 @@ function renderUseCase(code, uc, index) {
220
225
  </div>
221
226
  </div>
222
227
  <div class="uc-actors"><div class="uc-actor">${escapeHtml(uc.actor)}</div></div>
223
- ${uc.steps ? `<div class="uc-detail-label">Déroulement</div><div class="uc-detail">${escapeHtml(uc.steps).replace(/\n/g, '<br>')}</div>` : ''}
224
- ${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>` : ''}
228
+ ${description ? `<div class="uc-detail-label">Description</div><div class="uc-detail" style="font-style:italic;color:var(--text-muted);">${escapeHtml(description)}</div>` : ''}
229
+ ${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>` : ''}
230
+ ${uc.steps ? `<div class="uc-detail-label">Déroulement principal</div><div class="uc-detail">${escapeHtml(uc.steps).replace(/\n/g, '<br>')}</div>` : ''}
231
+ ${uc.alternative ? `<div class="uc-detail-label">Scénarios alternatifs</div><div class="uc-detail" style="color:var(--warning);">${escapeHtml(uc.alternative)}</div>` : ''}
232
+ ${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>` : ''}
233
+ ${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>` : ''}
225
234
  <div style="padding:0.5rem 0.75rem;border-top:1px solid var(--border);background:var(--bg-input);border-radius:0 0 8px 8px;">
226
235
  <label style="font-size:0.75rem;color:var(--text-muted);display:block;margin-bottom:0.25rem;">Commentaire / Feedback :</label>
227
236
  <textarea class="form-textarea" placeholder="Ajouter un commentaire sur ce cas d'utilisation..."
@@ -314,15 +323,16 @@ function renderEntity(code, ent, index) {
314
323
  </div>
315
324
  ${(ent.attributes || []).length > 0 ? `
316
325
  <table class="attr-table">
317
- <thead><tr><th>Information</th><th>Description</th></tr></thead>
326
+ <thead><tr><th>Information</th><th style="width:100px;">Type</th><th>Description</th></tr></thead>
318
327
  <tbody>
319
- ${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('')}
328
+ ${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('')}
320
329
  </tbody>
321
330
  </table>` : ''}
322
331
  <div style="padding:0.3rem 0.75rem;">
323
332
  <button class="add-btn" style="font-size:0.75rem;padding:0.4rem;" onclick="toggleForm('${attrFormId}')">+ Ajouter un attribut</button>
324
333
  <div class="inline-form" id="${attrFormId}">
325
334
  <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>
335
+ <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>
326
336
  <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>
327
337
  <div class="form-actions"><button class="btn" onclick="toggleForm('${attrFormId}')">Annuler</button><button class="btn btn-primary" onclick="addEntityAttribute('${code}',${index})">Ajouter</button></div>
328
338
  </div>
@@ -347,7 +357,7 @@ function addEntity(code) {
347
357
 
348
358
  const attrs = document.getElementById('ent-attrs-' + code).value.split('\n').filter(l => l.trim()).map(l => {
349
359
  const parts = l.split(' - ');
350
- return { name: parts[0]?.trim() || l.trim(), description: parts.slice(1).join(' - ').trim() };
360
+ return { name: parts[0]?.trim() || l.trim(), type: parts[1]?.trim() || 'string', description: parts.slice(2).join(' - ').trim() || parts.slice(1).join(' - ').trim() };
351
361
  });
352
362
  const rels = document.getElementById('ent-rels-' + code).value.split('\n').filter(l => l.trim());
353
363
 
@@ -371,13 +381,14 @@ function removeEntity(code, index) {
371
381
 
372
382
  function addEntityAttribute(code, entityIndex) {
373
383
  var attrName = document.getElementById('attr-name-' + code + '-' + entityIndex);
384
+ var attrType = document.getElementById('attr-type-' + code + '-' + entityIndex);
374
385
  var attrDesc = document.getElementById('attr-desc-' + code + '-' + entityIndex);
375
386
  if (!attrName || !attrName.value.trim()) return;
376
387
  if (!data.moduleSpecs[code]?.entities?.[entityIndex]) return;
377
388
  if (!data.moduleSpecs[code].entities[entityIndex].attributes) {
378
389
  data.moduleSpecs[code].entities[entityIndex].attributes = [];
379
390
  }
380
- data.moduleSpecs[code].entities[entityIndex].attributes.push({ name: attrName.value.trim(), description: (attrDesc?.value || '').trim() });
391
+ data.moduleSpecs[code].entities[entityIndex].attributes.push({ name: attrName.value.trim(), type: (attrType?.value || 'string').trim(), description: (attrDesc?.value || '').trim() });
381
392
  renderAllModuleSpecs();
382
393
  autoSave();
383
394
  }
@@ -488,13 +499,68 @@ function toggleWireframeView(wireframeId, view) {
488
499
  }
489
500
  }
490
501
 
502
+ // Normalize permissions array: handles 3 formats:
503
+ // Format 1 (string): "Role|Action" → already correct
504
+ // Format 2 (object): {role, permissions: ["Read:all", "Create:all", ...]}
505
+ // Format 3 (object): {role, permissions: ["Module.Entity.Read", ...]}
506
+ function normalizePermissions(permsArray) {
507
+ if (!permsArray || permsArray.length === 0) return [];
508
+ // If first element is a string with '|', already normalized
509
+ if (typeof permsArray[0] === 'string' && permsArray[0].includes('|')) return permsArray;
510
+ // If first element is a string without '|', return as-is (unknown format)
511
+ if (typeof permsArray[0] === 'string') return permsArray;
512
+ // Format 2/3: objects with {role, permissions[]}
513
+ const actionMap = {
514
+ 'read': 'Consulter', 'create': 'Créer', 'update': 'Modifier',
515
+ 'delete': 'Supprimer', 'approve': 'Valider', 'validate': 'Valider',
516
+ 'export': 'Exporter', 'admin': 'Administrer', 'import': 'Importer',
517
+ 'viewsalary': 'Consulter', 'assignteam': 'Modifier', 'cancel': 'Supprimer'
518
+ };
519
+ const normalized = [];
520
+ permsArray.forEach(function(entry) {
521
+ if (typeof entry !== 'object' || !entry) return;
522
+ var role = entry.role || entry.name || '';
523
+ if (!role) return;
524
+ var perms = entry.permissions || entry.actions || [];
525
+ if (perms.length === 0 && entry.permissionPattern && entry.permissionPattern.endsWith('*')) {
526
+ // Wildcard: expand to all standard actions
527
+ ['Consulter', 'Créer', 'Modifier', 'Supprimer', 'Valider', 'Exporter'].forEach(function(a) {
528
+ normalized.push(role + '|' + a);
529
+ });
530
+ return;
531
+ }
532
+ perms.forEach(function(perm) {
533
+ var action = '';
534
+ if (typeof perm === 'string') {
535
+ // "Read:all" → extract "Read"
536
+ var raw = perm.split(':')[0];
537
+ // "Module.Entity.Read" → extract "Read"
538
+ if (raw.includes('.')) raw = raw.split('.').pop();
539
+ action = actionMap[raw.toLowerCase()] || raw;
540
+ }
541
+ if (action) {
542
+ var key = role + '|' + action;
543
+ if (normalized.indexOf(key) === -1) normalized.push(key);
544
+ }
545
+ });
546
+ });
547
+ return normalized;
548
+ }
549
+
491
550
  function getPermRoles() {
492
- // Extract roles from actual permission data (handles English role names from JSON data)
551
+ // Extract roles from actual permission data (handles multiple formats)
493
552
  const rolesFromPerms = [];
494
553
  const seen = new Set();
495
554
  data.modules.forEach(m => {
496
- (data.moduleSpecs[m.code]?.permissions || []).forEach(p => {
497
- const role = p.split('|')[0];
555
+ var perms = data.moduleSpecs[m.code]?.permissions || [];
556
+ // Handle object format directly for role extraction
557
+ perms.forEach(p => {
558
+ var role = '';
559
+ if (typeof p === 'string') {
560
+ role = p.split('|')[0];
561
+ } else if (typeof p === 'object' && p) {
562
+ role = p.role || p.name || '';
563
+ }
498
564
  if (role && !seen.has(role)) { seen.add(role); rolesFromPerms.push(role); }
499
565
  });
500
566
  });
@@ -517,7 +583,9 @@ function renderPermissionGrid(code) {
517
583
  const baseRolesCount = roles.length - (data.customRoles || []).length;
518
584
  const baseActionsCount = 6;
519
585
 
520
- const perms = data.moduleSpecs[code]?.permissions || [];
586
+ // Normalize permissions to "Role|Action" format (handles object and string formats)
587
+ const rawPerms = data.moduleSpecs[code]?.permissions || [];
588
+ const perms = normalizePermissions(rawPerms);
521
589
 
522
590
  return `
523
591
  <table class="mock-table" style="background:var(--bg-card);border-radius:8px;overflow:hidden;">
@@ -722,7 +790,8 @@ function renderModuleMockups(code) {
722
790
  }) : [];
723
791
 
724
792
  // Priority 1: HTML mockups from screens[] specs (wireframes NOT shown when screens exist)
725
- if (screens.length > 0) {
793
+ var hasResources = screens.some(function(s) { return (s.resources || []).length > 0; });
794
+ if (screens.length > 0 && hasResources) {
726
795
  var html = '';
727
796
  if (typeof renderScreenMockups === 'function') {
728
797
  html = renderScreenMockups(code);
@@ -70,9 +70,9 @@ function renderDataModel() {
70
70
  ${ent.description ? `<div class="dm-entity-desc">${escapeHtml(ent.description)}</div>` : ''}
71
71
  ${attrs.length > 0 ? `
72
72
  <table class="dm-attr-table">
73
- <thead><tr><th>Champ</th><th>Description</th></tr></thead>
73
+ <thead><tr><th>Champ</th><th style="width:100px;">Type</th><th>Description</th></tr></thead>
74
74
  <tbody>
75
- ${attrs.map(a => `<tr><td class="dm-attr-name">${escapeHtml(a.name)}</td><td class="dm-attr-desc">${escapeHtml(a.description || '')}</td></tr>`).join('')}
75
+ ${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('')}
76
76
  </tbody>
77
77
  </table>` : ''}
78
78
  ${rels.length > 0 ? `
@@ -13,13 +13,16 @@ function renderScreenMockups(code) {
13
13
 
14
14
  return screens.map(function(screen, si) {
15
15
  var resources = screen.resources || [];
16
+ var content = resources.length > 0
17
+ ? resources.map(function(res, ri) {
18
+ return renderResourceMockup(code, screen.sectionCode, res, ri);
19
+ }).join('')
20
+ : '<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>';
16
21
  return '<div class="screen-section" id="screen-' + code + '-' + screen.sectionCode + '" style="margin-bottom:2rem;">' +
17
22
  '<h3 style="color:var(--text-bright);font-size:1rem;margin-bottom:1rem;">' +
18
23
  '<span style="color:var(--accent);">&#9656;</span> ' + escapeHtml(screen.sectionLabel || screen.sectionCode) +
19
24
  '</h3>' +
20
- resources.map(function(res, ri) {
21
- return renderResourceMockup(code, screen.sectionCode, res, ri);
22
- }).join('') +
25
+ content +
23
26
  '</div>';
24
27
  }).join('');
25
28
  }
@@ -24,6 +24,24 @@ function renderMermaidDiagrams() {
24
24
  }
25
25
  }
26
26
 
27
+ // 1b. MCD (Modèle Conceptuel de Données) — after ERD
28
+ if (diagrams.mcd) {
29
+ var erdContainer = document.getElementById('dataModelContainer');
30
+ if (erdContainer) {
31
+ var mcdDiv = document.createElement('div');
32
+ mcdDiv.className = 'diagram-container diagram-mcd';
33
+ mcdDiv.innerHTML =
34
+ '<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>' +
35
+ '<div class="mermaid">' + escapeHtml(diagrams.mcd) + '</div>';
36
+ var erdDiv = erdContainer.querySelector('.diagram-erd');
37
+ if (erdDiv && erdDiv.nextSibling) {
38
+ erdContainer.insertBefore(mcdDiv, erdDiv.nextSibling);
39
+ } else {
40
+ erdContainer.appendChild(mcdDiv);
41
+ }
42
+ }
43
+ }
44
+
27
45
  // 2. State machine diagrams — inject in module spec sections or consol-datamodel
28
46
  if (diagrams.stateMachines && Object.keys(diagrams.stateMachines).length > 0) {
29
47
  const smContainer = document.getElementById('dataModelContainer');
@@ -46,6 +64,34 @@ function renderMermaidDiagrams() {
46
64
  }
47
65
  }
48
66
 
67
+ // 2b. Use Case diagrams — inject in each module's UC tab
68
+ if (diagrams.useCases && Object.keys(diagrams.useCases).length > 0) {
69
+ Object.entries(diagrams.useCases).forEach(function(entry) {
70
+ var moduleCode = entry[0];
71
+ var def = entry[1];
72
+ var ucTab = document.getElementById('tab-' + moduleCode + '-uc');
73
+ if (!ucTab) {
74
+ // Try kebab-case version
75
+ var kebab = moduleCode.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
76
+ ucTab = document.getElementById('tab-' + kebab + '-uc');
77
+ }
78
+ if (ucTab) {
79
+ var ucDiagramDiv = document.createElement('div');
80
+ ucDiagramDiv.className = 'diagram-container diagram-usecase';
81
+ ucDiagramDiv.innerHTML =
82
+ '<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>' +
83
+ '<div class="mermaid">' + escapeHtml(def) + '</div>';
84
+ // Insert after the first paragraph (description text)
85
+ var firstP = ucTab.querySelector('p');
86
+ if (firstP && firstP.nextSibling) {
87
+ ucTab.insertBefore(ucDiagramDiv, firstP.nextSibling);
88
+ } else {
89
+ ucTab.insertBefore(ucDiagramDiv, ucTab.firstChild);
90
+ }
91
+ }
92
+ });
93
+ }
94
+
49
95
  // 3. Sequence diagrams in consol-flows
50
96
  if (diagrams.sequences && Object.keys(diagrams.sequences).length > 0) {
51
97
  var flowsContainer = document.getElementById('consolFlowsContainer');
@@ -36,10 +36,12 @@ moduleSpecs[moduleCode] = {
36
36
  name: br.name || br.id || "",
37
37
  sectionCode: br.sectionCode || "",
38
38
  category: br.category || "",
39
+ severity: br.severity || "",
39
40
  statement: br.statement || br.description || "",
40
- example: (br.examples || []).map(e =>
41
+ example: (br.examples || (br.example ? [br.example] : [])).map(e =>
41
42
  typeof e === 'string' ? e : ((e.input || "") + " → " + (e.expected || ""))
42
- ).join("; ")
43
+ ).join("; "),
44
+ domainSpecific: br.domainSpecific || false
43
45
  })),
44
46
  // ENTITY SAFETY NET: map fields[] → attributes[] if agent deviated
45
47
  entities: (mod.entities?.entities || []).map(ent => ({
@@ -133,6 +133,8 @@ for rendering in the HTML viewer.
133
133
  ```javascript
134
134
  mermaidDiagrams: {
135
135
  erd: consolidation.mermaidDiagrams?.erd || null, // ERD string or null
136
+ mcd: consolidation.mermaidDiagrams?.mcd || null, // MCD string or null
137
+ useCases: consolidation.mermaidDiagrams?.useCases || {}, // {moduleCode: mermaid_def}
136
138
  stateMachines: consolidation.mermaidDiagrams?.stateMachines || {}, // {entity: mermaid_def}
137
139
  sequences: consolidation.mermaidDiagrams?.sequences || {} // {flowName: mermaid_def}
138
140
  }
@@ -122,6 +122,7 @@ moduleSpecs[moduleCode] = {
122
122
  name: uc.name || uc.title || uc.id || "", // Format A: "name", Format B: "title" or "id"
123
123
  sectionCode: uc.sectionCode || "",
124
124
  actor: uc.primaryActor || uc.actor || "", // Format A: "primaryActor", Format B: "actor"
125
+ description: uc.description || "", // Optional description field
125
126
  // SAFETY NET: steps may be string[] or object[] ({step, action})
126
127
  steps: (uc.mainScenario || uc.steps || []).map(s =>
127
128
  typeof s === 'string' ? s : (s.action || s.description || "")
@@ -130,7 +131,10 @@ moduleSpecs[moduleCode] = {
130
131
  (a.name || a.trigger || "") + ": " + (a.steps || a.actions || []).map(s =>
131
132
  typeof s === 'string' ? s : (s.action || s.description || "")
132
133
  ).join(", ")
133
- ).join("\n")
134
+ ).join("\n"),
135
+ preconditions: uc.preconditions || [], // Array of precondition objects or strings
136
+ postconditions: uc.postconditions || [], // Array of postcondition objects or strings
137
+ errorScenarios: uc.errorScenarios || [] // Array of error scenario objects
134
138
  })),
135
139
  businessRules: rawBRs.map(br => ({
136
140
  name: br.name || br.id || "",
@@ -166,19 +170,28 @@ The HTML uses `"Role|Action"` format (e.g. `"RH Admin|Consulter"`):
166
170
 
167
171
  ```javascript
168
172
  // Input: permissions object from flat file (mod.permissions), NOT moduleFeature
169
- function buildPermissionKeys(permissionsData) {
173
+ // lang: optional language override ('fr' or 'en'), defaults to 'fr'
174
+ function buildPermissionKeys(permissionsData, lang = 'fr') {
170
175
  const keys = [];
171
- const actionMap = {
172
- Read: "Consulter", Create: "Créer", Update: "Modifier",
173
- Delete: "Supprimer", Validate: "Valider", Export: "Exporter",
174
- Submit: "Valider", Import: "Créer",
175
- read: "Consulter", create: "Créer", update: "Modifier",
176
- delete: "Supprimer", validate: "Valider", export: "Exporter",
177
- submit: "Valider", import: "Créer"
178
- };
176
+
177
+ // Bilingual actionMap based on language parameter
178
+ const actionMap = lang === 'en'
179
+ ? {
180
+ read: 'Read', create: 'Create', update: 'Update', delete: 'Delete',
181
+ approve: 'Approve', export: 'Export', validate: 'Validate', admin: 'Admin',
182
+ Read: 'Read', Create: 'Create', Update: 'Update', Delete: 'Delete',
183
+ Approve: 'Approve', Export: 'Export', Validate: 'Validate', Admin: 'Admin',
184
+ submit: 'Validate', import: 'Create', Submit: 'Validate', Import: 'Create'
185
+ }
186
+ : {
187
+ read: 'Consulter', create: 'Créer', update: 'Modifier', delete: 'Supprimer',
188
+ approve: 'Valider', export: 'Exporter', validate: 'Valider', admin: 'Administrer',
189
+ Read: 'Consulter', Create: 'Créer', Update: 'Modifier', Delete: 'Supprimer',
190
+ Approve: 'Valider', Export: 'Exporter', Validate: 'Valider', Admin: 'Administrer',
191
+ submit: 'Valider', import: 'Créer', Submit: 'Valider', Import: 'Créer'
192
+ };
179
193
 
180
194
  const matrix = permissionsData?.matrix || permissionsData?.permissionMatrix;
181
- if (!matrix) return keys;
182
195
 
183
196
  // Format A: matrix is array of { role, permissions[] } (ba-009+ format)
184
197
  if (Array.isArray(matrix)) {
@@ -202,25 +215,79 @@ function buildPermissionKeys(permissionsData) {
202
215
  }
203
216
 
204
217
  // Format B: legacy matrix.roleAssignments[] format
205
- (matrix.roleAssignments || []).forEach(ra => {
206
- (ra.permissions || []).forEach(permPath => {
207
- const action = permPath.split(".").pop();
208
- if (action === "*") {
209
- Object.values(actionMap).forEach(uiAction => {
218
+ if (matrix && matrix.roleAssignments) {
219
+ (matrix.roleAssignments || []).forEach(ra => {
220
+ (ra.permissions || []).forEach(permPath => {
221
+ const action = permPath.split(".").pop();
222
+ if (action === "*") {
223
+ Object.values(actionMap).forEach(uiAction => {
224
+ const key = ra.role + "|" + uiAction;
225
+ if (!keys.includes(key)) keys.push(key);
226
+ });
227
+ } else {
228
+ const uiAction = actionMap[action] || action;
210
229
  const key = ra.role + "|" + uiAction;
211
230
  if (!keys.includes(key)) keys.push(key);
231
+ }
232
+ });
233
+ });
234
+ return keys;
235
+ }
236
+
237
+ // Format C: permissions.json from step-03 with top-level roles[]
238
+ if (keys.length === 0 && permissionsData?.roles) {
239
+ const actionMap_C = lang === 'en'
240
+ ? { read: 'Read', create: 'Create', update: 'Update', delete: 'Delete', approve: 'Approve', export: 'Export', validate: 'Validate', admin: 'Admin' }
241
+ : { read: 'Consulter', create: 'Créer', update: 'Modifier', delete: 'Supprimer', approve: 'Valider', export: 'Exporter', validate: 'Valider', admin: 'Administrer' };
242
+
243
+ permissionsData.roles.forEach(entry => {
244
+ const role = entry.role || entry.name || '';
245
+ (entry.permissions || entry.actions || []).forEach(perm => {
246
+ const action = typeof perm === 'string' ? perm.split('.').pop().toLowerCase() : (perm.action || '').toLowerCase();
247
+ const uiAction = actionMap_C[action] || action;
248
+ const key = role + '|' + uiAction;
249
+ if (role && !keys.includes(key)) keys.push(key);
250
+ });
251
+ // Handle wildcard patterns like "HumanResources.*"
252
+ if ((entry.permissionPattern || '').endsWith('*')) {
253
+ Object.values(actionMap_C).forEach(a => {
254
+ const key = (entry.role || entry.name) + '|' + a;
255
+ if (!keys.includes(key)) keys.push(key);
212
256
  });
213
- } else {
214
- const uiAction = actionMap[action] || action;
215
- keys.push(ra.role + "|" + uiAction);
216
257
  }
217
258
  });
218
- });
259
+ }
260
+
261
+ // Format D: permissionMatrix.roleAssignments[]
262
+ if (keys.length === 0 && permissionsData?.permissionMatrix?.roleAssignments) {
263
+ const actionMap_D = lang === 'en'
264
+ ? { read: 'Read', create: 'Create', update: 'Update', delete: 'Delete', approve: 'Approve', export: 'Export', validate: 'Validate', admin: 'Admin' }
265
+ : { read: 'Consulter', create: 'Créer', update: 'Modifier', delete: 'Supprimer', approve: 'Valider', export: 'Exporter', validate: 'Valider', admin: 'Administrer' };
266
+
267
+ permissionsData.permissionMatrix.roleAssignments.forEach(entry => {
268
+ const role = entry.role || '';
269
+ (entry.permissions || []).forEach(perm => {
270
+ if (typeof perm === 'string' && perm.endsWith('.*')) {
271
+ // Wildcard: expand to all standard actions
272
+ Object.values(actionMap_D).forEach(a => {
273
+ const key = role + '|' + a;
274
+ if (!keys.includes(key)) keys.push(key);
275
+ });
276
+ } else {
277
+ const action = typeof perm === 'string' ? perm.split('.').pop().toLowerCase() : '';
278
+ const uiAction = actionMap_D[action] || action;
279
+ const key = role + '|' + uiAction;
280
+ if (role && action && !keys.includes(key)) keys.push(key);
281
+ }
282
+ });
283
+ });
284
+ }
285
+
219
286
  return keys;
220
287
  }
221
288
  ```
222
289
 
223
- > **Language handling:** The actionMap should use `metadata.language` to select the correct labels. For `fr`: Consulter/Créer/Modifier/Supprimer. For `en`: Read/Create/Update/Delete. Default to English if language is unknown.
290
+ > **Language handling:** The function accepts an optional `lang` parameter ('fr' or 'en'). Defaults to 'fr' (French). For English labels: Read/Create/Update/Delete/Export/Validate. For French: Consulter/Créer/Modifier/Supprimer/Exporter/Valider.
224
291
 
225
292
  ### Frequency Mapping
226
293
 
@@ -64,6 +64,12 @@ This step is NEVER blocking — ASCII-only wireframes are acceptable.
64
64
  - Wireframe fields use format/content (NOT mockupFormat/mockup)
65
65
  - Per-module: useCases/businessRules/entities count must match source (empty when source has data = BUG)
66
66
  - dependencies[] must be present (even if empty) to prevent HTML crashes
67
+ - **Permissions:** `buildPermissionKeys()` supports 4 formats:
68
+ - **Format A:** `permissions.matrix[]` with `{role, permissions[]}`
69
+ - **Format B:** `permissions.permissionMatrix.roleAssignments[]`
70
+ - **Format C:** `permissions.roles[]` with `{role, permissions[]}`
71
+ - **Format D:** `permissions.permissionMatrix.roleAssignments[]` with wildcard expansion
72
+ - Pass optional `lang` parameter ('fr'|'en') for bilingual labels. Defaults to 'fr'.
67
73
 
68
74
  ## NEXT STEP
69
75
 
@@ -8,7 +8,7 @@ model: opus
8
8
 
9
9
  ## YOUR TASK
10
10
 
11
- Run 9 blocking validations on the generated HTML file and display the completion summary.
11
+ Run 16 blocking validations on the generated HTML file and display the completion summary.
12
12
 
13
13
  ---
14
14
 
@@ -105,11 +105,97 @@ IF "modules" is missing:
105
105
  BLOCKING_ERROR("FEATURE_DATA.modules missing — page will crash")
106
106
  ```
107
107
 
108
- > **IF any check fails:** fix the issue and re-run the failing step before completing.
108
+ > **IF any check fails (1-9):** fix the issue and re-run the failing step before completing.
109
+
110
+ ### 2. Content Quality Validations (NEW)
111
+
112
+ **Check 10 — Language coherence:**
113
+ ```
114
+ Read FEATURE_DATA.metadata.language (or config.json language field)
115
+ IF language is set (e.g., "fr"):
116
+ FOR each module in FEATURE_DATA.modules:
117
+ const spec = FEATURE_DATA.moduleSpecs[module.code]
118
+
119
+ // Check UC names
120
+ FOR each uc in spec.useCases:
121
+ IF language == "fr" AND uc.name matches /^(Create|Update|Delete|View|List|Manage|Submit|Approve|Reject|Export)\b/:
122
+ WARNING("Module {module.code}: UC '{uc.name}' appears English (expected French)")
123
+
124
+ // Check BR statements
125
+ FOR each br in spec.businessRules:
126
+ IF language == "fr" AND br.statement matches /\b(must|should|cannot|when|if the|is required)\b/i:
127
+ WARNING("Module {module.code}: BR '{br.name}' statement appears English (expected French)")
128
+
129
+ Count total warnings. IF > 20% of items are in wrong language:
130
+ WARNING("⚠ Significant language mismatch: {count} items in wrong language")
131
+ ```
132
+
133
+ **Check 11 — Business rules have examples:**
134
+ ```
135
+ FOR each module in FEATURE_DATA.modules:
136
+ const brs = FEATURE_DATA.moduleSpecs[module.code].businessRules || []
137
+ IF brs.length === 0: SKIP
138
+ const withExamples = brs.filter(br => br.example && br.example.trim() !== '')
139
+ const rate = withExamples.length / brs.length
140
+ IF rate < 0.5:
141
+ WARNING("Module {module.code}: only {withExamples.length}/{brs.length} BRs have examples ({rate*100}%)")
142
+ ```
143
+
144
+ **Check 12 — Entity attributes have type field:**
145
+ ```
146
+ FOR each module in FEATURE_DATA.modules:
147
+ const entities = FEATURE_DATA.moduleSpecs[module.code].entities || []
148
+ FOR each entity in entities:
149
+ const attrs = entity.attributes || []
150
+ const withType = attrs.filter(a => a.type && a.type.trim() !== '' && a.type !== 'string')
151
+ IF attrs.length > 3 AND withType.length === 0:
152
+ WARNING("Module {module.code}: entity '{entity.name}' — all {attrs.length} attributes default to 'string' (types may be missing)")
153
+ ```
154
+
155
+ **Check 13 — Permissions populated per module:**
156
+ ```
157
+ FOR each module in FEATURE_DATA.modules:
158
+ const perms = FEATURE_DATA.moduleSpecs[module.code].permissions || []
159
+ IF perms.length === 0:
160
+ WARNING("Module {module.code}: 0 permissions — permission grid will be empty")
161
+ ```
162
+
163
+ **Check 14 — Mockups not empty (screens with actual resources):**
164
+ ```
165
+ FOR each module in FEATURE_DATA.modules:
166
+ const screens = FEATURE_DATA.moduleSpecs[module.code].screens || []
167
+ const wireframes = EMBEDDED_ARTIFACTS.wireframes?.[module.code] || []
168
+ IF screens.length === 0 AND wireframes.length === 0:
169
+ WARNING("Module {module.code}: no screens and no wireframes — Mockups tab will show 'Aucune maquette'")
170
+ ELSE IF screens.length > 0:
171
+ const totalResources = screens.reduce((sum, s) => sum + (s.resources || []).length, 0)
172
+ IF totalResources === 0:
173
+ WARNING("Module {module.code}: has screens[] but 0 resources — mockups will appear empty despite data existing")
174
+ ```
175
+
176
+ **Check 15 — MCD diagram present (multi-module projects):**
177
+ ```
178
+ const diagrams = FEATURE_DATA.consolidation?.mermaidDiagrams || {}
179
+ IF FEATURE_DATA.modules.length > 1:
180
+ IF !diagrams.erd:
181
+ WARNING("No ERD diagram — data model section will have no visual")
182
+ IF !diagrams.mcd:
183
+ WARNING("No MCD diagram — conceptual model not generated")
184
+ ```
185
+
186
+ **Check 16 — Use Case diagrams present:**
187
+ ```
188
+ const ucDiagrams = diagrams.useCases || {}
189
+ const totalUCs = Object.values(FEATURE_DATA.moduleSpecs).reduce((sum, s) => sum + (s.useCases?.length || 0), 0)
190
+ IF Object.keys(ucDiagrams).length === 0 AND totalUCs > 0:
191
+ WARNING("No use case diagrams generated — UC tabs will have no UML visual")
192
+ ```
193
+
194
+ > **Checks 10-16 produce WARNINGs (non-blocking).** Display all warnings in the completion summary so the user is aware of potential quality issues.
109
195
 
110
196
  ---
111
197
 
112
- ### 2. Display Completion Summary
198
+ ### 3. Display Completion Summary
113
199
 
114
200
  ```
115
201
  ══════════════════════════════════════════════════════════════
@@ -131,5 +217,8 @@ Modules: {count} ({names})
131
217
  4. If approved: /business-analyse-handoff then /business-analyse-develop to begin development
132
218
  5. If corrections needed: export ba-review.json from the HTML, then run /business-analyse-review
133
219
 
220
+ Quality Warnings: {warning_count}
221
+ {FOR each warning: display warning message}
222
+
134
223
  ══════════════════════════════════════════════════════════════
135
224
  ```