@atlashub/smartstack-cli 4.52.0 → 4.54.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/business-analyse/references/canonical-json-formats.md +200 -0
- package/templates/skills/business-analyse/steps/step-01-cadrage.md +64 -1
- package/templates/skills/business-analyse/steps/step-03-specify.md +127 -21
- package/templates/skills/business-analyse-design/steps/step-01-screens.md +41 -4
- package/templates/skills/business-analyse-handoff/steps/step-01-transform.md +12 -3
- package/templates/skills/business-analyse-html/html/ba-interactive.html +30 -3
- package/templates/skills/business-analyse-html/html/src/scripts/06-render-mockups.js +30 -3
- package/templates/skills/business-analyse-html/steps/step-02-build-data.md +82 -35
package/package.json
CHANGED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Canonical JSON Formats Reference
|
|
2
|
+
|
|
3
|
+
> **Purpose:** Single source of truth for all JSON file formats produced by the BA pipeline.
|
|
4
|
+
> **Used by:** `/business-analyse` (step-03), `/business-analyse-design` (step-01), `/business-analyse-html`, `/business-analyse-handoff`, `/business-analyse-develop`
|
|
5
|
+
> **Rule:** All producers MUST write canonical format. All consumers MUST accept canonical + deprecated fallbacks.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## entities.json
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"entities": [
|
|
14
|
+
{
|
|
15
|
+
"name": "Employee",
|
|
16
|
+
"description": "...",
|
|
17
|
+
"personRoleConfig": { "variant": "mandatory", "userFields": ["firstName", "lastName", "email"] },
|
|
18
|
+
"attributes": [
|
|
19
|
+
{ "name": "code", "type": "string", "required": true, "description": "..." },
|
|
20
|
+
{ "name": "userId", "type": "string", "required": true, "description": "FK auth_Users (ASP.NET Identity)" }
|
|
21
|
+
],
|
|
22
|
+
"versionedAttributes": [
|
|
23
|
+
{ "entity": "Salary", "attributes": ["grossAmount", "effectiveDate"], "reason": "..." }
|
|
24
|
+
],
|
|
25
|
+
"relationships": [
|
|
26
|
+
{ "target": "Department", "type": "ManyToOne", "description": "..." }
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Convention guards:**
|
|
34
|
+
- Person entities → `personRoleConfig` + `userId` (string), NO firstName/lastName/email
|
|
35
|
+
- Versioned data → `versionedAttributes[]`, NOT inline fields
|
|
36
|
+
- FK naming → always `{entity}Id` suffix
|
|
37
|
+
- FK to auth_Users → type `string` (ASP.NET Identity), NOT `guid`
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## usecases.json
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"useCases": [
|
|
46
|
+
{
|
|
47
|
+
"id": "UC-MODULE-001",
|
|
48
|
+
"name": "Créer un employé",
|
|
49
|
+
"sectionCode": "list",
|
|
50
|
+
"primaryActor": "Responsable RH",
|
|
51
|
+
"preconditions": ["..."],
|
|
52
|
+
"mainScenario": [
|
|
53
|
+
"L'utilisateur ouvre la page de création",
|
|
54
|
+
"Il remplit les champs obligatoires"
|
|
55
|
+
],
|
|
56
|
+
"alternativeScenarios": [
|
|
57
|
+
{ "name": "Données invalides", "steps": ["Le système affiche les erreurs"] }
|
|
58
|
+
],
|
|
59
|
+
"businessRules": ["BR-VAL-MODULE-001"],
|
|
60
|
+
"result": "L'entité est créée"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Canonical key | Deprecated alternatives | Notes |
|
|
67
|
+
|---------------|------------------------|-------|
|
|
68
|
+
| `useCases` (root) | `usecases` | camelCase is canonical |
|
|
69
|
+
| `primaryActor` | `actor` | |
|
|
70
|
+
| `mainScenario` (string[]) | `steps` (object[] or string[]) | MUST be strings, never `{step, action}` objects |
|
|
71
|
+
| `alternativeScenarios` (object[]) | `alternativeFlows`, `alternative` (string) | Must be `[{name, steps}]` |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## rules.json
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"rules": [
|
|
80
|
+
{
|
|
81
|
+
"id": "BR-VAL-MODULE-001",
|
|
82
|
+
"name": "Validation date",
|
|
83
|
+
"category": "validation",
|
|
84
|
+
"sectionCode": "list",
|
|
85
|
+
"statement": "La date ne peut pas être dans le futur",
|
|
86
|
+
"example": "Date = 2027-01-01 → erreur",
|
|
87
|
+
"entities": ["Employee"],
|
|
88
|
+
"severity": "blocking"
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| Canonical key | Deprecated alternatives |
|
|
95
|
+
|---------------|------------------------|
|
|
96
|
+
| `rules` (root) | `businessRules` |
|
|
97
|
+
| `statement` | `description` |
|
|
98
|
+
| `id` | `name` (as identifier) |
|
|
99
|
+
|
|
100
|
+
Categories: `validation`, `calculation`, `workflow`, `security`, `data`, `notification`
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## permissions.json
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"roles": [
|
|
109
|
+
{ "role": "RH Admin", "description": "..." }
|
|
110
|
+
],
|
|
111
|
+
"permissionPaths": [
|
|
112
|
+
"HumanResources.Employees.Read",
|
|
113
|
+
"HumanResources.Employees.Create"
|
|
114
|
+
],
|
|
115
|
+
"matrix": {
|
|
116
|
+
"roleAssignments": [
|
|
117
|
+
{ "role": "RH Admin", "permissions": ["HumanResources.Employees.*"] }
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## screens.json
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"sections": [
|
|
130
|
+
{
|
|
131
|
+
"sectionCode": "list",
|
|
132
|
+
"sectionLabel": "Liste des employés",
|
|
133
|
+
"resources": [
|
|
134
|
+
{
|
|
135
|
+
"code": "employees-grid",
|
|
136
|
+
"type": "SmartTable",
|
|
137
|
+
"label": "Grille des employés",
|
|
138
|
+
"columns": [
|
|
139
|
+
{ "field": "code", "label": "Code", "type": "text", "sortable": true }
|
|
140
|
+
],
|
|
141
|
+
"filters": [
|
|
142
|
+
{ "field": "status", "type": "select", "label": "Statut" }
|
|
143
|
+
],
|
|
144
|
+
"actions": ["create", "export"],
|
|
145
|
+
"permission": "HumanResources.Employees.Read"
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
| Canonical key | Deprecated alternatives | Notes |
|
|
154
|
+
|---------------|------------------------|-------|
|
|
155
|
+
| `sections` (root) | `screens` (flat array) | `sections[]` with `resources[]` is canonical |
|
|
156
|
+
| `sectionCode` | `id`, `code` | |
|
|
157
|
+
| `sectionLabel` | `displayName`, `label` | |
|
|
158
|
+
| resource `type` | `layout`, `componentType` | Must be: SmartTable, SmartForm, SmartDashboard, SmartKanban, SmartCard, SmartFilter |
|
|
159
|
+
| column `field` | `code`, `name`, `id`, `fieldCode` | |
|
|
160
|
+
| column `label` | `displayName` | |
|
|
161
|
+
|
|
162
|
+
**DEPRECATED format (do NOT produce):**
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"screens": [
|
|
166
|
+
{ "screen": "employees-list", "section": "list", "componentType": "SmartTable", "columns": [...] }
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Consumer normalization pattern
|
|
174
|
+
|
|
175
|
+
All downstream consumers should normalize input using this priority chain:
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
// usecases
|
|
179
|
+
const rawUCs = data.useCases || data.usecases || [];
|
|
180
|
+
const uc = {
|
|
181
|
+
name: raw.name || raw.title || raw.id,
|
|
182
|
+
actor: raw.primaryActor || raw.actor,
|
|
183
|
+
steps: (raw.mainScenario || raw.steps || []).map(s =>
|
|
184
|
+
typeof s === 'string' ? s : (s.action || s.description || "")
|
|
185
|
+
),
|
|
186
|
+
alternatives: raw.alternativeScenarios || raw.alternativeFlows || []
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// screens
|
|
190
|
+
const sections = data.sections || [];
|
|
191
|
+
const flatScreens = data.screens || [];
|
|
192
|
+
// If flat format: wrap each screen as a section with 1 resource
|
|
193
|
+
|
|
194
|
+
// columns
|
|
195
|
+
const col = {
|
|
196
|
+
field: raw.field || raw.code || raw.name || raw.id,
|
|
197
|
+
label: raw.label || raw.displayName || raw.code,
|
|
198
|
+
type: raw.renderAs === "badge" ? "badge" : (raw.type || raw.dataType || "text")
|
|
199
|
+
};
|
|
200
|
+
```
|
|
@@ -120,11 +120,68 @@ IF docs/**/*.md files found:
|
|
|
120
120
|
- Pre-identify integration points: new feature may depend on or extend existing modules
|
|
121
121
|
- NEVER re-specify what's already handed-off (status = "handed-off") — reference it instead
|
|
122
122
|
|
|
123
|
+
### 2d. Domain Research (WebSearch — BEFORE engaging the client)
|
|
124
|
+
|
|
125
|
+
> **Ground the analysis in real-world patterns. This prevents greenfield mistakes
|
|
126
|
+
> (e.g., putting firstName on Employee instead of linking to User).**
|
|
127
|
+
> This step takes 30-60 seconds and dramatically improves entity design quality.
|
|
128
|
+
|
|
129
|
+
**WHEN to search:** ALWAYS for the main business domain. Skip only if the domain is purely SmartStack-internal (e.g., config, admin tools).
|
|
130
|
+
|
|
131
|
+
**HOW to search:** Use WebSearch tool with targeted queries based on the detected domain.
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
1. DOMAIN MODEL SEARCH
|
|
135
|
+
Query: "{detected_domain} data model best practices" (e.g., "HR management data model best practices")
|
|
136
|
+
→ Extract: standard entities, common relationships, industry patterns
|
|
137
|
+
→ Store key findings in {domain_research}.dataModel
|
|
138
|
+
|
|
139
|
+
2. ENTITY PATTERN SEARCH (for the 2-3 most complex detected entities)
|
|
140
|
+
Query: "{entity_name} entity design pattern ERP" (e.g., "employee entity design pattern ERP")
|
|
141
|
+
→ Extract: standard attributes, versioning patterns, common pitfalls
|
|
142
|
+
→ Store in {domain_research}.entityPatterns[entity_name]
|
|
143
|
+
|
|
144
|
+
3. WORKFLOW SEARCH (if the domain involves approval/validation flows)
|
|
145
|
+
Query: "{domain} approval workflow pattern" (e.g., "timesheet approval workflow pattern")
|
|
146
|
+
→ Extract: standard states, transition rules, multi-level approval
|
|
147
|
+
→ Store in {domain_research}.workflows
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**WHAT to extract (ULTRATHINK — internal only):**
|
|
151
|
+
```yaml
|
|
152
|
+
domain_research:
|
|
153
|
+
domain: "{detected domain — e.g., HR, CRM, Billing}"
|
|
154
|
+
dataModel:
|
|
155
|
+
standardEntities: ["Entity1", "Entity2"] # what the industry considers standard
|
|
156
|
+
commonRelationships: ["Entity1 → Entity2"] # typical FK patterns
|
|
157
|
+
antiPatterns: ["storing salary directly on employee"] # mistakes to avoid
|
|
158
|
+
entityPatterns:
|
|
159
|
+
Employee:
|
|
160
|
+
standardAttributes: ["code", "userId", "departmentId", "hireDate", "status"]
|
|
161
|
+
versionedData: ["salary → separate table with effectiveDate"]
|
|
162
|
+
personPattern: "link to User via FK, never duplicate personal info"
|
|
163
|
+
Invoice:
|
|
164
|
+
standardAttributes: ["number", "clientId", "issueDate", "dueDate", "status"]
|
|
165
|
+
calculatedFields: ["subtotal", "taxAmount", "total"]
|
|
166
|
+
workflowStates: ["Draft", "Sent", "Paid", "Overdue", "Cancelled"]
|
|
167
|
+
workflows:
|
|
168
|
+
timesheetApproval: "Draft → Submitted → ManagerApproved → ClientApproved"
|
|
169
|
+
invoiceLifecycle: "Draft → Validated → Sent → Paid"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
> **CRITICAL:** This research is INTERNAL context. Do NOT show raw search results to the client.
|
|
173
|
+
> Use the findings to:
|
|
174
|
+
> - Improve pre-analysis quality (detect shadow zones the client won't mention)
|
|
175
|
+
> - Ground entity architecture decisions in Phase 4d-bis
|
|
176
|
+
> - Inform proactive suggestions in Phase 4 (ANTICIPATION)
|
|
177
|
+
> - Prevent common anti-patterns during step-03 entity specification
|
|
178
|
+
|
|
123
179
|
### 2b. Silent Pre-Analysis (ULTRATHINK — no output to client)
|
|
124
180
|
|
|
125
181
|
> **The AI prepares the conversation before speaking. This is NOT output — it's internal reasoning.**
|
|
182
|
+
> **INCLUDES domain research findings from 2d above.**
|
|
126
183
|
|
|
127
|
-
Analyze `{feature_description}` silently:
|
|
184
|
+
Analyze `{feature_description}` AND `{domain_research}` silently:
|
|
128
185
|
|
|
129
186
|
1. **Identify problem type** from keywords (replace, automate, centralize, new tool)
|
|
130
187
|
2. **Extract explicit modules/features** mentioned in the description
|
|
@@ -143,6 +200,12 @@ _preAnalysis:
|
|
|
143
200
|
detected_modules: [{name, description, detected_sections}]
|
|
144
201
|
shadow_zones: [{topic, why_it_matters, challenge_question}]
|
|
145
202
|
anticipated_suggestions: [{suggestion, justification, module}]
|
|
203
|
+
domain_research: # from Phase 2d web search
|
|
204
|
+
domain: "{detected domain}"
|
|
205
|
+
standardEntities: [...]
|
|
206
|
+
antiPatterns: [...]
|
|
207
|
+
entityPatterns: {entity: {standardAttributes, versionedData, personPattern}}
|
|
208
|
+
workflows: {name: "state1 → state2 → state3"}
|
|
146
209
|
```
|
|
147
210
|
|
|
148
211
|
---
|
|
@@ -43,9 +43,41 @@ For each module, execute these sub-steps:
|
|
|
43
43
|
|
|
44
44
|
Load via ba-reader:
|
|
45
45
|
- Module's `index.json` (if exists from previous run)
|
|
46
|
-
- Application `cadrage.json` for stakeholder context
|
|
46
|
+
- Application `cadrage.json` for stakeholder context (including `_preAnalysis.domain_research`)
|
|
47
47
|
- Completed modules' summaries (compact, <100 lines each)
|
|
48
48
|
|
|
49
|
+
### A-bis. Entity Pattern Research (WebSearch — per module)
|
|
50
|
+
|
|
51
|
+
> **Before defining entities, validate design against industry best practices.**
|
|
52
|
+
> Uses domain_research from step-01 as baseline, then does targeted searches for this specific module.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
IF cadrage._preAnalysis.domain_research exists:
|
|
56
|
+
→ Load as baseline (already researched in step-01)
|
|
57
|
+
ELSE:
|
|
58
|
+
→ Run a quick WebSearch: "{module_domain} entity data model best practices"
|
|
59
|
+
|
|
60
|
+
FOR each main entity in this module (max 2-3 searches):
|
|
61
|
+
Query: "{entity_name} entity design pattern {domain}"
|
|
62
|
+
(e.g., "invoice entity design ERP", "timesheet data model best practices")
|
|
63
|
+
|
|
64
|
+
EXTRACT (ULTRATHINK — internal only):
|
|
65
|
+
- Standard attributes for this entity type
|
|
66
|
+
- Common relationships and cardinality
|
|
67
|
+
- Versioned vs. inline data patterns (e.g., salary history)
|
|
68
|
+
- Workflow states if applicable
|
|
69
|
+
- Known anti-patterns to avoid
|
|
70
|
+
|
|
71
|
+
CROSS-CHECK against SmartStack conventions (B-bis rules):
|
|
72
|
+
- Would this entity trigger Person Extension Pattern?
|
|
73
|
+
- Are there attributes that should be versioned (salary, rate)?
|
|
74
|
+
- Does this entity already exist in the SmartStack socle DB?
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
> **CRITICAL:** This research informs entity design but does NOT override SmartStack conventions.
|
|
78
|
+
> If web research suggests putting firstName on Employee, the B-bis guard still blocks it
|
|
79
|
+
> because SmartStack uses auth_Users for personal data.
|
|
80
|
+
|
|
49
81
|
### B. Entities
|
|
50
82
|
|
|
51
83
|
For each entity identified in step 02:
|
|
@@ -54,25 +86,56 @@ For each entity identified in step 02:
|
|
|
54
86
|
{
|
|
55
87
|
"name": "Employee",
|
|
56
88
|
"description": "Représente un employé de l'entreprise",
|
|
89
|
+
"personRoleConfig": { "variant": "mandatory", "userFields": ["firstName", "lastName", "email"] },
|
|
57
90
|
"attributes": [
|
|
58
|
-
{ "name": "code", "type": "string", "required": true, "description": "Identifiant unique" },
|
|
59
|
-
{ "name": "userId", "type": "
|
|
91
|
+
{ "name": "code", "type": "string", "required": true, "description": "Identifiant unique auto-généré" },
|
|
92
|
+
{ "name": "userId", "type": "string", "required": true, "description": "FK vers auth_Users (ASP.NET Identity — type string, NOT guid)" },
|
|
60
93
|
{ "name": "departmentId", "type": "guid", "required": true, "description": "Département d'affectation" },
|
|
61
94
|
{ "name": "hireDate", "type": "date", "required": true, "description": "Date d'embauche" },
|
|
62
95
|
{ "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
|
-
|
|
96
|
+
{ "name": "status", "type": "enum", "options": ["Active", "Inactive", "OnLeave", "Terminated"], "defaultValue": "Active", "description": "Statut de l'employé" }
|
|
97
|
+
],
|
|
98
|
+
"versionedAttributes": [
|
|
99
|
+
{ "entity": "Salary", "attributes": ["grossAmount", "netAmount", "effectiveDate", "currency"], "reason": "Historique salarial versionné" }
|
|
65
100
|
],
|
|
66
101
|
"estimatedVolume": { "monthly": 50, "total2y": 1200 },
|
|
67
102
|
"searchableFields": ["code", "position", "status"],
|
|
68
103
|
"defaultFilters": ["status"],
|
|
69
104
|
"relationships": [
|
|
70
105
|
{ "target": "Department", "type": "ManyToOne", "description": "Appartient à un département" },
|
|
71
|
-
{ "target": "Contract", "type": "OneToMany", "description": "Possède plusieurs contrats" }
|
|
106
|
+
{ "target": "Contract", "type": "OneToMany", "description": "Possède plusieurs contrats" },
|
|
107
|
+
{ "target": "Salary", "type": "OneToMany", "description": "Historique de salaires versionnés" }
|
|
72
108
|
]
|
|
73
109
|
}
|
|
74
110
|
```
|
|
75
111
|
|
|
112
|
+
### B-bis. SmartStack Entity Convention Guards (MANDATORY)
|
|
113
|
+
|
|
114
|
+
Before finalizing each entity, apply these rules:
|
|
115
|
+
|
|
116
|
+
**B-bis-1. Person Extension Pattern** (ref: `entity-architecture-decision.md` section 0)
|
|
117
|
+
IF the entity matches a person role (Employee, Customer, Manager, Consultant, etc.):
|
|
118
|
+
- **DO NOT** add firstName, lastName, email, phoneNumber as direct attributes
|
|
119
|
+
- **DO** add `userId` (type: `string`, FK to auth_Users — ASP.NET Identity uses string IDs)
|
|
120
|
+
- **DO** add `personRoleConfig` metadata with variant (mandatory/optional)
|
|
121
|
+
- Personal fields come from User, not from the domain entity
|
|
122
|
+
|
|
123
|
+
**B-bis-2. Versioned Sensitive Data**
|
|
124
|
+
IF the entity has attributes that change over time with audit requirements (salary, rate, grade):
|
|
125
|
+
- **DO NOT** put them directly on the entity as single fields
|
|
126
|
+
- **DO** extract into a versioned satellite table (e.g., Employee → Salary with effectiveDate)
|
|
127
|
+
- Mark with `"versionedAttributes"` in entity spec
|
|
128
|
+
|
|
129
|
+
**B-bis-3. SmartStack Socle Entities (NEVER redefine)**
|
|
130
|
+
- Users/Identity → `auth_Users` (managed by SmartStack Identity)
|
|
131
|
+
- Tenants → `tenant_Tenants` (managed by SmartStack Core)
|
|
132
|
+
- Departments → `ref_Departments` or `rh_Departments` (check if exists in target DB)
|
|
133
|
+
|
|
134
|
+
**B-bis-4. Foreign Key Conventions**
|
|
135
|
+
- All FK attributes MUST end with `Id` suffix (e.g., `departmentId`, `userId`)
|
|
136
|
+
- FK to Identity users MUST use type `string` (ASP.NET Identity convention, NOT guid)
|
|
137
|
+
- FK to domain entities MUST use type `guid`
|
|
138
|
+
|
|
76
139
|
### C. Business Rules
|
|
77
140
|
|
|
78
141
|
For each entity/process, identify rules. **Each rule MUST specify `sectionCode`** matching a code from `anticipatedSections[]`:
|
|
@@ -111,23 +174,39 @@ Exemple de règle de calcul :
|
|
|
111
174
|
|
|
112
175
|
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
176
|
|
|
177
|
+
> **CANONICAL FORMAT (MANDATORY):** The usecases.json file MUST use these exact keys:
|
|
178
|
+
> - Root key: `"useCases"` (camelCase, NOT "usecases")
|
|
179
|
+
> - Actor field: `"primaryActor"` (NOT "actor")
|
|
180
|
+
> - Steps field: `"mainScenario"` (string[], NOT "steps" with objects)
|
|
181
|
+
> - Alternatives: `"alternativeScenarios"` (object[] with `{name, steps}`, NOT "alternative" as flat string)
|
|
182
|
+
> - This matches specification-schema.json. Deviation causes normalization overhead in 4+ downstream skills.
|
|
183
|
+
|
|
114
184
|
```json
|
|
115
185
|
{
|
|
116
|
-
"
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
186
|
+
"useCases": [
|
|
187
|
+
{
|
|
188
|
+
"id": "UC-EMPLOYEES-001",
|
|
189
|
+
"name": "Créer un employé",
|
|
190
|
+
"sectionCode": "list",
|
|
191
|
+
"primaryActor": "Responsable RH",
|
|
192
|
+
"preconditions": ["L'utilisateur a la permission HumanResources.Employees.Create"],
|
|
193
|
+
"mainScenario": [
|
|
194
|
+
"L'utilisateur ouvre la page de création",
|
|
195
|
+
"Il remplit les champs obligatoires (nom, département, date embauche)",
|
|
196
|
+
"Il valide le formulaire",
|
|
197
|
+
"Le système vérifie les règles métier (BR-VAL-EMPLOYEES-001)",
|
|
198
|
+
"Le système crée l'employé et affiche la fiche"
|
|
199
|
+
],
|
|
200
|
+
"alternativeScenarios": [
|
|
201
|
+
{
|
|
202
|
+
"name": "Données invalides",
|
|
203
|
+
"steps": ["Le système affiche les erreurs de validation", "L'utilisateur corrige et re-soumet"]
|
|
204
|
+
}
|
|
205
|
+
],
|
|
206
|
+
"businessRules": ["BR-VAL-EMPLOYEES-001"],
|
|
207
|
+
"result": "L'employé est créé avec le statut 'Actif'"
|
|
208
|
+
}
|
|
209
|
+
]
|
|
131
210
|
}
|
|
132
211
|
```
|
|
133
212
|
|
|
@@ -250,6 +329,33 @@ for (const mod of modules) {
|
|
|
250
329
|
for (const a of (e.attributes || [])) {
|
|
251
330
|
if (!a.type) warnings.push(mod.code + ": entity " + e.name + " attr '" + a.name + "' missing type");
|
|
252
331
|
if (a.type === 'enum' && !a.defaultValue) errors.push(mod.code + ": enum attr '" + a.name + "' missing defaultValue");
|
|
332
|
+
// FK naming convention: must end with "Id"
|
|
333
|
+
if ((a.type === 'guid' || a.type === 'string') && a.foreignKey && !a.name.endsWith('Id'))
|
|
334
|
+
warnings.push(mod.code + ": entity " + e.name + " FK attr '" + a.name + "' should end with 'Id'");
|
|
335
|
+
}
|
|
336
|
+
// Person Extension Pattern check
|
|
337
|
+
const personFields = ['firstName', 'lastName', 'email', 'phoneNumber', 'phone'];
|
|
338
|
+
const foundPersonFields = (e.attributes || []).filter(a => personFields.includes(a.name));
|
|
339
|
+
if (foundPersonFields.length >= 3 && !e.personRoleConfig) {
|
|
340
|
+
errors.push(mod.code + ": entity '" + e.name + "' has " + foundPersonFields.length +
|
|
341
|
+
" person fields (" + foundPersonFields.map(f => f.name).join(", ") +
|
|
342
|
+
") but no personRoleConfig — use Person Extension Pattern (entity-architecture-decision.md section 0). " +
|
|
343
|
+
"Person fields (firstName, lastName, email) belong on auth_Users, not on the domain entity.");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Canonical usecases key check
|
|
348
|
+
const rawUCFile = READ(mod.dir + '/usecases.json');
|
|
349
|
+
if (rawUCFile && !rawUCFile.useCases && rawUCFile.usecases) {
|
|
350
|
+
errors.push(mod.code + ": usecases.json uses 'usecases' key instead of canonical 'useCases' — fix before proceeding");
|
|
351
|
+
}
|
|
352
|
+
// Check steps serialization format
|
|
353
|
+
for (const uc of ucs) {
|
|
354
|
+
if (uc.steps && Array.isArray(uc.steps) && uc.steps.length > 0 && typeof uc.steps[0] === 'object') {
|
|
355
|
+
errors.push(mod.code + ": UC '" + uc.id + "' uses steps[] with objects — must use mainScenario[] with strings");
|
|
356
|
+
}
|
|
357
|
+
if (uc.actor && !uc.primaryActor) {
|
|
358
|
+
warnings.push(mod.code + ": UC '" + uc.id + "' uses 'actor' instead of canonical 'primaryActor'");
|
|
253
359
|
}
|
|
254
360
|
}
|
|
255
361
|
|
|
@@ -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
|
|
@@ -72,10 +72,19 @@ Transform module "{moduleCode}" for handoff.
|
|
|
72
72
|
## Instructions
|
|
73
73
|
Read the following 5 files from {moduleDir}:
|
|
74
74
|
1. entities.json → entities[]
|
|
75
|
-
2. usecases.json → useCases[]
|
|
76
|
-
3. rules.json → rules[]
|
|
75
|
+
2. usecases.json → useCases[] (canonical key: "useCases", fallback: "usecases")
|
|
76
|
+
3. rules.json → rules[] (canonical key: "rules", fallback: "businessRules")
|
|
77
77
|
4. permissions.json → roles[], matrix[], permissionPaths[]
|
|
78
|
-
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.
|
|
79
88
|
|
|
80
89
|
Then build the handoff data following these rules:
|
|
81
90
|
|
|
@@ -4663,16 +4663,43 @@ function renderSmartDashboardMockup(res) {
|
|
|
4663
4663
|
{ label: 'Taux', value: '80%' }
|
|
4664
4664
|
];
|
|
4665
4665
|
kpis.forEach(function(kpi) {
|
|
4666
|
-
|
|
4666
|
+
var displayValue = kpiDisplayValue(kpi);
|
|
4667
|
+
html += '<div class="mock-kpi"><div class="mock-kpi-value">' + escapeHtml(displayValue) + '</div><div class="mock-kpi-label">' + escapeHtml(kpi.label || '') + '</div></div>';
|
|
4667
4668
|
});
|
|
4668
4669
|
html += '</div>';
|
|
4669
4670
|
|
|
4670
|
-
// Chart
|
|
4671
|
-
|
|
4671
|
+
// Chart placeholders
|
|
4672
|
+
var charts = res.charts || [{ label: 'Graphique' }];
|
|
4673
|
+
charts.forEach(function(chart) {
|
|
4674
|
+
html += '<div class="mock-chart-placeholder">' + escapeHtml(chart.label || chart.type || 'Graphique') + '</div>';
|
|
4675
|
+
});
|
|
4672
4676
|
|
|
4673
4677
|
return html;
|
|
4674
4678
|
}
|
|
4675
4679
|
|
|
4680
|
+
function kpiDisplayValue(kpi) {
|
|
4681
|
+
var v = kpi.value || '0';
|
|
4682
|
+
// If value is already a simple display value (number, percentage), use it
|
|
4683
|
+
if (/^\d[\d,.\s]*%?$/.test(v)) return v;
|
|
4684
|
+
// Detect formula patterns and generate realistic sample values
|
|
4685
|
+
if (v.indexOf('(') !== -1) {
|
|
4686
|
+
var label = (kpi.label || '').toLowerCase();
|
|
4687
|
+
var color = (kpi.color || '').toLowerCase();
|
|
4688
|
+
// Percentage indicators
|
|
4689
|
+
if (v.indexOf('ratio') !== -1 || v.indexOf('%') !== -1 || label.indexOf('taux') !== -1 || label.indexOf('rate') !== -1)
|
|
4690
|
+
return '87.5%';
|
|
4691
|
+
// Average
|
|
4692
|
+
if (v.indexOf('avg') !== -1 || label.indexOf('moyen') !== -1 || label.indexOf('average') !== -1)
|
|
4693
|
+
return '12.4';
|
|
4694
|
+
// Sum (monetary or hours)
|
|
4695
|
+
if (v.indexOf('sum') !== -1)
|
|
4696
|
+
return label.indexOf('jour') !== -1 || label.indexOf('day') !== -1 ? '156' : '45,678';
|
|
4697
|
+
// Count (default)
|
|
4698
|
+
return '1,234';
|
|
4699
|
+
}
|
|
4700
|
+
return v;
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4676
4703
|
/* ---------- SmartFilter ---------- */
|
|
4677
4704
|
function renderSmartFilterMockup(res) {
|
|
4678
4705
|
var options = res.options || [];
|
|
@@ -303,16 +303,43 @@ function renderSmartDashboardMockup(res) {
|
|
|
303
303
|
{ label: 'Taux', value: '80%' }
|
|
304
304
|
];
|
|
305
305
|
kpis.forEach(function(kpi) {
|
|
306
|
-
|
|
306
|
+
var displayValue = kpiDisplayValue(kpi);
|
|
307
|
+
html += '<div class="mock-kpi"><div class="mock-kpi-value">' + escapeHtml(displayValue) + '</div><div class="mock-kpi-label">' + escapeHtml(kpi.label || '') + '</div></div>';
|
|
307
308
|
});
|
|
308
309
|
html += '</div>';
|
|
309
310
|
|
|
310
|
-
// Chart
|
|
311
|
-
|
|
311
|
+
// Chart placeholders
|
|
312
|
+
var charts = res.charts || [{ label: 'Graphique' }];
|
|
313
|
+
charts.forEach(function(chart) {
|
|
314
|
+
html += '<div class="mock-chart-placeholder">' + escapeHtml(chart.label || chart.type || 'Graphique') + '</div>';
|
|
315
|
+
});
|
|
312
316
|
|
|
313
317
|
return html;
|
|
314
318
|
}
|
|
315
319
|
|
|
320
|
+
function kpiDisplayValue(kpi) {
|
|
321
|
+
var v = kpi.value || '0';
|
|
322
|
+
// If value is already a simple display value (number, percentage), use it
|
|
323
|
+
if (/^\d[\d,.\s]*%?$/.test(v)) return v;
|
|
324
|
+
// Detect formula patterns and generate realistic sample values
|
|
325
|
+
if (v.indexOf('(') !== -1) {
|
|
326
|
+
var label = (kpi.label || '').toLowerCase();
|
|
327
|
+
var color = (kpi.color || '').toLowerCase();
|
|
328
|
+
// Percentage indicators
|
|
329
|
+
if (v.indexOf('ratio') !== -1 || v.indexOf('%') !== -1 || label.indexOf('taux') !== -1 || label.indexOf('rate') !== -1)
|
|
330
|
+
return '87.5%';
|
|
331
|
+
// Average
|
|
332
|
+
if (v.indexOf('avg') !== -1 || label.indexOf('moyen') !== -1 || label.indexOf('average') !== -1)
|
|
333
|
+
return '12.4';
|
|
334
|
+
// Sum (monetary or hours)
|
|
335
|
+
if (v.indexOf('sum') !== -1)
|
|
336
|
+
return label.indexOf('jour') !== -1 || label.indexOf('day') !== -1 ? '156' : '45,678';
|
|
337
|
+
// Count (default)
|
|
338
|
+
return '1,234';
|
|
339
|
+
}
|
|
340
|
+
return v;
|
|
341
|
+
}
|
|
342
|
+
|
|
316
343
|
/* ---------- SmartFilter ---------- */
|
|
317
344
|
function renderSmartFilterMockup(res) {
|
|
318
345
|
var options = res.options || [];
|
|
@@ -192,6 +192,29 @@ function normalizeAction(a) {
|
|
|
192
192
|
return a.code || a.id || a.label || a.actionCode || "";
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
// Helper: normalize a KPI — replace formula values with sample display values
|
|
196
|
+
function normalizeKpi(k) {
|
|
197
|
+
if (typeof k !== 'object') return { label: String(k), value: "0" };
|
|
198
|
+
const label = k.label || k.displayName || "";
|
|
199
|
+
const rawValue = k.value || k.metric || "0";
|
|
200
|
+
let displayValue = rawValue;
|
|
201
|
+
// Detect formula patterns (e.g., count(...), sum(...), ratio(...), avg(...))
|
|
202
|
+
if (typeof rawValue === 'string' && rawValue.includes('(')) {
|
|
203
|
+
const lbl = label.toLowerCase();
|
|
204
|
+
if (rawValue.includes('ratio') || rawValue.includes('%') || lbl.includes('taux') || lbl.includes('rate'))
|
|
205
|
+
displayValue = "87.5%";
|
|
206
|
+
else if (rawValue.includes('avg') || lbl.includes('moyen') || lbl.includes('average'))
|
|
207
|
+
displayValue = "12.4";
|
|
208
|
+
else if (rawValue.includes('sum'))
|
|
209
|
+
displayValue = (lbl.includes('jour') || lbl.includes('day') || lbl.includes('heure') || lbl.includes('hour')) ? "156" : "45,678";
|
|
210
|
+
else
|
|
211
|
+
displayValue = "1,234";
|
|
212
|
+
}
|
|
213
|
+
// Also handle format hint from the KPI definition
|
|
214
|
+
if (k.format === 'percentage' && displayValue === rawValue) displayValue = "80%";
|
|
215
|
+
return { label: label, value: displayValue };
|
|
216
|
+
}
|
|
217
|
+
|
|
195
218
|
if (flatScr.length > 0) {
|
|
196
219
|
// FORMAT A: flat screens[] array (ba-003 style)
|
|
197
220
|
const bySec = {};
|
|
@@ -214,9 +237,7 @@ if (flatScr.length > 0) {
|
|
|
214
237
|
fields: (t.fields || []).map(normalizeField)
|
|
215
238
|
})),
|
|
216
239
|
actions: (s.actions || []).map(normalizeAction),
|
|
217
|
-
kpis: (s.kpis || []).map(
|
|
218
|
-
? { label: k.label || k.displayName || "", value: k.value || k.metric || "0" }
|
|
219
|
-
: { label: String(k), value: "0" }),
|
|
240
|
+
kpis: (s.kpis || []).map(normalizeKpi),
|
|
220
241
|
options: s.options || [],
|
|
221
242
|
permission: s.permission || "",
|
|
222
243
|
notes: s.description || s.sectionDescription || ""
|
|
@@ -224,39 +245,65 @@ if (flatScr.length > 0) {
|
|
|
224
245
|
});
|
|
225
246
|
screens = Object.values(bySec);
|
|
226
247
|
} else if (sectionScr.length > 0) {
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
248
|
+
// DETECT sub-format: canonical (resources[] inside sections) vs ba-004 (data on section itself)
|
|
249
|
+
const hasNestedResources = sectionScr.some(sec => (sec.resources || []).length > 0);
|
|
250
|
+
|
|
251
|
+
if (hasNestedResources) {
|
|
252
|
+
// FORMAT B-CANONICAL: sections[] with resources[] (ba-005+ style — sectionCode/sectionLabel/resources[])
|
|
253
|
+
// Resources have their own code/type/columns/tabs/kpis — iterate over them
|
|
254
|
+
screens = sectionScr.map(sec => ({
|
|
255
|
+
sectionCode: sec.sectionCode || sec.id || sec.code || "",
|
|
256
|
+
sectionLabel: sec.sectionLabel || sec.displayName || sec.label || sec.sectionCode || "",
|
|
257
|
+
resources: (sec.resources || []).map(res => ({
|
|
258
|
+
code: res.code || res.id || "",
|
|
259
|
+
label: res.label || res.displayName || res.code || "",
|
|
260
|
+
type: res.type || res.componentType || res.layout || "unknown",
|
|
261
|
+
columns: (res.columns || []).map(normalizeColumn),
|
|
262
|
+
filters: (res.filters || []).map(normalizeFilter),
|
|
263
|
+
fields: [],
|
|
264
|
+
tabs: (res.tabs || []).map(t => ({
|
|
265
|
+
label: t.label || t.displayName || t.code || t.id || "",
|
|
266
|
+
fields: (t.fields || []).map(normalizeField)
|
|
267
|
+
})),
|
|
268
|
+
actions: (res.actions || []).map(normalizeAction),
|
|
269
|
+
kpis: (res.kpis || []).map(normalizeKpi),
|
|
270
|
+
options: res.options || [],
|
|
271
|
+
permission: res.permission || "",
|
|
272
|
+
notes: res.notes || res.description || ""
|
|
273
|
+
}))
|
|
274
|
+
}));
|
|
275
|
+
} else {
|
|
276
|
+
// FORMAT B-LEGACY: sections[] without resources[] (ba-004 style — data directly on section)
|
|
277
|
+
const bySec = {};
|
|
278
|
+
sectionScr.forEach(sec => {
|
|
279
|
+
const secId = sec.id || sec.code || sec.sectionCode || "";
|
|
280
|
+
const secLabel = sec.displayName || sec.label || sec.sectionLabel || secId;
|
|
281
|
+
const componentType = sec.layout || sec.componentType || "unknown";
|
|
282
|
+
if (!bySec[secId]) bySec[secId] = {
|
|
283
|
+
sectionCode: secId,
|
|
284
|
+
sectionLabel: secLabel,
|
|
285
|
+
resources: []
|
|
286
|
+
};
|
|
287
|
+
bySec[secId].resources.push({
|
|
288
|
+
code: secId,
|
|
289
|
+
label: secLabel,
|
|
290
|
+
type: componentType,
|
|
291
|
+
columns: (sec.columns || []).map(normalizeColumn),
|
|
292
|
+
filters: (sec.filters || []).map(normalizeFilter),
|
|
293
|
+
fields: [],
|
|
294
|
+
tabs: (sec.tabs || []).map(t => ({
|
|
295
|
+
label: t.displayName || t.label || t.code || t.id || "",
|
|
296
|
+
fields: (t.fields || []).map(normalizeField)
|
|
297
|
+
})),
|
|
298
|
+
actions: (sec.actions || []).map(normalizeAction),
|
|
299
|
+
kpis: (sec.kpis || []).map(normalizeKpi),
|
|
300
|
+
options: sec.options || [],
|
|
301
|
+
permission: (typeof sec.permissions === 'object' ? sec.permissions?.view : sec.permission) || "",
|
|
302
|
+
notes: sec.description || ""
|
|
303
|
+
});
|
|
257
304
|
});
|
|
258
|
-
|
|
259
|
-
|
|
305
|
+
screens = Object.values(bySec);
|
|
306
|
+
}
|
|
260
307
|
}
|
|
261
308
|
moduleSpecs[moduleCode].screens = screens;
|
|
262
309
|
```
|