@atlashub/smartstack-cli 3.32.0 → 3.34.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 (55) hide show
  1. package/.documentation/agents.html +5 -1
  2. package/.documentation/apex.html +644 -0
  3. package/.documentation/business-analyse.html +81 -1
  4. package/.documentation/cli-commands.html +5 -1
  5. package/.documentation/commands.html +5 -1
  6. package/.documentation/efcore.html +5 -1
  7. package/.documentation/gitflow.html +5 -1
  8. package/.documentation/hooks.html +5 -1
  9. package/.documentation/index.html +60 -2
  10. package/.documentation/init.html +5 -1
  11. package/.documentation/installation.html +5 -1
  12. package/.documentation/ralph-loop.html +365 -216
  13. package/.documentation/test-web.html +5 -1
  14. package/package.json +1 -1
  15. package/templates/agents/ba-writer.md +142 -15
  16. package/templates/skills/apex/SKILL.md +7 -1
  17. package/templates/skills/apex/_shared.md +49 -4
  18. package/templates/skills/{ralph-loop → apex}/references/core-seed-data.md +20 -11
  19. package/templates/skills/{ralph-loop → apex}/references/error-classification.md +2 -1
  20. package/templates/skills/apex/references/post-checks.md +238 -3
  21. package/templates/skills/apex/references/smartstack-api.md +47 -7
  22. package/templates/skills/apex/references/smartstack-frontend.md +47 -1
  23. package/templates/skills/apex/references/smartstack-layers.md +3 -1
  24. package/templates/skills/apex/steps/step-00-init.md +48 -1
  25. package/templates/skills/apex/steps/step-01-analyze.md +37 -0
  26. package/templates/skills/apex/steps/step-02-plan.md +36 -0
  27. package/templates/skills/apex/steps/step-03-execute.md +42 -2
  28. package/templates/skills/apex/steps/step-04-examine.md +110 -2
  29. package/templates/skills/business-analyse/SKILL.md +29 -19
  30. package/templates/skills/business-analyse/_module-loop.md +68 -9
  31. package/templates/skills/business-analyse/_shared.md +71 -21
  32. package/templates/skills/business-analyse/questionnaire/00-application.md +4 -2
  33. package/templates/skills/business-analyse/questionnaire/00b-project.md +85 -0
  34. package/templates/skills/business-analyse/references/deploy-modes.md +69 -0
  35. package/templates/skills/business-analyse/references/team-orchestration.md +158 -7
  36. package/templates/skills/business-analyse/schemas/application-schema.json +2 -1
  37. package/templates/skills/business-analyse/schemas/project-schema.json +490 -0
  38. package/templates/skills/business-analyse/schemas/sections/metadata-schema.json +2 -1
  39. package/templates/skills/business-analyse/steps/step-00-init.md +30 -4
  40. package/templates/skills/business-analyse/steps/step-01-cadrage.md +62 -2
  41. package/templates/skills/business-analyse/steps/step-01b-applications.md +252 -0
  42. package/templates/skills/business-analyse/steps/step-02-decomposition.md +23 -6
  43. package/templates/skills/business-analyse/steps/step-03d-validate.md +27 -6
  44. package/templates/skills/business-analyse/steps/step-04a-collect.md +111 -0
  45. package/templates/skills/business-analyse/steps/step-05a-handoff.md +296 -103
  46. package/templates/skills/business-analyse/steps/step-05b-deploy.md +46 -14
  47. package/templates/skills/documentation/SKILL.md +92 -2
  48. package/templates/skills/ralph-loop/SKILL.md +9 -17
  49. package/templates/skills/ralph-loop/references/category-rules.md +43 -692
  50. package/templates/skills/ralph-loop/references/compact-loop.md +104 -427
  51. package/templates/skills/ralph-loop/references/team-orchestration.md +13 -14
  52. package/templates/skills/ralph-loop/steps/step-02-execute.md +49 -704
  53. package/templates/skills/ralph-loop/steps/step-03-commit.md +38 -79
  54. package/templates/skills/ralph-loop/steps/step-04-check.md +39 -58
  55. package/templates/skills/ralph-loop/steps/step-05-report.md +12 -123
@@ -166,27 +166,57 @@ if [ -n "$PAGE_FILES" ]; then
166
166
  fi
167
167
  ```
168
168
 
169
- ### POST-CHECK 9: Create/Edit pages must exist as separate route pages
169
+ ### POST-CHECK 9: Create/Edit pages must exist as separate route pages (BLOCKING)
170
170
 
171
171
  ```bash
172
172
  # For each module with a list page, verify create and edit pages exist
173
+ # If ListPage has navigate() calls to /create or /:id/edit, the target pages MUST exist
173
174
  LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test)
175
+ FAIL=false
174
176
  if [ -n "$LIST_PAGES" ]; then
175
177
  for LIST_PAGE in $LIST_PAGES; do
176
178
  PAGE_DIR=$(dirname "$LIST_PAGE")
