@atlashub/smartstack-cli 4.49.0 → 4.50.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlashub/smartstack-cli",
3
- "version": "4.49.0",
3
+ "version": "4.50.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -27,6 +27,7 @@ Suggests companion modules based on primary module type:
27
27
  | **Notifications** | notification, alert, email, message, broadcast | Templates, Channels, Scheduling, Preferences | Notification systems need template management, multi-channel support, scheduling |
28
28
  | **Reporting** | report, dashboard, analytics, BI, metrics | Dashboards, Exports, Scheduling, AlertRules | Reporting needs visualization, scheduled distribution, and data export |
29
29
  | **API Management / Integrations** | api, external, integration, webhook, export, data-export, machine-to-machine | ExternalApps, DataExportEndpoints, ExportAccess, AuditLogs, ApiKeys | API platforms need app registration, key management, granular endpoint access, rate limiting, and audit logging |
30
+ | **Client Portal** | portal, external, client, fournisseur, partenaire, tiers | MyRequests, ClientDashboard, PortalNotifications | Les utilisateurs externes ont besoin de vues self-service, suivi de statut et soumission limitée |
30
31
 
31
32
  ---
32
33
 
@@ -48,6 +49,7 @@ Suggests standard sections based on detected conditions:
48
49
  | **Report generation** | Reporting Engine | Automated business reporting | Add section with template library, scheduling, distribution |
49
50
  | **External integrations** | Integration Layer | System-to-system communication | Add section with API specifications, webhook handling, sync strategy |
50
51
  | **Regulatory compliance** | Compliance & Audit | Legal/industry requirements | Add section with audit logging, data retention, retention periods |
52
+ | **Utilisateurs externes / portail détecté** | Sections portail client | Les utilisateurs externes ont besoin d'un espace dédié simplifié | Pour chaque module "shared" : suggérer section portal-specific (ex: `my-requests`, `portal-dashboard`) avec permissions read-only ou write limitée. Pour modules "external" : s'assurer que les sections ont une UX adaptée portail (navigation simplifiée, pas de fonctions admin). |
51
53
 
52
54
  ---
53
55
 
@@ -88,6 +88,7 @@
88
88
  | Question | Si la réponse est vague | Relance recommandée |
89
89
  |----------|------------------------|---------------------|
90
90
  | Q2.1 (utilisateurs) | Un seul type mentionné | "Pensez aux différents moments de la journée : qui saisit ? Qui consulte les rapports ? Qui gère les cas particuliers ?" |
91
+ | Q2.1 (utilisateurs) | Utilisateurs hors entreprise mentionnés (clients, fournisseurs, partenaires) | "Ces utilisateurs externes accéderont-ils via un espace dédié (portail) ou la même interface que vos employés ?" |
91
92
  | Q2.5 (tâches) | Tâches génériques | "Quand il arrive le matin et ouvre le système, quelle est sa première action ?" |
92
93
  | Q2.9 (restrictions) | Réponse ambiguë | "Un employé voit-il les données salariales ? Un manager voit-il uniquement son équipe ou toute l'entreprise ?" |
93
94
  | Q2.13 (indispensable) | Tout est indispensable | "Si vous ne pouviez garder que 3 fonctionnalités pour un premier lancement, lesquelles ?" |
@@ -103,6 +104,7 @@
103
104
  | Tout est vital (> 10 vitaux) | Classification non réfléchie | Appliquer le test de classification : "Si on enlevait X, le système aurait-il encore de la valeur ?" |
104
105
  | Aucune exclusion identifiée | Périmètre non borné | "Y a-t-il des aspects qui relèvent d'un autre projet ou d'une version future ?" |
105
106
  | Parcours linéaire sans alternative | Seul le cas idéal est décrit | "Que se passe-t-il à l'étape X si la condition Y n'est pas remplie ?" |
107
+ | "Nos clients doivent pouvoir..." ou "Les fournisseurs voient..." | Utilisateurs externes accédant au système | Déclencher détection portail : classifier les profils en interne/externe. SmartStack gère le déploiement multi-tenant automatiquement. |
106
108
 
107
109
  ---
108
110
 
@@ -66,6 +66,12 @@
66
66
  "type": "string",
67
67
  "description": "Lucide icon name for application navigation entry (e.g., users, shopping-cart). Confirmed in step-02."
68
68
  },
69
+ "portalMode": {
70
+ "type": "string",
71
+ "enum": ["internal-only", "portal-only", "dual"],
72
+ "default": "internal-only",
73
+ "description": "Whether the app serves internal users, external portal users, or both"
74
+ },
69
75
  "workflow": {
70
76
  "type": "object",
71
77
  "description": "Iterative module loop state",
@@ -126,7 +132,12 @@
126
132
  "involvement": { "type": "string", "enum": ["approver", "decision-maker", "consulted", "informed", "end-user"] },
127
133
  "tasks": { "type": "array", "items": { "type": "string" } },
128
134
  "frequency": { "type": "string" },
129
- "painPoints": { "type": "array", "items": { "type": "string" } }
135
+ "painPoints": { "type": "array", "items": { "type": "string" } },
136
+ "audience": {
137
+ "type": "string",
138
+ "enum": ["internal", "external", "shared"],
139
+ "description": "Whether this stakeholder is internal, external portal user, or both"
140
+ }
130
141
  }
131
142
  }
132
143
  },
@@ -204,6 +215,11 @@
204
215
  "type": "array",
205
216
  "items": { "type": "string" },
206
217
  "description": "Anticipated resources (Level 5) for this requirement — e.g., user-grid, user-form, user-card"
218
+ },
219
+ "audience": {
220
+ "type": "string",
221
+ "enum": ["internal", "external", "shared"],
222
+ "description": "Target audience. Only set when portalMode is 'dual'."
207
223
  }
208
224
  }
209
225
  }
@@ -257,6 +273,11 @@
257
273
  "type": "string",
258
274
  "enum": ["simple", "medium", "complex"]
259
275
  },
