@atlashub/smartstack-cli 4.27.0 → 4.29.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 (29) hide show
  1. package/package.json +1 -1
  2. package/templates/skills/apex/references/core-seed-data.md +27 -4
  3. package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +29 -7
  4. package/templates/skills/apex/references/post-checks.md +324 -0
  5. package/templates/skills/apex/references/smartstack-frontend.md +23 -8
  6. package/templates/skills/apex/references/smartstack-layers.md +53 -6
  7. package/templates/skills/apex/steps/step-02-plan.md +9 -0
  8. package/templates/skills/apex/steps/step-03-execute.md +49 -3
  9. package/templates/skills/apex/steps/step-04-examine.md +4 -0
  10. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +33 -0
  11. package/templates/skills/business-analyse/questionnaire/01-context.md +12 -12
  12. package/templates/skills/business-analyse/questionnaire/02-stakeholders-scope.md +45 -45
  13. package/templates/skills/business-analyse/questionnaire/03-data-ui.md +39 -39
  14. package/templates/skills/business-analyse/questionnaire/05-cross-module.md +32 -32
  15. package/templates/skills/business-analyse/questionnaire.md +11 -11
  16. package/templates/skills/business-analyse/references/consolidation-structural-checks.md +17 -0
  17. package/templates/skills/business-analyse/references/spec-auto-inference.md +12 -7
  18. package/templates/skills/business-analyse/steps/step-00-init.md +2 -2
  19. package/templates/skills/business-analyse/steps/step-01-cadrage.md +3 -3
  20. package/templates/skills/business-analyse/steps/step-02-structure.md +22 -8
  21. package/templates/skills/business-analyse/steps/step-03-specify.md +22 -15
  22. package/templates/skills/controller/references/mcp-scaffold-workflow.md +20 -0
  23. package/templates/skills/derive-prd/references/handoff-file-templates.md +25 -1
  24. package/templates/skills/derive-prd/references/handoff-seeddata-generation.md +3 -1
  25. package/templates/skills/ralph-loop/references/category-completeness.md +125 -0
  26. package/templates/skills/ralph-loop/references/compact-loop.md +66 -10
  27. package/templates/skills/ralph-loop/references/module-transition.md +60 -0
  28. package/templates/skills/ralph-loop/steps/step-04-check.md +207 -12
  29. package/templates/skills/ralph-loop/steps/step-05-report.md +205 -14
@@ -53,27 +53,27 @@
53
53
 
54
54
  ### Phase de cadrage (step-01)
55
55
 
56
- 1. **Debut :** Commencer par 01-context (contexte metier, identite application)
56
+ 1. **Début :** Commencer par 01-context (contexte métier, identité application)
57
57
  2. **Adapter :** Sauter les questions non pertinentes selon le contexte
58
- 3. **Approfondir :** Poser des questions de relance sur les reponses vagues
59
- 4. **Challenger :** Ne pas accepter "on verra plus tard" sur les fonctionnalites indispensables
60
- 5. **Par lots :** Presenter 3 a 4 questions par interaction (AskUserQuestion)
58
+ 3. **Approfondir :** Poser des questions de relance sur les réponses vagues
59
+ 4. **Challenger :** Ne pas accepter "on verra plus tard" sur les fonctionnalités indispensables
60
+ 5. **Par lots :** Présenter 3 a 4 questions par interaction (AskUserQuestion)
61
61
 
62
62
  ### Phase de specification (step-03)
63
63
 
64
- 1. **Par module :** Charger 03-data-ui pour chaque module en cours de specification
65
- 2. **Cross-module :** Charger 05-cross-module si le module a des dependances identifiees en step-02
64
+ 1. **Par module :** Charger 03-data-ui pour chaque module en cours de spécification
65
+ 2. **Cross-module :** Charger 05-cross-module si le module a des dépendances identifiées en step-02
66
66
 
67
67
  ### Questions de relance
68
68
 
69
- Pour chaque reponse vague, utiliser :
69
+ Pour chaque réponse vague, utiliser :
70
70
  - "Pouvez-vous me donner un exemple concret ?"
71
71
  - "Comment mesurez-vous cela aujourd'hui ?"
72
- - "Que se passe-t-il si cette regle n'est pas respectee ?"
73
- - "Qui est impacte par cette decision ?"
72
+ - "Que se passe-t-il si cette règle n'est pas respectée ?"
73
+ - "Qui est impacté par cette décision ?"
74
74
 
75
75
  ### Filtre de pertinence
76
76
 
77
- Chaque question conservee passe ce test :
78
- > "La reponse change-t-elle quelque chose dans le systeme a construire ?"
77
+ Chaque question conservée passe ce test :
78
+ > "La réponse change-t-elle quelque chose dans le système a construire ?"
79
79
  > Oui -> Conserver | Non -> Supprimer
