@atlashub/smartstack-cli 3.33.0 → 3.35.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 (65) 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 +414 -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/dist/index.js +32 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/mcp-entry.mjs +7 -24
  17. package/dist/mcp-entry.mjs.map +1 -1
  18. package/package.json +1 -2
  19. package/templates/agents/ba-writer.md +142 -15
  20. package/templates/mcp-scaffolding/controller.cs.hbs +5 -1
  21. package/templates/skills/apex/SKILL.md +9 -3
  22. package/templates/skills/apex/_shared.md +49 -4
  23. package/templates/skills/{ralph-loop → apex}/references/core-seed-data.md +20 -11
  24. package/templates/skills/{ralph-loop → apex}/references/error-classification.md +2 -1
  25. package/templates/skills/apex/references/post-checks.md +463 -3
  26. package/templates/skills/apex/references/smartstack-api.md +76 -8
  27. package/templates/skills/apex/references/smartstack-frontend.md +74 -1
  28. package/templates/skills/apex/references/smartstack-layers.md +21 -3
  29. package/templates/skills/apex/steps/step-00-init.md +121 -1
  30. package/templates/skills/apex/steps/step-01-analyze.md +58 -0
  31. package/templates/skills/apex/steps/step-02-plan.md +36 -0
  32. package/templates/skills/apex/steps/step-03-execute.md +114 -7
  33. package/templates/skills/apex/steps/step-04-examine.md +116 -2
  34. package/templates/skills/business-analyse/SKILL.md +31 -20
  35. package/templates/skills/business-analyse/_module-loop.md +68 -9
  36. package/templates/skills/business-analyse/_shared.md +80 -21
  37. package/templates/skills/business-analyse/questionnaire/00-application.md +4 -2
  38. package/templates/skills/business-analyse/questionnaire/00b-project.md +85 -0
  39. package/templates/skills/business-analyse/references/deploy-modes.md +69 -0
  40. package/templates/skills/business-analyse/references/team-orchestration.md +158 -7
  41. package/templates/skills/business-analyse/schemas/application-schema.json +15 -1
  42. package/templates/skills/business-analyse/schemas/project-schema.json +490 -0
  43. package/templates/skills/business-analyse/schemas/sections/metadata-schema.json +2 -1
  44. package/templates/skills/business-analyse/steps/step-00-init.md +220 -38
  45. package/templates/skills/business-analyse/steps/step-01-cadrage.md +184 -5
  46. package/templates/skills/business-analyse/steps/step-01b-applications.md +423 -0
  47. package/templates/skills/business-analyse/steps/step-02-decomposition.md +23 -6
  48. package/templates/skills/business-analyse/steps/step-03c-compile.md +14 -2
  49. package/templates/skills/business-analyse/steps/step-03d-validate.md +32 -7
  50. package/templates/skills/business-analyse/steps/step-04a-collect.md +111 -0
  51. package/templates/skills/business-analyse/steps/step-05a-handoff.md +296 -103
  52. package/templates/skills/business-analyse/steps/step-05b-deploy.md +46 -14
  53. package/templates/skills/documentation/SKILL.md +92 -2
  54. package/templates/skills/ralph-loop/SKILL.md +14 -17
  55. package/templates/skills/ralph-loop/references/category-rules.md +63 -683
  56. package/templates/skills/ralph-loop/references/compact-loop.md +188 -428
  57. package/templates/skills/ralph-loop/references/section-splitting.md +439 -0
  58. package/templates/skills/ralph-loop/references/team-orchestration.md +13 -14
  59. package/templates/skills/ralph-loop/steps/step-01-task.md +27 -0
  60. package/templates/skills/ralph-loop/steps/step-02-execute.md +80 -691
  61. package/templates/skills/ralph-loop/steps/step-03-commit.md +38 -79
  62. package/templates/skills/ralph-loop/steps/step-04-check.md +39 -58
  63. package/templates/skills/ralph-loop/steps/step-05-report.md +31 -123
  64. package/scripts/health-check.sh +0 -168
  65. package/scripts/postinstall.js +0 -18