276
+ "audience": {
277
+ "type": "string",
278
+ "enum": ["internal", "external", "shared"],
279
+ "description": "Target audience. Derived from coverage matrix."
280
+ },
260
281
  "anticipatedSections": {
261
282
  "type": "array",
262
283
  "description": "Anticipated sections (Level 4) with their resources (Level 5), from cadrage coverage matrix",
@@ -266,6 +287,20 @@
266
287
  "properties": {
267
288
  "code": { "type": "string", "description": "Section code (e.g., list, detail, create, edit, dashboard)" },
268
289
  "description": { "type": "string" },
290
+ "sectionType": {
291
+ "type": "string",
292
+ "enum": ["primary", "functional", "view", "embedded"],
293
+ "description": "Section classification: primary (menu entry), functional (independent), view (sub-page of parent), embedded (widget in parent)"
294
+ },
295
+ "permissionMode": {
296
+ "type": "string",
297
+ "enum": ["crud", "custom", "read-only", "inherit"],
298
+ "description": "Permission model: crud (standard CRUD), custom (section-specific), read-only, inherit (from parent)"
299
+ },
300
+ "parentSectionCode": {
301
+ "type": ["string", "null"],
302
+ "description": "Parent section code for view/embedded sections (e.g., 'list'). Null for primary/functional sections."
303
+ },
269
304
  "resources": {
270
305
  "type": "array",
271
306
  "items": { "type": "string" },
@@ -516,6 +516,20 @@
516
516
  "required": ["code", "labels", "route", "permission", "wireframe", "useCases", "resources"],
517
517
  "properties": {
518
518
  "code": { "type": "string", "description": "Section code (kebab-case: list, detail, dashboard, approve, import, etc.)" },
519
+ "sectionType": {
520
+ "type": "string",
521
+ "enum": ["primary", "functional", "view", "embedded"],
522
+ "description": "Section classification: primary (menu entry), functional (independent), view (sub-page of parent), embedded (widget in parent)"
523
+ },
524
+ "permissionMode": {
525
+ "type": "string",
526
+ "enum": ["crud", "custom", "read-only", "inherit"],
527
+ "description": "Permission model: crud (standard CRUD), custom (section-specific), read-only, inherit (from parent)"
528
+ },
529
+ "parentSectionCode": {
530
+ "type": ["string", "null"],
531
+ "description": "Parent section code for view/embedded sections (e.g., 'list'). Null for primary/functional sections."
532
+ },
519
533
  "labels": {
520
534
  "type": "object",
521
535
  "properties": {
@@ -531,6 +545,11 @@
531
545
  "wireframe": { "type": "string", "description": "Reference to uiWireframes[].screen" },
532
546
  "useCases": { "type": "array", "items": { "type": "string" } },
533
547
  "businessRules": { "type": "array", "items": { "type": "string" } },
548
+ "audience": {
549
+ "type": "string",
550
+ "enum": ["internal", "external", "shared"],
551
+ "description": "Target audience. Inherited from module unless overridden."
552
+ },
534
553
  "resources": {
535
554
  "type": "array",
536
555
  "description": "Level 5 (Resource) components within this section",
@@ -122,10 +122,10 @@ existing_projects: array of { projectName, projectId, applications[], version }
122
122
  ## Step 3b: Early Multi-Application Detection from Prompt (NEW)
123
123
 
124
124
  > **Detect multi-app structure BEFORE asking for a single application name.**
125
- > When the user's prompt explicitly describes multiple applications, we must recognize this immediately
126
- > and set `workflow.mode = "project"` to avoid doing cadrage for a single app.
125
+ > When the user's prompt describes multiple applications or spans multiple business domains,
126
+ > we must recognize this immediately and set `workflow.mode = "project"` to avoid doing cadrage for a single app.
127
127
 
128
- **Detection patterns (ANY match = multi-app detected):**
128
+ ### Tier 1 — Explicit patterns (ANY match = multi-app detected)
129
129
 
130
130
  French patterns:
131
131
  - "une application X ... une application Y" (two occurrences of "une application")
@@ -138,30 +138,65 @@ English patterns:
138
138
  - "application #1: ... application #2: ..." (numbered)
139
139
  - "first app ... second app"
140
140
 
141
- If ANY of these patterns is found in `{feature_description}`, set `isMultiApp = true`.
141
+ If ANY of these patterns is found in `{feature_description}`:
142
+ → `isMultiApp = true`, `detectionTier = "explicit"`
142
143
 
143
- **IF multi-app detected:**
144
+ ### Tier 2 — Domain diversity analysis (when Tier 1 does not match)
144
145
 
145
- 1. Extract candidate applications from the prompt:
146
+ ULTRATHINK: Analyze `{feature_description}` to extract distinct business domains.
146
147
 
147
- **Extraction algorithm (3 steps):**
148
+ **Definition:** A "domain" is an autonomous business perimeter with its own entities and processes (e.g., HR, CRM, Finance, Projects, Sales, Inventory).
149
+
150
+ **Algorithm:**
151
+ 1. List all business domains mentioned or implied in the description
152
+ 2. Classify each domain:
153
+ - **DISTINCT** — has its own entities, processes, and lifecycle (e.g., HR vs CRM)
154
+ - **RELATED** — is a sub-domain of another (e.g., Payroll is a sub-domain of HR)
155
+ 3. Count DISTINCT domains only
156
+
157
+ **Threshold:** 2+ DISTINCT domains → `isMultiApp = true`, `detectionTier = "domain-diversity"`
158
+
159
+ **False positive protection:**
160
+ - "RH complet avec employés, congés, paie, pointage" = 1 domain (HR) with 4 modules, NOT 4 domains
161
+ - "HR and Payroll management" = 1 domain (HR) with 2 modules — Payroll is RELATED to HR
162
+ - "gestion commerciale avec devis, commandes et facturation" = 1 domain (Sales) with 3 modules
163
+ - Only count as DISTINCT when domains have genuinely independent entity models and processes
164
+
165
+ ### Extraction algorithm (depends on detection tier)
166
+
167
+ **IF Tier 1 (explicit):**
148
168
 
149
169
  1. **Identify boundaries:** Split the prompt at each "une application" / "an application" occurrence. Each block = one candidate.
150
170
  2. **Extract per candidate:** For each block, derive `name` (application name), `description` (summary of purpose), and `modules[]` (listed modules/features).
151
- - Example result: `[{ name: "RH", description: "gestion des employes, conges, temps", modules: ["Employes", "Conges", "Temps"] }, ...]`
171
+ - Example: `[{ name: "RH", description: "gestion des employés, congés, temps", modules: ["Employés", "Congés", "Temps"] }, ...]`
152
172
  3. **Detect shared modules:** Collect all module names across all candidates. Any module appearing in more than one candidate is a shared module.
153
173
  - Example: `sharedModules = ["Temps"]` (appears in both RH and Projet)
154
174
 
155
- 3. Display detection result and confirm:
175
+ **IF Tier 2 (domain-diversity):**
176
+
177
+ 1. Each DISTINCT domain = 1 candidate application:
178
+ - `name` = short domain label (e.g., "RH", "CRM", "Finance")
179
+ - `description` = user's expressed need for that domain
180
+ - `modules[]` = sub-functionalities mentioned (can be empty → "(à définir au cadrage)")
181
+ 2. Group RELATED sub-domains under their parent DISTINCT domain as modules
182
+ 3. **Detect shared modules:** Same logic as Tier 1 — modules appearing in multiple candidates
183
+
184
+ ### Confirmation dialogue
185
+
186
+ Display detection result:
156
187
 
157
188
  ```
158
- {language == "fr"
159
- ? "### Détection multi-application\n\nJ'ai détecté **{candidates.length} applications** dans votre description :"
160
- : "### Multi-application detection\n\nI detected **{candidates.length} applications** in your description:"}
189
+ {detectionTier == "explicit"
190
+ ? (language == "fr"
191
+ ? "### Détection multi-application\n\nJ'ai détecté **{candidates.length} applications** dans votre description :"
192
+ : "### Multi-application detection\n\nI detected **{candidates.length} applications** in your description:")
193
+ : (language == "fr"
194
+ ? "### Détection multi-application\n\nVotre description couvre **{candidates.length} domaines métier distincts**. Chaque domaine pourrait constituer une application séparée :"
195
+ : "### Multi-application detection\n\nYour description covers **{candidates.length} distinct business domains**. Each domain could be a separate application:")}
161
196
 
162
197
  | # | Application | Modules identifiés |
163
198
  |---|-------------|-------------------|
164
- {for each candidate: index | name | modules.join(", ")}
199
+ {for each candidate: index | name | modules.join(", ") || "(à définir au cadrage)"}
165
200
 
166
201
  {sharedModules.length > 0
167
202
  ? "⚠️ **Modules partagés détectés :** {sharedModules.join(', ')} — ces modules apparaissent dans plusieurs applications. Ils pourraient constituer une application transversale dédiée."
@@ -177,6 +212,8 @@ options:
177
212
  description: "{language == 'fr' ? 'Créer un projet avec les applications identifiées' : 'Create a project with the identified applications'}"
178
213
  - label: "{language == 'fr' ? 'Extraire les modules partagés' : 'Extract shared modules'}" (only if sharedModules.length > 0)
179
214
  description: "{language == 'fr' ? 'Créer une application dédiée pour {sharedModules.join(', ')} ({candidates.length + 1} applications au total)' : 'Create a dedicated app for {sharedModules.join(', ')} ({candidates.length + 1} total)'}"
215
+ - label: "{language == 'fr' ? 'Regrouper certains domaines' : 'Regroup some domains'}" (only if detectionTier == "domain-diversity")
216
+ description: "{language == 'fr' ? 'Fusionner des domaines en moins d\\'applications' : 'Merge domains into fewer applications'}"
180
217
  - label: "{language == 'fr' ? 'Application unique' : 'Single application'}"
181
218
  description: "{language == 'fr' ? 'Tout regrouper en une seule application avec plusieurs modules' : 'Group everything into one application with multiple modules'}"
182
219
  ```
@@ -193,6 +230,19 @@ shared_modules_extracted: boolean # true if user chose extraction
193
230
  → Skip step 4 (application name) — applications will be confirmed in step-01-cadrage
194
231
  → Continue to step 5 (language selection)
195
232
 
233
+ **IF "Regrouper certains domaines" (Tier 2 only):**
234
+
235
+ Ask via AskUserQuestion:
236
+ ```
237
+ question: "{language == 'fr' ? 'Comment souhaitez-vous regrouper ces domaines ? (ex: \"RH + Paie ensemble, CRM seul\")' : 'How would you like to regroup these domains? (e.g., \"HR + Payroll together, CRM alone\")'}"
238
+ header: "Regroupement"
239
+ ```
240
+
241
+ Process user response:
242
+ 1. Parse the grouping instructions (natural language)
243
+ 2. Rebuild `candidate_applications` based on the specified groups
244
+ 3. Re-display the confirmation dialogue with the updated candidates
245
+
196
246
  **IF "Application unique":**
197
247
 
198
248
  ```yaml
@@ -201,7 +251,7 @@ workflow_mode: "application"
201
251
 
202
252
  → Continue to step 4 normally
203
253
 
204
- **IF no multi-app patterns detected:**
254
+ **IF no multi-app detected (neither Tier 1 nor Tier 2):**
205
255
  → Continue to step 4 normally
206
256
 
207
257
  ## Step 4: Determine Application Name
@@ -239,6 +239,44 @@ Ask in 1-2 batches. After each batch:
239
239
  - If "no restrictions" → probe: "Are there sensitive data (salary, contracts, personal info) that should be restricted to specific roles?"
240
240
  - If tasks are generic → ask for a concrete scenario: "Walk me through a typical day"
241
241
 
242
+ #### 4b-bis. Détection Portail Client (ULTRATHINK — après batch stakeholders)
243
+
244
+ Analyser les réponses stakeholders pour signaux d'utilisateurs externes :
245
+
246
+ ```
247
+ EXTERNAL_SIGNALS = ["client", "customer", "fournisseur", "supplier", "partenaire",
248
+ "partner", "portail", "portal", "externe", "external", "tiers", "third-party",
249
+ "organisation cliente"]
250
+ ```
251
+
252
+ SI un profil stakeholder match EXTERNAL_SIGNALS :
253
+ → Stocker dans `{pre_analysis}`: `_portalDetected: true`
254
+ → Poser la question via AskUserQuestion :
255
+
256
+ ```
257
+ question: "{language == 'fr'
258
+ ? 'J\'ai détecté des utilisateurs externes ({profiles}). Votre application comportera-t-elle un portail client en plus de la partie interne ?'
259
+ : 'I detected external users ({profiles}). Will your application include a client portal in addition to the internal part?'}"
260
+ header: "Portail client"
261
+ options:
262
+ - label: "{language == 'fr' ? 'Oui, portail + interne' : 'Yes, portal + internal'}"
263
+ description: "{language == 'fr' ? 'L\'application servira des utilisateurs internes ET externes via un portail dédié' : 'The app will serve internal AND external users via a dedicated portal'}"
264
+ - label: "{language == 'fr' ? 'Non, interne uniquement' : 'No, internal only'}"
265
+ description: "{language == 'fr' ? 'Seuls les employés internes utiliseront l\'application' : 'Only internal employees will use the application'}"
266
+ - label: "{language == 'fr' ? 'Non, portail uniquement' : 'No, portal only'}"
267
+ description: "{language == 'fr' ? 'L\'application est exclusivement destinée aux utilisateurs externes' : 'The application is exclusively for external users'}"
268
+ ```
269
+
270
+ | Réponse | Action |
271
+ |---------|--------|
272
+ | **Oui, portail + interne** | `_portalMode = "dual"` → Follow-up : classifier chaque stakeholder comme `audience: "internal" \| "external" \| "shared"` |
273
+ | **Portail uniquement** | `_portalMode = "portal-only"` |
274
+ | **Interne uniquement** | `_portalMode = "internal-only"` (défaut, pas d'action supplémentaire) |
275
+
276
+ > **Note SmartStack :** Le déploiement multi-tenant est géré automatiquement par SmartStack. La partie interne et la partie portail seront déployées sur des tenants séparés.
277
+
278
+ SI aucun profil ne match EXTERNAL_SIGNALS → `_portalMode = "internal-only"` (défaut silencieux, rien à demander).
279
+
242
280
  #### 4c. Functional Scope (ALWAYS — from `questionnaire/02-stakeholders-scope.md`)
243
281
 
244
282
  **Mandatory minimum:** Q2.13 (in-scope), Q2.15 (exclusions), Q2.16 (main journey).
@@ -596,6 +634,14 @@ BEFORE transitioning to step-02:
596
634
  - List anticipated resources (Level 5) for each section
597
635
  - List detail page tabs — for entities accessible via click from `list`
598
636
 
637
+ **SI `_portalMode === "dual"` :**
638
+ - Chaque entrée `coverageMatrix` reçoit `audience: "internal" | "external" | "shared"`
639
+ - Règles de dérivation :
640
+ - Items mentionnés uniquement par stakeholders internes → `"internal"`
641
+ - Items mentionnés uniquement par stakeholders externes → `"external"`
642
+ - Items mentionnés par les deux ou cross-cutting → `"shared"`
643
+ - Afficher colonne "Audience" dans le tableau de la matrice
644
+
599
645
  4. **RECONCILIATION CHECK (MANDATORY — BLOCKING):**
600
646
 
601
647
  > **Every explicitly requested item MUST appear in the coverage matrix.**
@@ -641,12 +687,13 @@ ba-writer.enrichSection({
641
687
  subsection: "cadrage.json",
642
688
  data: {
643
689
  metadata: {
644
- tablePrefix: "{from Phase 5, section 6b — validated prefix, e.g., rh_}"
690
+ tablePrefix: "{from Phase 5, section 6b — validated prefix, e.g., rh_}",
691
+ portalMode: "{_portalMode || 'internal-only'}"
645
692
  },
646
693
  problem: {from Phase 3, section 4a — Q1.1 answer or refined problem},
647
694
  asIs: {from Phase 3, section 4a — Q1.4 answer},
648
695
  toBe: {from Phase 3, section 4a — Q1.8 answer},
649
- stakeholders: [{from Phase 3, section 4b}],
696
+ stakeholders: [{from Phase 3, section 4b — each with audience: "internal"|"external"|"shared" if portalMode is "dual"}],
650
697
  globalScope: {
651
698
  inScope: [{from Phase 3, section 4c + Phase 4 accepted suggestions + coverage matrix}],
652
699
  outOfScope: [{from Phase 3, section 4c — Q2.15 exclusions}]
@@ -70,6 +70,9 @@ For each module:
70
70
  - Description
71
71
  - FeatureType (data-centric | workflow | reporting | integration | full-module)
72
72
  - Entities (preliminary list)
73
+ - Audience ("internal" | "external" | "shared") — if portalMode = "dual"
74
+ → Derived from coverageMatrix: if ALL items of the module are internal → "internal",
75
+ if ALL external → "external", else → "shared"
73
76
  ```
74
77
 
75
78
  ### 3. Section & Resource Anticipation
@@ -89,18 +92,20 @@ For each section:
89
92
 
90
93
  **Section classification rules:**
91
94
 
92
- | sectionType | permissionMode | When to use | Examples |
93
- |---|---|---|---|
94
- | `primary` | `crud` | Main entry point of the module, visible in menu | list |
95
- | `functional` | `crud` or `custom` | Independent functional section with own access control | approve, import, planning |
96
- | `view` | `inherit` | Subordinate view reached from a primary section | detail, edit |
97
- | `embedded` | `read-only` | Widget or tab embedded in another section | dashboard (when embedded in module) |
95
+ | sectionType | permissionMode | When to use | Parent | Examples |
96
+ |---|---|---|---|---|
97
+ | `primary` | `crud` | Main entry point of the module, visible in menu | none | list |
98
+ | `functional` | `crud` or `custom` | Independent functional section with own access control | none | approve, import, planning |
99
+ | `view` | `inherit` | Sub-page reached from a primary/functional section (e.g., click a row) | `parentSectionCode` | detail, edit |
100
+ | `embedded` | `read-only` | Widget or tab embedded in another section | `parentSectionCode` | dashboard (when embedded in module) |
101
+
102
+ **Parent-child rule:** `view` and `embedded` sections MUST specify `parentSectionCode` — the code of the section they are reached from. A detail page is NOT a sibling of list; it is a **child** of list (user clicks a row in the list to reach the detail).
98
103
 
99
104
  Common patterns:
100
- - **Data-centric module**: list (`primary`/`crud`) + detail (`view`/`inherit`)
101
- - **Workflow module**: list (`primary`/`crud`) + detail (`view`/`inherit`) + approve (`functional`/`custom`)
102
- - **Reporting module**: dashboard (`primary`/`read-only`) + detail (`view`/`inherit`)
103
- - **Full module**: list (`primary`/`crud`) + detail (`view`/`inherit`) + dashboard (`embedded`/`read-only`)
105
+ - **Data-centric**: list (`primary`/`crud`) detail (`view`/`inherit`, parent: list)
106
+ - **Workflow**: list (`primary`/`crud`) detail (`view`/`inherit`, parent: list) + approve (`functional`/`custom`)
107
+ - **Reporting**: dashboard (`primary`/`read-only`) detail (`view`/`inherit`, parent: dashboard)
108
+ - **Full module**: list (`primary`/`crud`) detail (`view`/`inherit`, parent: list) + dashboard (`embedded`/`read-only`, parent: list)
104
109
 
105
110
  ### 4. Dependency Graph
106
111
 
@@ -139,21 +144,40 @@ For EACH identified element, ask yourself:
139
144
  - Does the form need tabs?
140
145
  - Are there missing filter components?
141
146
 
147
+ **Split audience (if portalMode = "dual"):**
148
+ - Pour chaque module "shared" : le portail voit-il les MÊMES données ou un sous-ensemble filtré ?
149
+ - Y a-t-il des sections qui ne doivent apparaître QUE dans le portail ? (ex: "Mes demandes")
150
+ - Y a-t-il des sections qui ne doivent apparaître QUE en interne ? (ex: reporting admin)
151
+
142
152
  ### 6. Present to User
143
153
 
144
154
  Display the complete hierarchy for validation:
145
155
 
156
+ **Tree nesting rules:**
157
+ - `view` sections are **indented under their parent** (the section they are reached from)
158
+ - `functional` and `primary` sections stay at module level
159
+ - `embedded` sections are indented under their parent section
160
+ - Tabs are indented under detail
161
+
146
162
  ```
147
163
  [App: HumanResources]
148
164
  ├── [Module: Employees]
149
- │ ├── list → SmartTable (employees-grid)
150
- │ └── detail → SmartForm (employee-form)
165
+ │ ├── list (primary/crud) employees-grid, employees-filters
166
+ └── detail (view/inherit) → employee-form
167
+ │ │ └── Tabs: Infos, Contrats
151
168
  ├── [Module: Absences]
152
- │ ├── list → SmartTable (absences-grid)
153
- ├── detail → SmartForm (absence-form)
154
- │ └── calendar → SmartKanban (absences-calendar)
169
+ │ ├── list (primary/crud) absences-grid, absences-filters
170
+ │ └── detail (view/inherit) → absence-form
171
+ │ └── calendar (functional) → absences-calendar
155
172
  └── [Module: Reports]
156
- └── dashboard → SmartDashboard (hr-dashboard)
173
+ └── dashboard (primary/read-only) → hr-dashboard
174
+
175
+ IF portalMode = "dual", annotate each module with its audience:
176
+
177
+ [App: ServiceManagement]
178
+ ├── [Module: Projects] [partagé]
179
+ ├── [Module: TimeTracking] [interne]
180
+ └── [Module: ClientDashboard] [portail]
157
181
  ```
158
182
 
159
183
  Ask: "Does this structure match your vision? Any missing modules, sections, or resources?"
@@ -181,7 +205,7 @@ Write via ba-writer:
181
205
  "entities": ["Employee", "Contract"],
182
206
  "anticipatedSections": [
183
207
  { "code": "list", "label": "Liste", "sectionType": "primary", "permissionMode": "crud", "resources": [{ "code": "employees-grid", "type": "SmartTable" }] },
184
- { "code": "detail", "label": "Fiche", "sectionType": "view", "permissionMode": "inherit", "resources": [{ "code": "employee-form", "type": "SmartForm" }] }
208
+ { "code": "detail", "label": "Fiche", "sectionType": "view", "permissionMode": "inherit", "parentSectionCode": "list", "resources": [{ "code": "employee-form", "type": "SmartForm" }] }
185
209
  ]
186
210
  }
187
211
  ],
@@ -330,6 +330,175 @@ Les types possibles : `functional`, `business-rule`, `performance`, `security`.
330
330
 
331
331
  Les critères d'acceptation sont écrits dans `validation.json` au même niveau que `globalRiskAssessment`.
332
332
 
333
+ ### 7ter. Naming Audit (MANDATORY)
334
+
335
+ > **Challenge all names before final approval.** Every code, label, and route generated
336
+ > during the BA must be reviewed for coherence, convention compliance, and clarity.
337
+
338
+ **7ter-a. Collect all names from the BA:**
339
+
340
+ ```javascript
341
+ const namingRegistry = {
342
+ application: {
343
+ label: application_name,
344
+ code: applicationCode, // PascalCase
345
+ route: toKebabCase(applicationCode) // kebab-case
346
+ },
347
+ modules: completedModules.map(m => ({
348
+ label: m.name,
349
+ code: m.code, // PascalCase
350
+ route: toKebabCase(m.code) // kebab-case
351
+ })),
352
+ entities: completedModules.flatMap(m =>
353
+ m.entities.map(e => ({
354
+ name: e.name, // PascalCase (C# class name)
355
+ module: m.code,
356
+ tableName: e.tableName || pluralize(e.name) // Plural convention
357
+ }))
358
+ ),
359
+ permissionRoots: [...new Set(
360
+ permissionPaths.map(p => p.path.split('.').slice(0, 2).join('.'))
361
+ )]
362
+ };
363
+ ```
364
+
365
+ **7ter-b. Display naming recap table:**
366
+
367
+ ```
368
+ ═══════════════════════════════════════════════════════════════
369
+ NAMING AUDIT — {application_name}
370
+ ═══════════════════════════════════════════════════════════════
371
+
372
+ APPLICATION
373
+ | Label | Code (PascalCase) | Route (kebab-case) |
374
+ |-------|-------------------|--------------------|
375
+ | {application_name} | {applicationCode} | /{route} |
376
+
377
+ MODULES
378
+ | # | Label | Code (PascalCase) | Route (kebab-case) |
379
+ |---|-------|-------------------|--------------------|
380
+ {for each module: index | label | code | /app-route/module-route}
381
+
382
+ ENTITIES
383
+ | Entity (PascalCase) | Module | Table (plural) |
384
+ |----------------------|--------|----------------|
385
+ {for each entity: name | module | tableName}
386
+
387
+ PERMISSION ROOTS
388
+ {for each root: path}
389
+ ═══════════════════════════════════════════════════════════════
390
+ ```
391
+
392
+ **7ter-c. Coherence checks:**
393
+
394
+ ```javascript
395
+ const namingIssues = [];
396
+
397
+ // 1. Duplicate entity names across modules
398
+ const entityNames = namingRegistry.entities.map(e => e.name);
399
+ const duplicates = entityNames.filter((n, i) => entityNames.indexOf(n) !== i);
400
+ if (duplicates.length > 0) {
401
+ namingIssues.push({ severity: "ERROR", issue: `Duplicate entity names: ${[...new Set(duplicates)].join(', ')}` });
402
+ }
403
+
404
+ // 2. Module code vs label coherence (code should derive logically from label)
405
+ for (const mod of namingRegistry.modules) {
406
+ if (!mod.code || mod.code.length < 2) {
407
+ namingIssues.push({ severity: "ERROR", issue: `Module "${mod.label}" has invalid code: "${mod.code}"` });
408
+ }
409
+ }
410
+
411
+ // 3. Permission root alignment with module codes
412
+ for (const root of namingRegistry.permissionRoots) {
413
+ const [appSegment, moduleSegment] = root.split('.');
414
+ const matchingModule = namingRegistry.modules.find(m =>
415
+ m.code.toLowerCase() === moduleSegment.toLowerCase()
416
+ );
417
+ if (!matchingModule) {
418
+ namingIssues.push({ severity: "WARNING", issue: `Permission root "${root}" has no matching module code` });
419
+ }
420
+ }
421
+
422
+ // 4. Route collision detection
423
+ const allRoutes = namingRegistry.modules.map(m =>
424
+ `/${namingRegistry.application.route}/${m.route}`
425
+ );
426
+ const routeDuplicates = allRoutes.filter((r, i) => allRoutes.indexOf(r) !== i);
427
+ if (routeDuplicates.length > 0) {
428
+ namingIssues.push({ severity: "ERROR", issue: `Route collisions: ${routeDuplicates.join(', ')}` });
429
+ }
430
+ ```
431
+
432
+ **7ter-d. MCP validation:**
433
+
434
+ ```
435
+ mcp__smartstack__validate_conventions({
436
+ checks: ["tables"],
437
+ context: {
438
+ applicationCode: applicationCode,
439
+ modules: namingRegistry.modules.map(m => m.code),
440
+ entities: namingRegistry.entities.map(e => e.name)
441
+ }
442
+ })
443
+
444
+ → Merge MCP findings into namingIssues[]
445
+ ```
446
+
447
+ **7ter-e. Present findings and confirm:**
448
+
449
+ ```
450
+ IF namingIssues.length > 0:
451
+ Display issues table:
452
+ | # | Severity | Issue |
453
+ |---|----------|-------|
454
+ {for each issue}
455
+ ```
456
+
457
+ Ask via AskUserQuestion:
458
+ ```
459
+ question: "{language == 'fr'
460
+ ? 'Validez-vous les noms ci-dessus pour l\\'ensemble de l\\'application ?'
461
+ : 'Do you approve all the names above for the entire application?'}"
462
+ header: "Naming Audit"
463
+ options:
464
+ - label: "{language == 'fr' ? 'Approuvé' : 'Approved'}"
465
+ description: "{language == 'fr' ? 'Tous les noms sont corrects' : 'All names are correct'}"
466
+ - label: "{language == 'fr' ? 'Renommer certains éléments' : 'Rename some elements'}"
467
+ description: "{language == 'fr' ? 'Corriger des noms avant de finaliser' : 'Fix names before finalizing'}"
468
+ ```
469
+
470
+ **IF "Renommer certains éléments":**
471
+
472
+ Ask via AskUserQuestion (open-ended):
473
+ ```
474
+ question: "{language == 'fr'
475
+ ? 'Quels éléments souhaitez-vous renommer ? (ex: \"Module Ventes → Commerce\", \"Entity Invoice → BillingDocument\")'
476
+ : 'Which elements do you want to rename? (e.g., \"Module Sales → Commerce\", \"Entity Invoice → BillingDocument\")'}"
477
+ header: "Renaming"
478
+ ```
479
+
480
+ Process user response:
481
+ 1. Parse rename instructions
482
+ 2. Update `applicationCode`, module codes, entity names, permission paths accordingly in JSON files via ba-writer
483
+ 3. Re-run 7ter-c and 7ter-d checks on updated names
484
+ 4. Re-display the naming recap table for final confirmation
485
+
486
+ **IF "Approuvé":**
487
+ → Store naming audit result in `validation.json`:
488
+ ```javascript
489
+ ba-writer.enrichSection({
490
+ featureId: {feature_id},
491
+ section: "namingAudit",
492
+ data: {
493
+ auditedAt: now(),
494
+ issues: namingIssues,
495
+ approved: true,
496
+ renames: [] // or list of applied renames
497
+ }
498
+ });
499
+ ```
500
+ → Continue to section 8
501
+
333
502
  ### 8. Consolidation Summary Display
334
503
 
335
504
  ```
@@ -449,6 +618,7 @@ ba-writer.enrichSection({
449
618
  `E2E flows: ${e2eFlows.length} identified`,
450
619
  `Global risk: ${risks.length > 0 ? 'MEDIUM' : 'LOW'}`,
451
620
  `Semantic checks: PASSED`,
621
+ `Naming audit: ${namingIssues.length} issues found, APPROVED`,
452
622
  `Client approval: APPROVED`
453
623
  ]
454
624
  }
@@ -508,6 +678,7 @@ BA workflow complete. Next steps:
508
678
  - ✓ No circular dependencies
509
679
  - ✓ Permission coherence validated
510
680
  - ✓ Semantic checks: 0 errors
681
+ - ✓ Naming audit completed and approved
511
682
  - ✓ Client approval obtained (or auto-approved for single module)
512
683
  - ✓ Consolidation section written to validation.json
513
684
  - ✓ Status updated to "consolidated"
@@ -2454,8 +2454,10 @@ data.moduleSpecs = data.moduleSpecs || {};
2454
2454
  if (!data.moduleSpecs[m.code]) {
2455
2455
  data.moduleSpecs[m.code] = { useCases: [], businessRules: [], entities: [], permissions: [], notes: '' };
2456
2456
  }
2457
- // Ensure anticipatedSections array exists
2458
- m.anticipatedSections = m.anticipatedSections || [];
2457
+ // Ensure anticipatedSections array exists and normalize strings to objects
2458
+ m.anticipatedSections = (m.anticipatedSections || []).map(function(s) {
2459
+ return typeof s === 'string' ? { code: s, name: s, description: '', resources: [] } : s;
2460
+ });
2459
2461
  m.applicationCode = m.applicationCode || '';
2460
2462
  // Initialize screens array for interface specs
2461
2463
  if (!data.moduleSpecs[m.code].screens) {
@@ -2767,7 +2769,9 @@ function renderModuleNavItem(mod) {
2767
2769
  var ucCount = (spec.useCases || []).length;
2768
2770
  var brCount = (spec.businessRules || []).length;
2769
2771
  var entCount = (spec.entities || []).length;
2770
- var sections = mod.anticipatedSections || [];
2772
+ var sections = (mod.anticipatedSections || []).map(function(s) {
2773
+ return typeof s === 'string' ? { code: s, name: s, resources: [] } : s;
2774
+ });
2771
2775
  var groupId = 'mod-' + code;
2772
2776
  var collapsed = navCollapseState[groupId] === true;
2773
2777
 
@@ -3957,7 +3961,9 @@ function refreshAllPermGrids() {
3957
3961
 
3958
3962
  function renderModuleStructure(code) {
3959
3963
  const mod = data.modules.find(m => m.code === code);
3960
- const sections = mod ? (mod.anticipatedSections || []) : [];
3964
+ const sections = mod ? (mod.anticipatedSections || []).map(function(s) {
3965
+ return typeof s === 'string' ? { code: s, name: s, description: '', resources: [] } : s;
3966
+ }) : [];
3961
3967
 
3962
3968
  if (sections.length === 0) {
3963
3969
  return '<div class="card" style="text-align:center;padding:2rem;color:var(--text-muted);">' +
@@ -4036,7 +4042,9 @@ function renderModuleStructure(code) {
4036
4042
  /* ---------- Section-Grouped Rendering (Hierarchical Mode) ---------- */
4037
4043
 
4038
4044
  function renderSectionGroupedItems(mod, itemsKey, code, renderFn) {
4039
- var sections = mod.anticipatedSections || [];
4045
+ var sections = (mod.anticipatedSections || []).map(function(s) {
4046
+ return typeof s === 'string' ? { code: s, name: s, resources: [] } : s;
4047
+ });
4040
4048
  var html = '';
4041
4049
  sections.forEach(function(section) {
4042
4050
  var items = section[itemsKey] || [];
@@ -4059,7 +4067,9 @@ function renderModuleMockups(code) {
4059
4067
  var screens = spec.screens || [];
4060
4068
  var wireframes = EMBEDDED_ARTIFACTS?.wireframes?.[code] || [];
4061
4069
  var mod = data.modules.find(function(m) { return m.code === code; });
4062
- var sections = mod ? (mod.anticipatedSections || []) : [];
4070
+ var sections = mod ? (mod.anticipatedSections || []).map(function(s) {
4071
+ return typeof s === 'string' ? { code: s, name: s, resources: [] } : s;
4072
+ }) : [];
4063
4073
 
4064
4074
  // Priority 1: HTML mockups from screens[] specs
4065
4075
  if (screens.length > 0) {
@@ -66,8 +66,10 @@ data.moduleSpecs = data.moduleSpecs || {};
66
66
  if (!data.moduleSpecs[m.code]) {
67
67
  data.moduleSpecs[m.code] = { useCases: [], businessRules: [], entities: [], permissions: [], notes: '' };
68
68
  }
69
- // Ensure anticipatedSections array exists
70
- m.anticipatedSections = m.anticipatedSections || [];
69
+ // Ensure anticipatedSections array exists and normalize strings to objects
70
+ m.anticipatedSections = (m.anticipatedSections || []).map(function(s) {
71
+ return typeof s === 'string' ? { code: s, name: s, description: '', resources: [] } : s;
72
+ });
71
73
  m.applicationCode = m.applicationCode || '';
72
74
  // Initialize screens array for interface specs
73
75
  if (!data.moduleSpecs[m.code].screens) {
@@ -149,7 +149,9 @@ 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 || [];
152
+ var sections = (mod.anticipatedSections || []).map(function(s) {
153
+ return typeof s === 'string' ? { code: s, name: s, resources: [] } : s;
154
+ });
153
155
  var groupId = 'mod-' + code;
154
156
  var collapsed = navCollapseState[groupId] === true;
155
157
 
@@ -611,7 +611,9 @@ function refreshAllPermGrids() {
611
611
 
612
612
  function renderModuleStructure(code) {
613
613
  const mod = data.modules.find(m => m.code === code);
614
- const sections = mod ? (mod.anticipatedSections || []) : [];
614
+ const sections = mod ? (mod.anticipatedSections || []).map(function(s) {
615
+ return typeof s === 'string' ? { code: s, name: s, description: '', resources: [] } : s;
616
+ }) : [];
615
617
 
616
618
  if (sections.length === 0) {
617
619
  return '<div class="card" style="text-align:center;padding:2rem;color:var(--text-muted);">' +
@@ -690,7 +692,9 @@ function renderModuleStructure(code) {
690
692
  /* ---------- Section-Grouped Rendering (Hierarchical Mode) ---------- */
691
693
 
692
694
  function renderSectionGroupedItems(mod, itemsKey, code, renderFn) {
693
- var sections = mod.anticipatedSections || [];
695
+ var sections = (mod.anticipatedSections || []).map(function(s) {
696
+ return typeof s === 'string' ? { code: s, name: s, resources: [] } : s;
697
+ });
694
698
  var html = '';
695
699
  sections.forEach(function(section) {
696
700
  var items = section[itemsKey] || [];
@@ -713,7 +717,9 @@ function renderModuleMockups(code) {
713
717
  var screens = spec.screens || [];
714
718
  var wireframes = EMBEDDED_ARTIFACTS?.wireframes?.[code] || [];
715
719
  var mod = data.modules.find(function(m) { return m.code === code; });
716
- var sections = mod ? (mod.anticipatedSections || []) : [];
720
+ var sections = mod ? (mod.anticipatedSections || []).map(function(s) {
721
+ return typeof s === 'string' ? { code: s, name: s, resources: [] } : s;
722
+ }) : [];
717
723
 
718
724
  // Priority 1: HTML mockups from screens[] specs
719
725
  if (screens.length > 0) {
@@ -51,7 +51,10 @@ const FEATURE_DATA = {
51
51
  featureType: module.featureType || "data-centric",
52
52
  estimatedComplexity: module.estimatedComplexity || "medium",
53
53
  entities: module.entities || [],
54
- anticipatedSections: module.anticipatedSections || [],
54
+ // NORMALIZE: source may have strings (coverageMatrix) or objects (module schema)
55
+ anticipatedSections: (module.anticipatedSections || []).map(s =>
56
+ typeof s === 'string' ? { code: s, name: s, description: '', resources: [] } : s
57
+ ), // ALWAYS [{code, name, description, resources[]}]
55
58
  dependencies: module.dependencies || [],
56
59
  dependents: module.dependents || []
57
60
  }
@@ -57,7 +57,10 @@ Build a JSON object following this **exact mapping** from index.json to the HTML
57
57
  description: m.description || "",
58
58
  featureType: m.featureType || "data-centric",
59
59
  entities: m.entities || [],
60
- anticipatedSections: m.anticipatedSections || [], // [{code, description, resources[]}]
60
+ // NORMALIZE: source may have strings (coverageMatrix) or objects (module schema)
61
+ anticipatedSections: (m.anticipatedSections || []).map(s =>
62
+ typeof s === 'string' ? { code: s, name: s, description: '', resources: [] } : s
63
+ ), // ALWAYS [{code, name, description, resources[]}]
61
64
  dependencies: m.dependencies || [],
62
65
  dependents: m.dependents || [],
63
66
  estimatedComplexity: m.estimatedComplexity || "medium",
@@ -89,7 +89,11 @@ modules: master.modules.map(m => ({
89
89
  description: m.description || "",
90
90
  featureType: m.featureType || "data-centric",
91
91
  entities: m.entities || [],
92
- anticipatedSections: m.anticipatedSections || [],
92
+ anticipatedSections: (m.anticipatedSections || []).map(s =>
93
+ typeof s === 'string'
94
+ ? { code: s, name: s, description: "", resources: [] }
95
+ : { code: s.code || s.name || "", name: s.label || s.name || s.code || "", description: s.description || "", resources: s.resources || [], route: s.route || "", permission: s.permission || "" }
96
+ ),
93
97
  dependencies: m.dependencies || [],
94
98
  dependents: m.dependents || [],
95
99
  estimatedComplexity: m.estimatedComplexity || "medium",
@@ -135,6 +139,50 @@ moduleSpecs[moduleCode] = {
135
139
  permissions: buildPermissionKeys(mod.permissions),
136
140
  apiEndpoints: mod.usecases?.apiEndpoints || []
137
141
  }
142
+
143
+ // BUILD screens[] for HTML interactive mockups (SmartTable/SmartForm rendering)
144
+ const flatScr = mod.screens?.screens || [];
145
+ let screens = [];
146
+ if (flatScr.length > 0) {
147
+ const bySec = {};
148
+ flatScr.forEach(s => {
149
+ const sec = s.section || "default";
150
+ if (!bySec[sec]) bySec[sec] = {
151
+ sectionCode: sec,
152
+ sectionLabel: s.sectionLabel || s.sectionDescription || sec,
153
+ resources: []
154
+ };
155
+ bySec[sec].resources.push({
156
+ code: s.screen || s.name || "",
157
+ label: s.sectionLabel || s.description || s.screen || "",
158
+ type: s.componentType || "unknown",
159
+ columns: (s.columns || []).map(c => ({
160
+ field: c.code || c.field || "", label: c.label || c.code || "",
161
+ type: c.type || c.dataType || "text", sortable: !!c.sortable
162
+ })),
163
+ filters: (s.filters || []).map(f =>
164
+ typeof f === 'string' ? f : (f.label || f.filterLabel || f.code || f.filterCode || "")),
165
+ fields: [],
166
+ tabs: (s.tabs || []).map(t => ({
167
+ label: t.label || t.tabLabel || t.code || "",
168
+ fields: (t.fields || []).map(f => ({
169
+ field: f.code || f.fieldCode || f.field || "",
170
+ label: f.label || f.code || "",
171
+ type: f.type || f.dataType || "text",
172
+ required: !!f.required
173
+ }))
174
+ })),
175
+ actions: (s.actions || []).map(a =>
176
+ typeof a === 'string' ? a : (a.code || a.label || a.actionCode || "")),
177
+ kpis: s.kpis || [],
178
+ options: s.options || [],
179
+ permission: s.permission || "",
180
+ notes: s.description || ""
181
+ });
182
+ });
183
+ screens = Object.values(bySec);
184
+ }
185
+ moduleSpecs[moduleCode].screens = screens;
138
186
  ```
139
187
 
140
188
  > See `references/data-mapping.md` for `buildPermissionKeys()` implementation.
@@ -156,6 +204,44 @@ FEATURE_DATA.modules.forEach(m => {
156
204
 
157
205
  > This step is CRITICAL for the HTML viewer to display UC/BR grouped by section.
158
206
 
207
+ ### Post-Build Self-Check (MANDATORY — BLOCKING)
208
+
209
+ After enriching anticipatedSections, run this self-check to detect data loss:
210
+
211
+ ```javascript
212
+ // SELF-CHECK: compare source file counts vs FEATURE_DATA counts
213
+ const errors = [];
214
+ FEATURE_DATA.modules.forEach(m => {
215
+ const spec = FEATURE_DATA.moduleSpecs[m.code];
216
+ const source = collected_data.modules[m.code];
217
+ if (!spec) { errors.push(`MISSING moduleSpecs[${m.code}]`); return; }
218
+
219
+ const srcUC = (source?.usecases?.useCases || []).length;
220
+ const bltUC = (spec.useCases || []).length;
221
+ if (srcUC > 0 && bltUC === 0)
222
+ errors.push(`${m.code}: useCases EMPTY but source has ${srcUC}`);
223
+
224
+ const srcBR = (source?.rules?.rules || []).length;
225
+ const bltBR = (spec.businessRules || []).length;
226
+ if (srcBR > 0 && bltBR === 0)
227
+ errors.push(`${m.code}: businessRules EMPTY but source has ${srcBR}`);
228
+
229
+ const srcEnt = (source?.entities?.entities || []).length;
230
+ const bltEnt = (spec.entities || []).length;
231
+ if (srcEnt > 0 && bltEnt === 0)
232
+ errors.push(`${m.code}: entities EMPTY but source has ${srcEnt}`);
233
+ });
234
+
235
+ if (errors.length > 0) {
236
+ Display("⛔ SELF-CHECK FAILED — data loss detected:");
237
+ errors.forEach(e => Display(" - " + e));
238
+ // FIX: re-map the failing modules from collected_data before continuing
239
+ }
240
+ ```
241
+
242
+ > If self-check detects errors, you MUST re-read the source data and re-build the failing moduleSpecs entries.
243
+ > Do NOT proceed with empty arrays when source data exists.
244
+
159
245
  **consolidation:**
160
246
  ```javascript
161
247
  consolidation: {
@@ -223,6 +309,31 @@ for (const section of sections) {
223
309
  }
224
310
  }
225
311
 
312
+ // Source C: screens without mockup → auto-generate text description as wireframe fallback
313
+ const screensWithoutMockup = flatScreens.filter(s => !s.mockup && !s.mockupFormat);
314
+ for (const screen of screensWithoutMockup) {
315
+ const type = screen.componentType || "";
316
+ let desc = "";
317
+ if (type.includes("Table") || type.includes("Grid")) {
318
+ const cols = screen.columns || [];
319
+ desc = "Tableau avec " + cols.length + " colonnes : " + cols.map(c => c.label || c.code).join(", ");
320
+ } else if (type.includes("Form")) {
321
+ const tabs = screen.tabs || [];
322
+ desc = "Formulaire " + (tabs.length > 0 ? "avec " + tabs.length + " onglet(s)" : "");
323
+ } else if (type.includes("Dashboard")) {
324
+ desc = "Tableau de bord avec KPIs";
325
+ } else if (type.includes("Kanban")) {
326
+ desc = "Vue Kanban";
327
+ }
328
+ if (desc) {
329
+ rawWireframes.push({
330
+ screen: screen.screen || "", section: screen.section || "",
331
+ mockupFormat: "text", mockup: desc,
332
+ description: screen.sectionDescription || "", elements: [], componentMapping: []
333
+ });
334
+ }
335
+ }
336
+
226
337
  // STEP 2: Map to HTML format (RENAME: mockupFormat → format, mockup → content)
227
338
  wireframes: {
228
339
  [moduleCode]: rawWireframes.map(wf => ({
@@ -287,6 +398,7 @@ dependencyGraph: {
287
398
  - FEATURE_DATA.moduleSpecs must have ONE entry per module
288
399
  - cadrage.scope uses HTML keys (inscope/outofscope)
289
400
  - Wireframe fields use format/content (NOT mockupFormat/mockup)
401
+ - Per-module: useCases/businessRules/entities count must match source (empty when source has data = BUG)
290
402
 
291
403
  ## NEXT STEP
292
404
 
@@ -8,7 +8,7 @@ model: opus
8
8
 
9
9
  ## YOUR TASK
10
10
 
11
- Run 5 blocking validations on the generated HTML file and display the completion summary.
11
+ Run 6 blocking validations on the generated HTML file and display the completion summary.
12
12
 
13
13
  ---
14
14
 
@@ -53,6 +53,22 @@ FOR each module in FEATURE_DATA.modules:
53
53
  BLOCKING_ERROR("Module {module.code} not in wireframes")
54
54
  ```
55
55
 
56
+ **Check 6 — Per-module data completeness (source vs HTML):**
57
+ ```
58
+ FOR each module in FEATURE_DATA.modules:
59
+ Read source: docs/{app}/{module.code}/business-analyse/v{version}/usecases.json
60
+ sourceUCCount = count of useCases in source file
61
+ htmlUCCount = count in FEATURE_DATA.moduleSpecs[module.code].useCases
62
+ IF sourceUCCount > 0 AND htmlUCCount === 0:
63
+ BLOCKING_ERROR("Module {module.code}: 0 useCases in HTML but {sourceUCCount} in source")
64
+
65
+ Read source: docs/{app}/{module.code}/business-analyse/v{version}/rules.json
66
+ sourceBRCount = count of rules in source file
67
+ htmlBRCount = count in FEATURE_DATA.moduleSpecs[module.code].businessRules
68
+ IF sourceBRCount > 0 AND htmlBRCount === 0:
69
+ BLOCKING_ERROR("Module {module.code}: 0 businessRules in HTML but {sourceBRCount} in source")
70
+ ```
71
+
56
72
  > **IF any check fails:** fix the issue and re-run the failing step before completing.
57
73
 
58
74
  ---