@@ -89,6 +89,21 @@ FOR each entity in analysis.entities[]:
89
89
  IF endpoints.length === 0 -> ERROR: "Entity {entity.name} has NO endpoints — missing controller"
90
90
  ```
91
91
 
92
+ ## H. Permission Granularity Check
93
+
94
+ > Validates that section permission modes are respected in the generated permission data.
95
+
96
+ ```
97
+ FOR each module:
98
+ FOR each section in anticipatedSections[]:
99
+ IF section.sectionType == "view" OR section.permissionMode == "inherit":
100
+ IF section has own permissions in seedDataCore.permissions[] -> WARNING: "Section '{code}' is type 'view' but has own permissions — should inherit from module"
101
+ IF section.permissionMode == "read-only":
102
+ IF section has create/update/delete permissions -> ERROR: "Section '{code}' is read-only but has write permissions"
103
+ IF section.sectionType == "primary" AND NOT section.permissionMode:
104
+ -> WARNING: "Primary section '{code}' missing explicit permissionMode — defaulting to 'crud'"
105
+ ```
106
+
92
107
  ## Result Aggregation
93
108
 
94
109
  ```json
@@ -100,6 +115,8 @@ FOR each entity in analysis.entities[]:
100
115
  { "check": "id-uniqueness", "module": "...", "status": "PASS|ERROR", "details": "..." },
101
116
  { "check": "wireframe-layout", "module": "...", "status": "PASS|ERROR", "details": "..." },
102
117
  { "check": "seeddata-translations", "module": "...", "status": "PASS|ERROR", "details": "..." }
118
+ ,
119
+ { "check": "permission-granularity", "module": "...", "status": "PASS|WARNING|ERROR", "details": "..." }
103
120
  ]
104
121
  }
105
122
  ```
@@ -34,13 +34,18 @@
34
34
 
35
35
  > **RULE:** Sections = functional zones only. `create`/`edit` are separate pages with own URL routes (`/create` and `/:id/edit`). `detail` is a tabbed page reached from `list`.
36
36
 
37
- | featureType | Sections (functional zones) | List page includes | Form pages | Detail page tabs |
38
- |---|---|---|---|---|
39
- | data-centric | list | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations} |
40
- | workflow | list, approve | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations}, Historique |
41
- | integration | list | grid, filters, config button | `/create` page, `/:id/edit` page | Infos, Config, Logs |
42
- | reporting | dashboard | — | — | — |
43
- | full-module | list, dashboard | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations}, Historique |
37
+ | featureType | Sections (functional zones) | permissionMode | List page includes | Form pages | Detail page tabs |
38
+ |---|---|---|---|---|---|
39
+ | data-centric | list | `crud` | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations} |
40
+ | data-centric | (detail implicit) | `inherit` | | | |
41
+ | workflow | list | `crud` | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations}, Historique |
42
+ | workflow | approve | `custom:approve,reject` | — | — | — |
43
+ | integration | list | `crud` | grid, filters, config button | `/create` page, `/:id/edit` page | Infos, Config, Logs |
44
+ | reporting | dashboard | `read-only` | — | — | — |
45
+ | full-module | list | `crud` | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations}, Historique |
46
+ | full-module | dashboard | `read-only` | — | — | — |
47
+
48
+ > **RULE:** `detail` is NEVER a section with its own permission set. It is always `sectionType: view`, `permissionMode: inherit`. Detail pages inherit permissions from their parent module.
44
49
 
45
50
  ## Component Generation Rules
46
51
 
@@ -239,7 +239,7 @@ Determine the language for analysis and code generation.
239
239
 
240
240
  **Check config:**
241
241
  - Retrieve `language` from `.business-analyse/config.json`
242
- - Default: "fr" (Francais)
242
+ - Default: "fr" (Français)
243
243
 
244
244
  **If not in config:**
245
245
  ```
@@ -247,7 +247,7 @@ Ask via AskUserQuestion:
247
247
  question: "Quelle langue pour l'analyse ?"
248
248
  header: "Langue"
249
249
  options:
250
- - label: "Francais (fr)"
250
+ - label: "Français (fr)"
251
251
  - label: "English (en)"
252
252
  - label: "Italiano (it)"
253
253
  - label: "Deutsch (de)"
@@ -32,16 +32,16 @@ Frame the analysis scope at the **application level** through an interactive con
32
32
  ## EXECUTION FLOW — 5 PHASES
33
33
 
