@atlashub/smartstack-cli 4.74.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 (121) hide show
  1. package/dist/index.js +152 -31
  2. package/dist/index.js.map +1 -1
  3. package/dist/mcp-entry.mjs +14 -3
  4. package/dist/mcp-entry.mjs.map +1 -1
  5. package/package.json +1 -1
  6. package/templates/agents/ba-reader.md +17 -15
  7. package/templates/agents/ba-writer.md +49 -51
  8. package/templates/skills/apex/SKILL.md +2 -2
  9. package/templates/skills/apex/_shared.md +1 -1
  10. package/templates/skills/apex/references/checks/backend-checks.sh +21 -7
  11. package/templates/skills/apex/references/checks/frontend-checks.sh +26 -0
  12. package/templates/skills/apex/references/checks/infrastructure-checks.sh +47 -10
  13. package/templates/skills/apex/references/checks/seed-checks.sh +47 -7
  14. package/templates/skills/apex/references/core-seed-data.md +20 -18
  15. package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +3 -0
  16. package/templates/skills/apex/references/post-checks.md +23 -3
  17. package/templates/skills/apex/references/smartstack-api.md +4 -4
  18. package/templates/skills/apex/references/smartstack-frontend.md +54 -8
  19. package/templates/skills/apex/references/smartstack-layers.md +6 -6
  20. package/templates/skills/apex/steps/step-00-init.md +75 -1
  21. package/templates/skills/apex/steps/step-03-execute.md +16 -4
  22. package/templates/skills/apex/steps/step-03b-layer1-seed.md +65 -6
  23. package/templates/skills/apex/steps/step-03c-layer2-backend.md +50 -5
  24. package/templates/skills/apex/steps/step-03d-layer3-frontend.md +226 -4
  25. package/templates/skills/apex/steps/step-04-examine.md +163 -0
  26. package/templates/skills/apex-verify/SKILL.md +110 -0
  27. package/templates/skills/apex-verify/references/audit-rules.md +50 -0
  28. package/templates/skills/apex-verify/steps/step-00-init.md +119 -0
  29. package/templates/skills/apex-verify/steps/step-01-nav-audit.md +92 -0
  30. package/templates/skills/apex-verify/steps/step-02-crud-audit.md +127 -0
  31. package/templates/skills/apex-verify/steps/step-03-perm-audit.md +119 -0
  32. package/templates/skills/apex-verify/steps/step-04-route-audit.md +98 -0
  33. package/templates/skills/apex-verify/steps/step-05-report.md +110 -0
  34. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +3 -0
  35. package/templates/skills/application/templates-frontend.md +2 -2
  36. package/templates/skills/business-analyse/SKILL.md +17 -3
  37. package/templates/skills/business-analyse/_shared.md +64 -0
  38. package/templates/skills/business-analyse/patterns/suggestion-catalog.md +34 -26
  39. package/templates/skills/business-analyse/questionnaire/01-context.md +13 -9
  40. package/templates/skills/business-analyse/questionnaire/02-stakeholders-scope.md +20 -27
  41. package/templates/skills/business-analyse/questionnaire.md +86 -9
  42. package/templates/skills/business-analyse/references/03-json-schemas.md +221 -0
  43. package/templates/skills/business-analyse/references/03-post-check-validation.md +208 -0
  44. package/templates/skills/business-analyse/references/03-smartstack-entity-guards.md +32 -0
  45. package/templates/skills/business-analyse/references/04-cross-module-validation.md +95 -0
  46. package/templates/skills/business-analyse/references/04-file-allocation.md +162 -0
  47. package/templates/skills/business-analyse/references/04-naming-audit-checks.md +174 -0
  48. package/templates/skills/business-analyse/references/04-semantic-validation-matrix.md +118 -0
  49. package/templates/skills/business-analyse/references/canonical-json-formats.md +7 -3
  50. package/templates/skills/business-analyse/references/domain-research-playbook.md +234 -0
  51. package/templates/skills/business-analyse/references/entity-sourcing-presentation.md +166 -0
  52. package/templates/skills/business-analyse/references/init-resume-logic.md +70 -0
  53. package/templates/skills/business-analyse/references/module-completeness-challenge.md +174 -0
  54. package/templates/skills/business-analyse/references/multi-app-detection.md +149 -0
  55. package/templates/skills/business-analyse/references/portal-classification.md +52 -0
  56. package/templates/skills/business-analyse/references/robustness-checks.md +1 -1
  57. package/templates/skills/business-analyse/references/validation-checklist.md +35 -6
  58. package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +50 -6
  59. package/templates/skills/business-analyse/steps/step-00-init.md +22 -190
  60. package/templates/skills/business-analyse/steps/step-01-cadrage.md +365 -269
  61. package/templates/skills/business-analyse/steps/step-02-structure.md +98 -20
  62. package/templates/skills/business-analyse/steps/step-03-specify.md +810 -229
  63. package/templates/skills/business-analyse/steps/step-04-consolidate.md +509 -278
  64. package/templates/skills/business-analyse-design/SKILL.md +10 -0
  65. package/templates/skills/business-analyse-design/references/screens-post-check.md +221 -0
  66. package/templates/skills/business-analyse-design/references/screens-type-mapping.md +138 -0
  67. package/templates/skills/business-analyse-design/references/smartcomponents-templates.md +225 -0
  68. package/templates/skills/{business-analyse → business-analyse-design}/references/spec-auto-inference.md +117 -117
  69. package/templates/skills/business-analyse-design/steps/step-01-screens.md +36 -162
  70. package/templates/skills/business-analyse-design/steps/step-02-wireframes.md +8 -7
  71. package/templates/skills/business-analyse-design/steps/step-03-navigation.md +89 -42
  72. package/templates/skills/business-analyse-develop/references/compact-loop.md +9 -0
  73. package/templates/skills/business-analyse-develop/references/handoff-quality-gate.md +132 -0
  74. package/templates/skills/business-analyse-develop/references/prd-v3-transformation.md +326 -0
  75. package/templates/skills/business-analyse-develop/references/report-reconciliation.md +140 -0
  76. package/templates/skills/business-analyse-develop/references/report-template.md +142 -0
  77. package/templates/skills/business-analyse-develop/steps/step-01-task.md +5 -177
  78. package/templates/skills/business-analyse-develop/steps/step-02-execute.md +17 -4
  79. package/templates/skills/business-analyse-develop/steps/step-03-commit.md +6 -2
  80. package/templates/skills/business-analyse-develop/steps/step-04-check.md +6 -0
  81. package/templates/skills/business-analyse-develop/steps/step-05-report.md +3 -269
  82. package/templates/skills/business-analyse-handoff/SKILL.md +10 -0
  83. package/templates/skills/business-analyse-handoff/references/agent-handoff-transform-prompt.md +211 -0
  84. package/templates/skills/business-analyse-handoff/references/context-isolation-pattern.md +47 -0
  85. package/templates/skills/business-analyse-handoff/references/handoff-file-inventory.md +49 -0
  86. package/templates/skills/business-analyse-handoff/references/handoff-global-validation.md +142 -0
  87. package/templates/skills/business-analyse-handoff/references/prd-validation-checks.md +125 -0
  88. package/templates/skills/business-analyse-handoff/references/project-index-update.md +98 -0
  89. package/templates/skills/business-analyse-handoff/steps/step-01-transform.md +9 -160
  90. package/templates/skills/business-analyse-handoff/steps/step-02-export.md +10 -99
  91. package/templates/skills/business-analyse-html/SKILL.md +10 -0
  92. package/templates/skills/business-analyse-html/html/ba-interactive.html +504 -97
  93. package/templates/skills/business-analyse-html/html/src/scripts/01-data-init.js +79 -2
  94. package/templates/skills/business-analyse-html/html/src/scripts/02-navigation.js +6 -46
  95. package/templates/skills/business-analyse-html/html/src/scripts/05-render-specs.js +80 -11
  96. package/templates/skills/business-analyse-html/html/src/scripts/06-render-consolidation.js +2 -2
  97. package/templates/skills/business-analyse-html/html/src/scripts/06-render-mockups.js +94 -36
  98. package/templates/skills/business-analyse-html/html/src/scripts/12-render-diagrams.js +162 -0
  99. package/templates/skills/business-analyse-html/html/src/styles/10-diagrams.css +73 -0
  100. package/templates/skills/business-analyse-html/html/src/template.html +2 -0
  101. package/templates/skills/business-analyse-html/references/02-embedded-artifacts-building.md +144 -0
  102. package/templates/skills/business-analyse-html/references/02-feature-data-building.md +143 -0
  103. package/templates/skills/business-analyse-html/references/02-mapping-tables.md +442 -0
  104. package/templates/skills/business-analyse-html/references/02-normalization-helpers.md +139 -0
  105. package/templates/skills/business-analyse-html/references/02-screen-format-detection.md +283 -0
  106. package/templates/skills/business-analyse-html/references/02-self-check-validation.md +199 -0
  107. package/templates/skills/business-analyse-html/references/data-build.md +24 -1
  108. package/templates/skills/business-analyse-html/references/data-mapping.md +119 -17
  109. package/templates/skills/business-analyse-html/steps/step-02-build-data.md +18 -555
  110. package/templates/skills/business-analyse-html/steps/step-04-verify.md +92 -3
  111. package/templates/skills/business-analyse-quick/SKILL.md +807 -0
  112. package/templates/skills/{sketch → business-analyse-quick}/references/domain-heuristics.md +59 -3
  113. package/templates/skills/business-analyse-quick/references/prd-schema.md +268 -0
  114. package/templates/skills/business-analyse-review/SKILL.md +10 -0
  115. package/templates/skills/business-analyse-review/references/review-data-mapping.md +6 -0
  116. package/templates/skills/business-analyse-status/SKILL.md +8 -0
  117. package/templates/skills/dev-start/SKILL.md +143 -307
  118. package/templates/skills/efcore/SKILL.md +13 -0
  119. package/templates/skills/sketch/SKILL.md +15 -153
  120. package/templates/skills/ui-components/SKILL.md +1 -1
  121. package/templates/skills/ui-components/patterns/data-table.md +1 -1
