@atlashub/smartstack-cli 4.27.0 → 4.28.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": "4.27.0",
3
+ "version": "4.28.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -274,7 +274,7 @@ public static class {ModulePascal}NavigationSeedData
274
274
  Description = "{desc_en}",
275
275
  Icon = "{icon}", // Lucide React icon name
276
276
  IconType = IconType.Lucide,
277
- Route = ToKebabCase($"/{appCode}/{moduleCode}"),
277
+ Route = ToKebabCase($"/{appCode}/{moduleCode}"), // Absolute path for sidebar navigation (App.tsx routes are RELATIVE to module root)
278
278
  DisplayOrder = {displayOrder},
279
279
  IsActive = true
280
280
  };
@@ -731,13 +731,33 @@ public class PermissionSeedEntry
731
731
  }
732
732
  ```
733
733
 
734
- ### Step C2: Section-Level Permissions (CONDITIONAL: only if `navSections[]` defined)
734
+ ### Step C2: Section-Level Permissions (CONDITIONAL filtered by `permissionMode`)
735
735
 
736
736
  > When `seedDataCore.navigationSections` exists and is non-empty in feature.json,
737
737
  > add section-level permission GUIDs and entries to `PermissionsSeedData.cs`.
738
+ >
739
+ > **Permission generation is conditional on each section's `permissionMode`:**
740
+ >
741
+ > | permissionMode | Permissions generated |
742
+ > |---|---|
743
+ > | `crud` | wildcard + read + create + update + delete |
744
+ > | `custom:action1,action2` | wildcard + read + custom actions |
745
+ > | `read-only` | read only (NO CRUD wildcard) |
746
+ > | `inherit` | NO section-level permissions (inherits from module) |
747
+ >
748
+ > **Default inference (when `permissionMode` is absent):**
749
+ > - `code === "detail"` → `inherit`
750
+ > - `code === "dashboard"` → `read-only`
751
+ > - otherwise → `crud`
752
+ >
753
+ > **IMPORTANT:** Sections with `permissionMode: inherit` MUST be skipped entirely.
754
+ > Do NOT generate any permission GUIDs, entries, or constants for them.
738
755
 
739
756
  ```csharp
740
757
  // --- Add to {ModulePascal}PermissionsSeedData class AFTER module-level permissions ---
758
+ // FILTER: Only generate for sections where permissionMode is NOT "inherit"
759
+ // For "read-only" sections: generate ONLY ReadPermId (no Wildcard, Create, Update, Delete)
760
+ // For "custom:x,y" sections: generate Wildcard + Read + custom action GUIDs
741
761
 
742
762
  // Section-level permissions (for each section in navSections[])
743
763
  public static readonly Guid {SectionPascal}WildcardPermId = Guid.NewGuid();
@@ -1155,8 +1175,10 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1155
1175
  public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct)