34
34
  ```
35
- Phase 1: ECOUTE → Read brief + codebase pre-research + silent pre-analysis
35
+ Phase 1: ÉCOUTE → Read brief + codebase pre-research + silent pre-analysis
36
36
  Phase 2: REFORMULATION → Rephrase the need back to the client for validation
37
37
  Phase 3: APPROFONDISSEMENT → Challenge assumptions with targeted questionnaires
38
38
  Phase 4: ANTICIPATION → Suggest unexpressed needs from domain expertise
39
- Phase 5: PERIMETRE → Bound scope with roles, coverage matrix (sections + resources)
39
+ Phase 5: PÉRIMÈTRE → Bound scope with roles, coverage matrix (sections + resources)
40
40
  ```
41
41
 
42
42
  ---
43
43
 
44
- ## PHASE 1: ECOUTE (Listen)
44
+ ## PHASE 1: ÉCOUTE (Listen)
45
45
 
46
46
  ### 1. Read Current State
47
47
 
@@ -72,16 +72,27 @@ For each module, anticipate the navigation structure:
72
72
  For each section:
73
73
  - code (kebab-case, e.g., "list", "detail", "dashboard")
74
74
  - label (display name)
75
+ - sectionType: primary | functional | view | embedded
76
+ - permissionMode: crud | custom | read-only | inherit
75
77
  - resources: [
76
78
  { code, type (SmartTable|SmartForm|SmartCard|SmartKanban|SmartDashboard|SmartFilter), label }
77
79
  ]
78
80
  ```
79
81
 
82
+ **Section classification rules:**
83
+
84
+ | sectionType | permissionMode | When to use | Examples |
85
+ |---|---|---|---|
86
+ | `primary` | `crud` | Main entry point of the module, visible in menu | list |
87
+ | `functional` | `crud` or `custom` | Independent functional zone with own access control | approve, import, planning |
88
+ | `view` | `inherit` | Subordinate view reached from a primary section | detail, edit |
89
+ | `embedded` | `read-only` | Widget or tab embedded in another section | dashboard (when embedded in module) |
90
+
80
91
  Common patterns:
81
- - **Data-centric module**: list (SmartTable) + detail (SmartForm)
82
- - **Workflow module**: list + detail + kanban (SmartKanban)
83
- - **Reporting module**: dashboard (SmartDashboard) + detail
84
- - **Full module**: list + detail + dashboard
92
+ - **Data-centric module**: list (`primary`/`crud`) + detail (`view`/`inherit`)
93
+ - **Workflow module**: list (`primary`/`crud`) + detail (`view`/`inherit`) + approve (`functional`/`custom`)
94
+ - **Reporting module**: dashboard (`primary`/`read-only`) + detail (`view`/`inherit`)
95
+ - **Full module**: list (`primary`/`crud`) + detail (`view`/`inherit`) + dashboard (`embedded`/`read-only`)
85
96
 
86
97
  ### 4. Dependency Graph
87
98
 
@@ -111,6 +122,9 @@ For EACH identified element, ask yourself:
111
122
  - Does this module need a dashboard?
112
123
  - Is the list/detail pattern sufficient or are there other views?
113
124
  - Are there workflow steps that need dedicated sections?
125
+ - Does this section need its own access control? If not → `view` or `embedded`
126
+ - Is this a read-only view (dashboard, balances, statistics)? If yes → `permissionMode: read-only`
127
+ - Is this section reached by clicking a row in another section? If yes → always `view`
114
128
 
115
129
  **Resources:**
116
130
  - Is SmartTable the right component for this list?
@@ -154,18 +168,18 @@ Write via ba-writer:
154
168
  {
155
169
  "code": "Employees",
156
170
  "applicationCode": "HumanResources",
157
- "name": "Employes",
171
+ "name": "Employés",
158
172
  "featureType": "data-centric",
159
173
  "priority": "must",
160
174
  "entities": ["Employee", "Contract"],
161
175
  "anticipatedSections": [
162
- { "code": "list", "label": "Liste", "resources": [{ "code": "employees-grid", "type": "SmartTable" }] },
163
- { "code": "detail", "label": "Fiche", "resources": [{ "code": "employee-form", "type": "SmartForm" }] }
176
+ { "code": "list", "label": "Liste", "sectionType": "primary", "permissionMode": "crud", "resources": [{ "code": "employees-grid", "type": "SmartTable" }] },
177
+ { "code": "detail", "label": "Fiche", "sectionType": "view", "permissionMode": "inherit", "resources": [{ "code": "employee-form", "type": "SmartForm" }] }
164
178
  ]
165
179
  }
166
180
  ],
167
181
  "dependencies": [
168
- { "from": "Absences", "to": "Employees", "description": "Une absence reference un employe" }
182
+ { "from": "Absences", "to": "Employees", "description": "Une absence référence un employé" }
169
183
  ]
170
184
  }
171
185
  ```
