@atlashub/smartstack-cli 4.27.0 → 4.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/templates/skills/apex/references/core-seed-data.md +27 -4
  3. package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +29 -7
  4. package/templates/skills/apex/references/post-checks.md +324 -0
  5. package/templates/skills/apex/references/smartstack-frontend.md +23 -8
  6. package/templates/skills/apex/references/smartstack-layers.md +53 -6
  7. package/templates/skills/apex/steps/step-02-plan.md +9 -0
  8. package/templates/skills/apex/steps/step-03-execute.md +49 -3
  9. package/templates/skills/apex/steps/step-04-examine.md +4 -0
  10. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +33 -0
  11. package/templates/skills/business-analyse/questionnaire/01-context.md +12 -12
  12. package/templates/skills/business-analyse/questionnaire/02-stakeholders-scope.md +45 -45
  13. package/templates/skills/business-analyse/questionnaire/03-data-ui.md +39 -39
  14. package/templates/skills/business-analyse/questionnaire/05-cross-module.md +32 -32
  15. package/templates/skills/business-analyse/questionnaire.md +11 -11
  16. package/templates/skills/business-analyse/references/consolidation-structural-checks.md +17 -0
  17. package/templates/skills/business-analyse/references/spec-auto-inference.md +12 -7
  18. package/templates/skills/business-analyse/steps/step-00-init.md +2 -2
  19. package/templates/skills/business-analyse/steps/step-01-cadrage.md +3 -3
  20. package/templates/skills/business-analyse/steps/step-02-structure.md +22 -8
  21. package/templates/skills/business-analyse/steps/step-03-specify.md +22 -15
  22. package/templates/skills/controller/references/mcp-scaffold-workflow.md +20 -0
  23. package/templates/skills/derive-prd/references/handoff-file-templates.md +25 -1
  24. package/templates/skills/derive-prd/references/handoff-seeddata-generation.md +3 -1
  25. package/templates/skills/ralph-loop/references/category-completeness.md +125 -0
  26. package/templates/skills/ralph-loop/references/compact-loop.md +66 -10
  27. package/templates/skills/ralph-loop/references/module-transition.md +60 -0
  28. package/templates/skills/ralph-loop/steps/step-04-check.md +207 -12
  29. package/templates/skills/ralph-loop/steps/step-05-report.md +205 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlashub/smartstack-cli",
3
- "version": "4.27.0",
3
+ "version": "4.29.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
@@ -56,20 +56,42 @@ Read App.tsx and detect which pattern is used:
56
56
 
57
57
  → Add routes to `applicationRoutes.{application}[]` with **RELATIVE** paths (no leading `/`)
58
58
 
59
+ > **CRITICAL — Include Module Segment:** Paths are relative to `/{application}/`.
60
+ > For a 3-level hierarchy (app → module → sections), paths MUST be `{module_kebab}/{section_kebab}`.
61
+ > Using just `{section_kebab}` omits the module segment → route resolves to `/{app}/{section}` instead
62
+ > of `/{app}/{module}/{section}` → mismatch with backend navigation seed data → 404.
63
+
59
64
  ```tsx
60
65
  const applicationRoutes: ApplicationRouteExtensions = {
61
66
  'human-resources': [
62
67
  // existing routes...
68
+ // Paths include module segment: employee-management/employees (NOT just employees)
63
69
  { path: '{module_kebab}/{section_kebab}', element: <{EntityName}ListPage /> },
64
70
  { path: '{module_kebab}/{section_kebab}/create', element: <Create{EntityName}Page /> },
65
71
  { path: '{module_kebab}/{section_kebab}/:id', element: <{EntityName}DetailPage /> },
66
- { path: '{module_kebab}/{section_kebab}/:id/edit', element: <Create{EntityName}Page /> },
72
+ { path: '{module_kebab}/{section_kebab}/:id/edit', element: <Edit{EntityName}Page /> },
67
73
 
68
74
  // Parent redirect routes (MANDATORY — prevents /login redirect on parent navigation)
69
75
  { path: '{module_kebab}', element: <Navigate to="{module_kebab}/{first_section_kebab}" replace /> },
70
76
  { path: '', element: <Navigate to="{first_module_kebab}/{first_section_kebab}" replace /> },
71
77
  ],
72
78
  };
79
+
80
+ // Concrete example: app=human-resources, module=employee-management, sections=employees,absences
81
+ const applicationRoutes: ApplicationRouteExtensions = {
82
+ 'human-resources': [
83
+ { path: 'employee-management/employees', element: <EmployeesPage /> }, // ✅
84
+ { path: 'employee-management/employees/create', element: <CreateEmployeePage /> }, // ✅
85
+ { path: 'employee-management/employees/:id', element: <EmployeeDetailPage /> }, // ✅
86
+ // ...absences...
87
+ { path: 'employee-management', element: <Navigate to="employee-management/employees" replace /> },
88
+ { path: '', element: <Navigate to="employee-management/employees" replace /> },
89
+ ],
90
+ };
91
+
92
+ // ❌ WRONG — missing module segment:
93
+ // { path: 'employees', ... } → resolves to /human-resources/employees (backend expects /human-resources/employee-management/employees)
94
+ ```
73
95
  ```
74
96
 
75
97
  Routes are automatically injected into BOTH standard (`/{application}/...`) and tenant-prefixed (`/t/:slug/{application}/...`) route trees by `mergeRoutes()`. No manual duplication needed.
@@ -162,10 +184,10 @@ For each application, add:
162
184
  { path: '{module}', element: <Navigate to="{module}/{first_section}" replace /> }
163
185
  ```