@@ -52,9 +52,22 @@ if (!data.cadrage.scope || (!data.cadrage.scope.inscope?.length && data.cadrage.
52
52
  outofscope: (gs.outOfScope || []).map(s => typeof s === 'string' ? { name: s, description: '' } : s)
53
53
  };
54
54
  } else {
55
+ function normScopeItem(s) {
56
+ if (typeof s === 'string') return { name: s, description: '' };
57
+ if (s.name) return s;
58
+ var nm = s.feature || s.label || s.title || JSON.stringify(s);
59
+ var parts = [];
60
+ if (s.priority) parts.push(s.priority.toUpperCase());
61
+ if (s.module) parts.push('Module: ' + s.module);
62
+ return { name: nm, description: parts.join(' — ') };
63
+ }
55
64
  data.cadrage.scope = {
56
- inscope: (gs.inScope || []).map(s => typeof s === 'string' ? { name: s, description: '' } : s),
57
- outofscope: (gs.outOfScope || []).map(s => typeof s === 'string' ? { name: s, description: '' } : s)
65
+ inscope: (gs.inScope || []).map(normScopeItem),
66
+ outofscope: (gs.outOfScope || []).map(function(s) {
67
+ if (typeof s === 'string') return { name: s, description: '' };
68
+ if (s.name) return s;
69
+ return { name: s.feature || s.reason || JSON.stringify(s), description: s.reason || '' };
70
+ })
58
71
  };
59
72
  }