@@ -44,18 +44,18 @@ For each entity identified in step 02:
44
44
  ```json
45
45
  {
46
46
  "name": "Employee",
47
- "description": "Represente un employe de l'entreprise",
47
+ "description": "Représente un employé de l'entreprise",
48
48
  "attributes": [
49
49
  { "name": "code", "type": "string", "required": true, "description": "Identifiant unique" },
50
- { "name": "userId", "type": "guid", "required": true, "description": "Reference vers l'utilisateur" },
51
- { "name": "departmentId", "type": "guid", "required": true, "description": "Departement d'affectation" },
50
+ { "name": "userId", "type": "guid", "required": true, "description": "Référence vers l'utilisateur" },
51
+ { "name": "departmentId", "type": "guid", "required": true, "description": "Département d'affectation" },
52
52
  { "name": "hireDate", "type": "date", "required": true, "description": "Date d'embauche" },
53
- { "name": "position", "type": "string", "description": "Poste occupe" },
54
- { "name": "status", "type": "enum", "options": ["Active", "Inactive", "OnLeave", "Terminated"], "description": "Statut de l'employe" }
53
+ { "name": "position", "type": "string", "description": "Poste occupé" },
54
+ { "name": "status", "type": "enum", "options": ["Active", "Inactive", "OnLeave", "Terminated"], "description": "Statut de l'employé" }
55
55
  ],
56
56
  "relationships": [
57
- { "target": "Department", "type": "ManyToOne", "description": "Appartient a un departement" },
58
- { "target": "Contract", "type": "OneToMany", "description": "Possede plusieurs contrats" }
57
+ { "target": "Department", "type": "ManyToOne", "description": "Appartient à un département" },
58
+ { "target": "Contract", "type": "OneToMany", "description": "Possède plusieurs contrats" }
59
59
  ]
60
60
  }
61
61
  ```
@@ -69,7 +69,7 @@ For each entity/process, identify rules:
69
69
  "id": "BR-VAL-EMPLOYEES-001",
70
70
  "name": "Validation date embauche",
71
71
  "category": "validation",
72
- "statement": "La date d'embauche ne peut pas etre dans le futur",
72
+ "statement": "La date d'embauche ne peut pas être dans le futur",
73
73
  "example": "Date embauche = 2027-01-01 → erreur car > date du jour",
74
74
  "entities": ["Employee"],
75
75
  "severity": "blocking"
@@ -85,19 +85,19 @@ For each stakeholder action:
85
85
  ```json
86
86
  {
87
87
  "id": "UC-EMPLOYEES-001",
88
- "name": "Creer un employe",
88
+ "name": "Créer un employé",
89
89
  "actor": "Responsable RH",
90
90
  "preconditions": ["L'utilisateur a la permission HumanResources.Employees.Create"],
91
91
  "steps": [
92
- "L'utilisateur ouvre la page de creation",
93
- "Il remplit les champs obligatoires (nom, departement, date embauche)",
92
+ "L'utilisateur ouvre la page de création",
93
+ "Il remplit les champs obligatoires (nom, département, date embauche)",
94
94
  "Il valide le formulaire",
95
- "Le systeme verifie les regles metier (BR-VAL-EMPLOYEES-001)",
96
- "Le systeme cree l'employe et affiche la fiche"
95
+ "Le système vérifie les règles métier (BR-VAL-EMPLOYEES-001)",
96
+ "Le système crée l'employé et affiche la fiche"
97
97
  ],
98
- "alternative": "Si les donnees sont invalides, le systeme affiche les erreurs",
98
+ "alternative": "Si les données sont invalides, le système affiche les erreurs",
99
99
  "businessRules": ["BR-VAL-EMPLOYEES-001"],
100
- "result": "L'employe est cree avec le statut 'Actif'"
100
+ "result": "L'employé est créé avec le statut 'Actif'"
101
101
  }
102
102
  ```
103
103
 
@@ -123,6 +123,10 @@ Define the permission matrix:
123
123
  }