164
186
 
165
- **Example:** For NavRoutes `human-resources.employees.management` and `human-resources.employees.departments`:
187
+ **Example:** For NavRoutes `human-resources.employee-management.employees` and `human-resources.employee-management.absences`:
166
188
  ```tsx
167
- { path: 'employees', element: <Navigate to="employees/management" replace /> },
168
- { path: '', element: <Navigate to="employees/management" replace /> },
189
+ { path: 'employee-management', element: <Navigate to="employee-management/employees" replace /> },
190
+ { path: '', element: <Navigate to="employee-management/employees" replace /> },
169
191
  ```
170
192
 
171
193
  The `to` prop is resolved relative to the **parent route** (`/{application}`), so always use the full path from the application root.
@@ -186,12 +208,12 @@ The `to` prop is resolved relative to the **parent route** (`/{application}`), s
186
208
  { path: '/human-resources/employees/management', element: <EmployeePage /> },
187
209
  ];
188
210
  ```
189
- Custom application routes go in `applicationRoutes` with RELATIVE paths:
211
+ Custom application routes go in `applicationRoutes` with RELATIVE paths (including module segment):
190
212
  ```tsx
191
- // ✅ CORRECT
213
+ // ✅ CORRECT — module segment included
192
214
  const applicationRoutes: ApplicationRouteExtensions = {
193
215
  'human-resources': [
194
- { path: 'employees/management', element: <EmployeePage /> },
216
+ { path: 'employee-management/employees', element: <EmployeesPage /> },
195
217
  ],
196
218
  };