1156
1176
  {
1157
1177
  // Check idempotence — verify by Code (roles may already exist from SmartStack core)
1178
+ // Scope by ApplicationId — without this, roles from OTHER apps cause false positives
1158
1179
  var existingRoleCodes = await context.Roles
1159
- .Where(r => r.Code == "admin" || r.Code == "manager" || r.Code == "contributor" || r.Code == "viewer")
1180
+ .Where(r => r.ApplicationId == ApplicationRolesSeedData.ApplicationId
1181
+ && (r.Code == "admin" || r.Code == "manager" || r.Code == "contributor" || r.Code == "viewer"))
1160
1182
  .Select(r => r.Code)
1161
1183
  .ToListAsync(ct);
1162
1184
 
@@ -1213,8 +1235,9 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1213
1235
  // Application-scoped roles (admin, manager, contributor, viewer) are created by
1214
1236
  // SeedRolesAsync() above. System roles use their own IDs that do not match
1215
1237
  // DeterministicGuid("role:admin").
1238
+ // Load THIS application's roles + system roles only
1216
1239
  var roles = await context.Roles
1217
- .Where(r => r.ApplicationId != null || r.IsSystem)
1240
+ .Where(r => r.ApplicationId == ApplicationRolesSeedData.ApplicationId || r.IsSystem)
1218
1241
  .ToListAsync(ct);
1219
1242
 
1220
1243
  // Resolve permissions
@@ -132,10 +132,121 @@ if [ -n "$CTRL_FILES" ]; then
132
132
  fi
133
133
  ```
134
134
 
135
+ ### POST-CHECK S8: Write endpoints must NOT use Read permissions (BLOCKING)
136
+
137
+ > **Source:** audit ba-002 finding C1 — Cancel endpoint (POST) used `Permissions.Absences.Read`,
138
+ > allowing any reader to cancel absences. Write operations MUST use write permissions.
139
+
140
+ ```bash
141
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
142
+ if [ -n "$CTRL_FILES" ]; then
143
+ for f in $CTRL_FILES; do
144
+ # Extract blocks: find each method with Http verb + RequirePermission
145
+ # Check POST/PUT/DELETE/PATCH endpoints that use *.Read permission
146
+ WRITE_WITH_READ=$(grep -Pn '\[(HttpPost|HttpPut|HttpDelete|HttpPatch)' "$f" | while read -r line; do
147
+ LINE_NUM=$(echo "$line" | cut -d: -f1)
148
+ # Look within 5 lines for RequirePermission with .Read
149
+ sed -n "$((LINE_NUM-2)),$((LINE_NUM+5))p" "$f" | grep -P 'RequirePermission.*\.Read\b' | head -1
150
+ done)
151
+ if [ -n "$WRITE_WITH_READ" ]; then
152
+ echo "BLOCKING: Write endpoint uses Read permission in $f"
153
+ echo "$WRITE_WITH_READ"
154
+ echo "Fix: POST/PUT/DELETE/PATCH endpoints must use Create/Update/Delete permissions, never Read"
155
+ exit 1
156
+ fi
157
+ done
158
+ fi
159
+ ```
160
+
161
+ ### POST-CHECK S9: FK relationships must enforce tenant isolation (BLOCKING)
162
+
163
+ > **Source:** audit ba-002 finding C5 — Department.ManagerId FK referenced Employee without
164
+ > tenant constraint, allowing cross-tenant data leakage through navigation properties.
165
+
166
+ ```bash
167
+ CONFIG_FILES=$(find src/ -path "*/Configurations/*" -name "*Configuration.cs" 2>/dev/null)
168
+ if [ -n "$CONFIG_FILES" ]; then
169
+ for f in $CONFIG_FILES; do
170
+ # Check if entity implements ITenantEntity
171
+ ENTITY_NAME=$(grep -oP 'IEntityTypeConfiguration<(\w+)>' "$f" | grep -oP '<\K[^>]+')
172
+ if [ -z "$ENTITY_NAME" ]; then continue; fi
173
+
174
+ # Check if the entity is tenant-scoped (has TenantId in its properties or base class)
175
+ ENTITY_FILE=$(find src/ -path "*/Domain/*" -name "${ENTITY_NAME}.cs" 2>/dev/null | head -1)
176
+ if [ -z "$ENTITY_FILE" ]; then continue; fi
177
+
178
+ IS_TENANT=$(grep -cE 'ITenantEntity|BaseEntity|TenantId' "$ENTITY_FILE")
179
+ if [ "$IS_TENANT" -eq 0 ]; then continue; fi
180
+
181
+ # For tenant entities, check FK relationships point to other tenant entities
182
+ # Warning: FK to non-tenant entity from tenant entity = potential cross-tenant leak
183
+ FK_TARGETS=$(grep -oP 'HasOne<(\w+)>|HasOne\(e\s*=>\s*e\.(\w+)\)' "$f" | grep -oP '\w+(?=>|\))' | sort -u)
184
+ for TARGET in $FK_TARGETS; do
185
+ TARGET_FILE=$(find src/ -path "*/Domain/*" -name "${TARGET}.cs" 2>/dev/null | head -1)
186
+ if [ -n "$TARGET_FILE" ]; then
187
+ TARGET_IS_TENANT=$(grep -cE 'ITenantEntity|BaseEntity|TenantId' "$TARGET_FILE")
188
+ if [ "$TARGET_IS_TENANT" -gt 0 ]; then
189
+ # Both are tenant entities — verify the FK has a composite or filtered relationship
190
+ # At minimum, warn that cross-tenant reference is possible without query filter
191
+ HAS_QUERY_FILTER=$(grep -c 'HasQueryFilter' "$f")
192
+ if [ "$HAS_QUERY_FILTER" -eq 0 ]; then
193
+ echo "WARNING: $f — FK from tenant entity $ENTITY_NAME to tenant entity $TARGET without HasQueryFilter"
194
+ echo "Cross-tenant data leakage possible if FK is assigned without tenant validation"
195
+ echo "Fix: Add tenant validation in service layer before assigning FK, or use composite FK (TenantId + EntityId)"
196
+ fi
197
+ fi
198
+ fi
199
+ done
200
+ done
201
+ fi
202
+ ```
203
+
135
204
  ---
136
205
 
137
206
  ## Backend — Entity, Service & Controller Checks
138
207
 
208
+ ### POST-CHECK V1: Controllers with POST/PUT must have corresponding Validators (BLOCKING)
209
+
210
+ ```bash
211
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
212
+ if [ -n "$CTRL_FILES" ]; then
213
+ for f in $CTRL_FILES; do
214
+ # Check if controller has POST or PUT endpoints
215
+ HAS_WRITE=$(grep -cE "\[Http(Post|Put)\]" "$f")
216
+ if [ "$HAS_WRITE" -gt 0 ]; then
217
+ # Extract DTO names from POST/PUT method parameters
218
+ DTOS=$(grep -oP '(?:Create|Update)\w+Dto' "$f" | sort -u)
219
+ for DTO in $DTOS; do
220
+ VALIDATOR_NAME=$(echo "$DTO" | sed 's/Dto$/Validator/')
221
+ VALIDATOR_FILE=$(find src/ -path "*/Validators/*" -name "${VALIDATOR_NAME}.cs" 2>/dev/null)
222
+ if [ -z "$VALIDATOR_FILE" ]; then
223
+ echo "BLOCKING: Controller $f uses $DTO but no ${VALIDATOR_NAME}.cs found"
224
+ echo "Fix: Create Validator with FluentValidation rules from business rules"
225
+ exit 1
226
+ fi
227
+ done
228
+ fi
229
+ done
230
+ fi
231
+ ```
232
+
233
+ ### POST-CHECK V2: Validators must be registered in DI (WARNING)
234
+
235
+ ```bash
236
+ VALIDATOR_FILES=$(find src/ -path "*/Validators/*" -name "*Validator.cs" 2>/dev/null)
237
+ if [ -n "$VALIDATOR_FILES" ]; then
238
+ DI_FILE=$(find src/ -name "DependencyInjection.cs" -o -name "ServiceCollectionExtensions.cs" 2>/dev/null | head -1)
239
+ if [ -n "$DI_FILE" ]; then
240
+ for f in $VALIDATOR_FILES; do
241
+ VALIDATOR_NAME=$(basename "$f" .cs)
242
+ if ! grep -q "$VALIDATOR_NAME" "$DI_FILE"; then
243
+ echo "WARNING: Validator $VALIDATOR_NAME not registered in DI: $DI_FILE"
244
+ fi
245
+ done
246
+ fi
247
+ fi
248
+ ```
249
+
139
250
  ### POST-CHECK C1: Navigation routes must be full paths starting with /
140
251
 
141
252
  ```bash
@@ -1659,6 +1770,120 @@ if [ -n "$ENTITY_FILES" ]; then
1659
1770
  fi
1660
1771
  ```
1661
1772
 
1773
+ ### POST-CHECK C49: Route Ordering in App.tsx — static before dynamic (BLOCKING)
1774
+
1775
+ > **Source:** Dashboard 404 bug — `:id` route declared before `dashboard` route in applicationRoutes,
1776
+ > React Router matched `dashboard` as an `id` parameter → page not found.
1777
+
1778
+ ```bash
1779
+ APP_TSX=$(find web/ src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
1780
+ if [ -n "$APP_TSX" ]; then
1781
+ # Extract all route paths from applicationRoutes in order
1782
+ ROUTE_PATHS=$(grep -oP "path:\s*'([^']+)'" "$APP_TSX" | sed "s/path: '//;s/'//")
1783
+ FAIL=false
1784
+ PREV_PREFIX=""
1785
+ PREV_IS_DYNAMIC=false
1786
+
1787
+ while IFS= read -r ROUTE; do
1788
+ [ -z "$ROUTE" ] && continue
1789
+ # Extract prefix (everything before last segment)
1790
+ PREFIX=$(echo "$ROUTE" | sed 's|/[^/]*$||')
1791
+ LAST_SEGMENT=$(echo "$ROUTE" | grep -oP '[^/]+$')
1792
+ IS_DYNAMIC=$(echo "$LAST_SEGMENT" | grep -c '^:')
1793
+
1794
+ # Check: if previous route was dynamic and current route is static with same prefix
1795
+ if [ "$PREV_IS_DYNAMIC" = true ] && [ "$IS_DYNAMIC" -eq 0 ] && [ "$PREFIX" = "$PREV_PREFIX" ]; then
1796
+ echo "BLOCKING: Static route '$ROUTE' is AFTER a dynamic route with prefix '$PREFIX' in App.tsx"
1797
+ echo " React Router will match the dynamic route first → '$ROUTE' is unreachable (404)"
1798
+ echo " Fix: Move static routes BEFORE dynamic routes within the same prefix group"
1799
+ FAIL=true
1800
+ fi
1801
+
1802
+ PREV_PREFIX="$PREFIX"
1803
+ if [ "$IS_DYNAMIC" -gt 0 ]; then
1804
+ PREV_IS_DYNAMIC=true
1805
+ else
1806
+ PREV_IS_DYNAMIC=false
1807
+ fi
1808
+ done <<< "$ROUTE_PATHS"
1809
+
1810
+ # Also check: Navigate (redirect) routes must be at the end
1811
+ REDIRECT_LINE=$(grep -n "Navigate" "$APP_TSX" | head -1 | cut -d: -f1)
1812
+ if [ -n "$REDIRECT_LINE" ]; then
1813
+ ROUTES_AFTER=$(tail -n +"$((REDIRECT_LINE+1))" "$APP_TSX" | grep -cP "path:\s*'" 2>/dev/null)
1814
+ if [ "$ROUTES_AFTER" -gt 0 ]; then
1815
+ echo "WARNING: Route definitions found AFTER Navigate redirect — redirects should be LAST"
1816
+ fi
1817
+ fi
1818
+
1819
+ if [ "$FAIL" = true ]; then
1820
+ exit 1
1821
+ fi
1822
+ fi
1823
+ ```
1824
+
1825
+ ### POST-CHECK C50: NavRoute Uniqueness — no duplicate NavRoute values (BLOCKING)
1826
+
1827
+ > **Source:** Two controllers sharing the same NavRoute causes routing conflicts — one endpoint
1828
+ > silently overrides the other → 404 on the shadowed controller.
1829
+
1830
+ ```bash
1831
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1832
+ if [ -n "$CTRL_FILES" ]; then
1833
+ # Extract all NavRoute values with their file paths
1834
+ NAVROUTES=$(grep -Pn '\[NavRoute\("([^"]+)"\)' $CTRL_FILES 2>/dev/null | sed 's/.*\[NavRoute("\([^"]*\)").*/\1/' | sort)
1835
+ DUPLICATES=$(echo "$NAVROUTES" | uniq -d)
1836
+ if [ -n "$DUPLICATES" ]; then
1837
+ echo "BLOCKING: Duplicate NavRoute values detected:"
1838
+ for DUP in $DUPLICATES; do
1839
+ echo " NavRoute: $DUP"
1840
+ grep -l "\[NavRoute(\"$DUP\")\]" $CTRL_FILES 2>/dev/null | while read -r f; do
1841
+ echo " - $f"
1842
+ done
1843
+ done
1844
+ echo " Fix: Each controller MUST have a unique NavRoute value"
1845
+ exit 1
1846
+ fi
1847
+ fi
1848
+ ```
1849
+
1850
+ ### POST-CHECK C51: NavRoute Segments vs Controller Hierarchy (WARNING)
1851
+
1852
+ > **Source:** ContractsController in `Controllers/{App}/Employees/` subfolder had NavRoute
1853
+ > `human-resources.contracts` (2 segments) instead of `human-resources.employees.contracts`
1854
+ > (3 segments) → API resolved to wrong path → 404 on contracts endpoints.
1855
+
1856
+ ```bash
1857
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1858
+ if [ -n "$CTRL_FILES" ]; then
1859
+ for f in $CTRL_FILES; do
1860
+ NAVROUTE=$(grep -oP '\[NavRoute\("\K[^"]+' "$f")
1861
+ if [ -z "$NAVROUTE" ]; then continue; fi
1862
+
1863
+ DOTS=$(echo "$NAVROUTE" | tr -cd '.' | wc -c)
1864
+
1865
+ # Count directory depth after Controllers/
1866
+ # e.g., Controllers/HumanResources/Employees/ContractsController.cs = depth 2 (section)
1867
+ REL_PATH=$(echo "$f" | sed 's|.*/Controllers/||')
1868
+ DIR_DEPTH=$(echo "$REL_PATH" | tr '/' '\n' | wc -l)
1869
+ # DIR_DEPTH: 2 = module level (App/Controller.cs), 3 = section level (App/Module/Controller.cs)
1870
+
1871
+ if [ "$DIR_DEPTH" -ge 3 ] && [ "$DOTS" -le 1 ]; then
1872
+ echo "WARNING: Controller in section subfolder but NavRoute has only $((DOTS+1)) segments"
1873
+ echo " File: $f"
1874
+ echo " NavRoute: $NAVROUTE"
1875
+ echo " Expected: 3+ segments (app.module.section) for controllers in section subfolders"
1876
+ echo " Fix: Update NavRoute to include the module segment (e.g., 'app.module.section')"
1877
+ fi
1878
+
1879
+ if [ "$DOTS" -eq 0 ]; then
1880
+ echo "BLOCKING: NavRoute '$NAVROUTE' has only 1 segment (minimum 2 required): $f"
1881
+ exit 1
1882
+ fi
1883
+ done
1884
+ fi
1885
+ ```
1886
+
1662
1887
  ---
1663
1888
 
1664
1889
  ## Architecture — Clean Architecture Layer Isolation
@@ -1785,6 +2010,63 @@ if [ -n "$CTRL_FILES" ]; then
1785
2010
  fi
1786
2011
  ```
1787
2012
 
2013
+ ### POST-CHECK A8: API endpoints must match handoff apiEndpointSummary (BLOCKING)
2014
+
2015
+ > **LESSON LEARNED (audit ba-002):** 4/17 API endpoints (export, calculate, balance upsert, year-end)
2016
+ > were missing but all API tasks were marked COMPLETE. This check reconciles actual controller
2017
+ > endpoints against the handoff contract.
2018
+
2019
+ ```bash
2020
+ # Read apiEndpointSummary from PRD and verify each operation exists in controllers
2021
+ PRD_FILE=".ralph/prd.json"
2022
+ if [ ! -f "$PRD_FILE" ]; then
2023
+ # Try module-specific PRD
2024
+ if [ -f ".ralph/modules-queue.json" ]; then
2025
+ PRD_FILE=$(cat .ralph/modules-queue.json | grep -o '"prdFile":"[^"]*"' | tail -1 | cut -d'"' -f4)
2026
+ fi
2027
+ fi
2028
+
2029
+ if [ -f "$PRD_FILE" ]; then
2030
+ # Extract operation names from apiEndpointSummary
2031
+ OPERATIONS=$(cat "$PRD_FILE" | grep -o '"operation"\s*:\s*"[^"]*"' | cut -d'"' -f4 2>/dev/null)
2032
+
2033
+ if [ -n "$OPERATIONS" ]; then
2034
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
2035
+ MISSING_OPS=""
2036
+ TOTAL_OPS=0
2037
+ FOUND_OPS=0
2038
+
2039
+ for op in $OPERATIONS; do
2040
+ TOTAL_OPS=$((TOTAL_OPS + 1))
2041
+ FOUND=false
2042
+ if [ -n "$CTRL_FILES" ]; then
2043
+ for f in $CTRL_FILES; do
2044
+ if grep -q "$op" "$f" 2>/dev/null; then
2045
+ FOUND=true
2046
+ break
2047
+ fi
2048
+ done
2049
+ fi
2050
+ if [ "$FOUND" = true ]; then
2051
+ FOUND_OPS=$((FOUND_OPS + 1))
2052
+ else
2053
+ MISSING_OPS="$MISSING_OPS $op"
2054
+ fi
2055
+ done
2056
+
2057
+ if [ -n "$MISSING_OPS" ]; then
2058
+ echo "BLOCKING: API endpoints missing from controllers (handoff contract violation)"
2059
+ echo "Found: $FOUND_OPS/$TOTAL_OPS operations"
2060
+ echo "Missing operations:$MISSING_OPS"
2061
+ echo "Fix: Implement missing endpoints in the appropriate Controller"
2062
+ exit 1
2063
+ else
2064
+ echo "POST-CHECK A8: OK — $FOUND_OPS/$TOTAL_OPS API operations found"
2065
+ fi
2066
+ fi
2067
+ fi
2068
+ ```
2069
+
1788
2070
  ---
1789
2071
 
1790
2072
  ## Summary
@@ -257,6 +257,8 @@ var sectionRoute = $"{moduleRoute}/{ToKebabCase(sectionCode)}";
257
257
  - POST-CHECK C34 detects non-kebab-case NavRoute values. POST-CHECK C35 detects non-kebab-case permissions
258
258
  - Do not combine `[Route("api/...")]` with `[NavRoute]` — causes 404s (POST-CHECK C43)
259
259
  - `[NavRoute]` is the only route attribute needed — resolves routes from DB at startup
260
+ - **NavRoute uniqueness:** Each controller MUST have a unique NavRoute value. Two controllers sharing the same NavRoute causes routing conflicts and 404s (POST-CHECK C50)
261
+ - **NavRoute segment count vs hierarchy:** If a controller lives in a section subfolder (e.g., `Controllers/{App}/Employees/ContractsController.cs`), its NavRoute MUST have 3 segments (`app.module.section`), not 2. A 2-segment NavRoute on a section controller causes API 404s because the resolved route doesn't match the expected path (POST-CHECK C51)
260
262
  - Do not use `[Authorize]` without specific permission
261
263
  - Swagger XML documentation
262
264
  - Return DTOs, never domain entities
@@ -307,12 +309,41 @@ const [loading, setLoading] = useState(true);
307
309
 
308
310
  **Loader:** `Loader2` from `lucide-react` for spinners, `PageLoader` for Suspense fallback
309
311
 
312
+ ### RULE — Frontend Route Ordering
313
+
314
+ > **CRITICAL:** React Router matches routes top-to-bottom. Dynamic routes (`:id`, `:id/edit`) placed BEFORE static routes (`dashboard`, `create`) will swallow the static paths — e.g., `/employees/dashboard` matches `:id` with `id="dashboard"` → 404.
315
+
316
+ **Order (MANDATORY):**
317
+
318
+ 1. Index/list route (path: `''` or `'{section}'`)
319
+ 2. Create route (`'{section}/create'`)
320
+ 3. Static sections (`'{section}/dashboard'`, `'{section}/departments'`, etc.)
321
+ 4. Dynamic routes (`'{section}/:id'`, `'{section}/:id/edit'`)
322
+ 5. Redirect routes (`Navigate`) — ALWAYS LAST
323
+
324
+ ```tsx
325
+ // ✅ CORRECT — static before dynamic
326
+ { path: 'employees', element: <EmployeesPage /> },
327
+ { path: 'employees/create', element: <CreateEmployeePage /> },
328
+ { path: 'employees/dashboard', element: <EmployeeDashboardPage /> },
329
+ { path: 'employees/:id', element: <EmployeeDetailPage /> },
330
+ { path: 'employees/:id/edit', element: <EditEmployeePage /> },
331
+ { path: '', element: <Navigate to="employees" replace /> },
332
+
333
+ // ❌ WRONG — :id before dashboard → dashboard is unreachable (matched as id="dashboard")
334
+ { path: 'employees/:id', element: <EmployeeDetailPage /> },
335
+ { path: 'employees/dashboard', element: <EmployeeDashboardPage /> }, // NEVER REACHED
336
+ ```
337
+
338
+ POST-CHECK C49 detects this anti-pattern and BLOCKS.
339
+
310
340
  ### Section Routes (when module has sections)
311
341
 
312
342
  If the module defines `{sections}`, generate frontend routes for EACH section as children of the module route:
313
343
 
314
344
  ```tsx
315
345
  // Section routes are nested inside the module's children array
346
+ // IMPORTANT: static routes BEFORE dynamic routes (see Route Ordering rule above)
316
347
  { path: '{section-kebab}', element: <Suspense fallback={<PageLoader />}><{Section}Page /></Suspense> },
317
348
  { path: '{section-kebab}/create', element: <Suspense fallback={<PageLoader />}><Create{Section}Page /></Suspense> },
318
349
  { path: '{section-kebab}/:id', element: <Suspense fallback={<PageLoader />}><{Section}DetailPage /></Suspense> },
@@ -108,6 +108,15 @@ Verify the plan respects dependencies:
108
108
  - Build gate between EVERY layer (must pass): Layer 0 → 1 → 2 → 3 → 4
109
109
  ```
110
110
 
111
+ ### 4b. Module Code Collision Guard (BLOCKING)
112
+
113
+ ```
114
+ IF appCode == moduleCode:
115
+ BLOCK — module_code identique à app_code cause des segments doublés (e.g. /hr/hr).
116
+ Suggestion : "{appCode}-core", "{appCode}-management"
117
+ ASK user pour un module_code différent.
118
+ ```
119
+
111
120
  ---
112
121
 
113
122
  ## 5. Estimated Commits
@@ -298,6 +298,39 @@ After controller generation, verify `[NavRoute]` attribute is present on every c
298
298
  - When calling `scaffold_extension(type: "controller")`, always pass `navRoute` in options
299
299
  - This is REQUIRED for `scaffold_routes` to auto-detect routes in Layer 3
300
300
 
301
+ ### Guard: NavRoute Uniqueness and Segment Count (MANDATORY)
302
+
303
+ **BEFORE proceeding past Layer 2**, verify for EACH controller:
304
+
305
+ 1. **Unique NavRoute:** No two controllers may share the same `[NavRoute("...")]` value. Duplicate NavRoutes cause routing conflicts → 404s on one of the controllers.
306
+
307
+ 2. **Segment count matches hierarchy:** Count the dots in the NavRoute value:
308
+ - 1 dot = 2 segments (module-level, e.g., `human-resources.employees`) — controller is at `Controllers/{App}/`
309
+ - 2 dots = 3 segments (section-level, e.g., `human-resources.employees.contracts`) — controller is at `Controllers/{App}/{Module}/` or in a section subfolder
310
+ - **If a controller is in a section subfolder** (e.g., `Controllers/{App}/Employees/ContractsController.cs`) **but has only 2 segments** → the API route will be wrong → 404. It MUST have 3 segments.
311
+ - 0 dots = INVALID → BLOCK
312
+
313
+ ```bash
314
+ # Quick validation
315
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
316
+ for f in $CTRL_FILES; do
317
+ NAVROUTE=$(grep -oP '\[NavRoute\("\K[^"]+' "$f")
318
+ if [ -n "$NAVROUTE" ]; then
319
+ DOTS=$(echo "$NAVROUTE" | tr -cd '.' | wc -c)
320
+ if [ "$DOTS" -eq 0 ]; then
321
+ echo "BLOCKING: NavRoute '$NAVROUTE' has only 1 segment (need minimum 2): $f"
322
+ exit 1
323
+ fi
324
+ # Check if controller is in a section subfolder but NavRoute has only 2 segments
325
+ DEPTH=$(echo "$f" | grep -oP 'Controllers/[^/]+/[^/]+/' | wc -l)
326
+ if [ "$DEPTH" -gt 0 ] && [ "$DOTS" -eq 1 ]; then
327
+ echo "WARNING: Controller in section subfolder but NavRoute has only 2 segments: $f"
328
+ echo " NavRoute: $NAVROUTE — expected 3 segments (app.module.section)"
329
+ fi
330
+ fi
331
+ done
332
+ ```
333
+
301
334
  ```bash
302
335
  # Quick check: all controllers must have [NavRoute] (not just [Route])
303
336
  CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
@@ -386,11 +419,15 @@ TaskUpdate(taskId: progress_tracker_id,
386
419
 
387
420
  For each module:
388
421
  - API client: MCP scaffold_api_client
389
- - Routes: MCP scaffold_routes (outputFormat: 'applicationRoutes') → generates lazy imports + Suspense
422
+ - Routes TWO mandatory steps:
423
+ 1. Generate registry: MCP scaffold_routes (source: 'controllers', outputFormat: 'applicationRoutes', dryRun: false)
424
+ → Creates navRoutes.generated.ts (required by POST-CHECK C2)
425
+ 2. Wire to App.tsx (see below)
390
426
  - Wire Routes to App.tsx: After scaffold_routes, routes must be wired into App.tsx:
391
427
  → Read App.tsx and detect the routing pattern
392
428
  → Pattern A (`applicationRoutes: ApplicationRouteExtensions`): add routes to `applicationRoutes['{application_kebab}'][]` with RELATIVE paths
393
429
  → Pattern B (JSX `<Route>`): nest routes inside `<Route path="/{application}" element={<AppLayout />}>` + duplicate in tenant block
430
+ → **BEFORE wiring:** Verify route ordering — static routes (`create`, `dashboard`, `departments`) MUST come BEFORE dynamic routes (`:id`, `:id/edit`). Redirect routes (`Navigate`) MUST be LAST. See `references/smartstack-layers.md` "RULE — Frontend Route Ordering".
394
431
  → Do not add business routes to `clientRoutes[]` — it is only for non-app routes (`/about`, `/pricing`)
395
432
  → All business applications use `<AppLayout />` as layout wrapper
396
433
  → See `references/frontend-route-wiring-app-tsx.md` for full Pattern A/B detection and examples
@@ -480,7 +517,7 @@ IF NOT economy_mode AND entities.length > 1:
480
517
  prompt='Execute Layer 3 frontend for {EntityName}:
481
518
  **MANDATORY: Read references/smartstack-frontend.md FIRST**
482
519
  - API client: MCP scaffold_api_client
483
- - Routes: MCP scaffold_routes (outputFormat: "applicationRoutes")
520
+ - Routes: MCP scaffold_routes (outputFormat: "applicationRoutes", dryRun: false) → MUST generate navRoutes.generated.ts
484
521
  - Wire Routes to App.tsx (BLOCKING): detect Pattern A/B, wire accordingly
485
522
  → See references/frontend-route-wiring-app-tsx.md for full patterns
486
523
  → Verify: mcp__smartstack__validate_frontend_routes (scope: "routes")
@@ -69,6 +69,39 @@ const applicationRoutes: ApplicationRouteExtensions = {
69
69
 
70
70
  Routes are automatically injected into BOTH standard (`/{application}/...`) and tenant-prefixed (`/t/:slug/{application}/...`) route trees by `mergeRoutes()`. No manual duplication needed.
71
71
 
72
+ #### RULE — Route Ordering in applicationRoutes
73
+
74
+ > **CRITICAL:** Routes within each application key MUST follow static-before-dynamic order.
75
+ > React Router matches top-to-bottom — a `:id` route placed before a `dashboard` route
76
+ > will match `dashboard` as an `id` parameter → 404.
77
+
78
+ ```tsx
79
+ const applicationRoutes: ApplicationRouteExtensions = {
80
+ 'human-resources': [
81
+ // ✅ CORRECT ORDER — static before dynamic
82
+ { path: 'employees', element: <EmployeesPage /> },
83
+ { path: 'employees/create', element: <CreateEmployeePage /> },
84
+ { path: 'employees/dashboard', element: <EmployeeDashboardPage /> },
85
+ { path: 'employees/:id', element: <EmployeeDetailPage /> },
86
+ { path: 'employees/:id/edit', element: <EditEmployeePage /> },
87
+
88
+ // Redirect routes ALWAYS LAST
89
+ { path: '', element: <Navigate to="employees" replace /> },
90
+ ],
91
+ };
92
+ ```
93
+
94
+ ```tsx
95
+ // ❌ FORBIDDEN — :id before static routes
96
+ 'human-resources': [
97
+ { path: 'employees/:id', element: <EmployeeDetailPage /> }, // ← WRONG: catches 'dashboard'
98
+ { path: 'employees/dashboard', element: <DashboardPage /> }, // ← NEVER REACHED
99
+ ]
100
+ ```
101
+
102
+ See `smartstack-layers.md` "RULE — Frontend Route Ordering" for the full ordering specification.
103
+ POST-CHECK C49 detects and BLOCKS this anti-pattern.
104
+
72
105
  #### Custom Applications (Pattern A)
73
106
 
74
107
  Custom application keys (any key **not** in the built-in list: `administration`, `support`, `user`, `api`) are fully supported in `applicationRoutes`. `mergeRoutes()` automatically:
@@ -89,6 +89,21 @@ FOR each entity in analysis.entities[]:
89
89
  IF endpoints.length === 0 -> ERROR: "Entity {entity.name} has NO endpoints — missing controller"
90
90
  ```
91
91
 
92
+ ## H. Permission Granularity Check
93
+
94
+ > Validates that section permission modes are respected in the generated permission data.
95
+
96
+ ```
97
+ FOR each module:
98
+ FOR each section in anticipatedSections[]:
99
+ IF section.sectionType == "view" OR section.permissionMode == "inherit":
100
+ IF section has own permissions in seedDataCore.permissions[] -> WARNING: "Section '{code}' is type 'view' but has own permissions — should inherit from module"
101
+ IF section.permissionMode == "read-only":
102
+ IF section has create/update/delete permissions -> ERROR: "Section '{code}' is read-only but has write permissions"
103
+ IF section.sectionType == "primary" AND NOT section.permissionMode:
104
+ -> WARNING: "Primary section '{code}' missing explicit permissionMode — defaulting to 'crud'"
105
+ ```
106
+
92
107
  ## Result Aggregation
93
108
 
94
109
  ```json
@@ -100,6 +115,8 @@ FOR each entity in analysis.entities[]:
100
115
  { "check": "id-uniqueness", "module": "...", "status": "PASS|ERROR", "details": "..." },
101
116
  { "check": "wireframe-layout", "module": "...", "status": "PASS|ERROR", "details": "..." },
102
117
  { "check": "seeddata-translations", "module": "...", "status": "PASS|ERROR", "details": "..." }
118
+ ,
119
+ { "check": "permission-granularity", "module": "...", "status": "PASS|WARNING|ERROR", "details": "..." }
103
120
  ]
104
121
  }
105
122
  ```
@@ -34,13 +34,18 @@
34
34
 
35
35
  > **RULE:** Sections = functional zones only. `create`/`edit` are separate pages with own URL routes (`/create` and `/:id/edit`). `detail` is a tabbed page reached from `list`.
36
36
 
37
- | featureType | Sections (functional zones) | List page includes | Form pages | Detail page tabs |
38
- |---|---|---|---|---|
39
- | data-centric | list | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations} |
40
- | workflow | list, approve | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations}, Historique |
41
- | integration | list | grid, filters, config button | `/create` page, `/:id/edit` page | Infos, Config, Logs |
42
- | reporting | dashboard | — | — | — |
43
- | full-module | list, dashboard | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations}, Historique |
37
+ | featureType | Sections (functional zones) | permissionMode | List page includes | Form pages | Detail page tabs |
38
+ |---|---|---|---|---|---|
39
+ | data-centric | list | `crud` | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations} |
40
+ | data-centric | (detail implicit) | `inherit` | | | |
41
+ | workflow | list | `crud` | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations}, Historique |
42
+ | workflow | approve | `custom:approve,reject` | — | — | — |
43
+ | integration | list | `crud` | grid, filters, config button | `/create` page, `/:id/edit` page | Infos, Config, Logs |
44
+ | reporting | dashboard | `read-only` | — | — | — |
45
+ | full-module | list | `crud` | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations}, Historique |
46
+ | full-module | dashboard | `read-only` | — | — | — |
47
+
48
+ > **RULE:** `detail` is NEVER a section with its own permission set. It is always `sectionType: view`, `permissionMode: inherit`. Detail pages inherit permissions from their parent module.
44
49
 
45
50
  ## Component Generation Rules
46
51