177
179
  MODULE_NAME=$(basename "$PAGE_DIR")
180
+
181
+ # Detect if ListPage navigates to /create or /edit routes
182
+ HAS_CREATE_NAV=$(grep -P "navigate\(.*['/]create" "$LIST_PAGE" 2>/dev/null)
183
+ HAS_EDIT_NAV=$(grep -P "navigate\(.*['/]edit|navigate\(.*/:id/edit" "$LIST_PAGE" 2>/dev/null)
184
+
178
185
  # Check for create page
179
186
  CREATE_PAGE=$(find "$PAGE_DIR" -name "*CreatePage.tsx" 2>/dev/null)
180
187
  if [ -z "$CREATE_PAGE" ]; then
181
- echo "WARNING: Module $MODULE_NAME has a list page but no CreatePage — expected EntityCreatePage.tsx"
188
+ if [ -n "$HAS_CREATE_NAV" ]; then
189
+ echo "BLOCKING: Module $MODULE_NAME ListPage navigates to /create but CreatePage does NOT exist"
190
+ echo " Dead link: $HAS_CREATE_NAV"
191
+ echo " Fix: Create ${MODULE_NAME}CreatePage.tsx in $PAGE_DIR"
192
+ FAIL=true
193
+ else
194
+ echo "WARNING: Module $MODULE_NAME has a list page but no CreatePage — expected EntityCreatePage.tsx"
195
+ fi
182
196
  fi
197
+
183
198
  # Check for edit page
184
199
  EDIT_PAGE=$(find "$PAGE_DIR" -name "*EditPage.tsx" 2>/dev/null)
185
200
  if [ -z "$EDIT_PAGE" ]; then
186
- echo "WARNING: Module $MODULE_NAME has a list page but no EditPage — expected EntityEditPage.tsx"
201
+ if [ -n "$HAS_EDIT_NAV" ]; then
202
+ echo "BLOCKING: Module $MODULE_NAME ListPage navigates to /:id/edit but EditPage does NOT exist"
203
+ echo " Dead link: $HAS_EDIT_NAV"
204
+ echo " Fix: Create ${MODULE_NAME}EditPage.tsx in $PAGE_DIR"
205
+ FAIL=true
206
+ else
207
+ echo "WARNING: Module $MODULE_NAME has a list page but no EditPage — expected EntityEditPage.tsx"
208
+ fi
187
209
  fi
188
210
  done
189
211
  fi
212
+
213
+ if [ "$FAIL" = true ]; then
214
+ echo ""
215
+ echo "BLOCKING: Create/Edit pages are MISSING but ListPage buttons link to them."
216
+ echo "Users will see white screen / 404 when clicking Create or Edit buttons."
217
+ echo "Fix: Generate form pages using /ui-components skill patterns (smartstack-frontend.md section 3b)"
218
+ exit 1
219
+ fi
190
220
  ```
191
221
 
192
222
  ### POST-CHECK 10: Form pages must have companion test files
@@ -1016,4 +1046,209 @@ if [ -n "$PROVIDER" ]; then
1016
1046
  fi
1017
1047
  ```
1018
1048
 