197
219
  ```
@@ -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,162 @@ 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
+
1887
+ ### POST-CHECK C52: Frontend route paths must include module segment (BLOCKING)
1888
+
1889
+ > **Source:** APEX regression — frontend routes used `employees` instead of `employee-management/employees`,
1890
+ > causing mismatch with backend navigation seed data routes (`/human-resources/employee-management/employees`).
1891
+ > Nav links produced 404 because React Router had no matching route for the full path.
1892
+
1893
+ ```bash
1894
+ # Compare frontend route paths in App.tsx against backend navigation seed data routes
1895
+ APP_TSX=$(find . -path "*/src/App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
1896
+ if [ -n "$APP_TSX" ]; then
1897
+ # Extract all route paths from applicationRoutes
1898
+ ROUTE_PATHS=$(grep -oP "path:\s*'[^']+'" "$APP_TSX" | grep -v "^path: ''" | grep -v ":id" | grep -v "create" | grep -v "edit" | sed "s/path: '//;s/'//")
1899
+
1900
+ # Find navigation seed data files to get backend routes
1901
+ NAV_SEED_FILES=$(find src/ -name "*NavigationSeedData.cs" -o -name "*NavigationModuleSeedData.cs" 2>/dev/null)
1902
+ if [ -n "$NAV_SEED_FILES" ]; then
1903
+ # Extract module codes from seed data
1904
+ MODULE_CODES=$(grep -oP 'Code\s*=\s*"\K[^"]+' $NAV_SEED_FILES 2>/dev/null | sort -u)
1905
+
1906
+ for route in $ROUTE_PATHS; do
1907
+ # Skip redirect paths (empty or just module)
1908
+ if [ -z "$route" ]; then continue; fi
1909
+
1910
+ # Check if route contains a slash (module/section pattern)
1911
+ if ! echo "$route" | grep -q "/"; then
1912
+ # Single segment route — check if it matches a section code (not a module code)
1913
+ IS_MODULE=false
1914
+ for mod in $MODULE_CODES; do
1915
+ if [ "$route" = "$mod" ]; then IS_MODULE=true; break; fi
1916
+ done
1917
+ if [ "$IS_MODULE" = false ]; then
1918
+ echo "BLOCKING: Frontend route '$route' is missing module segment"
1919
+ echo " Expected: '{module}/{route}' (e.g., 'employee-management/$route')"
1920
+ echo " Backend navigation seed data defines routes with full hierarchy: /app/module/section"
1921
+ echo " Frontend routes must match: module/section (relative to app root)"
1922
+ fi
1923
+ fi
1924
+ done
1925
+ fi
1926
+ fi
1927
+ ```
1928
+
1662
1929
  ---
1663
1930
 
1664
1931
  ## Architecture — Clean Architecture Layer Isolation
@@ -1785,6 +2052,63 @@ if [ -n "$CTRL_FILES" ]; then
1785
2052
  fi
1786
2053
  ```
1787
2054
 
2055
+ ### POST-CHECK A8: API endpoints must match handoff apiEndpointSummary (BLOCKING)
2056
+
2057
+ > **LESSON LEARNED (audit ba-002):** 4/17 API endpoints (export, calculate, balance upsert, year-end)
2058
+ > were missing but all API tasks were marked COMPLETE. This check reconciles actual controller
2059
+ > endpoints against the handoff contract.
2060
+
2061
+ ```bash
2062
+ # Read apiEndpointSummary from PRD and verify each operation exists in controllers
2063
+ PRD_FILE=".ralph/prd.json"
2064
+ if [ ! -f "$PRD_FILE" ]; then
2065
+ # Try module-specific PRD
2066
+ if [ -f ".ralph/modules-queue.json" ]; then
2067
+ PRD_FILE=$(cat .ralph/modules-queue.json | grep -o '"prdFile":"[^"]*"' | tail -1 | cut -d'"' -f4)
2068
+ fi
2069
+ fi
2070
+
2071
+ if [ -f "$PRD_FILE" ]; then
2072
+ # Extract operation names from apiEndpointSummary
2073
+ OPERATIONS=$(cat "$PRD_FILE" | grep -o '"operation"\s*:\s*"[^"]*"' | cut -d'"' -f4 2>/dev/null)
2074
+
2075
+ if [ -n "$OPERATIONS" ]; then
2076
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
2077
+ MISSING_OPS=""
2078
+ TOTAL_OPS=0
2079
+ FOUND_OPS=0
2080
+
2081
+ for op in $OPERATIONS; do
2082
+ TOTAL_OPS=$((TOTAL_OPS + 1))
2083
+ FOUND=false
2084
+ if [ -n "$CTRL_FILES" ]; then
2085
+ for f in $CTRL_FILES; do
2086
+ if grep -q "$op" "$f" 2>/dev/null; then
2087
+ FOUND=true
2088
+ break
2089
+ fi
2090
+ done
2091
+ fi
2092
+ if [ "$FOUND" = true ]; then
2093
+ FOUND_OPS=$((FOUND_OPS + 1))
2094
+ else
2095
+ MISSING_OPS="$MISSING_OPS $op"
2096
+ fi
2097
+ done
2098
+
2099
+ if [ -n "$MISSING_OPS" ]; then
2100
+ echo "BLOCKING: API endpoints missing from controllers (handoff contract violation)"
2101
+ echo "Found: $FOUND_OPS/$TOTAL_OPS operations"
2102
+ echo "Missing operations:$MISSING_OPS"
2103
+ echo "Fix: Implement missing endpoints in the appropriate Controller"
2104
+ exit 1
2105
+ else
2106
+ echo "POST-CHECK A8: OK — $FOUND_OPS/$TOTAL_OPS API operations found"
2107
+ fi
2108
+ fi
2109
+ fi
2110
+ ```
2111
+
1788
2112
  ---
1789
2113
 
1790
2114
  ## Summary
@@ -14,8 +14,9 @@
14
14
 
15
15
  ```tsx
16
16
  // Named exports — use .then() to wrap
17
+ // Path: @/pages/{App}/{Module}/{Section}/{Page}
17
18
  const EmployeesPage = lazy(() =>
18
- import('@/pages/HumanResources/Employees/EmployeesPage')
19
+ import('@/pages/HumanResources/EmployeeManagement/Employees/EmployeesPage')
19
20
  .then(m => ({ default: m.EmployeesPage }))
20
21
  );
21
22
 
@@ -49,7 +50,7 @@ element: (
49
50
  **Incorrect patterns:**
50
51
  ```tsx
51
52
  // WRONG: static import in route file
52
- import { EmployeesPage } from '@/pages/HumanResources/Employees/EmployeesPage';
53
+ import { EmployeesPage } from '@/pages/HumanResources/EmployeeManagement/Employees/EmployeesPage';
53
54
 
54
55
  // WRONG: no Suspense wrapper
55
56
  element: <EmployeesPage />
@@ -64,8 +65,9 @@ In the client `App.tsx` (where application routes are defined), all page imports
64
65
 
65
66
  **Correct — Lazy imports in client App.tsx:**
66
67
  ```tsx
68
+ // Path includes module level: {App}/{Module}/{Section}/{Page}
67
69
  const ClientsListPage = lazy(() =>
68
- import('@/pages/HumanResources/Clients/ClientsListPage')
70
+ import('@/pages/HumanResources/ClientManagement/Clients/ClientsListPage')
69
71
  .then(m => ({ default: m.ClientsListPage }))
70
72
  );
71
73
  ```
@@ -73,7 +75,7 @@ const ClientsListPage = lazy(() =>
73
75
  **Do not use — Static imports in client App.tsx:**
74
76
  ```tsx
75
77
  // WRONG: Static import kills code splitting
76
- import { ClientsListPage } from '@/pages/HumanResources/Clients/ClientsListPage';
78
+ import { ClientsListPage } from '@/pages/HumanResources/ClientManagement/Clients/ClientsListPage';
77
79
  ```
78
80
 
79
81
  > **Note:** The `smartstackRoutes.tsx` from the npm package may use static imports internally — this is acceptable for the package. But client `App.tsx` code MUST always use lazy imports for business pages.
@@ -698,18 +700,19 @@ export function EntityEditPage() {
698
700
 
699
701
  ```tsx
700
702
  // In route files — form pages are also lazy-loaded
703
+ // Path: @/pages/{App}/{Module}/{Section}/{Page}
701
704
  const EntityCreatePage = lazy(() =>
702
- import('@/pages/HumanResources/Employees/EntityCreatePage')
705
+ import('@/pages/HumanResources/EmployeeManagement/Employees/EntityCreatePage')
703
706
  .then(m => ({ default: m.EntityCreatePage }))
704
707
  );
705
708
  const EntityEditPage = lazy(() =>
706
- import('@/pages/HumanResources/Employees/EntityEditPage')
709
+ import('@/pages/HumanResources/EmployeeManagement/Employees/EntityEditPage')
707
710
  .then(m => ({ default: m.EntityEditPage }))
708
711
  );
709
712
 
710
- // Route registration — form pages have their own routes
713
+ // Route registration — Pattern B (JSX nested children):
711
714
  {
712
- path: 'employees',
715
+ path: 'employee-management/employees',
713
716
  children: [
714
717
  { index: true, element: <Suspense fallback={<PageLoader />}><EmployeesPage /></Suspense> },
715
718
  { path: 'create', element: <Suspense fallback={<PageLoader />}><EntityCreatePage /></Suspense> },
@@ -718,6 +721,18 @@ const EntityEditPage = lazy(() =>
718
721
  ]
719
722
  }
720
723
 
724
+ // Route registration — Pattern A (applicationRoutes flat paths):
725
+ // CRITICAL: paths are RELATIVE to /{app}/ and MUST include the module segment
726
+ const applicationRoutes: ApplicationRouteExtensions = {
727
+ 'human-resources': [
728
+ { path: 'employee-management/employees', element: <EmployeesPage /> }, // ✅ includes module
729
+ { path: 'employee-management/employees/create', element: <CreateEmployeePage /> }, // ✅
730
+ { path: 'employee-management/employees/:id', element: <EmployeeDetailPage /> }, // ✅
731
+ { path: 'employee-management/employees/:id/edit', element: <EditEmployeePage /> }, // ✅
732
+ // ❌ WRONG: { path: 'employees', ... } — missing module segment → 404 on nav click
733
+ ],
734
+ };
735
+
721
736
  // Section-level routes — children of the module route (when module has sections)
722
737
  //
723
738
  // > **IMPORTANT:** The `list` and `detail` sections do NOT generate additional route entries.
@@ -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,16 +309,61 @@ 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: `'{module}/{section}'`)
319
+ 2. Create route (`'{module}/{section}/create'`)
320
+ 3. Static sections (`'{module}/{section}/dashboard'`, etc.)
321
+ 4. Dynamic routes (`'{module}/{section}/:id'`, `'{module}/{section}/:id/edit'`)
322
+ 5. Module redirect (`'{module}'` → `'{module}/{first_section}'`)
323
+ 6. Application redirect (`''` → `'{first_module}/{first_section}'`) — ALWAYS LAST
324
+
325
+ ```tsx
326
+ // ✅ CORRECT — module segment included, static before dynamic
327
+ // (inside applicationRoutes['human-resources'], paths are relative to /human-resources/)
328
+ { path: 'employee-management/employees', element: <EmployeesPage /> },
329
+ { path: 'employee-management/employees/create', element: <CreateEmployeePage /> },
330
+ { path: 'employee-management/employees/dashboard', element: <EmployeeDashboardPage /> },
331
+ { path: 'employee-management/employees/:id', element: <EmployeeDetailPage /> },
332
+ { path: 'employee-management/employees/:id/edit', element: <EditEmployeePage /> },
333
+ { path: 'employee-management', element: <Navigate to="employee-management/employees" replace /> },
334
+ { path: '', element: <Navigate to="employee-management/employees" replace /> },
335
+
336
+ // ❌ WRONG — missing module segment (resolves to /human-resources/employees instead of /human-resources/employee-management/employees)
337
+ { path: 'employees', element: <EmployeesPage /> },
338
+
339
+ // ❌ WRONG — :id before dashboard → dashboard is unreachable (matched as id="dashboard")
340
+ { path: 'employee-management/employees/:id', element: <EmployeeDetailPage /> },
341
+ { path: 'employee-management/employees/dashboard', element: <EmployeeDashboardPage /> }, // NEVER REACHED
342
+ ```
343
+
344
+ POST-CHECK C49 detects this anti-pattern and BLOCKS.
345
+
310
346
  ### Section Routes (when module has sections)
311
347
 
312
- If the module defines `{sections}`, generate frontend routes for EACH section as children of the module route:
348
+ If the module defines `{sections}`, generate frontend routes for EACH section as children of the application route.
349
+
350
+ > **CRITICAL — Module Segment Required:** Route paths in `applicationRoutes['{app}']` are RELATIVE to the application root.
351
+ > They MUST include the module segment: `{module-kebab}/{section-kebab}`, NOT just `{section-kebab}`.
352
+ > Without the module segment, the route resolves to `/{app}/{section}` instead of `/{app}/{module}/{section}`,
353
+ > causing a mismatch with backend navigation seed data routes → nav links produce 404s.
313
354
 
314
355
  ```tsx
315
- // Section routes are nested inside the module's children array
316
- { path: '{section-kebab}', element: <Suspense fallback={<PageLoader />}><{Section}Page /></Suspense> },
317
- { path: '{section-kebab}/create', element: <Suspense fallback={<PageLoader />}><Create{Section}Page /></Suspense> },
318
- { path: '{section-kebab}/:id', element: <Suspense fallback={<PageLoader />}><{Section}DetailPage /></Suspense> },
319
- { path: '{section-kebab}/:id/edit', element: <Suspense fallback={<PageLoader />}><Edit{Section}Page /></Suspense> },
356
+ // Routes in applicationRoutes['human-resources'] RELATIVE paths include module segment
357
+ // IMPORTANT: static routes BEFORE dynamic routes (see Route Ordering rule above)
358
+ // Example: app=human-resources, module=employee-management, section=employees
359
+ { path: '{module-kebab}/{section-kebab}', element: <Suspense fallback={<PageLoader />}><{Section}Page /></Suspense> },
360
+ { path: '{module-kebab}/{section-kebab}/create', element: <Suspense fallback={<PageLoader />}><Create{Section}Page /></Suspense> },
361
+ { path: '{module-kebab}/{section-kebab}/:id', element: <Suspense fallback={<PageLoader />}><{Section}DetailPage /></Suspense> },
362
+ { path: '{module-kebab}/{section-kebab}/:id/edit', element: <Suspense fallback={<PageLoader />}><Edit{Section}Page /></Suspense> },
363
+
364
+ // Concrete example:
365
+ // { path: 'employee-management/employees', element: ... }, → /human-resources/employee-management/employees ✅
366
+ // { path: 'employees', element: ... }, → /human-resources/employees ❌ WRONG (missing module)
320
367
  ```
321
368
 
322
369
  Section pages live in `src/pages/{AppPascal}/{Module}/{Section}/`.