@@ -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,434 @@ 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
+
1254
+ ### POST-CHECK 44: Migration ModelSnapshot must contain ALL entities registered in DbContext (BLOCKING)
1255
+
1256
+ ```bash
1257
+ # Root cause (test-apex-007): 7 entities registered in DbContext but migration only covered 3.
1258
+ # Happens when migration is created ONCE in Layer 0 for the first batch, then additional entities
1259
+ # are added in subsequent iterations without re-running migration.
1260
+ SNAPSHOT=$(find src/ -name "*ModelSnapshot.cs" -path "*/Migrations/*" 2>/dev/null | head -1)
1261
+ DBCONTEXT=$(find src/ -name "*DbContext.cs" -path "*/Persistence/*" ! -name "*DesignTime*" 2>/dev/null | head -1)
1262
+ if [ -n "$SNAPSHOT" ] && [ -n "$DBCONTEXT" ]; then
1263
+ # Extract DbSet entity names from DbContext (DbSet<EntityName>)
1264
+ DBSET_ENTITIES=$(grep -oP 'DbSet<(\w+)>' "$DBCONTEXT" 2>/dev/null | grep -oP '<\K\w+(?=>)' | sort -u)
1265
+ FAIL=false
1266
+ for ENTITY in $DBSET_ENTITIES; do
1267
+ # Skip base SmartStack entities (handled by core migrations)
1268
+ if echo "$ENTITY" | grep -qP '^(Navigation|Tenant|User|Role|Permission|AuditLog|ApplicationTracking)'; then
1269
+ continue
1270
+ fi
1271
+ # Check if the entity appears in ModelSnapshot (builder.Entity<EntityName>)
1272
+ if ! grep -q "Entity<$ENTITY>" "$SNAPSHOT" 2>/dev/null; then
1273
+ echo "BLOCKING: Entity '$ENTITY' is registered as DbSet in $DBCONTEXT but MISSING from ModelSnapshot"
1274
+ echo " This means no migration was created for this entity — it will not exist in the database."
1275
+ echo " Fix: Run 'dotnet ef migrations add' to include all new entities"
1276
+ FAIL=true
1277
+ fi
1278
+ done
1279
+ if [ "$FAIL" = true ]; then
1280
+ echo ""
1281
+ echo " Root cause: Migration was likely created once for the first batch of entities,"
1282
+ echo " but additional entities were added later without regenerating the migration."
1283
+ echo " Fix: Create a new migration that covers ALL missing entities."
1284
+ exit 1
1285
+ fi
1286
+ fi
1287
+ ```
1288
+
1289
+ ### POST-CHECK 45: I18n namespace files must be registered in i18n config (BLOCKING)
1290
+
1291
+ ```bash
1292
+ # Root cause (test-apex-007): i18n JSON files existed in src/i18n/locales/ but were never
1293
+ # registered in the i18n config (config.ts or index.ts). Pages calling useTranslation(['module'])
1294
+ # got empty translations at runtime.
1295
+ I18N_CONFIG=$(find src/ web/ -path "*/i18n/config.ts" -o -path "*/i18n/index.ts" -o -path "*/i18n/i18n.ts" 2>/dev/null | grep -v node_modules | head -1)
1296
+ if [ -n "$I18N_CONFIG" ]; then
1297
+ # Find all module JSON files in the primary language (fr)
1298
+ FR_FILES=$(find src/ web/ -path "*/i18n/locales/fr/*.json" 2>/dev/null | grep -v node_modules | grep -v common.json | grep -v navigation.json)
1299
+ if [ -n "$FR_FILES" ]; then
1300
+ FAIL=false
1301
+ for JSON_FILE in $FR_FILES; do
1302
+ NS=$(basename "$JSON_FILE" .json)
1303
+ # Check if namespace is referenced in config (import or resource key)
1304
+ if ! grep -q "$NS" "$I18N_CONFIG" 2>/dev/null; then
1305
+ echo "BLOCKING: i18n namespace '$NS' (from $JSON_FILE) is not registered in $I18N_CONFIG"
1306
+ echo " Pages using useTranslation(['$NS']) will get empty translations at runtime"
1307
+ echo " Fix: Add '$NS' to the resources/ns configuration in $I18N_CONFIG"
1308
+ FAIL=true
1309
+ fi
1310
+ done
1311
+ if [ "$FAIL" = true ]; then
1312
+ exit 1
1313
+ fi
1314
+ fi
1315
+ fi
1316
+ ```
1317
+
1318
+ ### POST-CHECK 46: FluentValidation validators must be registered via DI (BLOCKING)
1319
+
1320
+ ```bash
1321
+ # Root cause (test-apex-007): Validators existed but were never registered in DI.
1322
+ # Without DI registration, [FromBody] DTOs are never validated — any data is accepted.
1323
+ VALIDATOR_FILES=$(find src/ -name "*Validator.cs" -path "*/Validators/*" 2>/dev/null | grep -v test | grep -v Test)
1324
+ if [ -n "$VALIDATOR_FILES" ]; then
1325
+ # Check DI registration file exists
1326
+ DI_FILE=$(find src/ -name "DependencyInjection.cs" -o -name "ServiceCollectionExtensions.cs" 2>/dev/null | grep -v test | head -1)
1327
+ if [ -z "$DI_FILE" ]; then
1328
+ echo "BLOCKING: Validators exist but no DependencyInjection.cs found for DI registration"
1329
+ exit 1
1330
+ fi
1331
+ # Check for AddValidatorsFromAssembly or individual validator registration
1332
+ HAS_ASSEMBLY_REG=$(grep -c "AddValidatorsFromAssembly\|AddValidatorsFromAssemblyContaining" "$DI_FILE" 2>/dev/null)
1333
+ if [ "$HAS_ASSEMBLY_REG" -eq 0 ]; then
1334
+ # Check individual registrations as fallback
1335
+ VALIDATOR_COUNT=$(echo "$VALIDATOR_FILES" | wc -l)
1336
+ REGISTERED_COUNT=0
1337
+ for VF in $VALIDATOR_FILES; do
1338
+ VN=$(basename "$VF" .cs)
1339
+ if grep -q "$VN" "$DI_FILE" 2>/dev/null; then
1340
+ REGISTERED_COUNT=$((REGISTERED_COUNT + 1))
1341
+ fi
1342
+ done
1343
+ if [ "$REGISTERED_COUNT" -eq 0 ]; then
1344
+ echo "BLOCKING: $VALIDATOR_COUNT validators exist but NONE are registered in DI ($DI_FILE)"
1345
+ echo " Fix: Add 'services.AddValidatorsFromAssemblyContaining<Create{Entity}DtoValidator>();' to $DI_FILE"
1346
+ echo " Or use 'services.AddValidatorsFromAssembly(typeof(Create{Entity}DtoValidator).Assembly);'"
1347
+ exit 1
1348
+ fi
1349
+ fi
1350
+ fi
1351
+ ```
1352
+
1353
+ ### POST-CHECK 47: Date/date properties in DTOs must use DateOnly, not string (BLOCKING)
1354
+
1355
+ ```bash
1356
+ # Root cause (test-apex-007): WorkLog DTO had Date property typed as string instead of DateOnly.
1357
+ # This causes: invalid date parsing, no date validation, inconsistent formats across clients.
1358
+ DTO_FILES=$(find src/ -name "*Dto.cs" -path "*/DTOs/*" 2>/dev/null)
1359
+ if [ -n "$DTO_FILES" ]; then
1360
+ FAIL=false
1361
+ for f in $DTO_FILES; do
1362
+ # Find string properties whose name contains "Date" (case-insensitive)
1363
+ BAD_DATES=$(grep -Pn 'string\??\s+\w*[Dd]ate\w*\s*[{;,]' "$f" 2>/dev/null | grep -vi "Updated\|Created\|format\|pattern\|string\|parse")
1364
+ if [ -n "$BAD_DATES" ]; then
1365
+ echo "BLOCKING: DTO has string type for date field — must use DateOnly: $f"
1366
+ echo "$BAD_DATES"
1367
+ echo " Fix: Change 'string Date' to 'DateOnly Date' (or 'DateOnly? Date' if nullable)"
1368
+ echo " DateOnly is the correct .NET type for date-only values (no time component)"
1369
+ FAIL=true
1370
+ fi
1371
+ done
1372
+ if [ "$FAIL" = true ]; then
1373
+ exit 1
1374
+ fi
1375
+ fi
1376
+ ```
1377
+
1378
+ ### POST-CHECK 48: NavRoute attribute values must use kebab-case (BLOCKING)
1379
+
1380
+ ```bash
1381
+ # Root cause (test-apex-007): Controllers had [NavRoute("business.humanresources.employees")]
1382
+ # instead of [NavRoute("business.human-resources.employees")]. This causes route mismatch with
1383
+ # seed data and permission codes, resulting in 404s at runtime.
1384
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1385
+ if [ -n "$CTRL_FILES" ]; then
1386
+ FAIL=false
1387
+ for f in $CTRL_FILES; do
1388
+ NAVROUTE_VALS=$(grep -oP 'NavRoute\("([^"]+)"' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
1389
+ for NR in $NAVROUTE_VALS; do
1390
+ # Check each segment for concatenated multi-word without hyphens
1391
+ SEGMENTS=$(echo "$NR" | tr '.' '\n')
1392
+ for SEG in $SEGMENTS; do
1393
+ # Detect segments that look like concatenated words (lowercase, 8+ chars, no hyphens)
1394
+ # Use a simpler heuristic: lowercase-only segment with known multi-word patterns
1395
+ if echo "$SEG" | grep -qP '^[a-z]{8,}$'; then
1396
+ # Additional check: does it contain a known multi-word pattern?
1397
+ if echo "$SEG" | grep -qP '(human|project|leave|client|support|email|time|work|resource)'; then
1398
+ echo "BLOCKING: NavRoute segment '$SEG' in $f appears to be concatenated multi-word without hyphens"
1399
+ echo " Full NavRoute: $NR"
1400
+ echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources', 'projectmanagement' → 'project-management'"
1401
+ FAIL=true
1402
+ fi
1403
+ fi
1404
+ done
1405
+ done
1406
+ done
1407
+ if [ "$FAIL" = true ]; then
1408
+ exit 1
1409
+ fi
1410
+ fi
1411
+ ```
1412
+
1413
+ ### POST-CHECK 49: Every module with entities must have a migration covering them (BLOCKING)
1414
+
1415
+ ```bash
1416
+ # Complementary to POST-CHECK 44 — checks from the entity side.
1417
+ # Finds entity .cs files in Domain/ and verifies they appear in at least one migration file.
1418
+ ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null | grep -v test)
1419
+ MIGRATION_DIR=$(find src/ -path "*/Migrations" -type d 2>/dev/null | head -1)
1420
+ if [ -n "$ENTITY_FILES" ] && [ -n "$MIGRATION_DIR" ]; then
1421
+ MIGRATION_FILES=$(find "$MIGRATION_DIR" -name "*.cs" ! -name "*ModelSnapshot*" ! -name "*DesignTime*" 2>/dev/null)
1422
+ if [ -z "$MIGRATION_FILES" ]; then
1423
+ echo "BLOCKING: Entity files exist in Domain/Entities but NO migration files found in $MIGRATION_DIR"
1424
+ exit 1
1425
+ fi
1426
+ FAIL=false
1427
+ for EF in $ENTITY_FILES; do
1428
+ ENTITY_NAME=$(basename "$EF" .cs)
1429
+ # Skip abstract base classes and interfaces
1430
+ if grep -qP '^\s*(public\s+)?(abstract|interface)\s' "$EF" 2>/dev/null; then continue; fi
1431
+ # Check if entity appears in any migration (CreateTable or AddColumn or entity reference)
1432
+ FOUND=$(grep -l "$ENTITY_NAME" $MIGRATION_FILES 2>/dev/null)
1433
+ if [ -z "$FOUND" ]; then
1434
+ echo "BLOCKING: Entity '$ENTITY_NAME' ($EF) not found in any migration file"
1435
+ echo " This entity will NOT have a database table."
1436
+ echo " Fix: Run 'dotnet ef migrations add' to create a migration covering this entity"
1437
+ FAIL=true
1438
+ fi
1439
+ done
1440
+ if [ "$FAIL" = true ]; then
1441
+ exit 1
1442
+ fi
1443
+ fi
1444
+ ```
1445
+
1446
+ ### POST-CHECK 50: Controllers must NOT have both [Route] and [NavRoute] attributes (BLOCKING)
1447
+
1448
+ ```bash
1449
+ # Root cause (test-apex-007): All 7 controllers had BOTH [Route("api/...")] and [NavRoute("...")].
1450
+ # In SmartStack, [NavRoute] resolves routes dynamically from Navigation entities at startup.
1451
+ # [Route] is standard ASP.NET Core static routing. When both exist:
1452
+ # - NavRoute middleware tries to resolve from DB → fails if seed data not applied → no route
1453
+ # - [Route] may or may not take over depending on middleware order
1454
+ # - Result: 404 on ALL endpoints
1455
+ # The MCP validate_conventions previously ENCOURAGED adding [Route] with [NavRoute] — this was a bug.
1456
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1457
+ if [ -n "$CTRL_FILES" ]; then
1458
+ FAIL=false
1459
+ for f in $CTRL_FILES; do
1460
+ HAS_NAVROUTE=$(grep -c '\[NavRoute(' "$f" 2>/dev/null)
1461
+ HAS_ROUTE=$(grep -c '\[Route(' "$f" 2>/dev/null)
1462
+ if [ "$HAS_NAVROUTE" -gt 0 ] && [ "$HAS_ROUTE" -gt 0 ]; then
1463
+ NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"' "$f" 2>/dev/null | head -1)
1464
+ ROUTE_VAL=$(grep -oP 'Route\("([^"]+)"' "$f" 2>/dev/null | head -1)
1465
+ echo "BLOCKING: Controller has BOTH [Route] and [NavRoute] — remove [Route]: $f"
1466
+ echo " Found: [$ROUTE_VAL] + [$NAVROUTE_VAL]"
1467
+ echo " In SmartStack, [NavRoute] resolves routes dynamically from the database."
1468
+ echo " Having [Route] alongside it causes route conflicts and 404s."
1469
+ echo " Fix: Remove the [Route(...)] attribute, keep only [NavRoute(...)]"
1470
+ FAIL=true
1471
+ fi
1472
+ done
1473
+ if [ "$FAIL" = true ]; then
1474
+ exit 1
1475
+ fi
1476
+ fi
1477
+ ```
1478
+
1019
1479
  **If ANY POST-CHECK fails → fix in step-03, re-validate.**