1049
+ ### POST-CHECK 39: Controllers must NOT have [Route] alongside [NavRoute] (BLOCKING)
1050
+
1051
+ ```bash
1052
+ # [NavRoute] REPLACES [Route] — it resolves HTTP routes from the navigation DB at startup.
1053
+ # Having both is redundant and may cause route conflicts.
1054
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1055
+ if [ -n "$CTRL_FILES" ]; then
1056
+ for f in $CTRL_FILES; do
1057
+ HAS_NAVROUTE=$(grep -P '\[NavRoute\(' "$f" 2>/dev/null)
1058
+ HAS_ROUTE=$(grep -P '\[Route\(' "$f" 2>/dev/null)
1059
+ if [ -n "$HAS_NAVROUTE" ] && [ -n "$HAS_ROUTE" ]; then
1060
+ echo "BLOCKING: Controller has both [Route] and [NavRoute] — [NavRoute] replaces [Route]: $f"
1061
+ echo " Found [NavRoute]: $HAS_NAVROUTE"
1062
+ echo " Found [Route]: $HAS_ROUTE"
1063
+ echo "Fix: Remove the [Route(\"api/...\")] attribute. [NavRoute] resolves routes from navigation DB at startup."
1064
+ exit 1
1065
+ fi
1066
+ done
1067
+ fi
1068
+ ```
1069
+
1070
+ ### POST-CHECK 40: NavRoute segments must use kebab-case for multi-word codes (BLOCKING)
1071
+
1072
+ ```bash
1073
+ # NavRoute segments are navigation entity Codes joined by dots.
1074
+ # Multi-word codes MUST use kebab-case (e.g., "human-resources", NOT "humanresources").
1075
+ # Verified from SmartStack.app: "business.support-client.my-tickets", "platform.administration.access-requests"
1076
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1077
+ if [ -n "$CTRL_FILES" ]; then
1078
+ for f in $CTRL_FILES; do
1079
+ NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
1080
+ if [ -n "$NAVROUTE_VAL" ]; then
1081
+ # Check each segment for concatenated multi-word (10+ lowercase chars without hyphens)
1082
+ for SEG in $(echo "$NAVROUTE_VAL" | tr '.' '\n'); do
1083
+ if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
1084
+ echo "BLOCKING: NavRoute segment '$SEG' in $f appears to be concatenated multi-word without hyphens"
1085
+ echo " Full NavRoute: $NAVROUTE_VAL"
1086
+ echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
1087
+ echo " SmartStack convention (from SmartStack.app): 'business.support-client.my-tickets'"
1088
+ exit 1
1089
+ fi
1090
+ done
1091
+ fi
1092
+ done
1093
+ fi
1094
+
1095
+ # Also check seed data Code values for navigation entities
1096
+ SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -o -name "NavigationApplicationSeedData.cs" 2>/dev/null)
1097
+ if [ -n "$SEED_FILES" ]; then
1098
+ CODES=$(grep -oP 'Code\s*=\s*"([^"]+)"' $SEED_FILES 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
1099
+ for CODE in $CODES; do
1100
+ if echo "$CODE" | grep -qP '^[a-z]{10,}$'; then
1101
+ echo "BLOCKING: Navigation seed data Code '$CODE' appears to be concatenated multi-word without hyphens"
1102
+ echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
1103
+ exit 1
1104
+ fi
1105
+ done
1106
+ fi
1107
+ ```
1108
+
1109
+ ### POST-CHECK 41: Permission codes must use kebab-case matching NavRoute codes (BLOCKING)
1110
+
1111
+ ```bash
1112
+ # Permission codes in [RequirePermission] and Permissions.cs MUST use kebab-case for multi-word segments.
1113
+ # SmartStack.app convention: "business.support-client.my-tickets.read" (kebab-case everywhere)
1114
+ # FORBIDDEN: "business.humanresources.employees.read" — must be "business.human-resources.employees.read"
1115
+
1116
+ # Check [RequirePermission] attributes in controllers
1117
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1118
+ if [ -n "$CTRL_FILES" ]; then
1119
+ for f in $CTRL_FILES; do
1120
+ PERM_VALS=$(grep -oP 'RequirePermission\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
1121
+ for PERM in $PERM_VALS; do
1122
+ # Check each segment (except the action suffix) for concatenated multi-word without hyphens
1123
+ SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1) # remove last segment (action: read/create/update/delete)
1124
+ for SEG in $SEGMENTS; do
1125
+ if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
1126
+ echo "BLOCKING: Permission code segment '$SEG' in $f appears concatenated without hyphens"
1127
+ echo " Full permission: $PERM"
1128
+ echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
1129
+ echo " SmartStack convention: 'business.support-client.my-tickets.read'"
1130
+ exit 1
1131
+ fi
1132
+ done
1133
+ done
1134
+ done
1135
+ fi
1136
+
1137
+ # Check Permissions.cs constants
1138
+ PERM_FILES=$(find src/ -path "*/Authorization/Permissions.cs" 2>/dev/null)
1139
+ if [ -n "$PERM_FILES" ]; then
1140
+ for f in $PERM_FILES; do
1141
+ CONST_VALS=$(grep -oP '=\s*"([^"]+)"' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
1142
+ for PERM in $CONST_VALS; do
1143
+ SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
1144
+ for SEG in $SEGMENTS; do
1145
+ if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
1146
+ echo "BLOCKING: Permissions.cs constant segment '$SEG' in $f appears concatenated without hyphens"
1147
+ echo " Full permission: $PERM"
1148
+ echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
1149
+ exit 1
1150
+ fi
1151
+ done
1152
+ done
1153
+ done
1154
+ fi
1155
+
1156
+ # Check PermissionsSeedData.cs for mismatched paths
1157
+ SEED_PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*PermissionsSeedData.cs" 2>/dev/null)
1158
+ if [ -n "$SEED_PERM_FILES" ]; then
1159
+ PATHS=$(grep -oP '"[a-z][a-z0-9.-]+\.(read|create|update|delete|\*)"' $SEED_PERM_FILES 2>/dev/null | tr -d '"')
1160
+ for PERM in $PATHS; do
1161
+ SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
1162
+ for SEG in $SEGMENTS; do
1163
+ if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
1164
+ echo "BLOCKING: PermissionsSeedData path segment '$SEG' appears concatenated without hyphens"
1165
+ echo " Full permission path: $PERM"
1166
+ echo " Fix: Use kebab-case matching NavRoute: 'humanresources' → 'human-resources'"
1167
+ exit 1
1168
+ fi
1169
+ done
1170
+ done
1171
+ fi
1172
+ ```
1173
+
1174
+ ### POST-CHECK 42: Frontend navigate() calls must have matching route definitions (BLOCKING)
1175
+
1176
+ ```bash
1177
+ # Detect dead links: navigate() calls to paths that don't have corresponding page components.
1178
+ # Example: LeavesPage has navigate('../leave-types') but no LeaveTypesPage or route exists.
1179
+ PAGE_FILES=$(find web/ -name "*.tsx" -path "*/pages/*" ! -name "*.test.tsx" 2>/dev/null)
1180
+ if [ -n "$PAGE_FILES" ]; then
1181
+ # Extract navigate targets (relative paths like '../leave-types', './create', etc.)
1182
+ NAV_TARGETS=$(grep -oP "navigate\(['\"]([^'\"]+)['\"]" $PAGE_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u)
1183
+ # Extract route paths from App.tsx or route config
1184
+ APP_FILES=$(find web/ -name "App.tsx" -o -name "routes.tsx" -o -name "clientRoutes*.tsx" 2>/dev/null)
1185
+ if [ -n "$APP_FILES" ] && [ -n "$NAV_TARGETS" ]; then
1186
+ ROUTE_PATHS=$(grep -oP "path:\s*['\"]([^'\"]+)['\"]" $APP_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u)
1187
+ for TARGET in $NAV_TARGETS; do
1188
+ # Skip dynamic segments (:id), back navigation (-1), and absolute URLs
1189
+ if echo "$TARGET" | grep -qP '^(:|/api|http|-[0-9])'; then continue; fi
1190
+ # Extract the last path segment for matching (e.g., '../leave-types' → 'leave-types')
1191
+ LAST_SEG=$(echo "$TARGET" | grep -oP '[a-z][-a-z0-9]*$')
1192
+ if [ -z "$LAST_SEG" ]; then continue; fi
1193
+ # Check if any route path contains this segment
1194
+ FOUND=$(echo "$ROUTE_PATHS" | grep -F "$LAST_SEG" 2>/dev/null)
1195
+ if [ -z "$FOUND" ]; then
1196
+ # Verify no page component exists for this path
1197
+ SEG_PASCAL=$(echo "$LAST_SEG" | sed -r 's/(^|-)([a-z])/\U\2/g')
1198
+ PAGE_EXISTS=$(find web/ -name "${SEG_PASCAL}Page.tsx" -o -name "${SEG_PASCAL}ListPage.tsx" -o -name "${SEG_PASCAL}sPage.tsx" 2>/dev/null)
1199
+ if [ -z "$PAGE_EXISTS" ]; then
1200
+ # Find which file has this navigate call
1201
+ SOURCE_FILE=$(grep -rl "navigate(['\"].*${LAST_SEG}" $PAGE_FILES 2>/dev/null | head -1)
1202
+ echo "BLOCKING: Dead link detected — navigate('$TARGET') in $SOURCE_FILE"
1203
+ echo " Route segment '$LAST_SEG' has no matching route in App.tsx and no page component"
1204
+ echo " Fix: Either create the page component + route, or remove the navigate() button"
1205
+ exit 1
1206
+ fi
1207
+ fi
1208
+ done
1209
+ fi
1210
+ fi
1211
+ ```
1212
+
1213
+ ### POST-CHECK 43: Detail page tabs must NOT navigate() — content switches locally (BLOCKING)
1214
+
1215
+ ```bash
1216
+ # Tabs on detail pages MUST use local state (setActiveTab) — NEVER navigate() to other pages.
1217
+ # Root cause (test-apex-006): EmployeeDetailPage tabs navigated to ../leaves and ../time-tracking
1218
+ # instead of rendering sub-resource content inline. Users lost detail page context.
1219
+ DETAIL_PAGES=$(find src/ web/ -name "*DetailPage.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
1220
+ if [ -n "$DETAIL_PAGES" ]; then
1221
+ FAIL=false
1222
+ for DP in $DETAIL_PAGES; do
1223
+ # Check if the page has tabs (activeTab state)
1224
+ HAS_TABS=$(grep -P "useState.*activeTab|setActiveTab" "$DP" 2>/dev/null)
1225
+ if [ -z "$HAS_TABS" ]; then continue; fi
1226
+
1227
+ # Check if any tab click handler calls navigate()
1228
+ # Pattern: function that both references setActiveTab AND navigate()
1229
+ # Look for navigate() calls inside handlers that also set tab state
1230
+ TAB_NAVIGATE=$(grep -Pn "navigate\(" "$DP" 2>/dev/null | grep -v "navigate\(\s*['\"]edit['\"]" | grep -v "navigate\(\s*-1\s*\)" | grep -v "navigate\(\s*['\`].*/:id/edit" | grep -v "//")
1231
+ if [ -n "$TAB_NAVIGATE" ]; then
1232
+ # Verify this navigate is in a tab handler context (near setActiveTab usage)
1233
+ # Simple heuristic: if file has both setActiveTab AND navigate() to relative paths
1234
+ RELATIVE_NAV=$(echo "$TAB_NAVIGATE" | grep -P "navigate\(['\"\`]\.\./" 2>/dev/null)
1235
+ if [ -n "$RELATIVE_NAV" ]; then
1236
+ echo "BLOCKING: Detail page tabs use navigate() instead of local content switching: $DP"
1237
+ echo " Tab click handlers MUST only call setActiveTab() — render content inline"
1238
+ echo " Found navigate() calls (likely in tab handlers):"
1239
+ echo "$RELATIVE_NAV"
1240
+ echo ""
1241
+ echo " Fix: Remove navigate() from tab handlers. Render sub-resource content inline:"
1242
+ echo " {activeTab === 'leaves' && <LeaveRequestsTable employeeId={entity.id} />}"
1243
+ echo " See smartstack-frontend.md section 3 'Tab Behavior Rules' for the correct pattern."
1244
+ FAIL=true
1245
+ fi
1246
+ fi
1247
+ done
1248
+ if [ "$FAIL" = true ]; then
1249
+ exit 1
1250
+ fi
1251
+ fi
1252
+ ```
1253
+
1019
1254
  **If ANY POST-CHECK fails → fix in step-03, re-validate.**