124
124
  ```
125
125
 
126
+ **Auto-detection rules:**
127
+ - If the cadrage mentions "export", "Excel", "CSV", or "télécharger" → automatically add `.export` permission and an export use case (UC-{PREFIX}-EXPORT)
128
+ - If the cadrage mentions "import", "importer", "upload" → automatically add `.import` permission and an import use case (UC-{PREFIX}-IMPORT)
129
+
126
130
  ### F. Interface Specs — Delegated to /ba-design-ui
127
131
 
128
132
  > **Screen specifications are NOT produced in this step.**
@@ -163,6 +167,9 @@ Before advancing to step 04, verify:
163
167
  - [ ] All entity relationships reference existing entities
164
168
  - [ ] All UC business rule references exist
165
169
  - [ ] All permission paths follow the convention
170
+ - [ ] Sections with `sectionType: view` do NOT appear in `permissionPaths` (they inherit from module)
171
+ - [ ] Sections with `permissionMode: read-only` only have `.read` in `permissionPaths` (no create/update/delete)
172
+ - [ ] Sections with `permissionMode: inherit` have ZERO entries in `permissionPaths`
166
173
 
167
174
  ## Transition
168
175
 
@@ -123,6 +123,26 @@ if (actualNavRoute !== permission_path) {
123
123
  console.warn(` Got: "${actualNavRoute}"`);
124
124
  // Warning only — proceed
125
125
  }
126
+
127
+ // Validate segment count
128
+ const segments = actualNavRoute ? actualNavRoute.split('.').length : 0;
129
+ if (segments < 2) {
130
+ console.error(`BLOCKING: NavRoute "${actualNavRoute}" has only ${segments} segment(s) — minimum 2 required`);
131
+ console.error(` Format: "app.module" (2 segments) or "app.module.section" (3 segments)`);
132
+ STOP;
133
+ }
134
+
135
+ // If entity is in a section subfolder, NavRoute must have 3+ segments
136
+ // Check: controller path has 3+ levels after Controllers/ → section-level
137
+ const controllerDir = controllerCall.controllerFile.replace(/[^/]*$/, '');
138
+ const depthAfterControllers = controllerDir.split('Controllers/')[1]?.split('/').filter(Boolean).length || 0;
139
+ if (depthAfterControllers >= 2 && segments < 3) {
140
+ console.warn(`WARNING: Controller is in a section subfolder (depth=${depthAfterControllers})`);
141
+ console.warn(` but NavRoute "${actualNavRoute}" has only ${segments} segments`);
142
+ console.warn(` Expected 3 segments: "app.module.section"`);
143
+ console.warn(` A 2-segment NavRoute on a section controller causes API 404s`);
144
+ // Warning — developer should verify and fix
145
+ }
126
146
  ```
127
147
 
128
148
  ---
@@ -30,6 +30,12 @@ From `usecases.json > useCases[]`:
30
30
 
31
31
  Include: Service per UC cluster, DTOs for API contracts, Validators (FluentValidation), Query handlers
32
32
 
33
+ **Validator generation rules:**
34
+ - Every entity with Create and/or Update use cases MUST have a corresponding Validator
35
+ - Validators MUST be registered in DI (`services.AddScoped<IValidator<CreateXxxDto>, CreateXxxValidator>()`)
36
+ - Validators MUST be injected into controllers/services that handle POST/PUT operations
37
+ - NO TODO/placeholder comments allowed in Validators — all validation rules from business rules (BR-VAL-*) must be implemented
38
+
33
39
  ## 4.3 Infrastructure Files
34
40
 
35
41
  From `entities.json > entities[]`:
@@ -48,7 +54,8 @@ Generated from `usecases.json` + `entities.json`:
48
54
 
49
55
  ```json
50
56
  "api": [
51
- { "path": "src/API/Controllers/{ApplicationName}/{EntityName}Controller.cs", "type": "ApiController", "linkedUCs": [], "linkedFRs": [], "module": "{moduleCode}" }
57
+ { "path": "src/API/Controllers/{ApplicationName}/{EntityName}Controller.cs", "type": "ApiController", "navRoute": "{app-kebab}.{module-kebab}", "isSection": false, "linkedUCs": [], "linkedFRs": [], "module": "{moduleCode}" },
58
+ { "path": "src/API/Controllers/{ApplicationName}/{ModuleName}/{SectionEntityName}Controller.cs", "type": "ApiController", "navRoute": "{app-kebab}.{module-kebab}.{section-kebab}", "isSection": true, "linkedUCs": [], "linkedFRs": [], "module": "{moduleCode}" }
52
59
  ]
53
60
  ```
54
61
 
@@ -80,6 +87,23 @@ From `screens.json > screens[]` and `usecases.json > useCases[]`:
80
87
 
81
88
  **Dashboard acceptance criteria:** Chart library (Recharts), chart types matching spec, KPI cards, filters, CSS variables, responsive layout, wireframe-matching positions.
82
89
 
90
+ ## 4.5b Notification Files (CONDITIONAL)
91
+
92
+ > Generated only when `lifeCycles[].transitions[].effects[]` contains entries with `type: "notification"`.
93
+
94
+ ```json
95
+ "notifications": [
96
+ { "path": "src/Application/Notifications/{ApplicationName}/{ModuleName}/{NotificationName}Notification.cs", "type": "Notification", "linkedUCs": [], "module": "{moduleCode}", "description": "Notification triggered by lifecycle transition" },
97
+ { "path": "src/Application/Notifications/{ApplicationName}/{ModuleName}/{NotificationName}NotificationHandler.cs", "type": "NotificationHandler", "linkedUCs": [], "module": "{moduleCode}", "description": "Handler that sends in-app/email notification" }
98
+ ]
99
+ ```
100
+
101
+ **Generation rules:**
102
+ - One Notification + Handler pair per unique `notification` effect in lifecycle transitions
103
+ - NotificationName derived from transition: `{Entity}{TransitionName}` (e.g., `OrderApproved`)
104
+ - Handler must use `INotificationService` for in-app and `IEmailService` for email type
105
+ - Notification must include: recipient resolution, template reference, and payload mapping
106
+
83
107
  ## 4.6 SeedData Files
