@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.
- package/package.json +1 -1
- package/templates/skills/apex/references/core-seed-data.md +27 -4
- package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +29 -7
- package/templates/skills/apex/references/post-checks.md +324 -0
- package/templates/skills/apex/references/smartstack-frontend.md +23 -8
- package/templates/skills/apex/references/smartstack-layers.md +53 -6
- package/templates/skills/apex/steps/step-02-plan.md +9 -0
- package/templates/skills/apex/steps/step-03-execute.md +49 -3
- package/templates/skills/apex/steps/step-04-examine.md +4 -0
- package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +33 -0
- package/templates/skills/business-analyse/questionnaire/01-context.md +12 -12
- package/templates/skills/business-analyse/questionnaire/02-stakeholders-scope.md +45 -45
- package/templates/skills/business-analyse/questionnaire/03-data-ui.md +39 -39
- package/templates/skills/business-analyse/questionnaire/05-cross-module.md +32 -32
- package/templates/skills/business-analyse/questionnaire.md +11 -11
- package/templates/skills/business-analyse/references/consolidation-structural-checks.md +17 -0
- package/templates/skills/business-analyse/references/spec-auto-inference.md +12 -7
- package/templates/skills/business-analyse/steps/step-00-init.md +2 -2
- package/templates/skills/business-analyse/steps/step-01-cadrage.md +3 -3
- package/templates/skills/business-analyse/steps/step-02-structure.md +22 -8
- package/templates/skills/business-analyse/steps/step-03-specify.md +22 -15
- package/templates/skills/controller/references/mcp-scaffold-workflow.md +20 -0
- package/templates/skills/derive-prd/references/handoff-file-templates.md +25 -1
- package/templates/skills/derive-prd/references/handoff-seeddata-generation.md +3 -1
- package/templates/skills/ralph-loop/references/category-completeness.md +125 -0
- package/templates/skills/ralph-loop/references/compact-loop.md +66 -10
- package/templates/skills/ralph-loop/references/module-transition.md +60 -0
- package/templates/skills/ralph-loop/steps/step-04-check.md +207 -12
- 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. **
|
|
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
|
|
59
|
-
4. **Challenger :** Ne pas accepter "on verra plus tard" sur les
|
|
60
|
-
5. **Par lots :**
|
|
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
|
|
65
|
-
2. **Cross-module :** Charger 05-cross-module si le module a des
|
|
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
|
|
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
|
|
73
|
-
- "Qui est
|
|
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
|
|
78
|
-
> "La
|
|
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
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
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" (
|
|
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: "
|
|
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:
|
|
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:
|
|
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:
|
|
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 (
|
|
82
|
-
- **Workflow module**: list + detail +
|
|
83
|
-
- **Reporting module**: dashboard (
|
|
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": "
|
|
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
|
|
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": "
|
|
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": "
|
|
51
|
-
{ "name": "departmentId", "type": "guid", "required": true, "description": "
|
|
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
|
|
54
|
-
{ "name": "status", "type": "enum", "options": ["Active", "Inactive", "OnLeave", "Terminated"], "description": "Statut de l'
|
|
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
|
|
58
|
-
{ "target": "Contract", "type": "OneToMany", "description": "
|
|
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
|
|
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": "
|
|
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
|
|
93
|
-
"Il remplit les champs obligatoires (nom,
|
|
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
|
|
96
|
-
"Le
|
|
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
|
|
98
|
+
"alternative": "Si les données sont invalides, le système affiche les erreurs",
|
|
99
99
|
"businessRules": ["BR-VAL-EMPLOYEES-001"],
|
|
100
|
-
"result": "L'
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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);
|