@@ -493,6 +493,12 @@ public class {Name}Controller : ControllerBase
493
493
 
494
494
  **CRITICAL:** Use `[RequirePermission(Permissions.{Module}.{Action})]` on EVERY endpoint — NEVER `[Authorize]` alone (no RBAC enforcement).
495
495
 
496
+ **CRITICAL — Permission paths use IDENTICAL segments to NavRoute codes (kebab-case):**
497
+ - NavRoute: `business.human-resources.employees` → Permission: `business.human-resources.employees.read`
498
+ - NavRoute: `business.human-resources.employees.leaves` → Permission: `business.human-resources.employees.leaves.read`
499
+ - FORBIDDEN: `business.humanresources.employees.read` (no kebab-case — mismatches NavRoute)
500
+ - SmartStack.app convention: `business.support-client.my-tickets.read` (always kebab-case)
501
+
496
502
  ### Section-Level Controller (NavRoute with 4 segments)
497
503
 
498
504
  When a module has sections, each section gets its own controller with a 4-segment navRoute:
@@ -504,7 +510,7 @@ When a module has sections, each section gets its own controller with a 4-segmen
504
510
  [Authorize]
505
511
  public class {Section}Controller : ControllerBase
506
512
  {
507
- // Example: business.humanresources.employees.departments
513
+ // Example: business.human-resources.employees.departments
508
514
  [HttpGet]
509
515
  [RequirePermission(Permissions.{Section}.Read)]
510
516
  public async Task<ActionResult<PaginatedResult<{Section}ResponseDto>>> GetAll(
@@ -519,13 +525,46 @@ public class {Section}Controller : ControllerBase
519
525
  **NavRoute segment rules:**
520
526
  | Level | NavRoute format | Example |
521
527
  |-------|----------------|---------|
522
- | Module | `{context}.{app}.{module}` (3 segments) | `business.humanresources.employees` |
523
- | Section | `{context}.{app}.{module}.{section}` (4 segments) | `business.humanresources.employees.departments` |
528
+ | Module | `{context}.{app}.{module}` (3 segments) | `business.human-resources.employees` |
529
+ | Section | `{context}.{app}.{module}.{section}` (4 segments) | `business.human-resources.employees.departments` |
524
530
 
525
531
  **Namespace:** `SmartStack.Api.Routing` (NOT `SmartStack.Api.Core.Routing`)
526
532
 
527
533
  **NavRoute resolves at startup from DB:** `platform.administration.users` → `api/platform/administration/users`
528
534
 
535
+ ### Sub-Resource Pattern (NavRoute Suffix)
536
+
537
+ When an entity is a child of another entity (e.g., LeaveTypes under Leaves), use `[NavRoute(..., Suffix = "types")]`:
538
+
539
+ ```csharp
540
+ // Sub-resource controller: types are nested under leaves
541
+ [ApiController]
542
+ [NavRoute("business.human-resources.employees.leaves", Suffix = "types")]
543
+ [Authorize]
544
+ public class LeaveTypesController : ControllerBase
545
+ {
546
+ [HttpGet]
547
+ [RequirePermission(Permissions.Leaves.Read)] // inherits parent section permission
548
+ public async Task<ActionResult<PaginatedResult<LeaveTypeResponseDto>>> GetAll(...)
549
+ => Ok(await _service.GetAllAsync(search, page, pageSize, ct));
550
+ }
551
+ ```
552
+
553
+ **Alternative pattern** (sub-resource endpoints within parent controller):
554
+ ```csharp
555
+ // LeaveTypes as endpoints within LeavesController
556
+ [HttpGet("types")]
557
+ [RequirePermission(Permissions.Leaves.Read)]
558
+ public async Task<ActionResult<PaginatedResult<LeaveTypeResponseDto>>> GetAllLeaveTypes(...)
559
+ ```
560
+
561
+ > **CRITICAL — Sub-resource frontend completeness:**
562
+ > If a parent page has a button (e.g., "Manage Leave Types") that `navigate()`s to a sub-resource route,
563
+ > the frontend MUST include a page component for that route. Otherwise → dead link → white screen.
564
+ > - Either create a dedicated sub-resource ListPage (e.g., `LeaveTypesPage.tsx`)
565
+ > - Or DON'T include the navigate() button if pages won't be created
566
+ > - **Prefer separate controllers** (with Suffix) over sub-endpoints in parent controller — easier to route
567
+
529
568
  ---
530
569
 
531
570
  ## Navigation Seed Data Pattern (CRITICAL — routes must be full paths)
@@ -579,9 +618,9 @@ public static class SeedConstants
579
618
  // Deterministic GUIDs (SHA256-based, reproducible across environments)
580
619
  // NOTE: Application/Module/Section/Resource IDs are deterministic.
581
620
  // Context IDs are NOT — they are pre-seeded by SmartStack core.
582
- public static readonly Guid ApplicationId = DeterministicGuid("nav:business.humanresources");
583
- public static readonly Guid ModuleId = DeterministicGuid("nav:business.humanresources.employees");
584
- public static readonly Guid SectionId = DeterministicGuid("nav:business.humanresources.employees.departments");
621
+ public static readonly Guid ApplicationId = DeterministicGuid("nav:business.human-resources");
622
+ public static readonly Guid ModuleId = DeterministicGuid("nav:business.human-resources.employees");
623
+ public static readonly Guid SectionId = DeterministicGuid("nav:business.human-resources.employees.departments");
585
624
 
586
625
  // FORBIDDEN — Context IDs are NOT deterministic, they come from SmartStack core:
587
626
  // public static readonly Guid BusinessContextId = DeterministicGuid("nav:business"); // WRONG!
@@ -609,7 +648,7 @@ if (businessCtx == null) return; // Context not yet seeded by SmartStack core
609
648
 
610
649
  // Application: /business/human-resources
611
650
  var app = NavigationApplication.Create(
612
- businessCtx.Id, "humanresources", "Human Resources", "HR Management",
651
+ businessCtx.Id, "human-resources", "Human Resources", "HR Management",
613
652
  "Users", IconType.Lucide,
614
653
  "/business/human-resources", // FULL PATH — starts with /, kebab-case
615
654
  10);
@@ -684,6 +723,7 @@ services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
684
723
  | `UnauthorizedAccessException("Tenant context is required")` | Returns 401 → clears frontend token. Use `TenantContextRequiredException()` (400) |
685
724
  | Route `"humanresources"` in seed data | Must be full path `"/business/human-resources"` |
686
725
  | Route without leading `/` | All routes must start with `/` |
726
+ | `business.humanresources.employees.read` in permissions | Permission segments MUST match NavRoute kebab-case: `business.human-resources.employees.read` |
687
727
  | `Permission.Create()` | Does NOT exist — use `CreateForModule()`, `CreateForSection()`, etc. |
688
728
  | `GetAllAsync()` without search param | ALL GetAll endpoints MUST support `?search=` for EntityLookup |
689
729
  | FK field as plain text input | Frontend MUST use `EntityLookup` component for Guid FK fields |
@@ -325,7 +325,7 @@ export function EntityDetailPage() {
325
325
  useEffect(() => {
326
326
  if (!visitedTabsRef.current.has(activeTab)) {
327
327
  visitedTabsRef.current.add(activeTab);
328
- // Load tab-specific data here
328
+ // Load tab-specific data here (e.g., fetch leaves for this employee)
329
329
  }
330
330
  }, [activeTab]);
331
331
 
@@ -336,6 +336,52 @@ export function EntityDetailPage() {
336
336
  }
337
337
  ```
338
338
 
339
+ ### Tab Behavior Rules (CRITICAL)
340
+
341
+ > **CRITICAL: Tabs on detail pages switch content LOCALLY — they NEVER navigate to other pages.**
342
+ > Each tab renders its content INLINE within the same page component.
343
+ > Sub-resource data (e.g., an employee's leaves) loads via API call filtered by the parent entity ID.
344
+
345
+ **Tab state management:**
346
+ - Tabs use `useState<TabKey>('info')` for the active tab — LOCAL React state only
347
+ - Tab click handler: `onClick={() => setActiveTab(tabKey)}` — NEVER `navigate()`
348
+ - Tab content: conditional rendering `{activeTab === 'tabKey' && <TabContent />}`
349
+ - Lazy loading: `visitedTabsRef` tracks which tabs have been visited to avoid redundant API calls
350
+
351
+ **Tab content for sub-resources:**
352
+ ```tsx
353
+ // CORRECT — sub-resource data loaded INLINE within the tab
354
+ {activeTab === 'leaves' && (
355
+ <div>
356
+ <LeaveRequestsTable employeeId={entity.id} />
357
+ {/* Optional "View all" link INSIDE the tab content area */}
358
+ <Link to={`../leaves?employee=${entity.id}`}>
359
+ {t('employees:tabs.viewAllLeaves', 'View all leave requests')}
360
+ </Link>
361
+ </div>
362
+ )}
363
+ ```
364
+
365
+ **FORBIDDEN tab patterns:**
366
+ ```tsx
367
+ // FORBIDDEN — tab click handler navigates to another page
368
+ const handleTabClick = (tab: TabKey) => {
369
+ setActiveTab(tab);
370
+ if (tab === 'leaves') navigate(`../leaves?employee=${id}`); // ← BREAKS tab UX
371
+ };
372
+
373
+ // FORBIDDEN — tab content is empty because navigation already left the page
374
+ {activeTab === 'info' && <div>...</div>}
375
+ // Leaves tab: nothing renders here, user is already on another page
376
+ ```
377
+
378
+ **Why this matters:**
379
+ - Navigating away loses the detail page context (entity data, scroll position, other tab state)
380
+ - Users expect tabs to switch content in-place, not redirect to a different page
381
+ - The browser back button should go to the list page, not toggle between tabs
382
+
383
+ **POST-CHECK 43 enforces this rule.**
384
+
339
385
  ---
340
386
 
341
387
  ## 3b. Form Pages Pattern (Create / Edit)
@@ -101,6 +101,8 @@
101
101
 
102
102
  **Rules:**
103
103
  - `[RequirePermission(Permissions.{Module}.{Action})]` on EVERY endpoint
104
+ - Permission paths MUST use kebab-case matching NavRoute codes (e.g., `business.human-resources.employees.read`)
105
+ - FORBIDDEN: concatenated segments like `humanresources` — must be `human-resources`
104
106
  - NEVER use `[Authorize]` without specific permission
105
107
  - Swagger XML documentation
106
108
  - Return DTOs, never domain entities
@@ -178,7 +180,7 @@ private static Guid GenerateDeterministicGuid(string input)
178
180
 
179
181
  | Level | Code (C#) | Route (DB) |
180
182
  |-------|-----------|-----------|
181
- | Application | `humanresources` | `/business/human-resources` |
183
+ | Application | `human-resources` | `/business/human-resources` |
182
184
  | Module | `employees` | `/business/human-resources/employees` |
183
185
  | Section | `departments` | `/business/human-resources/employees/departments` |
184
186
  | Resource | `export` | `/business/human-resources/employees/departments/export` |
@@ -28,7 +28,7 @@ Extract flags from the raw input:
28
28
  ```
29
29
  Input: "{raw_input}"
30
30
 
31
- Flags to detect: -a, -x, -s, -e, -r, -pr
31
+ Flags to detect: -a, -x, -s, -e, -r, -pr, -d {path}
32
32
  Remaining text after flag removal = {task_description}
33
33
  ```
34
34
 
@@ -41,6 +41,53 @@ save_mode: false
41
41
  economy_mode: false
42
42
  pr_mode: false
43
43
  resume_mode: false
44
+ delegate_mode: false
45
+ delegate_prd_path: null
46
+ ```
47
+
48
+ **If `-d {path}` detected:** set `delegate_mode = true`, `delegate_prd_path = {path}`, force `auto_mode = true`, `economy_mode = true`.
49
+
50
+ ---
51
+
52
+ ## 1b. Delegate Mode Fast Path (if -d)
53
+
54
+ > **When `/ralph-loop` invokes `/apex -d {prd_path}`, all context is extracted from the PRD file.**
55
+ > Skip interactive sections (hierarchy definition, challenge questions).
56
+
57
+ ```
58
+ IF delegate_mode:
59
+ Read prd.json from {delegate_prd_path}
60
+
61
+ Extract context:
62
+ {context_code} = prd.project.context (e.g., "business")
63
+ {app_name} = prd.project.application (e.g., "HumanResources")
64
+ {module_code} = prd.project.module (e.g., "EmployeeManagement")
65
+ {prd_path} = {delegate_prd_path}
66
+ {feature_path} = prd.source.feature_json_path (if exists)
67
+
68
+ Extract sections from infrastructure tasks with _seedDataMeta:
69
+ Find task where _seedDataMeta.coreSeedData.navigationSections exists
70
+ {sections} = _seedDataMeta.coreSeedData.navigationSections[]
71
+ → map to: { code, label, description, route, displayOrder, navigation }
72
+
73
+ Extract entities from domain tasks:
74
+ {entities} = prd.tasks.filter(t => t.category == 'domain').map(t => entity name from t.description)
75
+
76
+ Extract complexity:
77
+ {module_complexity} = heuristic from task count:
78
+ <= 10 tasks → "simple-crud"
79
+ <= 20 tasks → "crud-rules"
80
+ <= 30 tasks → "crud-workflow"
81
+ > 30 tasks → "complex"
82
+
83
+ Set needs:
84
+ {needs_seed_data} = prd.tasks.some(t => t.category == 'seedData' || t.category == 'infrastructure')
85
+ {needs_migration} = prd.tasks.some(t => t._migrationMeta != null)
86
+ {has_dependencies} = prd.tasks.some(t => t.dependencies.length > 0) ? "references" : "none"
87
+ {key_properties} = [] (extracted during step-01 from feature.json if available)
88
+
89
+ SKIP sections 2 (detect context), 4 (hierarchy), 5 (challenge questions)
90
+ Jump to section 3 (MCP verify) → then section 6 (determine needs, already set) → section 9 (summary)
44
91
  ```
45
92
 
46
93
  ---
@@ -34,6 +34,43 @@ This saves ~2 minutes of Glob searches on an empty project.
34
34
 
35
35
  ---
36
36
 
37
+ ## 0b. Delegate Mode Fast Path (if delegate_mode)
38
+
39
+ > **When called via `/ralph-loop -d`, PRD tasks provide pre-computed scope. Reduce exploration to verification only.**
40
+
41
+ ```
42
+ IF delegate_mode:
43
+ Read PRD from {delegate_prd_path} (already loaded in step-00)
44
+
45
+ 1. Extract entity list from domain tasks:
46
+ entities[] = prd.tasks.filter(category == 'domain').map(entity name from description)
47
+
48
+ 2. Extract FK relationships from frontend tasks with _frontendMeta:
49
+ fkFields[] = _frontendMeta.fkFields[] (e.g., ["DepartmentId→Department", "CompanyId→Company"])
50
+
51
+ 3. Determine tenant modes per entity:
52
+ IF feature.json exists at {feature_path}:
53
+ Read entity definitions → extract tenantMode per entity
54
+ ELSE:
55
+ Default all entities to tenantMode: "strict"
56
+
57
+ 4. Read feature.json for wireframes/componentMapping if {feature_path} exists:
58
+ → enriches frontend generation in step-03
59
+
60
+ 5. Perform LIGHTWEIGHT code exploration (verification only):
61
+ - Glob("**/Domain/Entities/**/{module_code}/**/*.cs") → what already exists?
62
+ - Glob("**/Infrastructure/Persistence/Seeding/Data/**/*SeedData*.cs") → existing seed data?
63
+ - Glob("src/pages/**/{module_code}/**/*.tsx") → existing pages?
64
+ → Only to determine create vs modify vs skip (NOT deep analysis)
65
+
66
+ 6. Gap Analysis: Mark all PRD tasks as "create" unless existing code found
67
+
68
+ SKIP: Full Agent Teams exploration (section 2), challenge question follow-up
69
+ Jump to section 5 (Gap Analysis summary) → section 7 (Analysis Summary)
70
+ ```
71
+
72
+ ---
73
+
37
74
  ## 1. Context Sources
38
75
 
39
76
  Read available context (in order of priority):
@@ -16,6 +16,42 @@ Read `references/smartstack-layers.md` for layer execution rules and skill/MCP m
16
16
 
17
17
  ---
18
18
 
19
+ ## 0. Delegate Mode Fast Path (if delegate_mode)
20
+
21
+ > **When called via `/ralph-loop -d`, PRD tasks already define the scope. Map directly to layers.**
22
+
23
+ ```
24
+ IF delegate_mode:
25
+ Read PRD from {delegate_prd_path}
26
+
27
+ Map each PRD task to a layer based on task.category:
28
+ domain → Layer 0
29
+ infrastructure → Layer 0
30
+ application → Layer 1
31
+ api → Layer 1
32
+ seedData → Layer 1
33
+ frontend → Layer 2
34
+ test → Layer 3
35
+
36
+ For each task:
37
+ file_path = task.files_changed.created[0] (primary file)
38
+ action = "create" (default for delegate mode)
39
+ tool = infer from category:
40
+ domain/infrastructure → MCP scaffold_extension
41
+ api → /controller skill or MCP scaffold_extension
42
+ seedData → MCP generate_permissions + ref core-seed-data.md
43
+ frontend → /ui-components skill + MCP scaffold_api_client + MCP scaffold_routes
44
+ test → MCP scaffold_tests
45
+ acceptance_criteria = task.acceptance_criteria
46
+
47
+ Use task._seedDataMeta, _frontendMeta, _migrationMeta for enriched context.
48
+
49
+ SKIP: User checkpoint (section 6) — auto_mode implied
50
+ Jump to section 5 (Estimated Commits) → NEXT STEP
51
+ ```
52
+
53
+ ---
54
+
19
55
  ## 1. Map Changes to Layers
20
56
 
21
57
  For each element identified in step-01 (create or modify), assign to a SmartStack layer: