@atlashub/smartstack-cli 4.51.0 → 4.53.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/dist/index.js +53 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/skills/apex/references/core-seed-data.md +15 -0
- package/templates/skills/apex/references/error-classification.md +27 -3
- package/templates/skills/apex/references/post-checks.md +3 -1
- package/templates/skills/apex/steps/step-00-init.md +57 -0
- package/templates/skills/apex/steps/step-03-execute.md +33 -5
- package/templates/skills/apex/steps/step-03b-layer1-seed.md +18 -0
- package/templates/skills/apex/steps/step-03d-layer3-frontend.md +3 -0
- package/templates/skills/apex/steps/step-04-examine.md +35 -0
- package/templates/skills/business-analyse/references/canonical-json-formats.md +200 -0
- package/templates/skills/business-analyse/steps/step-03-specify.md +94 -20
- package/templates/skills/business-analyse-design/steps/step-01-screens.md +41 -4
- package/templates/skills/business-analyse-develop/references/init-resume-recovery.md +54 -0
- package/templates/skills/business-analyse-develop/steps/step-00-init.md +10 -3
- package/templates/skills/business-analyse-develop/steps/step-01-task.md +14 -2
- package/templates/skills/business-analyse-develop/steps/step-04-check.md +12 -2
- package/templates/skills/business-analyse-handoff/references/entity-canonicalization.md +158 -0
- package/templates/skills/business-analyse-handoff/steps/step-01-transform.md +26 -3
- package/templates/skills/business-analyse-handoff/steps/step-02-export.md +14 -0
- package/templates/skills/business-analyse-html/SKILL.md +4 -0
- package/templates/skills/business-analyse-html/references/data-build.md +24 -17
- package/templates/skills/business-analyse-html/references/data-mapping.md +79 -35
- package/templates/skills/business-analyse-html/references/output-modes.md +2 -1
- package/templates/skills/business-analyse-html/steps/step-01-collect.md +7 -2
- package/templates/skills/business-analyse-html/steps/step-02-build-data.md +155 -40
- package/templates/skills/business-analyse-html/steps/step-04-verify.md +22 -4
|
@@ -54,25 +54,56 @@ For each entity identified in step 02:
|
|
|
54
54
|
{
|
|
55
55
|
"name": "Employee",
|
|
56
56
|
"description": "Représente un employé de l'entreprise",
|
|
57
|
+
"personRoleConfig": { "variant": "mandatory", "userFields": ["firstName", "lastName", "email"] },
|
|
57
58
|
"attributes": [
|
|
58
|
-
{ "name": "code", "type": "string", "required": true, "description": "Identifiant unique" },
|
|
59
|
-
{ "name": "userId", "type": "
|
|
59
|
+
{ "name": "code", "type": "string", "required": true, "description": "Identifiant unique auto-généré" },
|
|
60
|
+
{ "name": "userId", "type": "string", "required": true, "description": "FK vers auth_Users (ASP.NET Identity — type string, NOT guid)" },
|
|
60
61
|
{ "name": "departmentId", "type": "guid", "required": true, "description": "Département d'affectation" },
|
|
61
62
|
{ "name": "hireDate", "type": "date", "required": true, "description": "Date d'embauche" },
|
|
62
63
|
{ "name": "position", "type": "string", "description": "Poste occupé" },
|
|
63
|
-
{ "name": "status", "type": "enum", "options": ["Active", "Inactive", "OnLeave", "Terminated"], "defaultValue": "Active", "description": "Statut de l'employé" }
|
|
64
|
-
|
|
64
|
+
{ "name": "status", "type": "enum", "options": ["Active", "Inactive", "OnLeave", "Terminated"], "defaultValue": "Active", "description": "Statut de l'employé" }
|
|
65
|
+
],
|
|
66
|
+
"versionedAttributes": [
|
|
67
|
+
{ "entity": "Salary", "attributes": ["grossAmount", "netAmount", "effectiveDate", "currency"], "reason": "Historique salarial versionné" }
|
|
65
68
|
],
|
|
66
69
|
"estimatedVolume": { "monthly": 50, "total2y": 1200 },
|
|
67
70
|
"searchableFields": ["code", "position", "status"],
|
|
68
71
|
"defaultFilters": ["status"],
|
|
69
72
|
"relationships": [
|
|
70
73
|
{ "target": "Department", "type": "ManyToOne", "description": "Appartient à un département" },
|
|
71
|
-
{ "target": "Contract", "type": "OneToMany", "description": "Possède plusieurs contrats" }
|
|
74
|
+
{ "target": "Contract", "type": "OneToMany", "description": "Possède plusieurs contrats" },
|
|
75
|
+
{ "target": "Salary", "type": "OneToMany", "description": "Historique de salaires versionnés" }
|
|
72
76
|
]
|
|
73
77
|
}
|
|
74
78
|
```
|
|
75
79
|
|
|
80
|
+
### B-bis. SmartStack Entity Convention Guards (MANDATORY)
|
|
81
|
+
|
|
82
|
+
Before finalizing each entity, apply these rules:
|
|
83
|
+
|
|
84
|
+
**B-bis-1. Person Extension Pattern** (ref: `entity-architecture-decision.md` section 0)
|
|
85
|
+
IF the entity matches a person role (Employee, Customer, Manager, Consultant, etc.):
|
|
86
|
+
- **DO NOT** add firstName, lastName, email, phoneNumber as direct attributes
|
|
87
|
+
- **DO** add `userId` (type: `string`, FK to auth_Users — ASP.NET Identity uses string IDs)
|
|
88
|
+
- **DO** add `personRoleConfig` metadata with variant (mandatory/optional)
|
|
89
|
+
- Personal fields come from User, not from the domain entity
|
|
90
|
+
|
|
91
|
+
**B-bis-2. Versioned Sensitive Data**
|
|
92
|
+
IF the entity has attributes that change over time with audit requirements (salary, rate, grade):
|
|
93
|
+
- **DO NOT** put them directly on the entity as single fields
|
|
94
|
+
- **DO** extract into a versioned satellite table (e.g., Employee → Salary with effectiveDate)
|
|
95
|
+
- Mark with `"versionedAttributes"` in entity spec
|
|
96
|
+
|
|
97
|
+
**B-bis-3. SmartStack Socle Entities (NEVER redefine)**
|
|
98
|
+
- Users/Identity → `auth_Users` (managed by SmartStack Identity)
|
|
99
|
+
- Tenants → `tenant_Tenants` (managed by SmartStack Core)
|
|
100
|
+
- Departments → `ref_Departments` or `rh_Departments` (check if exists in target DB)
|
|
101
|
+
|
|
102
|
+
**B-bis-4. Foreign Key Conventions**
|
|
103
|
+
- All FK attributes MUST end with `Id` suffix (e.g., `departmentId`, `userId`)
|
|
104
|
+
- FK to Identity users MUST use type `string` (ASP.NET Identity convention, NOT guid)
|
|
105
|
+
- FK to domain entities MUST use type `guid`
|
|
106
|
+
|
|
76
107
|
### C. Business Rules
|
|
77
108
|
|
|
78
109
|
For each entity/process, identify rules. **Each rule MUST specify `sectionCode`** matching a code from `anticipatedSections[]`:
|
|
@@ -111,23 +142,39 @@ Exemple de règle de calcul :
|
|
|
111
142
|
|
|
112
143
|
For each stakeholder action. **Each use case MUST specify `sectionCode`** matching a code from `anticipatedSections[]` — this links the UC to the screen/page where it happens:
|
|
113
144
|
|
|
145
|
+
> **CANONICAL FORMAT (MANDATORY):** The usecases.json file MUST use these exact keys:
|
|
146
|
+
> - Root key: `"useCases"` (camelCase, NOT "usecases")
|
|
147
|
+
> - Actor field: `"primaryActor"` (NOT "actor")
|
|
148
|
+
> - Steps field: `"mainScenario"` (string[], NOT "steps" with objects)
|
|
149
|
+
> - Alternatives: `"alternativeScenarios"` (object[] with `{name, steps}`, NOT "alternative" as flat string)
|
|
150
|
+
> - This matches specification-schema.json. Deviation causes normalization overhead in 4+ downstream skills.
|
|
151
|
+
|
|
114
152
|
```json
|
|
115
153
|
{
|
|
116
|
-
"
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
154
|
+
"useCases": [
|
|
155
|
+
{
|
|
156
|
+
"id": "UC-EMPLOYEES-001",
|
|
157
|
+
"name": "Créer un employé",
|
|
158
|
+
"sectionCode": "list",
|
|
159
|
+
"primaryActor": "Responsable RH",
|
|
160
|
+
"preconditions": ["L'utilisateur a la permission HumanResources.Employees.Create"],
|
|
161
|
+
"mainScenario": [
|
|
162
|
+
"L'utilisateur ouvre la page de création",
|
|
163
|
+
"Il remplit les champs obligatoires (nom, département, date embauche)",
|
|
164
|
+
"Il valide le formulaire",
|
|
165
|
+
"Le système vérifie les règles métier (BR-VAL-EMPLOYEES-001)",
|
|
166
|
+
"Le système crée l'employé et affiche la fiche"
|
|
167
|
+
],
|
|
168
|
+
"alternativeScenarios": [
|
|
169
|
+
{
|
|
170
|
+
"name": "Données invalides",
|
|
171
|
+
"steps": ["Le système affiche les erreurs de validation", "L'utilisateur corrige et re-soumet"]
|
|
172
|
+
}
|
|
173
|
+
],
|
|
174
|
+
"businessRules": ["BR-VAL-EMPLOYEES-001"],
|
|
175
|
+
"result": "L'employé est créé avec le statut 'Actif'"
|
|
176
|
+
}
|
|
177
|
+
]
|
|
131
178
|
}
|
|
132
179
|
```
|
|
133
180
|
|
|
@@ -250,6 +297,33 @@ for (const mod of modules) {
|
|
|
250
297
|
for (const a of (e.attributes || [])) {
|
|
251
298
|
if (!a.type) warnings.push(mod.code + ": entity " + e.name + " attr '" + a.name + "' missing type");
|
|
252
299
|
if (a.type === 'enum' && !a.defaultValue) errors.push(mod.code + ": enum attr '" + a.name + "' missing defaultValue");
|
|
300
|
+
// FK naming convention: must end with "Id"
|
|
301
|
+
if ((a.type === 'guid' || a.type === 'string') && a.foreignKey && !a.name.endsWith('Id'))
|
|
302
|
+
warnings.push(mod.code + ": entity " + e.name + " FK attr '" + a.name + "' should end with 'Id'");
|
|
303
|
+
}
|
|
304
|
+
// Person Extension Pattern check
|
|
305
|
+
const personFields = ['firstName', 'lastName', 'email', 'phoneNumber', 'phone'];
|
|
306
|
+
const foundPersonFields = (e.attributes || []).filter(a => personFields.includes(a.name));
|
|
307
|
+
if (foundPersonFields.length >= 3 && !e.personRoleConfig) {
|
|
308
|
+
errors.push(mod.code + ": entity '" + e.name + "' has " + foundPersonFields.length +
|
|
309
|
+
" person fields (" + foundPersonFields.map(f => f.name).join(", ") +
|
|
310
|
+
") but no personRoleConfig — use Person Extension Pattern (entity-architecture-decision.md section 0). " +
|
|
311
|
+
"Person fields (firstName, lastName, email) belong on auth_Users, not on the domain entity.");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Canonical usecases key check
|
|
316
|
+
const rawUCFile = READ(mod.dir + '/usecases.json');
|
|
317
|
+
if (rawUCFile && !rawUCFile.useCases && rawUCFile.usecases) {
|
|
318
|
+
errors.push(mod.code + ": usecases.json uses 'usecases' key instead of canonical 'useCases' — fix before proceeding");
|
|
319
|
+
}
|
|
320
|
+
// Check steps serialization format
|
|
321
|
+
for (const uc of ucs) {
|
|
322
|
+
if (uc.steps && Array.isArray(uc.steps) && uc.steps.length > 0 && typeof uc.steps[0] === 'object') {
|
|
323
|
+
errors.push(mod.code + ": UC '" + uc.id + "' uses steps[] with objects — must use mainScenario[] with strings");
|
|
324
|
+
}
|
|
325
|
+
if (uc.actor && !uc.primaryActor) {
|
|
326
|
+
warnings.push(mod.code + ": UC '" + uc.id + "' uses 'actor' instead of canonical 'primaryActor'");
|
|
253
327
|
}
|
|
254
328
|
}
|
|
255
329
|
|
|
@@ -149,28 +149,65 @@ Map entity attribute types to UI component types:
|
|
|
149
149
|
|
|
150
150
|
For each module, write via ba-writer:
|
|
151
151
|
|
|
152
|
+
> **CANONICAL FORMAT (MANDATORY):** screens.json MUST use the `sections[]` wrapper format below.
|
|
153
|
+
> Keys MUST be `sectionCode` and `sectionLabel` (not `id`/`displayName`/`code`/`label`).
|
|
154
|
+
> Each section MUST have a `resources[]` array with typed resource objects (SmartTable, SmartForm, etc.).
|
|
155
|
+
> The flat `screens[]` format (one object per screen with componentType at top level) is **DEPRECATED**.
|
|
156
|
+
> All downstream consumers (business-analyse-html, business-analyse-handoff, business-analyse-develop) expect `sections[]`.
|
|
157
|
+
|
|
152
158
|
```json
|
|
153
159
|
{
|
|
154
160
|
"sections": [
|
|
155
161
|
{
|
|
156
162
|
"sectionCode": "list",
|
|
157
163
|
"sectionLabel": "Liste des employes",
|
|
158
|
-
"resources": [
|
|
164
|
+
"resources": [
|
|
165
|
+
{
|
|
166
|
+
"code": "employees-grid",
|
|
167
|
+
"type": "SmartTable",
|
|
168
|
+
"label": "Grille des employes",
|
|
169
|
+
"columns": [ /* ... */ ],
|
|
170
|
+
"filters": [ /* ... */ ],
|
|
171
|
+
"actions": ["create", "export"],
|
|
172
|
+
"permission": "HumanResources.Employees.Read"
|
|
173
|
+
}
|
|
174
|
+
]
|
|
159
175
|
},
|
|
160
176
|
{
|
|
161
|
-
"sectionCode": "
|
|
177
|
+
"sectionCode": "detail",
|
|
162
178
|
"sectionLabel": "Fiche employe",
|
|
163
|
-
"resources": [
|
|
179
|
+
"resources": [
|
|
180
|
+
{
|
|
181
|
+
"code": "employee-form",
|
|
182
|
+
"type": "SmartForm",
|
|
183
|
+
"label": "Fiche employe",
|
|
184
|
+
"tabs": [ /* ... */ ],
|
|
185
|
+
"actions": ["save", "cancel"],
|
|
186
|
+
"permission": "HumanResources.Employees.Update"
|
|
187
|
+
}
|
|
188
|
+
]
|
|
164
189
|
},
|
|
165
190
|
{
|
|
166
191
|
"sectionCode": "dashboard",
|
|
167
192
|
"sectionLabel": "Tableau de bord",
|
|
168
|
-
"resources": [
|
|
193
|
+
"resources": [
|
|
194
|
+
{
|
|
195
|
+
"code": "employees-dashboard",
|
|
196
|
+
"type": "SmartDashboard",
|
|
197
|
+
"label": "Tableau de bord RH",
|
|
198
|
+
"kpis": [ /* ... */ ],
|
|
199
|
+
"charts": [ /* ... */ ],
|
|
200
|
+
"permission": "HumanResources.Employees.Read"
|
|
201
|
+
}
|
|
202
|
+
]
|
|
169
203
|
}
|
|
170
204
|
]
|
|
171
205
|
}
|
|
172
206
|
```
|
|
173
207
|
|
|
208
|
+
> **Key naming:** Use `sectionCode` (not `id`, `code`). Use `sectionLabel` (not `displayName`, `label`).
|
|
209
|
+
> Resource `type` must be one of: `SmartTable`, `SmartForm`, `SmartDashboard`, `SmartKanban`, `SmartCard`, `SmartFilter`.
|
|
210
|
+
|
|
174
211
|
Update `index.json` with screens.json hash and status.
|
|
175
212
|
|
|
176
213
|
## Validation
|
|
@@ -65,6 +65,60 @@ if (resumeValid) {
|
|
|
65
65
|
}
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
### Ralph State Recovery (context compression defense)
|
|
69
|
+
|
|
70
|
+
> **When context compression occurs mid-execution**, conversation variables are lost.
|
|
71
|
+
> `ralph-state.json` tracks the current position and is written before each major transition.
|
|
72
|
+
> This section reads it back to recover position.
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
// ALWAYS attempt to read ralph-state.json on resume — regardless of -r flag
|
|
76
|
+
// This file is written by: step-00 init, step-02 execute, compact-loop, module-transition
|
|
77
|
+
const statePath = '.ralph/ralph-state.json';
|
|
78
|
+
if (fileExists(statePath)) {
|
|
79
|
+
const state = readJSON(statePath);
|
|
80
|
+
|
|
81
|
+
console.log(`Ralph state recovered: step=${state.currentStep}, module=${state.currentModule}, ` +
|
|
82
|
+
`iteration=${state.iteration || '?'}, reason=${state.reason || 'normal'}`);
|
|
83
|
+
|
|
84
|
+
// Restore conversation variables from state
|
|
85
|
+
if (state.currentModule) {
|
|
86
|
+
{current_module} = state.currentModule;
|
|
87
|
+
}
|
|
88
|
+
if (state.iteration) {
|
|
89
|
+
{current_iteration} = state.iteration;
|
|
90
|
+
}
|
|
91
|
+
if (state.prdVersion) {
|
|
92
|
+
{prd_version} = state.prdVersion;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Route to the correct step based on saved state
|
|
96
|
+
// This replaces the default "always go to step-01" behavior
|
|
97
|
+
const stepRouting = {
|
|
98
|
+
'step-01-task': 'steps/step-01-task.md',
|
|
99
|
+
'step-02-execute': 'steps/step-02-execute.md',
|
|
100
|
+
'step-03-commit': 'steps/step-03-commit.md',
|
|
101
|
+
'step-04-check': 'steps/step-04-check.md',
|
|
102
|
+
'compact-loop': 'steps/step-04-check.md', // compact-loop re-enters via step-04
|
|
103
|
+
'step-05-report': 'steps/step-05-report.md'
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const targetStep = stepRouting[state.currentStep] || 'steps/step-01-task.md';
|
|
107
|
+
|
|
108
|
+
// Special case: module transition — go to step-01 to detect module-changed.json
|
|
109
|
+
if (state.reason === 'module-transition') {
|
|
110
|
+
console.log(`Module transition recovery: ${state.fromModule} → ${state.currentModule}`);
|
|
111
|
+
console.log('Routing to step-01-task.md to detect module-changed.json');
|
|
112
|
+
// targetStep stays 'steps/step-01-task.md'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(`Recovery routing: → ${targetStep}`);
|
|
116
|
+
// The calling code (step-00) should load targetStep instead of the default step-01
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**IMPORTANT:** This recovery is triggered BEFORE the default "Proceed to step-01" routing in step-00-init.md. If ralph-state.json provides a different target step, that step should be loaded instead.
|
|
121
|
+
|
|
68
122
|
---
|
|
69
123
|
|
|
70
124
|
## Auto-Recovery: BA Artifacts Without PRD
|
|
@@ -76,10 +76,11 @@ If tests are slow (>30s in logs), display warning but **continue execution**:
|
|
|
76
76
|
|
|
77
77
|
## 4. Resume or Initialize
|
|
78
78
|
|
|
79
|
-
See `references/init-resume-recovery.md` for complete Resume Mode
|
|
79
|
+
See `references/init-resume-recovery.md` for complete Resume Mode, Auto-Recovery, and Ralph State Recovery logic.
|
|
80
80
|
|
|
81
81
|
**Quick:**
|
|
82
|
-
- If `-r` flag: restore state from .ralph/prd.json
|
|
82
|
+
- If `-r` flag: restore state from .ralph/prd.json + .ralph/ralph-state.json
|
|
83
|
+
- Else if `.ralph/ralph-state.json` exists: recover position (step, module, iteration) — see init-resume-recovery.md "Ralph State Recovery"
|
|
83
84
|
- Else if BA artifacts exist: auto-recover PRDs via `ss business-analyse-handoff`
|
|
84
85
|
- Else: fresh start
|
|
85
86
|
|
|
@@ -222,14 +223,20 @@ MCP: Ready | Branch: {branch} | PRD: v{prd_version}
|
|
|
222
223
|
|
|
223
224
|
### 7b. Initialize State File (context compression defense)
|
|
224
225
|
|
|
226
|
+
> **ralph-state.json is written here AND updated by step-02, compact-loop, and module-transition.**
|
|
227
|
+
> On resume or after context compression, `init-resume-recovery.md` reads it back to route
|
|
228
|
+
> to the correct step instead of always restarting from step-01.
|
|
229
|
+
|
|
225
230
|
```javascript
|
|
226
231
|
writeJSON('.ralph/ralph-state.json', {
|
|
227
232
|
currentStep: 'step-01-task',
|
|
228
233
|
currentModule: {current_module},
|
|
229
234
|
iteration: 1,
|
|
230
235
|
prdVersion: {prd_version},
|
|
236
|
+
modulesQueuePath: fileExists('.ralph/modules-queue.json') ? '.ralph/modules-queue.json' : null,
|
|
231
237
|
timestamp: new Date().toISOString()
|
|
232
238
|
});
|
|
233
239
|
```
|
|
234
240
|
|
|
235
|
-
**
|
|
241
|
+
**Step routing:** If ralph-state recovery (section 4) provided a different target step, load THAT step.
|
|
242
|
+
Otherwise, proceed to step-01-task.md.
|
|
@@ -6,8 +6,20 @@ next_step: steps/step-02-execute.md
|
|
|
6
6
|
|
|
7
7
|
# Step 1: Load Task
|
|
8
8
|
|
|
9
|
-
> **STATE RECOVERY:** If
|
|
10
|
-
|
|
9
|
+
> **STATE RECOVERY (executable):** If context was compressed and you are unsure of your position:
|
|
10
|
+
```javascript
|
|
11
|
+
if (fileExists('.ralph/ralph-state.json')) {
|
|
12
|
+
const state = readJSON('.ralph/ralph-state.json');
|
|
13
|
+
if (state.currentStep && state.currentStep !== 'step-01-task') {
|
|
14
|
+
console.warn(`RECOVERY: ralph-state.json says currentStep="${state.currentStep}" but we are in step-01.`);
|
|
15
|
+
console.warn(`This may indicate context compression routed to the wrong step.`);
|
|
16
|
+
console.warn(`Current module: ${state.currentModule}, iteration: ${state.iteration}`);
|
|
17
|
+
}
|
|
18
|
+
// Restore module context
|
|
19
|
+
if (state.currentModule) { {current_module} = state.currentModule; }
|
|
20
|
+
if (state.iteration) { {current_iteration} = state.iteration; }
|
|
21
|
+
}
|
|
22
|
+
```
|
|
11
23
|
|
|
12
24
|
> **MODULE TRANSITION CHECK:** Before "Only Read Once" rule, check for module transition:
|
|
13
25
|
|
|
@@ -6,8 +6,18 @@ next_step: steps/step-05-report.md OR steps/step-01-task.md
|
|
|
6
6
|
|
|
7
7
|
# Step 4: Check Completion
|
|
8
8
|
|
|
9
|
-
> **STATE RECOVERY:** If
|
|
10
|
-
|
|
9
|
+
> **STATE RECOVERY (executable):** If context was compressed:
|
|
10
|
+
```javascript
|
|
11
|
+
if (fileExists('.ralph/ralph-state.json')) {
|
|
12
|
+
const state = readJSON('.ralph/ralph-state.json');
|
|
13
|
+
if (state.currentStep && state.currentStep !== 'step-04-check' && state.currentStep !== 'compact-loop') {
|
|
14
|
+
console.warn(`RECOVERY: ralph-state.json says currentStep="${state.currentStep}" but we are in step-04.`);
|
|
15
|
+
}
|
|
16
|
+
// Restore module context if lost
|
|
17
|
+
if (state.currentModule) { {current_module} = state.currentModule; }
|
|
18
|
+
if (state.iteration) { {current_iteration} = state.iteration; }
|
|
19
|
+
}
|
|
20
|
+
```
|
|
11
21
|
|
|
12
22
|
## YOUR TASK:
|
|
13
23
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Entity Name Canonicalization
|
|
2
|
+
|
|
3
|
+
> **Used by:** step-01-transform (section 2: file mapping) and step-02-export (POST-CHECK)
|
|
4
|
+
> **Purpose:** Convert BA entity names (potentially French, with spaces/apostrophes/accents) into valid C# identifiers for file paths.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Why This Exists
|
|
9
|
+
|
|
10
|
+
Business analysts write entity names in the user's language (e.g., French: "Type d'absence", "Employé", "Congé maladie"). These names are used as-is in `entities.json`. However, C# file paths and class names **MUST** be valid identifiers:
|
|
11
|
+
- No spaces
|
|
12
|
+
- No apostrophes
|
|
13
|
+
- No accents (diacritics)
|
|
14
|
+
- PascalCase convention
|
|
15
|
+
|
|
16
|
+
Without canonicalization, the handoff generates paths like `src/Domain/Entities/Type d'absence.cs` → build failure (CS1001).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Canonicalization Rules
|
|
21
|
+
|
|
22
|
+
### Step 1: Map to English PascalCase (PREFERRED)
|
|
23
|
+
|
|
24
|
+
If the BA provides an explicit `codeIdentifier` or `englishName` field on the entity, use it directly.
|
|
25
|
+
|
|
26
|
+
### Step 2: Strip Diacritics
|
|
27
|
+
|
|
28
|
+
Remove accents from characters:
|
|
29
|
+
|
|
30
|
+
| Input | Output |
|
|
31
|
+
|-------|--------|
|
|
32
|
+
| é, è, ê, ë | e |
|
|
33
|
+
| à, â, ä | a |
|
|
34
|
+
| ù, û, ü | u |
|
|
35
|
+
| ô, ö | o |
|
|
36
|
+
| î, ï | i |
|
|
37
|
+
| ç | c |
|
|
38
|
+
|
|
39
|
+
Example: `Employé` → `Employe`, `Congé` → `Conge`
|
|
40
|
+
|
|
41
|
+
### Step 3: Remove Apostrophes and Split on Spaces
|
|
42
|
+
|
|
43
|
+
Split the name on spaces, apostrophes, hyphens, and underscores:
|
|
44
|
+
|
|
45
|
+
| Input | Tokens |
|
|
46
|
+
|-------|--------|
|
|
47
|
+
| `Type d'absence` | `["Type", "d", "absence"]` |
|
|
48
|
+
| `Congé maladie` | `["Conge", "maladie"]` |
|
|
49
|
+
| `Mise à jour` | `["Mise", "a", "jour"]` |
|
|
50
|
+
|
|
51
|
+
### Step 4: Remove Articles and Prepositions (1-2 letter tokens)
|
|
52
|
+
|
|
53
|
+
Remove French articles and prepositions that don't contribute to the identifier:
|
|
54
|
+
|
|
55
|
+
**Remove:** `d`, `de`, `du`, `l`, `la`, `le`, `les`, `un`, `une`, `des`, `a`, `au`, `aux`, `en`
|
|
56
|
+
|
|
57
|
+
| Input Tokens | Filtered |
|
|
58
|
+
|-------------|----------|
|
|
59
|
+
| `["Type", "d", "absence"]` | `["Type", "absence"]` |
|
|
60
|
+
| `["Mise", "a", "jour"]` | `["Mise", "jour"]` |
|
|
61
|
+
|
|
62
|
+
### Step 5: PascalCase Assembly
|
|
63
|
+
|
|
64
|
+
Capitalize first letter of each remaining token, join without separator:
|
|
65
|
+
|
|
66
|
+
| Filtered Tokens | Result |
|
|
67
|
+
|----------------|--------|
|
|
68
|
+
| `["Type", "absence"]` | `TypeAbsence` |
|
|
69
|
+
| `["Conge", "maladie"]` | `CongeMaladie` |
|
|
70
|
+
| `["Mise", "jour"]` | `MiseJour` |
|
|
71
|
+
| `["Employe"]` | `Employe` |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Canonicalization Function (Pseudocode)
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
function canonicalizeEntityName(baName) {
|
|
79
|
+
// Step 1: Use explicit code identifier if available
|
|
80
|
+
// (handled by caller — check entity.codeIdentifier || entity.englishName first)
|
|
81
|
+
|
|
82
|
+
// Step 2: Strip diacritics
|
|
83
|
+
let name = baName.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
84
|
+
|
|
85
|
+
// Step 3: Split on non-alphanumeric
|
|
86
|
+
let tokens = name.split(/[\s'\-_]+/).filter(t => t.length > 0);
|
|
87
|
+
|
|
88
|
+
// Step 4: Remove French articles/prepositions
|
|
89
|
+
const stopWords = new Set(['d', 'de', 'du', 'l', 'la', 'le', 'les', 'un', 'une', 'des', 'a', 'au', 'aux', 'en']);
|
|
90
|
+
tokens = tokens.filter(t => !stopWords.has(t.toLowerCase()));
|
|
91
|
+
|
|
92
|
+
// Step 5: PascalCase
|
|
93
|
+
return tokens.map(t => t.charAt(0).toUpperCase() + t.slice(1).toLowerCase()).join('');
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Validation: Is Valid C# Identifier?
|
|
100
|
+
|
|
101
|
+
After canonicalization, verify the result is a valid C# identifier:
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
function isValidCSharpIdentifier(name) {
|
|
105
|
+
// Must start with letter or underscore
|
|
106
|
+
// Must contain only letters, digits, underscores
|
|
107
|
+
// Must not be a C# keyword
|
|
108
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name)
|
|
109
|
+
&& !CSHARP_KEYWORDS.has(name);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const CSHARP_KEYWORDS = new Set([
|
|
113
|
+
'abstract', 'as', 'base', 'bool', 'break', 'byte', 'case', 'catch',
|
|
114
|
+
'char', 'checked', 'class', 'const', 'continue', 'decimal', 'default',
|
|
115
|
+
'delegate', 'do', 'double', 'else', 'enum', 'event', 'explicit',
|
|
116
|
+
'extern', 'false', 'finally', 'fixed', 'float', 'for', 'foreach',
|
|
117
|
+
'goto', 'if', 'implicit', 'in', 'int', 'interface', 'internal',
|
|
118
|
+
'is', 'lock', 'long', 'namespace', 'new', 'null', 'object',
|
|
119
|
+
'operator', 'out', 'override', 'params', 'private', 'protected',
|
|
120
|
+
'public', 'readonly', 'ref', 'return', 'sbyte', 'sealed', 'short',
|
|
121
|
+
'sizeof', 'stackalloc', 'static', 'string', 'struct', 'switch',
|
|
122
|
+
'this', 'throw', 'true', 'try', 'typeof', 'uint', 'ulong',
|
|
123
|
+
'unchecked', 'unsafe', 'ushort', 'using', 'virtual', 'void',
|
|
124
|
+
'volatile', 'while'
|
|
125
|
+
]);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Examples
|
|
131
|
+
|
|
132
|
+
| BA Entity Name (French) | Canonicalized | File Path |
|
|
133
|
+
|-------------------------|---------------|-----------|
|
|
134
|
+
| `Type d'absence` | `TypeAbsence` | `src/Domain/Entities/App/Module/TypeAbsence.cs` |
|
|
135
|
+
| `Employé` | `Employe` | `src/Domain/Entities/App/Module/Employe.cs` |
|
|
136
|
+
| `Congé maladie` | `CongeMaladie` | `src/Domain/Entities/App/Module/CongeMaladie.cs` |
|
|
137
|
+
| `Mise à jour` | `MiseJour` | `src/Domain/Entities/App/Module/MiseJour.cs` |
|
|
138
|
+
| `Département` | `Departement` | `src/Domain/Entities/App/Module/Departement.cs` |
|
|
139
|
+
| `Employee` | `Employee` | `src/Domain/Entities/App/Module/Employee.cs` (no change) |
|
|
140
|
+
| `AbsenceType` | `AbsenceType` | `src/Domain/Entities/App/Module/AbsenceType.cs` (no change) |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Integration Points
|
|
145
|
+
|
|
146
|
+
1. **step-01-transform.md**: Apply `canonicalizeEntityName()` to ALL entity names BEFORE generating `filesToCreate` paths
|
|
147
|
+
2. **step-02-export.md**: POST-CHECK validates all paths in `filesToCreate` are valid C# identifiers
|
|
148
|
+
3. **entity-domain-mapping.md**: `{EntityName}` in path templates refers to the CANONICALIZED name
|
|
149
|
+
4. **handoff-file-templates.md**: All `{EntityName}` placeholders use canonicalized names
|
|
150
|
+
|
|
151
|
+
## Anti-Patterns
|
|
152
|
+
|
|
153
|
+
| Anti-Pattern | Risk | Correct Approach |
|
|
154
|
+
|-------------|------|-----------------|
|
|
155
|
+
| Translate to English semantically | `MiseJour` → `Update`? No — loses domain meaning | Only strip illegal chars, keep semantics |
|
|
156
|
+
| Remove all short words | `TypeAbsence` missing `Type`? | Only remove articles/prepositions, not nouns |
|
|
157
|
+
| Lowercase everything | `typeabsence` is not PascalCase | PascalCase each token |
|
|
158
|
+
| Skip validation | `Task` is a valid name but also a C# type | Warn on namespace conflicts, don't block |
|
|
@@ -19,6 +19,7 @@ next_step: steps/step-02-export.md
|
|
|
19
19
|
- **NEVER** invent entities/BRs not in module JSON files
|
|
20
20
|
- **NEVER** load all module data in the main conversation (causes context overflow on 5+ modules)
|
|
21
21
|
- **ALWAYS** generate API endpoints from use cases + entities (BA does not produce apiEndpoints)
|
|
22
|
+
- **ALWAYS** canonicalize entity names before generating file paths — see `references/entity-canonicalization.md`
|
|
22
23
|
|
|
23
24
|
## YOUR TASK
|
|
24
25
|
|
|
@@ -71,10 +72,19 @@ Transform module "{moduleCode}" for handoff.
|
|
|
71
72
|
## Instructions
|
|
72
73
|
Read the following 5 files from {moduleDir}:
|
|
73
74
|
1. entities.json → entities[]
|
|
74
|
-
2. usecases.json → useCases[]
|
|
75
|
-
3. rules.json → rules[]
|
|
75
|
+
2. usecases.json → useCases[] (canonical key: "useCases", fallback: "usecases")
|
|
76
|
+
3. rules.json → rules[] (canonical key: "rules", fallback: "businessRules")
|
|
76
77
|
4. permissions.json → roles[], matrix[], permissionPaths[]
|
|
77
|
-
5. screens.json →
|
|
78
|
+
5. screens.json → sections[] (canonical key: "sections", fallback: "screens")
|
|
79
|
+
|
|
80
|
+
### Normalization Safety Net (BACKWARD COMPAT)
|
|
81
|
+
When reading flat files, prefer canonical keys but fall back to alternatives:
|
|
82
|
+
- useCases: `data.useCases || data.usecases || []`
|
|
83
|
+
- primaryActor: `uc.primaryActor || uc.actor`
|
|
84
|
+
- mainScenario: `uc.mainScenario || uc.steps` (if steps[] contains objects, extract `.action`)
|
|
85
|
+
- rules: `data.rules || data.businessRules || []`
|
|
86
|
+
- screens: `data.sections || data.screens` (if screens[] exists, treat each screen as a section with 1 resource)
|
|
87
|
+
This safety net handles pre-4.52 BA outputs. It should become unnecessary once step-03-specify enforces canonical keys.
|
|
78
88
|
|
|
79
89
|
Then build the handoff data following these rules:
|
|
80
90
|
|
|
@@ -96,6 +106,19 @@ Then build the handoff data following these rules:
|
|
|
96
106
|
### Section UC/BR Enrichment
|
|
97
107
|
Using sectionCode from usecases.json and rules.json, link each UC and BR to its section.
|
|
98
108
|
|
|
109
|
+
### Entity Name Canonicalization (MANDATORY)
|
|
110
|
+
Before generating any file paths, canonicalize ALL entity names from `entities.json`:
|
|
111
|
+
1. Read `references/entity-canonicalization.md` for complete rules
|
|
112
|
+
2. For each entity in `entities.json > entities[]`:
|
|
113
|
+
- If entity has `codeIdentifier` or `englishName` → use it directly
|
|
114
|
+
- Otherwise → apply canonicalization: strip diacritics, remove apostrophes/spaces, remove French articles (d, de, du, l, la, le, les, un, une, des, a, au, aux, en), PascalCase
|
|
115
|
+
3. Use the CANONICALIZED name in ALL file path templates (`{EntityName}`, `{ServiceName}`, `{DtoName}`, etc.)
|
|
116
|
+
4. Store the mapping `{ originalName: "Type d'absence", canonicalName: "TypeAbsence" }` in handoff metadata for traceability
|
|
117
|
+
|
|
118
|
+
**Example:** Entity `"Type d'absence"` → canonicalized to `"TypeAbsence"` → path `src/Domain/Entities/App/Module/TypeAbsence.cs`
|
|
119
|
+
|
|
120
|
+
**BLOCKING:** If ANY entity name after canonicalization is not a valid C# identifier (`/^[A-Za-z_][A-Za-z0-9_]*$/`), STOP and report the error.
|
|
121
|
+
|
|
99
122
|
### File Mapping (8 Categories)
|
|
100
123
|
Read `references/handoff-file-templates.md` for complete JSON templates.
|
|
101
124
|
All backend paths MUST include {ApplicationName}/ hierarchy.
|
|
@@ -111,6 +111,20 @@ for (const key of specKeys) {
|
|
|
111
111
|
BLOCKING_ERROR(`Companion file empty or too small: ${companionPath} (${companionSize} bytes)`);
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
// Check 6: File paths must be valid C# identifiers (no spaces, apostrophes, accents)
|
|
116
|
+
// Reference: references/entity-canonicalization.md
|
|
117
|
+
for (const [cat, files] of Object.entries(ftc)) {
|
|
118
|
+
for (const file of (files || [])) {
|
|
119
|
+
const filePath = file.path || file;
|
|
120
|
+
// Extract filename without extension
|
|
121
|
+
const fileName = filePath.split('/').pop().replace(/\.(cs|tsx|ts|json)$/, '');
|
|
122
|
+
// Check for illegal characters in C# file names
|
|
123
|
+
if (/[\s'àâäéèêëïîôùûüçÀÂÄÉÈÊËÏÎÔÙÛÜÇ]/.test(fileName)) {
|
|
124
|
+
BLOCKING_ERROR(`${cat}: File path contains illegal characters for C# identifier: "${filePath}" — entity name must be canonicalized (see references/entity-canonicalization.md)`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
114
128
|
```
|
|
115
129
|
|
|
116
130
|
Display verification table showing all 8 categories match between module JSON files and prd.json.
|
|
@@ -42,9 +42,13 @@ Generate the interactive HTML document of the business analysis from the JSON an
|
|
|
42
42
|
|
|
43
43
|
- **NEVER** deploy an empty template — FEATURE_DATA must be injected
|
|
44
44
|
- **moduleSpecs** MUST have ONE entry per module (empty = BROKEN)
|
|
45
|
+
- **moduleSpecs[].screens[]** MUST have populated resources when source screens.json has data (empty resources = NO HTML mockups)
|
|
45
46
|
- **Scope keys** MUST be converted: `inScope` → `inscope`, `outOfScope` → `outofscope`
|
|
46
47
|
- **Wireframe fields** MUST be renamed: `mockupFormat` → `format`, `mockup` → `content`
|
|
48
|
+
- **Input normalization** MUST handle both JSON schemas: Format A (`screens[]`, `useCases`, `mainScenario`) and Format B (`sections[]`, `usecases`, `steps[]` as objects)
|
|
49
|
+
- **NEVER** call `.join()` on arrays without checking element types — object elements produce `[object Object]`
|
|
47
50
|
- **Final file** MUST be > 100KB
|
|
51
|
+
- **Final file** MUST NOT contain the string `[object Object]`
|
|
48
52
|
|
|
49
53
|
## References
|
|
50
54
|
|