@atlashub/smartstack-cli 3.16.0 → 3.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlashub/smartstack-cli",
3
- "version": "3.16.0",
3
+ "version": "3.17.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -77,10 +77,26 @@ git fetch origin develop:refs/remotes/origin/develop --force --quiet
77
77
 
78
78
  ### 4. Version Bump (Release/Hotfix)
79
79
 
80
- For npm projects:
80
+ After merge-back to develop, invoke `/version-bump` to set the next development version:
81
+
82
+ | Finished type | Next version | Example |
83
+ |---------------|-------------|---------|
84
+ | Release X.Y.Z | X.(Y+1).0 | 2.8.0 → 2.9.0 |
85
+ | Hotfix X.Y.Z | X.Y.(Z+1) | 2.8.1 → 2.8.2 |
86
+
81
87
  ```bash
82
- npm version $NEXT_VERSION --no-git-tag-version
83
- git add package.json package-lock.json
88
+ # Calculate NEXT_VERSION from the released version
89
+ # Then call: /version-bump $NEXT_VERSION
90
+ ```
91
+
92
+ This updates **all** version sources in sync:
93
+ - `Directory.Build.props` → `<VersionPrefix>X.Y.Z</VersionPrefix>`
94
+ - `web/smartstack-web/package.json` → `"version": "X.Y.Z"`
95
+ - `.claude/gitflow/config.json` → `versioning.current`
96
+
97
+ Then commit and push:
98
+ ```bash
99
+ git add Directory.Build.props web/smartstack-web/package.json .claude/gitflow/config.json
84
100
  git commit -m "$(cat <<'EOF'
85
101
  chore: bump version to $NEXT_VERSION for development
86
102
 
@@ -90,6 +106,8 @@ EOF
90
106
  git push origin develop
91
107
  ```
92
108
 
109
+ > **IMPORTANT**: Do NOT use `npm version` directly. Always use `/version-bump` for unified versioning.
110
+
93
111
  ### 5. Cleanup Worktree + Branch
94
112
 
95
113
  ```bash
@@ -75,14 +75,24 @@ git fetch origin $FULL_BRANCH:refs/remotes/origin/$FULL_BRANCH --force --quiet
75
75
 
76
76
  **Port allocation:** Hash branch name for unique ports (API: 5200+, Web: 5300+)
77
77
 
78
- ### 7. Version Bump (Release only)
78
+ ### 7. Version Bump (Release/Hotfix only)
79
+
80
+ For `release/*` and `hotfix/*` branches, invoke the `/version-bump` skill to synchronize all version files:
79
81
 
80
82
  ```bash
81
- # For release: bump version in source files
82
- npm version $VERSION --no-git-tag-version # if package.json
83
- # OR update .csproj <Version> tag
83
+ # Extract version from branch name: release/2.8.0 2.8.0, hotfix/2.8.1 → 2.8.1
84
+ VERSION=$(echo "$FULL_BRANCH" | sed 's|^[^/]*/||')
84
85
  ```
85
86
 
87
+ Then call: `/version-bump $VERSION`
88
+
89
+ This updates **all** version sources in sync:
90
+ - `Directory.Build.props` → `<VersionPrefix>X.Y.Z</VersionPrefix>`
91
+ - `web/smartstack-web/package.json` → `"version": "X.Y.Z"`
92
+ - `.claude/gitflow/config.json` → `versioning.current`
93
+
94
+ > **IMPORTANT**: Do NOT use `npm version` or edit `.csproj` directly. Always use `/version-bump` for unified versioning.
95
+
86
96
  ### 8. Summary
87
97
 
88
98
  ```
@@ -82,6 +82,12 @@ public interface I$MODULE_PASCALService
82
82
 
83
83
  ## TEMPLATE: SERVICE IMPLEMENTATION
84
84
 
