@atlashub/smartstack-cli 4.75.0 → 4.76.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 (54) 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/skills/apex/SKILL.md +2 -2
  5. package/templates/skills/apex/references/checks/frontend-checks.sh +26 -0
  6. package/templates/skills/apex/references/checks/seed-checks.sh +47 -7
  7. package/templates/skills/apex/references/core-seed-data.md +20 -18
  8. package/templates/skills/apex/references/post-checks.md +18 -1
  9. package/templates/skills/apex/references/smartstack-api.md +4 -4
  10. package/templates/skills/apex/references/smartstack-frontend.md +1 -1
  11. package/templates/skills/apex/references/smartstack-layers.md +6 -6
  12. package/templates/skills/apex/steps/step-00-init.md +1 -1
  13. package/templates/skills/apex/steps/step-03b-layer1-seed.md +26 -0
  14. package/templates/skills/apex/steps/step-03d-layer3-frontend.md +124 -2
  15. package/templates/skills/apex/steps/step-04-examine.md +163 -0
  16. package/templates/skills/apex-verify/SKILL.md +110 -0
  17. package/templates/skills/apex-verify/references/audit-rules.md +50 -0
  18. package/templates/skills/apex-verify/steps/step-00-init.md +119 -0
  19. package/templates/skills/apex-verify/steps/step-01-nav-audit.md +92 -0
  20. package/templates/skills/apex-verify/steps/step-02-crud-audit.md +127 -0
  21. package/templates/skills/apex-verify/steps/step-03-perm-audit.md +119 -0
  22. package/templates/skills/apex-verify/steps/step-04-route-audit.md +98 -0
  23. package/templates/skills/apex-verify/steps/step-05-report.md +110 -0
  24. package/templates/skills/application/templates-frontend.md +2 -2
  25. package/templates/skills/business-analyse/SKILL.md +3 -3
  26. package/templates/skills/business-analyse/_shared.md +37 -0
  27. package/templates/skills/business-analyse/references/03-json-schemas.md +11 -3
  28. package/templates/skills/business-analyse/references/03-post-check-validation.md +64 -0
  29. package/templates/skills/business-analyse/references/canonical-json-formats.md +7 -3
  30. package/templates/skills/business-analyse/references/robustness-checks.md +1 -1
  31. package/templates/skills/business-analyse/references/validation-checklist.md +5 -5
  32. package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +15 -4
  33. package/templates/skills/business-analyse/steps/step-03-specify.md +162 -4
  34. package/templates/skills/business-analyse/steps/step-04-consolidate.md +211 -1
  35. package/templates/skills/business-analyse-handoff/references/agent-handoff-transform-prompt.md +3 -0
  36. package/templates/skills/business-analyse-html/html/ba-interactive.html +198 -16
  37. package/templates/skills/business-analyse-html/html/src/scripts/01-data-init.js +64 -0
  38. package/templates/skills/business-analyse-html/html/src/scripts/05-render-specs.js +80 -11
  39. package/templates/skills/business-analyse-html/html/src/scripts/06-render-consolidation.js +2 -2
  40. package/templates/skills/business-analyse-html/html/src/scripts/06-render-mockups.js +6 -3
  41. package/templates/skills/business-analyse-html/html/src/scripts/12-render-diagrams.js +46 -0
  42. package/templates/skills/business-analyse-html/references/02-feature-data-building.md +4 -2
  43. package/templates/skills/business-analyse-html/references/data-build.md +2 -0
  44. package/templates/skills/business-analyse-html/references/data-mapping.md +88 -21
  45. package/templates/skills/business-analyse-html/steps/step-02-build-data.md +6 -0
  46. package/templates/skills/business-analyse-html/steps/step-04-verify.md +92 -3
  47. package/templates/skills/business-analyse-quick/SKILL.md +807 -0
  48. package/templates/skills/{sketch → business-analyse-quick}/references/domain-heuristics.md +59 -3
  49. package/templates/skills/business-analyse-quick/references/prd-schema.md +268 -0
  50. package/templates/skills/business-analyse-review/references/review-data-mapping.md +6 -0
  51. package/templates/skills/dev-start/SKILL.md +7 -7
  52. package/templates/skills/sketch/SKILL.md +15 -153
  53. package/templates/skills/ui-components/SKILL.md +1 -1
  54. package/templates/skills/ui-components/patterns/data-table.md +1 -1
@@ -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');
@@ -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
  }