84
108
 
85
109
  **OBLIGATORY: 2 app-level CORE + per module CORE (NavigationModule + NavigationSections + Permissions + Roles) + business per module:**
@@ -127,7 +127,9 @@ const seedDataCore = {
127
127
  ? `/${toKebabCase(appCode)}/${toKebabCase(m.code)}/:id`
128
128
  : `/${toKebabCase(appCode)}/${toKebabCase(m.code)}/${toKebabCase(s.code)}`,
129
129
  displayOrder: (j + 1) * 10,
130
- navigation: s.code === "detail" ? "hidden" : "visible"
130
+ navigation: s.code === "detail" ? "hidden" : "visible",
131
+ // Propagate permissionMode for section-level permission generation
132
+ permissionMode: s.permissionMode || (s.code === "detail" ? "inherit" : s.code === "dashboard" ? "read-only" : "crud")
131
133
  }))
132
134
  );
133
135
 
@@ -191,6 +191,129 @@ for (const [cat, check] of Object.entries(artifactChecks)) {
191
191
  }
192
192
  ```
193
193
 
194
+ ## Entity-Level File Completeness Check (BLOCKING)
195
+
196
+ > **LESSON LEARNED (audit ba-002):** Artifact verification checked "at least one file per category"
197
+ > but never reconciled against the **handoff contract** (`prd.implementation.filesToCreate`).
198
+ > Result: 4/17 API endpoints were missing but the category showed "complete" because *some* controllers existed.
199
+
200
+ ```javascript
201
+ // BLOCKING: Verify EVERY file from prd.implementation.filesToCreate exists on disk
202
+ const filesToCreate = prd.implementation?.filesToCreate;
203
+ if (filesToCreate) {
204
+ const handoffMissing = [];
205
+ const handoffPresent = [];
206
+
207
+ for (const [category, files] of Object.entries(filesToCreate)) {
208
+ for (const file of (files || [])) {
209
+ const filePath = file.path || file;
210
+ if (fileExists(filePath)) {
211
+ handoffPresent.push({ category, path: filePath });
212
+ } else {
213
+ handoffMissing.push({ category, path: filePath });
214
+ }
215
+ }
216
+ }
217
+
218
+ const totalHandoff = handoffPresent.length + handoffMissing.length;
219
+ const coveragePct = totalHandoff > 0 ? Math.round((handoffPresent.length / totalHandoff) * 100) : 100;
220
+ console.log(`Handoff file coverage: ${handoffPresent.length}/${totalHandoff} (${coveragePct}%)`);
221
+
222
+ if (handoffMissing.length > 0) {
223
+ console.error(`BLOCKING: ${handoffMissing.length} files from handoff contract missing on disk`);
224
+
225
+ // Group missing files by category for targeted remediation
226
+ const missingByCategory = {};
227
+ for (const m of handoffMissing) {
228
+ if (!missingByCategory[m.category]) missingByCategory[m.category] = [];
229
+ missingByCategory[m.category].push(m.path);
230
+ }
231
+
232
+ // Inject remediation tasks for each missing file
233
+ let maxIdNum = Math.max(...prd.tasks.map(t => {
234
+ const num = parseInt(t.id.replace(/[^0-9]/g, ''), 10);
235
+ return isNaN(num) ? 0 : num;
236
+ }), 0);
237
+ const prefix = prd.tasks[0]?.id?.replace(/[0-9]+$/, '') || 'HNDOFF-';
238
+
239
+ for (const [cat, paths] of Object.entries(missingByCategory)) {
240
+ for (const p of paths) {
241
+ // Skip if a remediation task already exists for this file
242
+ const alreadyExists = prd.tasks.some(t =>
243
+ t.description.includes(p) && t.description.includes('[HANDOFF-REMEDIATION]')
244
+ );
245
+ if (alreadyExists) continue;
246
+
247
+ maxIdNum++;
248
+ prd.tasks.push({
249
+ id: `${prefix}${String(maxIdNum).padStart(3, '0')}`,
250
+ description: `[HANDOFF-REMEDIATION] Create missing file: ${p}`,
251
+ status: 'pending',
252
+ category: cat,
253
+ dependencies: [],
254
+ acceptance_criteria: `File ${p} exists on disk and compiles`,
255
+ started_at: null, completed_at: null, iteration: null,
256
+ commit_hash: null, files_changed: [], validation: null, error: null
257
+ });
258
+ }
259
+ console.error(` ${cat}: ${paths.length} missing — ${paths.map(p => p.split('/').pop()).join(', ')}`);
260
+ }
261
+
262
+ writeJSON(currentPrdPath, prd);
263
+ }
264
+ } // end filesToCreate check
265
+ ```
266
+
267
+ ## Type Reference Verification (Cross-File Integrity)
268
+
269
+ > **LESSON LEARNED (audit ba-002):** Artifact verification counted files but didn't check
270
+ > that types referenced in code actually exist. `EmployeeListDto` was used in services
271
+ > but the file was missing — a "phantom reference" that artifact file-counting missed.
272
+
273
+ ```javascript
274
+ // After artifact checks, verify cross-file type references for completed categories
275
+ if (completedCats.has('application') && completedCats.has('api')) {
276
+ const serviceFiles = glob('src/**/Services/**/*Service.cs');
277
+ const dtoFiles = glob('src/**/DTOs/**/*.cs');
278
+ const entityFiles = glob('src/**/Domain/**/*.cs');
279
+
280
+ // Extract all DTO type names from actual files
281
+ const existingTypes = new Set();
282
+ for (const f of [...dtoFiles, ...entityFiles]) {
283
+ const content = readFile(f);
284
+ const typeMatches = content.matchAll(/(?:class|record|struct|enum|interface)\s+(\w+)/g);
285
+ for (const m of typeMatches) existingTypes.add(m[1]);
286
+ }
287
+
288
+ // Check service files for references to types that don't exist
289
+ const phantomRefs = [];
290
+ for (const f of serviceFiles) {
291
+ const content = readFile(f);
292
+ // Look for Dto/Entity type references in return types and method params
293
+ const typeRefs = content.matchAll(/(?:<|,\s*|\(|new\s+)(\w+(?:Dto|Entity|Exception))\b/g);
294
+ for (const m of typeRefs) {
295
+ if (!existingTypes.has(m[1]) && !m[1].startsWith('I')) {
296
+ phantomRefs.push({ file: f, type: m[1] });
297
+ }
298
+ }
299
+ }
300
+
301
+ if (phantomRefs.length > 0) {
302
+ console.error(`PHANTOM TYPE REFERENCES DETECTED (${phantomRefs.length}):`);
303
+ phantomRefs.forEach(r => console.error(` ${r.file}: references ${r.type} — TYPE DOES NOT EXIST`));
304
+
305
+ // Reset application category tasks to pending — code references non-existent types
306
+ prd.tasks.filter(t => t.category === 'application' && t.status === 'completed')
307
+ .forEach(t => {
308
+ t.status = 'pending';
309
+ t.error = `Phantom type references: ${phantomRefs.map(r=>r.type).join(', ')}`;
310
+ t.completed_at = null;
311
+ });
312
+ writeJSON(currentPrdPath, prd);
313
+ }
314
+ }
315
+ ```
316
+
194
317
  ---
195
318
 
196
319
  ## Key Rules
@@ -198,4 +321,6 @@ for (const [cat, check] of Object.entries(artifactChecks)) {
198
321
  - **Inject EVERY iteration:** Not just once during load
199
322
  - **Frontend depends on seedData:** Not just API
200
323
  - **Check artifacts:** Mark tasks pending if files don't exist
324
+ - **Check type references:** Verify types used in code actually exist (phantom reference detection)
325
+ - **Check handoff files:** Verify EVERY file from `prd.implementation.filesToCreate` exists on disk
201
326
  - **Never skip:** This is the blocker for "missing frontend/test" failures
@@ -289,6 +289,35 @@ if (pending > 0) {
289
289
  console.log(`Apex completed: ${completed}/${batchIds.length} tasks`);
290
290
  ```
291
291
 
292
+ ### B3b. Post-Batch Build Verification (BLOCKING)
293
+
294
+ > **LESSON LEARNED (audit ba-002):** Without build checks between batches, compilation
295
+ > errors from early batches propagate silently through all subsequent batches.
296
+ > The final "Build PASS" was never actually verified.
297
+
298
+ ```bash
299
+ # Quick build check after each batch — BLOCKING if fails
300
+ dotnet build --no-restore --verbosity quiet
301
+ if [ $? -ne 0 ]; then
302
+ echo "BLOCKING: Build fails after batch [${firstCategory}]"
303
+ # Inject immediate fix task — will be picked up next iteration
304
+ fi
305
+ ```
306
+
307
+ ```javascript
308
+ if (BUILD_FAILED) {
309
+ const maxId = Math.max(...updatedPrd.tasks.map(t => parseInt(t.id.replace(/\D/g,''))||0));
310
+ updatedPrd.tasks.push({
311
+ id: `FIX-${maxId+1}`,
312
+ description: `BLOCKING: Fix build regression after ${firstCategory} batch`,
313
+ status: 'pending', category: 'validation', dependencies: [],
314
+ acceptance_criteria: 'dotnet build passes with 0 errors'
315
+ });
316
+ writeJSON(currentPrdPath, updatedPrd);
317
+ // Continue to section C (commit) then D (loop) — fix task will be picked up next iteration
318
+ }
319
+ ```
320
+
292
321
  ---
293
322
 
294
323
  ## C. Commit PRD State
@@ -310,27 +339,54 @@ appendFile('.ralph/progress.txt',
310
339
 
311
340
  After apex returns, check for newly skipped tasks:
312
341
 
342
+ > **LESSON LEARNED (audit ba-002):** Individual skips in critical categories (api, test, domain,
343
+ > infrastructure) allowed 4/17 endpoints and 74% stub tests to pass undetected. The old logic
344
+ > only blocked when ALL tasks in a category were skipped — individual skips slipped through.
345
+
313
346
  ```javascript
347
+ const CRITICAL_CATEGORIES = ['api', 'test', 'domain', 'infrastructure'];
348
+ const MAX_SKIP_RETRIES = 3;
349
+
314
350
  const newlySkipped = prdCheck.tasks.filter(t =>
315
351
  batchIds.includes(t.id) && t.status === 'skipped'
316
352
  );
317
353
  if (newlySkipped.length > 0) {
318
354
  console.warn(`⚠ ${newlySkipped.length} tasks were SKIPPED by apex:`);
319
- newlySkipped.forEach(t => console.warn(` - ${t.id}: ${t.description}`));
355
+ newlySkipped.forEach(t => console.warn(` - ${t.id}: ${t.description} [${t.category}]`));
320
356
 
321
- // If ALL tasks in a category were skipped → BLOCKING, reset for retry
322
357
  const skippedCategories = [...new Set(newlySkipped.map(t => t.category))];
323
358
  for (const cat of skippedCategories) {
359
+ const isCritical = CRITICAL_CATEGORIES.includes(cat);
324
360
  const allInCat = prdCheck.tasks.filter(t => t.category === cat);
325
361
  const allSkipped = allInCat.every(t => t.status === 'skipped');
326
- if (allSkipped) {
327
- console.error(`BLOCKING: ALL tasks in category "${cat}" were skipped — investigate root cause`);
328
- // Reset to pending for retry
329
- allInCat.forEach(t => {
330
- t.status = 'pending';
331
- t._retryCount = (t._retryCount || 0) + 1;
332
- t.error = `All tasks in category "${cat}" skipped — auto-retry`;
333
- });
362
+
363
+ if (isCritical) {
364
+ // CRITICAL CATEGORY: Block on INDIVIDUAL skips (not just when ALL are skipped)
365
+ const skippedInCat = newlySkipped.filter(t => t.category === cat);
366
+ for (const t of skippedInCat) {
367
+ const retryCount = t._skipRetryCount || 0;
368
+ if (retryCount < MAX_SKIP_RETRIES) {
369
+ console.error(`BLOCKING: Task ${t.id} skipped in critical category "${cat}" — reset to pending (retry ${retryCount + 1}/${MAX_SKIP_RETRIES})`);
370
+ t.status = 'pending';
371
+ t._skipRetryCount = retryCount + 1;
372
+ t._retryCount = (t._retryCount || 0) + 1;
373
+ t.error = `Skipped in critical category "${cat}" — auto-retry ${retryCount + 1}/${MAX_SKIP_RETRIES}`;
374
+ } else {
375
+ console.error(`FAILED: Task ${t.id} skipped ${MAX_SKIP_RETRIES} times in critical category "${cat}" — marking failed`);
376
+ t.status = 'failed';
377
+ t.error = `Skipped ${MAX_SKIP_RETRIES} times in critical category "${cat}" — max retries exhausted`;
378
+ }
379
+ }
380
+ } else {
381
+ // NON-CRITICAL CATEGORY: Block only when ALL tasks in category are skipped (existing behavior)
382
+ if (allSkipped) {
383
+ console.error(`BLOCKING: ALL tasks in category "${cat}" were skipped — investigate root cause`);
384
+ allInCat.forEach(t => {
385
+ t.status = 'pending';
386
+ t._retryCount = (t._retryCount || 0) + 1;
387
+ t.error = `All tasks in category "${cat}" skipped — auto-retry`;
388
+ });
389
+ }
334
390
  }
335
391
  }
336
392
  writeJSON(currentPrdPath, prdCheck);