85
+ > **TENANT ISOLATION RULES (BLOCKING):**
86
+ > - ALL queries MUST include `.Where(x => x.TenantId == _currentUser.TenantId)`
87
+ > - ALL entity creation MUST pass `_currentUser.TenantId` as first parameter to `Entity.Create()`
88
+ > - NEVER use `new Entity { }` without `TenantId =` — always prefer the factory method
89
+ > - NEVER use `Guid.Empty` as a placeholder — resolve the actual value from `_currentUser`
90
+
85
91
  ```csharp
86
92
  // src/SmartStack.Infrastructure/Services/$CONTEXT_PASCAL/$APPLICATION_PASCAL/$MODULE_PASCAL/$MODULE_PASCALService.cs
87
93
 
@@ -114,7 +120,8 @@ public class $MODULE_PASCALService : I$MODULE_PASCALService
114
120
  CancellationToken cancellationToken = default)
115
121
  {
116
122
  var query = _context.$ENTITY_PLURAL
117
- .AsNoTracking();
123
+ .AsNoTracking()
124
+ .Where(x => x.TenantId == _currentUser.TenantId); // MANDATORY: tenant isolation
118
125
 
119
126
  // Apply filters
120
127
  if (!string.IsNullOrWhiteSpace(parameters.SearchTerm))
@@ -167,6 +174,7 @@ public class $MODULE_PASCALService : I$MODULE_PASCALService
167
174
  {
168
175
  var entity = await _context.$ENTITY_PLURAL
169
176
  .AsNoTracking()
177
+ .Where(x => x.TenantId == _currentUser.TenantId) // MANDATORY: tenant isolation
170
178
  .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
171
179
 
172
180
  if (entity == null)
@@ -188,6 +196,7 @@ public class $MODULE_PASCALService : I$MODULE_PASCALService
188
196
  CancellationToken cancellationToken = default)
189
197
  {
190
198
  var entity = $ENTITY_PASCAL.Create(
199
+ _currentUser.TenantId, // MANDATORY: tenant isolation — always first parameter
191
200
  request.Name,
192
201
  request.Description);
193
202
 
@@ -216,6 +225,7 @@ public class $MODULE_PASCALService : I$MODULE_PASCALService
216
225
  CancellationToken cancellationToken = default)
217
226
  {
218
227
  var entity = await _context.$ENTITY_PLURAL
228
+ .Where(x => x.TenantId == _currentUser.TenantId) // MANDATORY: tenant isolation
219
229
  .FirstOrDefaultAsync(x => x.Id == id, cancellationToken)
220
230
  ?? throw new NotFoundException("$ENTITY_PASCAL", id);
221
231
 
@@ -245,6 +255,7 @@ public class $MODULE_PASCALService : I$MODULE_PASCALService
245
255
  CancellationToken cancellationToken = default)
246
256
  {
247
257
  var entity = await _context.$ENTITY_PLURAL
258
+ .Where(x => x.TenantId == _currentUser.TenantId) // MANDATORY: tenant isolation
248
259
  .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
249
260
 
250
261
  if (entity == null)
@@ -3074,20 +3074,26 @@ function renderModuleMockups(code) {
3074
3074
  </div>` : ''}
3075
3075
  ${(wf.elements || []).length > 0 ? `
3076
3076
  <div class="wireframe-metadata">
3077
- <div><strong>Elements:</strong> ${wf.elements.join(', ')}</div>
3077
+ <div><strong>Elements:</strong> ${wf.elements.map(e => typeof e === 'string' ? e : (e.type || e.label || e.id || '')).filter(Boolean).join(', ')}</div>
3078
3078
  </div>` : ''}
3079
- ${(wf.componentMapping || []).length > 0 ? `
3079
+ ${(() => {
3080
+ const cm = wf.componentMapping;
3081
+ const mappings = Array.isArray(cm) ? cm
3082
+ : (typeof cm === 'object' && cm !== null) ? Object.entries(cm).map(([k,v]) => ({wireframeElement: k, reactComponent: v}))
3083
+ : [];
3084
+ return mappings.length > 0 ? `
3080
3085
  <details class="wireframe-details">
3081
3086
  <summary>Mapping composants SmartStack</summary>
3082
3087
  <table class="mapping-table">
3083
3088
  <thead><tr><th>Element maquette</th><th>Composant React</th></tr></thead>
3084
3089
  <tbody>
3085
- ${wf.componentMapping.map(m =>
3086
- `<tr><td>${m.wireframeElement}</td><td><code>${m.reactComponent}</code></td></tr>`
3090
+ ${mappings.map(m =>
3091
+ '<tr><td>' + (m.wireframeElement || '') + '</td><td><code>' + (m.reactComponent || '') + '</code></td></tr>'
3087
3092
  ).join('')}
3088
3093
  </tbody>
3089
3094
  </table>
3090
- </details>` : ''}
3095
+ </details>` : '';
3096
+ })()}
3091
3097
  <div class="wireframe-comment">
3092
3098
  <label style="font-size:0.8rem;color:var(--text-muted);display:block;margin-bottom:0.3rem;">Commentaire / Feedback :</label>
3093
3099
  <textarea class="form-textarea"