@@ -491,8 +491,20 @@ public class {Name}Controller : ControllerBase
491
491
  }
492
492
  ```
493
493
 
494
+ **CRITICAL — Route attribute rules:**
495
+ - `[NavRoute]` is the ONLY route attribute needed — it resolves routes dynamically from Navigation entities at startup
496
+ - **FORBIDDEN:** `[Route("api/...")]` alongside `[NavRoute]` — causes route conflicts and 404s at runtime
497
+ - **FORBIDDEN:** `[Route("api/[controller]")]` — this is standard ASP.NET Core, NOT SmartStack
498
+ - If a controller has `[NavRoute]`, there must be NO `[Route]` attribute on the class
499
+
494
500
  **CRITICAL:** Use `[RequirePermission(Permissions.{Module}.{Action})]` on EVERY endpoint — NEVER `[Authorize]` alone (no RBAC enforcement).
495
501
 
502
+ **CRITICAL — Permission paths use IDENTICAL segments to NavRoute codes (kebab-case):**
503
+ - NavRoute: `business.human-resources.employees` → Permission: `business.human-resources.employees.read`
504
+ - NavRoute: `business.human-resources.employees.leaves` → Permission: `business.human-resources.employees.leaves.read`
505
+ - FORBIDDEN: `business.humanresources.employees.read` (no kebab-case — mismatches NavRoute)
506
+ - SmartStack.app convention: `business.support-client.my-tickets.read` (always kebab-case)
507
+
496
508
  ### Section-Level Controller (NavRoute with 4 segments)
497
509
 
498
510
  When a module has sections, each section gets its own controller with a 4-segment navRoute:
@@ -504,7 +516,7 @@ When a module has sections, each section gets its own controller with a 4-segmen
504
516
  [Authorize]
505
517
  public class {Section}Controller : ControllerBase
506
518
  {
507
- // Example: business.humanresources.employees.departments
519
+ // Example: business.human-resources.employees.departments
508
520
  [HttpGet]
509
521
  [RequirePermission(Permissions.{Section}.Read)]
510
522
  public async Task<ActionResult<PaginatedResult<{Section}ResponseDto>>> GetAll(
@@ -519,13 +531,46 @@ public class {Section}Controller : ControllerBase
519
531
  **NavRoute segment rules:**
520
532
  | Level | NavRoute format | Example |
521
533
  |-------|----------------|---------|
522
- | Module | `{context}.{app}.{module}` (3 segments) | `business.humanresources.employees` |
523
- | Section | `{context}.{app}.{module}.{section}` (4 segments) | `business.humanresources.employees.departments` |
534
+ | Module | `{context}.{app}.{module}` (3 segments) | `business.human-resources.employees` |
535
+ | Section | `{context}.{app}.{module}.{section}` (4 segments) | `business.human-resources.employees.departments` |
524
536
 
525
537
  **Namespace:** `SmartStack.Api.Routing` (NOT `SmartStack.Api.Core.Routing`)
526
538
 
527
539
  **NavRoute resolves at startup from DB:** `platform.administration.users` → `api/platform/administration/users`
528
540
 
541
+ ### Sub-Resource Pattern (NavRoute Suffix)
542
+
543
+ When an entity is a child of another entity (e.g., LeaveTypes under Leaves), use `[NavRoute(..., Suffix = "types")]`:
544
+
545
+ ```csharp
546
+ // Sub-resource controller: types are nested under leaves
547
+ [ApiController]
548
+ [NavRoute("business.human-resources.employees.leaves", Suffix = "types")]
549
+ [Authorize]
550
+ public class LeaveTypesController : ControllerBase
551
+ {
552
+ [HttpGet]
553
+ [RequirePermission(Permissions.Leaves.Read)] // inherits parent section permission
554
+ public async Task<ActionResult<PaginatedResult<LeaveTypeResponseDto>>> GetAll(...)
555
+ => Ok(await _service.GetAllAsync(search, page, pageSize, ct));
556
+ }
557
+ ```
558
+
559
+ **Alternative pattern** (sub-resource endpoints within parent controller):
560
+ ```csharp
561
+ // LeaveTypes as endpoints within LeavesController
562
+ [HttpGet("types")]
563
+ [RequirePermission(Permissions.Leaves.Read)]
564
+ public async Task<ActionResult<PaginatedResult<LeaveTypeResponseDto>>> GetAllLeaveTypes(...)
565
+ ```
566
+
567
+ > **CRITICAL — Sub-resource frontend completeness:**
568
+ > If a parent page has a button (e.g., "Manage Leave Types") that `navigate()`s to a sub-resource route,
569
+ > the frontend MUST include a page component for that route. Otherwise → dead link → white screen.
570
+ > - Either create a dedicated sub-resource ListPage (e.g., `LeaveTypesPage.tsx`)
571
+ > - Or DON'T include the navigate() button if pages won't be created
572
+ > - **Prefer separate controllers** (with Suffix) over sub-endpoints in parent controller — easier to route
573
+
529
574
  ---
530
575
 
531
576
  ## Navigation Seed Data Pattern (CRITICAL — routes must be full paths)
@@ -579,9 +624,9 @@ public static class SeedConstants
579
624
  // Deterministic GUIDs (SHA256-based, reproducible across environments)
580
625
  // NOTE: Application/Module/Section/Resource IDs are deterministic.
581
626
  // 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");
627
+ public static readonly Guid ApplicationId = DeterministicGuid("nav:business.human-resources");
628
+ public static readonly Guid ModuleId = DeterministicGuid("nav:business.human-resources.employees");
629
+ public static readonly Guid SectionId = DeterministicGuid("nav:business.human-resources.employees.departments");
585
630
 
586
631
  // FORBIDDEN — Context IDs are NOT deterministic, they come from SmartStack core:
587
632
  // public static readonly Guid BusinessContextId = DeterministicGuid("nav:business"); // WRONG!
@@ -609,7 +654,7 @@ if (businessCtx == null) return; // Context not yet seeded by SmartStack core
609
654
 
610
655
  // Application: /business/human-resources
611
656
  var app = NavigationApplication.Create(
612
- businessCtx.Id, "humanresources", "Human Resources", "HR Management",
657
+ businessCtx.Id, "human-resources", "Human Resources", "HR Management",
613
658
  "Users", IconType.Lucide,
614
659
  "/business/human-resources", // FULL PATH — starts with /, kebab-case
615
660
  10);
@@ -664,6 +709,26 @@ services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
664
709
 
665
710
  ---
666
711
 
712
+ ## DTO Type Mapping (CRITICAL)
713
+
714
+ > **Use the correct .NET type for each property.** Incorrect types cause runtime parsing errors.
715
+
716
+ | Property Pattern | .NET Type | JSON Format | Example |
717
+ |-----------------|-----------|-------------|---------|
718
+ | `*Date`, `StartDate`, `EndDate`, `BirthDate` | `DateOnly` | `"2025-03-15"` | `public DateOnly Date { get; set; }` |
719
+ | `CreatedAt`, `UpdatedAt` | `DateTime` | `"2025-03-15T10:30:00Z"` | `public DateTime CreatedAt { get; set; }` |
720
+ | `*Time`, `StartTime` | `TimeOnly` | `"14:30:00"` | `public TimeOnly StartTime { get; set; }` |
721
+ | Duration, hours | `decimal` | `8.5` | `public decimal HoursWorked { get; set; }` |
722
+ | FK reference | `Guid` | `"uuid-string"` | `public Guid EmployeeId { get; set; }` |
723
+
724
+ **FORBIDDEN in DTOs:**
725
+ - `string Date` / `string StartDate` — use `DateOnly`
726
+ - `string Time` — use `TimeOnly`
727
+ - `DateTime BirthDate` — use `DateOnly` (no time component needed)
728
+ - `int` for hours/duration — use `decimal` for fractional values
729
+
730
+ ---
731
+
667
732
  ## Common Mistakes to Avoid
668
733
 
669
734
  | Mistake | Reality |
@@ -674,7 +739,7 @@ services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
674
739
  | `e.IsDeleted` filter | Does NOT exist — no soft delete |
675
740
  | `SmartStack.Api.Core.Routing` | Wrong — use `SmartStack.Api.Routing` |
676
741
  | `SystemEntity` base class | Does NOT exist — use `BaseEntity` for all |
677
- | `[Route] + [NavRoute]` | Only `[NavRoute]` needed (resolves route from DB) |
742
+ | `[Route("api/...")] + [NavRoute]` | **FORBIDDEN** — causes 404s. Only `[NavRoute]` needed (resolves route from DB at startup). Remove ALL `[Route]` attributes when `[NavRoute]` is present. |
678
743
  | `SmartStack.Domain.Common.Interfaces` | Wrong — interfaces are in `SmartStack.Domain.Common` directly |
679
744
  | `[Authorize]` without `[RequirePermission]` | No RBAC enforcement — always use `[RequirePermission]` |
680
745
  | `tenantId: Guid.Empty` in services | OWASP A01 — always use validated `_currentTenant.TenantId` |
@@ -684,8 +749,11 @@ services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
684
749
  | `UnauthorizedAccessException("Tenant context is required")` | Returns 401 → clears frontend token. Use `TenantContextRequiredException()` (400) |
685
750
  | Route `"humanresources"` in seed data | Must be full path `"/business/human-resources"` |
686
751
  | Route without leading `/` | All routes must start with `/` |
752
+ | `business.humanresources.employees.read` in permissions | Permission segments MUST match NavRoute kebab-case: `business.human-resources.employees.read` |
687
753
  | `Permission.Create()` | Does NOT exist — use `CreateForModule()`, `CreateForSection()`, etc. |
688
754
  | `GetAllAsync()` without search param | ALL GetAll endpoints MUST support `?search=` for EntityLookup |
755
+ | `string Date` in DTO | Date-only fields MUST use `DateOnly`, NEVER `string` |
756
+ | `DateTime` for date-only | Use `DateOnly` when no time component needed |
689
757
  | FK field as plain text input | Frontend MUST use `EntityLookup` component for Guid FK fields |
690
758
  | `PagedResult<T>` / `PaginatedResultDto<T>` | FORBIDDEN — use `PaginatedResult<T>` only |
691
759