60
73
  }
@@ -92,6 +105,70 @@ data.moduleSpecs = data.moduleSpecs || {};
92
105
  });
93
106
  });
94
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
+
95
172
  // Detect if modules use section-level specs (hierarchical mode)
96
173
  function hasHierarchicalSpecs(mod) {
97
174
  return (mod.anticipatedSections || []).some(function(s) {
@@ -149,54 +149,14 @@ function renderModuleNavItem(mod) {
149
149
  var ucCount = (spec.useCases || []).length;
150
150
  var brCount = (spec.businessRules || []).length;
151
151
  var entCount = (spec.entities || []).length;
152
- var sections = (mod.anticipatedSections || []).map(function(s) {
153
- return typeof s === 'string' ? { code: s, name: s, resources: [] } : s;
154
- });
155
- var groupId = 'mod-' + code;
156
- var collapsed = navCollapseState[groupId] === true;
157
-
158
- var html = '<div class="nav-module" data-group-id="' + groupId + '">';
159
-
160
- // Module header (clickable to expand + navigate to module spec)
161
152
  var totalItems = ucCount + brCount + entCount;
162
- html += '<a class="nav-item nav-module-header" onclick="toggleNavGroup(\'' + groupId + '\');showSection(\'module-spec-' + code + '\')" data-section="module-spec-' + code + '">';
163
- html += '<span class="nav-chevron ' + (collapsed ? '' : 'expanded') + '">&#9656;</span> ';
164
- html += (mod.name || mod.code);
165
- if (totalItems > 0) html += ' <span class="nav-badge">' + totalItems + '</span>';
166
- html += '</a>';
167
-
168
- // Children: sections/resources (navigable sub-tree)
169
- // Tab-level items (UC, BR, Données, etc.) are NOT shown here — they are accessed
170
- // via the tab bar in the module content area to avoid redundancy.
171
- html += '<div class="nav-children"' + (collapsed ? ' style="display:none;"' : '') + '>';
172
- if (sections.length > 0) {
173
- sections.forEach(function(section) {
174
- var resources = section.resources || [];
175
- var sectionUCs = (section.useCases || []).length;
176
- var sectionBRs = (section.businessRules || []).length;
177
- var contentCount = sectionUCs + sectionBRs + resources.length;
178
- html += '<div class="nav-section-item">';
179
- html += '<a class="nav-item nav-section-link" onclick="showSection(\'module-spec-' + code + '\');switchTab(\'' + code + '\',\'struct\')" data-section="module-struct-' + code + '-' + section.code + '">';
180
- html += '<span class="nav-icon nav-icon-section">&#9655;</span> ' + escapeHtml(section.code || section.name || '');
181
- if (contentCount > 0) html += ' <span class="nav-badge">' + contentCount + '</span>';
182
- html += '</a>';
183
- if (resources.length > 0) {
184
- html += '<div class="nav-children nav-resources">';
185
- resources.forEach(function(res) {
186
- var resName = typeof res === 'string' ? res : (res.code || res.name || '');
187
- html += '<a class="nav-item nav-resource-link" onclick="showSection(\'module-spec-' + code + '\');switchTab(\'' + code + '\',\'mock\');scrollToMockup(\'' + code + '\',\'' + section.code + '\')">';
188
- html += '<span class="nav-icon nav-icon-resource">&#8226;</span> ' + escapeHtml(resName);
189
- html += '</a>';
190
- });
191
- html += '</div>';
192
- }
193
- html += '</div>';
194
- });
195
- }
196
153
 
197
- html += '</div>'; // nav-children
198
- html += '</div>'; // nav-module
199
- return html;
154
+ // Flat module link — sections/resources are accessible via tabs (Structure, Maquettes)
155
+ return '<a class="nav-item nav-module-header" onclick="showSection(\'module-spec-' + code + '\')" data-section="module-spec-' + code + '">' +
156
+ '<span class="nav-icon">&#9679;</span> ' +
157
+ (mod.name || mod.code) +
158
+ (totalItems > 0 ? ' <span class="nav-badge">' + totalItems + '</span>' : '') +
159
+ '</a>';
200
160
  }
201
161
 
202
162
  function renderModuleTabNavItem(code, tabId, label, badge) {
@@ -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
  }
@@ -194,31 +197,36 @@ function renderSmartFormMockup(res) {
194
197
  html += '<div class="mock-form-tab-content' + (ti === 0 || isOnly ? ' active' : '') + '"';
195
198
  html += ' data-mockup="' + mockupId + '" data-tab="' + ti + '">';
196
199
 
197
- var fields = tab.fields || [];
198
- var rows = [];
199
- for (var i = 0; i < fields.length; i += 2) {
200
- rows.push(fields.slice(i, i + 2));
201
- }
200
+ // Tab-level subtable: the entire tab is a subtable (type: "subtable", entity, columns)
201
+ if (tab.type === 'subtable') {
202
+ html += renderSubtableMockup(tab);
203
+ } else {
204
+ var fields = tab.fields || [];
205
+ var rows = [];
206
+ for (var i = 0; i < fields.length; i += 2) {
207
+ rows.push(fields.slice(i, i + 2));
208
+ }
202
209
 
203
- rows.forEach(function(row) {
204
- if (row.length === 1 && row[0].type === 'subtable') {
205
- html += renderSubtableMockup(row[0]);
206
- } else {
207
- html += '<div class="mock-form-row">';
208
- row.forEach(function(field) {
209
- html += '<div class="mock-form-group">';
210
- html += '<label class="mock-label">' + escapeHtml(field.label || field.field);
211
- if (field.required) html += ' <span style="color:var(--error);">*</span>';
212
- html += '</label>';
213
- html += renderFormFieldMockup(field);
210
+ rows.forEach(function(row) {
211
+ if (row.length === 1 && row[0].type === 'subtable') {
212
+ html += renderSubtableMockup(row[0]);
213
+ } else {
214
+ html += '<div class="mock-form-row">';
215
+ row.forEach(function(field) {
216
+ html += '<div class="mock-form-group">';
217
+ html += '<label class="mock-label">' + escapeHtml(field.label || field.field);
218
+ if (field.required) html += ' <span style="color:var(--error);">*</span>';
219
+ html += '</label>';
220
+ html += renderFormFieldMockup(field);
221
+ html += '</div>';
222
+ });
214
223
  html += '</div>';
215
- });
216
- html += '</div>';
217
- }
218
- });
224
+ }
225
+ });
219
226
 
220
- if (fields.length === 0) {
221
- html += '<div style="padding:2rem;text-align:center;color:var(--text-muted);font-style:italic;">Contenu de l\'onglet &laquo; ' + escapeHtml(tab.label) + ' &raquo;</div>';
227
+ if (fields.length === 0) {
228
+ html += '<div style="padding:2rem;text-align:center;color:var(--text-muted);font-style:italic;">Contenu de l\'onglet &laquo; ' + escapeHtml(tab.label) + ' &raquo;</div>';
229
+ }
222
230
  }
223
231
 
224
232
  html += '</div>';
@@ -263,20 +271,39 @@ function renderSubtableMockup(field) {
263
271
 
264
272
  /* ---------- SmartCard ---------- */
265
273
  function renderSmartCardMockup(res) {
266
- var columns = res.columns || res.fields || [];
274
+ var fields = res.fields || res.columns || [];
267
275
  var html = '<div class="mock-header"><span class="mock-title">' + escapeHtml(res.label || 'Cartes') + '</span></div>';
268
276
  html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:1rem;">';
269
277
 
270
278
  for (var i = 0; i < 4; i++) {
271
279
  html += '<div style="background:var(--bg-hover);border:1px solid var(--border);border-radius:8px;padding:1rem;">';
272
- columns.forEach(function(col, ci) {
273
- var label = col.label || col.field || col;
274
- if (ci === 0) {
280
+ // Title/Subtitle pattern (SmartCard with title + subtitle + fields + actions)
281
+ if (res.title) {
282
+ html += '<div style="font-weight:600;color:var(--text-bright);margin-bottom:0.25rem;">' + escapeHtml(res.title) + ' #' + (i + 1) + '</div>';
283
+ }
284
+ if (res.subtitle) {
285
+ html += '<div style="font-size:0.8rem;color:var(--accent);margin-bottom:0.5rem;">' + escapeHtml(res.subtitle) + '</div>';
286
+ }
287
+ // Fields
288
+ fields.forEach(function(col, ci) {
289
+ var label = col.label || col.field || (typeof col === 'string' ? col : '');
290
+ var iconMap = { calendar: '&#128197; ', 'dollar-sign': '&#128176; ', 'alert-circle': '&#9888; ', mail: '&#9993; ', building: '&#127970; ', user: '&#128100; ' };
291
+ var icon = col.icon ? (iconMap[col.icon] || '&#9679; ') : '';
292
+ if (!res.title && ci === 0) {
275
293
  html += '<div style="font-weight:600;color:var(--text-bright);margin-bottom:0.5rem;">' + escapeHtml(label) + ' #' + (i + 1) + '</div>';
276
294
  } else {
277
- html += '<div style="font-size:0.8rem;color:var(--text-muted);margin-bottom:0.25rem;">' + escapeHtml(label) + ': <span style="color:var(--text);">valeur</span></div>';
295
+ html += '<div style="font-size:0.8rem;color:var(--text-muted);margin-bottom:0.25rem;">' + icon + escapeHtml(label) + ': <span style="color:var(--text);">valeur</span></div>';
278
296
  }
279
297
  });
298
+ // Actions
299
+ if (res.actions && res.actions.length > 0) {
300
+ html += '<div style="margin-top:0.75rem;display:flex;gap:0.4rem;">';
301
+ res.actions.forEach(function(a) {
302
+ var actionLabel = typeof a === 'string' ? a.replace(/-/g, ' ') : (a.label || a.action || '');
303
+ html += '<span class="mock-btn" style="font-size:0.75rem;">' + escapeHtml(actionLabel) + '</span>';
304
+ });
305
+ html += '</div>';
306
+ }
280
307
  html += '</div>';
281
308
  }
282
309
  html += '</div>';
@@ -285,7 +312,7 @@ function renderSmartCardMockup(res) {
285
312
 
286
313
  /* ---------- SmartKanban ---------- */
287
314
  function renderSmartKanbanMockup(res) {
288
- var options = res.options || res.columns || ['À faire', 'En cours', 'Terminé'];
315
+ var options = (res.options && res.options.length > 0) ? res.options : (res.columns && res.columns.length > 0) ? res.columns : ['À faire', 'En cours', 'Terminé'];
289
316
  var html = '<div class="mock-header"><span class="mock-title">' + escapeHtml(res.label || 'Kanban') + '</span></div>';
290
317
  html += '<div style="display:flex;gap:1rem;overflow-x:auto;padding-bottom:0.5rem;">';
291
318
 
@@ -359,11 +386,42 @@ function kpiDisplayValue(kpi) {
359
386
 
360
387
  /* ---------- SmartFilter ---------- */
361
388
  function renderSmartFilterMockup(res) {
362
- var options = res.options || [];
363
- var html = '<div style="display:flex;gap:0.4rem;flex-wrap:wrap;padding:0.5rem 0;">';
364
- html += '<span style="padding:0.3rem 0.7rem;border-radius:16px;font-size:0.8rem;background:var(--primary);color:#fff;cursor:pointer;">Tous</span>';
365
- options.forEach(function(opt) {
366
- html += '<span style="padding:0.3rem 0.7rem;border-radius:16px;font-size:0.8rem;background:var(--bg-hover);color:var(--text);border:1px solid var(--border);cursor:pointer;">' + escapeHtml(opt) + '</span>';
389
+ var filters = res.filters || [];
390
+ if (filters.length === 0) {
391
+ // Fallback: legacy options[] format (tag pills)
392
+ var options = res.options || [];
393
+ if (options.length === 0) return '<div style="padding:1rem;text-align:center;color:var(--text-muted);">Aucun filtre défini</div>';
394
+ var html = '<div style="display:flex;gap:0.4rem;flex-wrap:wrap;padding:0.5rem 0;">';
395
+ html += '<span style="padding:0.3rem 0.7rem;border-radius:16px;font-size:0.8rem;background:var(--primary);color:#fff;cursor:pointer;">Tous</span>';
396
+ options.forEach(function(opt) {
397
+ html += '<span style="padding:0.3rem 0.7rem;border-radius:16px;font-size:0.8rem;background:var(--bg-hover);color:var(--text);border:1px solid var(--border);cursor:pointer;">' + escapeHtml(typeof opt === 'string' ? opt : opt.label || '') + '</span>';
398
+ });
399
+ html += '</div>';
400
+ return html;
401
+ }
402
+
403
+ // Rich filter bar: render each filter based on its type
404
+ var html = '<div style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:flex-end;padding:0.5rem 0;">';
405
+ filters.forEach(function(f) {
406
+ var label = typeof f === 'string' ? f : (f.label || f.field || '');
407
+ var type = typeof f === 'string' ? 'text' : (f.type || 'text');
408
+ html += '<div style="display:flex;flex-direction:column;gap:0.2rem;">';
409
+ html += '<span style="font-size:0.7rem;color:var(--text-muted);">' + escapeHtml(label) + '</span>';
410
+ switch (type) {
411
+ case 'select':
412
+ var opts = f.options || ['Option 1', 'Option 2'];
413
+ html += '<span class="mock-input" style="width:auto;min-width:130px;font-size:0.8rem;color:var(--text-muted);">' + escapeHtml(opts[0]) + ' &#9662;</span>';
414
+ break;
415
+ case 'lookup':
416
+ html += '<span class="mock-input" style="width:auto;min-width:130px;font-size:0.8rem;color:var(--accent);">' + escapeHtml(f.entity || label) + ' &#128269;</span>';
417
+ break;
418
+ case 'daterange':
419
+ html += '<span class="mock-input" style="width:auto;min-width:180px;font-size:0.8rem;color:var(--text-muted);">01/01/2025 — 31/12/2025 &#128197;</span>';
420
+ break;
421
+ default:
422
+ html += '<span class="mock-input" style="width:auto;min-width:160px;font-size:0.8rem;color:var(--text-muted);">Rechercher... &#128269;</span>';
423
+ }
424
+ html += '</div>';
367
425
  });
368
426
  html += '</div>';
369
427
  return html;