@@ -392,20 +392,26 @@ function renderModuleMockups(code) {
392
392
  </div>` : ''}
393
393
  ${(wf.elements || []).length > 0 ? `
394
394
  <div class="wireframe-metadata">
395
- <div><strong>Elements:</strong> ${wf.elements.join(', ')}</div>
395
+ <div><strong>Elements:</strong> ${wf.elements.map(e => typeof e === 'string' ? e : (e.type || e.label || e.id || '')).filter(Boolean).join(', ')}</div>
396
396
  </div>` : ''}
397
- ${(wf.componentMapping || []).length > 0 ? `
397
+ ${(() => {
398
+ const cm = wf.componentMapping;
399
+ const mappings = Array.isArray(cm) ? cm
400
+ : (typeof cm === 'object' && cm !== null) ? Object.entries(cm).map(([k,v]) => ({wireframeElement: k, reactComponent: v}))
401
+ : [];
402
+ return mappings.length > 0 ? `
398
403
  <details class="wireframe-details">
399
404
  <summary>Mapping composants SmartStack</summary>
400
405
  <table class="mapping-table">
401
406
  <thead><tr><th>Element maquette</th><th>Composant React</th></tr></thead>
402
407
  <tbody>
403
- ${wf.componentMapping.map(m =>
404
- `<tr><td>${m.wireframeElement}</td><td><code>${m.reactComponent}</code></td></tr>`
408
+ ${mappings.map(m =>
409
+ '<tr><td>' + (m.wireframeElement || '') + '</td><td><code>' + (m.reactComponent || '') + '</code></td></tr>'
405
410
  ).join('')}
406
411
  </tbody>
407
412
  </table>
408
- </details>` : ''}
413
+ </details>` : '';
414
+ })()}
409
415
  <div class="wireframe-comment">
410
416
  <label style="font-size:0.8rem;color:var(--text-muted);display:block;margin-bottom:0.3rem;">Commentaire / Feedback :</label>
411
417
  <textarea class="form-textarea"
@@ -42,7 +42,19 @@ const FEATURE_DATA = {
42
42
  "{moduleCode}": {
43
43
  useCases: module.specification.useCases || [],
44
44
  businessRules: module.analysis.businessRules || [],
45
- entities: module.analysis.entities || [],
45
+ // ENTITY SAFETY NET: map fields[] → attributes[] if agent deviated from canonical format
46
+ entities: (module.analysis.entities || []).map(ent => ({
47
+ name: ent.name,
48
+ description: ent.description || "",
49
+ attributes: (ent.attributes || []).length > 0
50
+ ? ent.attributes.map(a => ({ name: a.name, description: a.description || "" }))
51
+ : (ent.fields || []).map(f => ({ name: f.name, description: f.description || f.type || "" })),
52
+ relationships: (ent.relationships || []).length > 0
53
+ ? ent.relationships.map(r =>
54
+ typeof r === 'string' ? r : `${r.target} (${r.type}) - ${r.description || ""}`)
55
+ : (ent.fields || []).filter(f => f.foreignKey)
56
+ .map(f => `${f.foreignKey.table} (N:1) - ${f.description || f.name}`)
57
+ })),
46
58
  permissions: module.specification.permissions || [],
47
59
  apiEndpoints: module.specification.apiEndpoints || []
48
60
  }
@@ -85,15 +97,19 @@ const EMBEDDED_ARTIFACTS = {
85
97
  // PER-MODULE keyed object (NOT a flat array)
86
98
  // FOR EACH module: extract from specification.uiWireframes[]
87
99
  [moduleCode]: moduleFeature.specification.uiWireframes.map(wf => ({
88
- screen: wf.screen, // e.g. "employees-list"
89
- section: wf.section, // e.g. "list"
90
- format: wf.mockupFormat || "ascii", // RENAME: mockupFormat → format
91
- content: wf.mockup, // RENAME: mockup → content (ASCII art string)
100
+ screen: wf.screen || wf.name || wf.id || "", // SAFETY NET: fallback name/id → screen
101
+ section: wf.section || "", // e.g. "list"
102
+ format: wf.mockupFormat || "ascii", // RENAME: mockupFormat → format
103
+ content: wf.mockup, // RENAME: mockup → content (ASCII art string)
92
104
  description: wf.description || "",
93
- elements: wf.elements || [], // ["DataGrid", "FilterBar", ...]
94
- actions: wf.actions || [], // ["filter", "sort", "create", ...]
95
- componentMapping: wf.componentMapping || [], // [{ wireframeElement, reactComponent }]
96
- layout: wf.layout || null, // { type, regions: [...] }
105
+ elements: wf.elements || [], // [{ id, type, label }] or ["DataGrid", ...]
106
+ actions: wf.actions || [], // [{ trigger, action }] or ["filter", ...]
107
+ // SAFETY NET: convert object array format if agent used { "Header": "PageHeader" } instead of [{ wireframeElement, reactComponent }]
108
+ componentMapping: Array.isArray(wf.componentMapping) ? wf.componentMapping
109
+ : typeof wf.componentMapping === 'object' && wf.componentMapping !== null
110
+ ? Object.entries(wf.componentMapping).map(([k, v]) => ({ wireframeElement: k, reactComponent: v }))
111
+ : [],
112
+ layout: typeof wf.layout === 'object' ? wf.layout : null, // SAFETY NET: ignore string layout ("grid")
97
113
  permissionsRequired: wf.permissionsRequired || []
98
114
  }))
99
115
  },
@@ -32,6 +32,18 @@ const checklist = {
32
32
  details: "Standalone entities should be rare in business modules"
33
33
  },
34
34
 
35
+ entitySchemaCompliance: {
36
+ check: "All entities use canonical format: attributes[] (not fields[]), relationships[] array present",
37
+ status: specification.entities.every(e =>
38
+ Array.isArray(e.attributes) && e.attributes.length > 0 &&
39
+ !e.fields && !e.tableName && !e.primaryKey
40
+ ) ? "PASS" : "FAIL",
41
+ blocking: true,
42
+ details: "Entities with 'fields' instead of 'attributes' break HTML data model display and PRD extraction. " +
43
+ "Check: every entity must have attributes[] (not fields[]), every entity must have relationships[], " +
44
+ "no entity should have tableName or primaryKey (technical fields belong in implementation, not analysis)"
45
+ },
46
+
35
47
  // SECTION 2: BUSINESS RULES (BLOCKING)
36
48
  businessRules: {
37
49
  minimum: 4,
@@ -133,6 +145,21 @@ const checklist = {
133
145
  details: "EVERY section MUST have a wireframe (ASCII/SVG)"
134
146
  },
135
147
 
148
+ wireframeSchemaCompliance: {
149
+ check: "All wireframes have required fields: screen, section, mockupFormat, elements[], componentMapping[], layout (object), permissionsRequired[]",
150
+ status: specification.uiWireframes.every(wf =>
151
+ wf.screen && wf.section && wf.mockupFormat &&
152
+ Array.isArray(wf.elements) && wf.elements.length > 0 &&
153
+ Array.isArray(wf.componentMapping) && wf.componentMapping.length > 0 &&
154
+ typeof wf.layout === 'object' && wf.layout !== null &&
155
+ Array.isArray(wf.permissionsRequired)
156
+ ) ? "PASS" : "FAIL",
157
+ blocking: true,
158
+ details: "Wireframes missing metadata render as empty frames in HTML documentation. " +
159
+ "Check: screen (not name/id), section, mockupFormat, elements[] non-empty, " +
160
+ "componentMapping[] as array of {wireframeElement, reactComponent}, layout as object (not string)"
161
+ },
162
+
136
163
  navigation: {
137
164
  check: "Module has ≥1 navigation entry",
138
165
  status: specification.navigation.entries.length >= 1 ? "PASS" : "FAIL",
@@ -261,11 +288,11 @@ ELSE:
261
288
 
262
289
  | Category | Checks | Passed | Failed | Warnings |
263
290
  |----------|--------|--------|--------|----------|
264
- | Data Model | 3 | {n} | {n} | {n} |
291
+ | Data Model | 4 | {n} | {n} | {n} |
265
292
  | Business Rules | 3 | {n} | {n} | {n} |
266
293
  | Use Cases & FRs | 4 | {n} | {n} | {n} |
267
294
  | Permissions | 3 | {n} | {n} | {n} |
268
- | UI & Navigation | 3 | {n} | {n} | {n} |
295
+ | UI & Navigation | 4 | {n} | {n} | {n} |
269
296
  | I18N & Messages | 3 | {n} | {n} | {n} |
270
297
  | Seed Data | 2 | {n} | {n} | {n} |
271
298
  | API Endpoints | 2 | {n} | {n} | {n} |
@@ -60,6 +60,20 @@ Define entities for this module (business attributes, not technical):
60
60
  > ```
61
61
  > **FORBIDDEN fields in attributes:** Do NOT use `type`, `values`, `rules`.
62
62
  > Use `description` to explain what the attribute is, `validation` for format/constraint rules, `unique` as boolean.
63
+ >
64
+ > **FORBIDDEN FORMAT — DO NOT USE (technical/database schema):**
65
+ > ```json
66
+ > {
67
+ > "name": "Project",
68
+ > "tableName": "rh_projects",
69
+ > "primaryKey": "id",
70
+ > "fields": [
71
+ > { "name": "code", "type": "UUID", "required": true }
72
+ > ]
73
+ > }
74
+ > ```
75
+ > **Why forbidden:** `tableName`, `primaryKey` are technical fields that do NOT belong in analysis. `fields[]` is NOT recognized by the build pipeline — only `attributes[]` is. `type` with SQL types (UUID, string, timestamp) belongs in implementation, not analysis. Using this format causes **empty entity data in the HTML documentation** and **broken PRD extraction**.
76
+ > The canonical format uses `attributes[]` (not `fields[]`), `relationships[]` (not FK inside fields), and NO technical fields (tableName, primaryKey).
63
77
 
64
78
  #### 6c. Process Flow
65
79
 
@@ -130,9 +144,13 @@ Before proceeding to step-03b-ui.md, VERIFY:
130
144
 
131
145
  1. **Module feature.json exists** at expected path (`docs/business/{app}/{module}/business-analyse/v{X.Y}/feature.json`)
132
146
  2. **Module feature.json has analysis section** with `entities[]` (at least 1) and `businessRules[]` (at least 1)
133
- 3. **Module status in master** = "in-progress" (set by section 2 above)
134
- 4. **Master modules[].featureJsonPath** for this module null (set by ba-writer.create)
135
- 5. **Sections identified** with clear roles and entities assigned
147
+ 3. **Entity schema compliance (BLOCKING)** EVERY entity has `attributes[]` (NOT `fields[]`) and `relationships[]` arrays.
148
+ Quick check: `analysis.entities.every(e => Array.isArray(e.attributes) && e.attributes.length >= 1)`
149
+ IF any entity has `fields[]` instead of `attributes[]` → STRUCTURAL ERROR, fix before proceeding.
150
+ IF any entity has `tableName` or `primaryKey` → STRUCTURAL ERROR, these are technical fields that do not belong in analysis.
151
+ 4. **Module status in master** = "in-progress" (set by section 2 above)
152
+ 5. **Master modules[].featureJsonPath** for this module ≠ null (set by ba-writer.create)
153
+ 6. **Sections identified** with clear roles and entities assigned
136
154
 
137
155
  **IF any check fails → FIX before proceeding.** Do NOT load step-03b-ui with incomplete data.
138
156
 
@@ -167,9 +167,36 @@ Example for a list section:
167
167
  Store in specification.uiWireframes[] (**MANDATORY** for every section):
168
168
 
169
169
  See [references/ui-resource-cards.md](../references/ui-resource-cards.md) for exact JSON format of `specification.uiWireframes[]`.
170
- **REQUIRED fields:** `screen`, `mockup`, `elements`, `section`, `componentMapping`, `layout` are ALL mandatory.
170
+ **REQUIRED fields:** `screen`, `mockup`, `mockupFormat`, `elements`, `section`, `actions`, `componentMapping`, `layout`, `permissionsRequired` are ALL mandatory.
171
171
  A wireframe without `componentMapping` or `layout` will FAIL validation in step 9.
172
172
 
173
+ > **STRUCTURE CARD: specification.uiWireframes[]** — ALL fields are MANDATORY. Do NOT omit any.
174
+ > ```json
175
+ > {
176
+ > "screen": "{module}-{section}", // MANDATORY — unique screen ID (NOT "name" or "id")
177
+ > "section": "list|detail|create|approve|...", // MANDATORY — section code
178
+ > "description": "Description en français",
179
+ > "mockupFormat": "ascii", // MANDATORY — "ascii" or "svg"
180
+ > "mockup": "┌───...┘", // MANDATORY — ASCII art string
181
+ > "elements": [ // MANDATORY — array of element objects
182
+ > { "id": "elem-grid", "type": "SmartTable", "label": "Grille" }
183
+ > ],
184
+ > "actions": [ // MANDATORY — user interactions
185
+ > { "trigger": "Click row", "action": "Navigate to detail" }
186
+ > ],
187
+ > "componentMapping": [ // MANDATORY — array of mapping objects
188
+ > { "wireframeElement": "DataGrid", "reactComponent": "SmartTable" }
189
+ > ],
190
+ > "layout": { // MANDATORY — object with regions (NOT a string)
191
+ > "type": "page",
192
+ > "regions": [{ "id": "main", "position": "main", "span": 12, "components": [...] }]
193
+ > },
194
+ > "permissionsRequired": ["read"] // MANDATORY — required permissions
195
+ > }
196
+ > ```
197
+ > **FORBIDDEN FORMAT:** wireframe with only `name` + `mockup` + `layout: "grid"` (string).
198
+ > Every wireframe MUST have ALL fields above. Missing metadata causes empty frames in the HTML documentation.
199
+
173
200
  > **IF client rejects a mockup:** Revise and re-propose until validated. Do NOT proceed without client approval on the layout.
174
201
 
175
202
  ### 3b-bis. Wireframe-to-Component Mapping
@@ -250,6 +277,9 @@ Before proceeding to step-03c-compile.md, VERIFY:
250
277
  6. **State machines defined** (if module has status fields) in `specification.lifeCycles[]`
251
278
  7. **Form field completeness** — SmartForm fields[] covers ALL entity attributes except system/audit fields
252
279
  8. **Navigation entries for all sections** — Every section (including dashboard) has a corresponding entry in `specification.navigation.entries[]`
280
+ 9. **Wireframe structural compliance (BLOCKING)** — ALL wireframes have: `screen` (not just `name`), `section`, `mockupFormat`, `elements[]` (non-empty array of objects), `actions[]`, `componentMapping[]` (array of {wireframeElement, reactComponent}), `layout` (object with regions, NOT a string), `permissionsRequired[]`
281
+ Quick check: `uiWireframes.every(wf => wf.screen && wf.section && wf.mockupFormat && Array.isArray(wf.elements) && wf.elements.length > 0 && typeof wf.layout === 'object' && wf.layout !== null)`
282
+ IF any wireframe fails → FIX before proceeding. DO NOT load step-03c with degraded wireframes.
253
283
 
254
284
  **IF any check fails → FIX before proceeding.** Do NOT load step-03c-compile with incomplete data.
255
285
 
@@ -41,7 +41,9 @@ Validate the module specification for completeness and consistency, write to fea
41
41
  | functionalRequirements | 4 | PASS/FAIL |
42
42
  | permissionMatrix | 1 resource × 2 roles | PASS/FAIL |
43
43
  | entities | 1 | PASS/FAIL |
44
+ | entitySchemaFormat | attributes[] not fields[] (BLOCKING) | PASS/FAIL |
44
45
  | wireframes | 1 per section (BLOCKING) | PASS/FAIL |
46
+ | wireframeSchema | All required fields present (BLOCKING) | PASS/FAIL |
45
47
  | gherkinScenarios | 2 per UC | PASS/FAIL |
46
48
  | validations | 1 | PASS/FAIL |
47
49
  | messages | 4 | PASS/FAIL |
@@ -65,6 +67,8 @@ Validate the module specification for completeness and consistency, write to fea
65
67
  - BR-{CATEGORY}-NNN format
66
68
  - Entity names PascalCase
67
69
  - Field names camelCase
70
+ - Entity attribute format: `attributes[]` with {name, description}, NOT `fields[]` with {name, type} — entities must NOT have tableName or primaryKey
71
+ - Wireframe structure: `screen` (not `name`), `componentMapping` is array of {wireframeElement, reactComponent} (not plain key-value object), `layout` is object with regions (not string)
68
72
  - Permission paths dot-separated lowercase
69
73
 
70
74
  #### 9d. Decision
@@ -132,9 +136,9 @@ ba-writer.enrichSection({
132
136
 
133
137
  **Execute the comprehensive validation checklist:**
134
138
 
135
- Run the 25-check validation process across 10 categories:
136
- - Data Model (3 checks) | Business Rules (3 checks) | Use Cases & FRs (4 checks)
137
- - Permissions (3 checks) | UI & Navigation (3 checks) | I18N & Messages (3 checks)
139
+ Run the 27-check validation process across 10 categories:
140
+ - Data Model (4 checks) | Business Rules (3 checks) | Use Cases & FRs (4 checks)
141
+ - Permissions (3 checks) | UI & Navigation (4 checks) | I18N & Messages (3 checks)
138
142
  - Seed Data (2 checks) | API Endpoints (2 checks) | Validations (1 check) | Gherkin (1 check)
139
143
 
140
144
  ```javascript
@@ -141,6 +141,13 @@ Rules:
141
141
  - DTOs separate from domain entities
142
142
  - Service interfaces in Application, implementations in Infrastructure
143
143
 
144
+ **Tenant isolation (BLOCKING):**
145
+ - ALL queries on tenant entities MUST include `.Where(x => x.TenantId == _currentUser.TenantId)`
146
+ - ALL entity creation MUST pass `_currentUser.TenantId` as first parameter to `Entity.Create(tenantId, ...)`
147
+ - NEVER use `new Entity { }` without `TenantId =` — always prefer factory method `Entity.Create()`
148
+ - NEVER use `Guid.Empty` as a placeholder for userId, tenantId, or any business identifier
149
+ - Service constructor MUST inject `ICurrentUserService _currentUser` to access TenantId
150
+
144
151
  **Lifecycle-aware services:**
145
152
  - Services operating on entities with a `lifeCycle` (status field) MUST validate entity state before mutations
146
153
  - Example: `if (entity.Status == EmployeeStatus.Terminated) throw new BusinessException("Cannot update terminated employee")`
@@ -158,6 +165,10 @@ Rules:
158
165
  - Empty DependencyInjection.cs with `// TODO` placeholder
159
166
  - CreateValidator without matching UpdateValidator
160
167
  - Validators not registered in DI container
168
+ - Service query without TenantId filter (cross-tenant data leak)
169
+ - `new Entity { }` without TenantId assignment
170
+ - `Guid.Empty` as a business value in services or controllers
171
+ - Entity.Create() without tenantId as first parameter
161
172
 
162
173
  ---
163
174
 
@@ -385,6 +385,52 @@ public class NavigationResourceSeedEntry
385
385
 
386
386
  ## 3. PermissionsSeedData.cs — MCP-First
387
387
 
388
+ ### CRITICAL: PermissionAction Safety Rules
389
+
390
+ > **NEVER use `Enum.Parse<PermissionAction>("...")` — this causes runtime crashes if the string is invalid.**
391
+ > The error only manifests at application startup, not at compile time.
392
+
393
+ **Valid PermissionAction values (from SmartStack.Domain.Authorization):**
394
+
395
+ | Enum Value | Int | Description |
396
+ |------------|-----|-------------|
397
+ | `PermissionAction.Access` | 0 | Wildcard permissions only (IsWildcard = true) |
398
+ | `PermissionAction.Read` | 1 | GET/HEAD — View data |
399
+ | `PermissionAction.Create` | 2 | POST — Create new records |
400
+ | `PermissionAction.Update` | 3 | PUT/PATCH — Modify existing records |
401
+ | `PermissionAction.Delete` | 4 | DELETE — Remove records |
402
+ | `PermissionAction.Export` | 5 | Export data (CSV, Excel, etc.) |
403
+ | `PermissionAction.Import` | 6 | Import data |
404
+ | `PermissionAction.Approve` | 7 | Approve workflow items |
405
+ | `PermissionAction.Reject` | 8 | Reject workflow items |
406
+ | `PermissionAction.Assign` | 9 | Assign items to users |
407
+ | `PermissionAction.Execute` | 10 | Execute actions (sync, run, etc.) |
408
+
409
+ **Anti-patterns (FORBIDDEN):**
410
+
411
+ ```csharp
412
+ // FORBIDDEN — Runtime crash if string is not a valid enum value
413
+ Enum.Parse<PermissionAction>("Validate"); // ArgumentException at startup
414
+ (PermissionAction)Enum.Parse(typeof(PermissionAction), "Validate"); // Same crash
415
+
416
+ // FORBIDDEN — String-based action in anonymous objects
417
+ new { Action = "read" }; // Not type-safe, silent mismatch possible
418
+ ```
419
+
420
+ **Correct patterns (MANDATORY):**
421
+
422
+ ```csharp
423
+ // ALWAYS use the typed enum directly — compile-time safe
424
+ Action = PermissionAction.Read // Compile-time checked
425
+ Action = PermissionAction.Create // Compile-time checked
426
+ Action = PermissionAction.Approve // Compile-time checked
427
+
428
+ // For custom actions beyond standard CRUD, pick from the enum:
429
+ // Export, Import, Approve, Reject, Assign, Execute
430
+ ```
431
+
432
+ **MCP validation:** `validate_conventions` with `checks: ["permissions"]` will detect and flag these anti-patterns.
433
+
388
434
  ### Step A: Call MCP (PRIMARY)
389
435
 
390
436
  ```
@@ -932,6 +978,8 @@ Before marking the task as completed, verify ALL:
932
978
  - [ ] NavigationResources seeded (if `seedDataCore.navigationResources` present in feature.json)
933
979
  - [ ] Section/Resource translations created (4 languages each, EntityType = Section/Resource)
934
980
  - [ ] `dotnet build` passes after generation
981
+ - [ ] NO `Enum.Parse<PermissionAction>` usage anywhere in seeding code (use typed enum directly)
982
+ - [ ] ALL PermissionAction values are from the valid enum: Access, Read, Create, Update, Delete, Export, Import, Approve, Reject, Assign, Execute
935
983
 
936
984
  **If ANY check fails, the task status = 'failed'.**
937
985