@atlashub/smartstack-cli 3.39.0 → 3.40.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 (471) hide show
  1. package/.documentation/apex.html +644 -644
  2. package/.documentation/css/styles.css +2320 -2320
  3. package/.documentation/init.html +1377 -1377
  4. package/.documentation/js/app.js +780 -780
  5. package/.documentation/prd-json-v2.0.0.md +396 -396
  6. package/.documentation/testing-ba-e2e.md +462 -462
  7. package/config/default-config.json +95 -95
  8. package/config/mcp-defaults.json +62 -62
  9. package/config/settings.json +53 -53
  10. package/config/settings.local.example.json +16 -16
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp-entry.mjs +6 -4
  13. package/dist/mcp-entry.mjs.map +1 -1
  14. package/package.json +115 -115
  15. package/scripts/extract-api-endpoints.ts +325 -325
  16. package/scripts/extract-business-rules.ts +440 -440
  17. package/scripts/generate-doc-with-mock-ui.ts +804 -804
  18. package/scripts/health-check.sh +168 -168
  19. package/scripts/postinstall.js +18 -18
  20. package/templates/agents/action.md +37 -37
  21. package/templates/agents/ba-reader.md +378 -378
  22. package/templates/agents/ba-writer.md +861 -861
  23. package/templates/agents/code-reviewer.md +163 -163
  24. package/templates/agents/db-reader.md +149 -149
  25. package/templates/agents/docs-context-reader.md +143 -143
  26. package/templates/agents/docs-sync-checker.md +122 -122
  27. package/templates/agents/efcore/conflicts.md +84 -84
  28. package/templates/agents/efcore/db-deploy.md +74 -74
  29. package/templates/agents/efcore/db-reset.md +85 -85
  30. package/templates/agents/efcore/db-seed.md +61 -61
  31. package/templates/agents/efcore/db-status.md +86 -86
  32. package/templates/agents/efcore/migration.md +186 -186
  33. package/templates/agents/efcore/rebase-snapshot.md +108 -108
  34. package/templates/agents/efcore/scan.md +92 -92
  35. package/templates/agents/efcore/squash.md +161 -161
  36. package/templates/agents/explore-codebase.md +66 -66
  37. package/templates/agents/explore-docs.md +98 -98
  38. package/templates/agents/fix-grammar.md +50 -50
  39. package/templates/agents/gitflow/abort.md +45 -45
  40. package/templates/agents/gitflow/cleanup.md +96 -96
  41. package/templates/agents/gitflow/commit.md +236 -236
  42. package/templates/agents/gitflow/exec.md +48 -48
  43. package/templates/agents/gitflow/finish.md +146 -146
  44. package/templates/agents/gitflow/init-clone.md +199 -199
  45. package/templates/agents/gitflow/init-detect.md +137 -137
  46. package/templates/agents/gitflow/init-validate.md +225 -225
  47. package/templates/agents/gitflow/init.md +340 -340
  48. package/templates/agents/gitflow/merge.md +145 -145
  49. package/templates/agents/gitflow/plan.md +42 -42
  50. package/templates/agents/gitflow/pr.md +191 -191
  51. package/templates/agents/gitflow/review.md +49 -49
  52. package/templates/agents/gitflow/start.md +147 -147
  53. package/templates/agents/gitflow/status.md +95 -95
  54. package/templates/agents/mcp-healthcheck.md +163 -163
  55. package/templates/agents/snipper.md +37 -37
  56. package/templates/agents/websearch.md +46 -46
  57. package/templates/hooks/appsettings-guard.sh +76 -76
  58. package/templates/hooks/docs-drift-check.md +96 -96
  59. package/templates/hooks/ef-migration-check.md +139 -139
  60. package/templates/hooks/hooks.json +58 -58
  61. package/templates/hooks/mcp-check.md +64 -64
  62. package/templates/hooks/ralph-mcp-logger.sh +46 -46
  63. package/templates/hooks/ralph-session-end.sh +69 -69
  64. package/templates/hooks/stop-hook.sh +177 -177
  65. package/templates/hooks/wsl-dotnet-cleanup.sh +24 -24
  66. package/templates/mcp-scaffolding/component.tsx.hbs +318 -318
  67. package/templates/mcp-scaffolding/controller.cs.hbs +192 -192
  68. package/templates/mcp-scaffolding/entity-extension.cs.hbs +239 -239
  69. package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +116 -116
  70. package/templates/mcp-scaffolding/frontend/nav-routes.ts.hbs +133 -133
  71. package/templates/mcp-scaffolding/frontend/routes.tsx.hbs +126 -126
  72. package/templates/mcp-scaffolding/migrations/seed-roles.cs.hbs +261 -261
  73. package/templates/mcp-scaffolding/service-extension.cs.hbs +53 -53
  74. package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +436 -436
  75. package/templates/mcp-scaffolding/tests/entity.test.cs.hbs +239 -239
  76. package/templates/mcp-scaffolding/tests/repository.test.cs.hbs +441 -441
  77. package/templates/mcp-scaffolding/tests/security.test.cs.hbs +442 -442
  78. package/templates/mcp-scaffolding/tests/service.test.cs.hbs +402 -402
  79. package/templates/mcp-scaffolding/tests/validator.test.cs.hbs +428 -428
  80. package/templates/project/DependencyInjection.Application.cs.template +25 -25
  81. package/templates/project/DependencyInjection.Infrastructure.cs.template +61 -61
  82. package/templates/project/DesignTimeExtensionsDbContextFactory.cs.template +70 -70
  83. package/templates/project/ExampleEntity.cs.template +116 -116
  84. package/templates/project/ExampleEntityConfiguration.cs.template +64 -64
  85. package/templates/project/ExampleService.cs.template +146 -146
  86. package/templates/project/ExtensionsDbContext.cs.template +41 -41
  87. package/templates/project/IExtensionsDbContext.cs.template +22 -22
  88. package/templates/project/Program.cs.template +47 -47
  89. package/templates/project/README.md +79 -79
  90. package/templates/project/api.ts.template +12 -12
  91. package/templates/project/appsettings.json.template +170 -170
  92. package/templates/project/claude-settings.json.template +5 -5
  93. package/templates/project/test-frontend/msw/handlers.ts +58 -58
  94. package/templates/project/test-frontend/msw/server.ts +25 -25
  95. package/templates/project/test-frontend/setup.ts +16 -16
  96. package/templates/project/test-frontend/test-utils.tsx +59 -59
  97. package/templates/project/test-frontend/vitest.config.ts +31 -31
  98. package/templates/ralph/README.md +93 -93
  99. package/templates/ralph/ralph.config.yaml +113 -113
  100. package/templates/scripts/setup-ralph-loop.sh +173 -173
  101. package/templates/skills/_resources/config-safety.md +61 -61
  102. package/templates/skills/_resources/context-digest-template.md +53 -53
  103. package/templates/skills/_resources/doc-context-cache.md +60 -60
  104. package/templates/skills/_resources/docs-manifest-schema.md +155 -155
  105. package/templates/skills/_resources/formatting-guide.md +124 -124
  106. package/templates/skills/_resources/mcp-validate-documentation-spec.md +181 -181
  107. package/templates/skills/_shared.md +228 -228
  108. package/templates/skills/admin/SKILL.md +48 -48
  109. package/templates/skills/ai-prompt/SKILL.md +107 -107
  110. package/templates/skills/ai-prompt/steps/step-00-init.md +47 -47
  111. package/templates/skills/ai-prompt/steps/step-01-implementation.md +122 -122
  112. package/templates/skills/apex/SKILL.md +168 -168
  113. package/templates/skills/apex/_shared.md +141 -141
  114. package/templates/skills/apex/references/agent-teams-protocol.md +164 -164
  115. package/templates/skills/apex/references/analysis-methods.md +141 -141
  116. package/templates/skills/apex/references/challenge-questions.md +145 -145
  117. package/templates/skills/apex/references/code-generation.md +412 -412
  118. package/templates/skills/apex/references/core-seed-data.md +1437 -1437
  119. package/templates/skills/apex/references/error-classification.md +144 -144
  120. package/templates/skills/apex/references/examine-build-validation.md +82 -82
  121. package/templates/skills/apex/references/execution-frontend-gates.md +177 -177
  122. package/templates/skills/apex/references/execution-frontend-patterns.md +105 -105
  123. package/templates/skills/apex/references/execution-layer1-rules.md +96 -96
  124. package/templates/skills/apex/references/initialization-challenge-flow.md +110 -110
  125. package/templates/skills/apex/references/planning-layer-mapping.md +151 -151
  126. package/templates/skills/apex/references/post-checks.md +1584 -1584
  127. package/templates/skills/apex/references/smartstack-api.md +1053 -1053
  128. package/templates/skills/apex/references/smartstack-frontend.md +1571 -1571
  129. package/templates/skills/apex/references/smartstack-layers.md +402 -402
  130. package/templates/skills/apex/steps/step-00-init.md +307 -307
  131. package/templates/skills/apex/steps/step-01-analyze.md +165 -165
  132. package/templates/skills/apex/steps/step-02-plan.md +144 -144
  133. package/templates/skills/apex/steps/step-03-execute.md +328 -328
  134. package/templates/skills/apex/steps/step-04-examine.md +263 -263
  135. package/templates/skills/apex/steps/step-05-deep-review.md +129 -129
  136. package/templates/skills/apex/steps/step-06-resolve.md +101 -101
  137. package/templates/skills/apex/steps/step-07-tests.md +238 -238
  138. package/templates/skills/apex/steps/step-08-run-tests.md +125 -125
  139. package/templates/skills/application/SKILL.md +4 -4
  140. package/templates/skills/application/references/application-roles-template.md +227 -227
  141. package/templates/skills/application/references/backend-controller-hierarchy.md +58 -58
  142. package/templates/skills/application/references/backend-entity-seeding.md +72 -72
  143. package/templates/skills/application/references/backend-seeding-and-dto-output.md +83 -83
  144. package/templates/skills/application/references/backend-table-prefix-mapping.md +79 -79
  145. package/templates/skills/application/references/backend-verification.md +88 -88
  146. package/templates/skills/application/references/frontend-i18n-and-output.md +67 -67
  147. package/templates/skills/application/references/frontend-route-naming.md +117 -117
  148. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +107 -107
  149. package/templates/skills/application/references/frontend-verification.md +156 -156
  150. package/templates/skills/application/references/migration-checklist-troubleshooting.md +1 -1
  151. package/templates/skills/application/references/provider-template.md +177 -177
  152. package/templates/skills/application/references/roles-client-project-handling.md +55 -55
  153. package/templates/skills/application/references/roles-fallback-procedure.md +149 -149
  154. package/templates/skills/application/references/test-coverage-requirements.md +213 -213
  155. package/templates/skills/application/references/test-frontend.md +73 -73
  156. package/templates/skills/application/references/test-prerequisites.md +72 -72
  157. package/templates/skills/application/steps/step-05-frontend.md +176 -176
  158. package/templates/skills/application/steps/step-06-migration.md +193 -193
  159. package/templates/skills/application/steps/step-07-tests.md +356 -356
  160. package/templates/skills/application/steps/step-08-documentation.md +137 -137
  161. package/templates/skills/application/templates-backend.md +463 -463
  162. package/templates/skills/application/templates-frontend.md +685 -685
  163. package/templates/skills/application/templates-i18n.md +520 -520
  164. package/templates/skills/application/templates-seed.md +1096 -1096
  165. package/templates/skills/business-analyse/SKILL.md +327 -327
  166. package/templates/skills/business-analyse/_architecture.md +123 -123
  167. package/templates/skills/business-analyse/_elicitation.md +206 -206
  168. package/templates/skills/business-analyse/_module-loop.md +115 -115
  169. package/templates/skills/business-analyse/_shared.md +383 -383
  170. package/templates/skills/business-analyse/_suggestions.md +34 -34
  171. package/templates/skills/business-analyse/html/ba-interactive.html +4477 -4477
  172. package/templates/skills/business-analyse/html/build-html.js +77 -77
  173. package/templates/skills/business-analyse/html/src/scripts/01-data-init.js +150 -150
  174. package/templates/skills/business-analyse/html/src/scripts/02-navigation.js +227 -227
  175. package/templates/skills/business-analyse/html/src/scripts/03-render-cadrage.js +199 -199
  176. package/templates/skills/business-analyse/html/src/scripts/04-render-modules.js +205 -205
  177. package/templates/skills/business-analyse/html/src/scripts/05-render-specs.js +647 -647
  178. package/templates/skills/business-analyse/html/src/scripts/06-render-consolidation.js +195 -195
  179. package/templates/skills/business-analyse/html/src/scripts/07-render-handoff.js +92 -92
  180. package/templates/skills/business-analyse/html/src/scripts/08-editing.js +135 -135
  181. package/templates/skills/business-analyse/html/src/scripts/09-export.js +168 -168
  182. package/templates/skills/business-analyse/html/src/scripts/10-comments.js +171 -171
  183. package/templates/skills/business-analyse/html/src/scripts/11-review-panel.js +166 -166
  184. package/templates/skills/business-analyse/html/src/styles/01-variables.css +38 -38
  185. package/templates/skills/business-analyse/html/src/styles/02-layout.css +101 -101
  186. package/templates/skills/business-analyse/html/src/styles/03-navigation.css +120 -120
  187. package/templates/skills/business-analyse/html/src/styles/04-cards.css +196 -196
  188. package/templates/skills/business-analyse/html/src/styles/05-modules.css +454 -454
  189. package/templates/skills/business-analyse/html/src/styles/06-wireframes.css +272 -272
  190. package/templates/skills/business-analyse/html/src/styles/07-comments.css +184 -184
  191. package/templates/skills/business-analyse/html/src/styles/08-review-panel.css +241 -241
  192. package/templates/skills/business-analyse/html/src/template.html +516 -516
  193. package/templates/skills/business-analyse/patterns/suggestion-catalog.md +546 -546
  194. package/templates/skills/business-analyse/questionnaire/00-application.md +160 -160
  195. package/templates/skills/business-analyse/questionnaire/00b-project.md +85 -85
  196. package/templates/skills/business-analyse/questionnaire/01-context.md +185 -185
  197. package/templates/skills/business-analyse/questionnaire/02-stakeholders.md +189 -189
  198. package/templates/skills/business-analyse/questionnaire/03-scope.md +164 -164
  199. package/templates/skills/business-analyse/questionnaire/04-data.md +88 -88
  200. package/templates/skills/business-analyse/questionnaire/05-integrations.md +58 -58
  201. package/templates/skills/business-analyse/questionnaire/06-security.md +68 -68
  202. package/templates/skills/business-analyse/questionnaire/07-ui.md +76 -76
  203. package/templates/skills/business-analyse/questionnaire/08-performance.md +42 -42
  204. package/templates/skills/business-analyse/questionnaire/09-constraints.md +45 -45
  205. package/templates/skills/business-analyse/questionnaire/10-documentation.md +43 -43
  206. package/templates/skills/business-analyse/questionnaire/11-data-lifecycle.md +59 -59
  207. package/templates/skills/business-analyse/questionnaire/12-migration.md +58 -58
  208. package/templates/skills/business-analyse/questionnaire/13-cross-module.md +69 -69
  209. package/templates/skills/business-analyse/questionnaire/14-risk-assumptions.md +135 -135
  210. package/templates/skills/business-analyse/questionnaire/15-success-metrics.md +136 -136
  211. package/templates/skills/business-analyse/questionnaire.md +337 -337
  212. package/templates/skills/business-analyse/react/application-viewer.md +242 -242
  213. package/templates/skills/business-analyse/react/components.md +551 -551
  214. package/templates/skills/business-analyse/react/i18n-template.md +306 -306
  215. package/templates/skills/business-analyse/references/acceptance-criteria.md +169 -169
  216. package/templates/skills/business-analyse/references/agent-module-prompt.md +362 -362
  217. package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +557 -557
  218. package/templates/skills/business-analyse/references/analysis-semantic-checks.md +190 -190
  219. package/templates/skills/business-analyse/references/cache-warming-strategy.md +566 -566
  220. package/templates/skills/business-analyse/references/cadrage-challenge-patterns.md +41 -41
  221. package/templates/skills/business-analyse/references/cadrage-coverage-matrix.md +74 -74
  222. package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +115 -115
  223. package/templates/skills/business-analyse/references/cadrage-shared-modules.md +68 -69
  224. package/templates/skills/business-analyse/references/cadrage-structure-cards.md +85 -85
  225. package/templates/skills/business-analyse/references/compilation-structure-cards.md +297 -297
  226. package/templates/skills/business-analyse/references/consolidation-structural-checks.md +107 -107
  227. package/templates/skills/business-analyse/references/deploy-data-build.md +180 -180
  228. package/templates/skills/business-analyse/references/deploy-modes.md +118 -118
  229. package/templates/skills/business-analyse/references/detection-strategies.md +424 -424
  230. package/templates/skills/business-analyse/references/entity-architecture-decision.md +218 -218
  231. package/templates/skills/business-analyse/references/handoff-file-templates.md +120 -120
  232. package/templates/skills/business-analyse/references/handoff-mappings.md +81 -81
  233. package/templates/skills/business-analyse/references/handoff-seeddata-generation.md +312 -312
  234. package/templates/skills/business-analyse/references/html-data-mapping.md +299 -299
  235. package/templates/skills/business-analyse/references/init-schema-deployment.md +65 -65
  236. package/templates/skills/business-analyse/references/naming-conventions.md +243 -243
  237. package/templates/skills/business-analyse/references/prd-generation.md +258 -258
  238. package/templates/skills/business-analyse/references/review-data-mapping.md +363 -363
  239. package/templates/skills/business-analyse/references/robustness-checks.md +542 -542
  240. package/templates/skills/business-analyse/references/spec-auto-inference.md +111 -111
  241. package/templates/skills/business-analyse/references/team-orchestration.md +1022 -1022
  242. package/templates/skills/business-analyse/references/ui-dashboard-spec.md +85 -85
  243. package/templates/skills/business-analyse/references/ui-resource-cards.md +259 -259
  244. package/templates/skills/business-analyse/references/validate-incremental-html.md +121 -121
  245. package/templates/skills/business-analyse/references/validation-checklist.md +347 -347
  246. package/templates/skills/business-analyse/references/wireframe-svg-style-guide.md +335 -335
  247. package/templates/skills/business-analyse/schemas/application-schema.json +453 -453
  248. package/templates/skills/business-analyse/schemas/feature-schema.json +53 -53
  249. package/templates/skills/business-analyse/schemas/project-schema.json +485 -485
  250. package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +201 -201
  251. package/templates/skills/business-analyse/schemas/sections/discovery-schema.json +82 -82
  252. package/templates/skills/business-analyse/schemas/sections/handoff-schema.json +80 -80
  253. package/templates/skills/business-analyse/schemas/sections/metadata-schema.json +70 -70
  254. package/templates/skills/business-analyse/schemas/sections/specification-schema.json +547 -547
  255. package/templates/skills/business-analyse/schemas/sections/validation-schema.json +93 -93
  256. package/templates/skills/business-analyse/schemas/shared/common-defs.json +226 -226
  257. package/templates/skills/business-analyse/steps/step-00-init.md +575 -576
  258. package/templates/skills/business-analyse/steps/step-01-cadrage.md +767 -767
  259. package/templates/skills/business-analyse/steps/step-01b-applications.md +419 -419
  260. package/templates/skills/business-analyse/steps/step-02-decomposition.md +387 -387
  261. package/templates/skills/business-analyse/steps/step-03a-data.md +16 -16
  262. package/templates/skills/business-analyse/steps/step-03a1-setup.md +506 -506
  263. package/templates/skills/business-analyse/steps/step-03a2-analysis.md +252 -252
  264. package/templates/skills/business-analyse/steps/step-03b-ui.md +425 -425
  265. package/templates/skills/business-analyse/steps/step-03c-compile.md +611 -611
  266. package/templates/skills/business-analyse/steps/step-03d-validate.md +783 -783
  267. package/templates/skills/business-analyse/steps/step-04-consolidation.md +17 -17
  268. package/templates/skills/business-analyse/steps/step-04a-collect.md +415 -415
  269. package/templates/skills/business-analyse/steps/step-04b-analyze.md +163 -163
  270. package/templates/skills/business-analyse/steps/step-04c-decide.md +186 -186
  271. package/templates/skills/business-analyse/steps/step-05a-handoff.md +840 -840
  272. package/templates/skills/business-analyse/steps/step-05b-deploy.md +522 -522
  273. package/templates/skills/business-analyse/steps/step-05c-ralph-readiness.md +703 -703
  274. package/templates/skills/business-analyse/steps/step-06-review.md +278 -278
  275. package/templates/skills/business-analyse/templates/tpl-frd.md +168 -168
  276. package/templates/skills/business-analyse/templates/tpl-handoff.md +186 -186
  277. package/templates/skills/business-analyse/templates/tpl-launch-displays.md +59 -59
  278. package/templates/skills/business-analyse/templates/tpl-progress.md +172 -172
  279. package/templates/skills/business-analyse/templates-frd.md +476 -476
  280. package/templates/skills/business-analyse/templates-react.md +574 -574
  281. package/templates/skills/cc-agent/SKILL.md +129 -129
  282. package/templates/skills/cc-agent/references/agent-behavior-patterns.md +95 -95
  283. package/templates/skills/cc-agent/references/agent-frontmatter.md +213 -213
  284. package/templates/skills/cc-agent/references/permission-modes.md +102 -102
  285. package/templates/skills/cc-agent/references/tools-reference.md +144 -144
  286. package/templates/skills/cc-agent/steps/step-00-init.md +134 -134
  287. package/templates/skills/cc-agent/steps/step-01-design.md +186 -186
  288. package/templates/skills/cc-agent/steps/step-02-generate.md +131 -131
  289. package/templates/skills/cc-agent/steps/step-03-validate.md +130 -130
  290. package/templates/skills/cc-agent/templates/agent-categorized.md +67 -67
  291. package/templates/skills/cc-agent/templates/agent-standalone.md +56 -56
  292. package/templates/skills/cc-agent/templates/agent-with-skills.md +94 -94
  293. package/templates/skills/cc-audit/SKILL.md +108 -108
  294. package/templates/skills/cc-audit/references/agent-checklist.md +91 -91
  295. package/templates/skills/cc-audit/references/hook-checklist.md +110 -110
  296. package/templates/skills/cc-audit/references/skill-checklist.md +70 -70
  297. package/templates/skills/cc-audit/steps/step-00-init.md +98 -98
  298. package/templates/skills/cc-audit/steps/step-01-scan.md +142 -142
  299. package/templates/skills/cc-audit/steps/step-02-analyze.md +158 -158
  300. package/templates/skills/cc-audit/steps/step-03-report.md +142 -142
  301. package/templates/skills/cc-skill/SKILL.md +134 -134
  302. package/templates/skills/cc-skill/references/best-practices.md +167 -167
  303. package/templates/skills/cc-skill/references/frontmatter-reference.md +182 -182
  304. package/templates/skills/cc-skill/references/skill-patterns.md +199 -199
  305. package/templates/skills/cc-skill/steps/step-00-init.md +119 -119
  306. package/templates/skills/cc-skill/steps/step-01-design.md +199 -199
  307. package/templates/skills/cc-skill/steps/step-02-generate.md +145 -145
  308. package/templates/skills/cc-skill/steps/step-03-steps.md +151 -151
  309. package/templates/skills/cc-skill/steps/step-04-validate.md +124 -124
  310. package/templates/skills/cc-skill/templates/skill-forked.md +85 -85
  311. package/templates/skills/cc-skill/templates/skill-progressive.md +102 -102
  312. package/templates/skills/cc-skill/templates/skill-simple.md +75 -75
  313. package/templates/skills/cc-skill/templates/step-template.md +82 -82
  314. package/templates/skills/check-version/SKILL.md +196 -196
  315. package/templates/skills/controller/SKILL.md +162 -162
  316. package/templates/skills/controller/postman-templates.md +614 -614
  317. package/templates/skills/controller/references/controller-code-templates.md +159 -159
  318. package/templates/skills/controller/references/mcp-scaffold-workflow.md +209 -209
  319. package/templates/skills/controller/references/permission-sync-templates.md +149 -149
  320. package/templates/skills/controller/steps/step-00-init.md +193 -191
  321. package/templates/skills/controller/steps/step-01-analyze.md +146 -146
  322. package/templates/skills/controller/steps/step-02-plan.md +176 -176
  323. package/templates/skills/controller/steps/step-03-generate.md +189 -189
  324. package/templates/skills/controller/steps/step-04-perms.md +80 -80
  325. package/templates/skills/controller/steps/step-05-validate.md +107 -107
  326. package/templates/skills/controller/templates.md +1555 -1555
  327. package/templates/skills/debug/SKILL.md +70 -70
  328. package/templates/skills/debug/references/team-protocol.md +232 -232
  329. package/templates/skills/debug/steps/step-00-init.md +57 -57
  330. package/templates/skills/debug/steps/step-01-analyze.md +219 -219
  331. package/templates/skills/debug/steps/step-02-resolve.md +85 -85
  332. package/templates/skills/documentation/SKILL.md +132 -132
  333. package/templates/skills/documentation/data-schema.md +227 -227
  334. package/templates/skills/documentation/steps/step-00-init.md +70 -70
  335. package/templates/skills/documentation/steps/step-01-scan.md +113 -113
  336. package/templates/skills/documentation/steps/step-02-generate.md +231 -231
  337. package/templates/skills/documentation/steps/step-03-validate.md +251 -238
  338. package/templates/skills/documentation/templates.md +662 -663
  339. package/templates/skills/efcore/SKILL.md +167 -167
  340. package/templates/skills/efcore/references/both-contexts.md +32 -32
  341. package/templates/skills/efcore/references/database-operations.md +67 -67
  342. package/templates/skills/efcore/references/destructive-operations.md +38 -38
  343. package/templates/skills/efcore/references/reset-operations.md +81 -81
  344. package/templates/skills/efcore/references/seed-methods.md +86 -86
  345. package/templates/skills/efcore/references/shared-init-functions.md +250 -250
  346. package/templates/skills/efcore/references/sql-objects-injection.md +61 -61
  347. package/templates/skills/efcore/references/troubleshooting.md +81 -81
  348. package/templates/skills/efcore/references/zero-downtime-patterns.md +227 -227
  349. package/templates/skills/efcore/steps/db/step-deploy.md +217 -217
  350. package/templates/skills/efcore/steps/db/step-reset.md +186 -186
  351. package/templates/skills/efcore/steps/db/step-seed.md +166 -166
  352. package/templates/skills/efcore/steps/db/step-status.md +173 -173
  353. package/templates/skills/efcore/steps/migration/step-00-init.md +102 -102
  354. package/templates/skills/efcore/steps/migration/step-01-check.md +164 -164
  355. package/templates/skills/efcore/steps/migration/step-02-create.md +160 -160
  356. package/templates/skills/efcore/steps/migration/step-03-validate.md +168 -168
  357. package/templates/skills/efcore/steps/rebase-snapshot/step-00-init.md +173 -173
  358. package/templates/skills/efcore/steps/rebase-snapshot/step-01-backup.md +100 -100
  359. package/templates/skills/efcore/steps/rebase-snapshot/step-02-fetch.md +115 -115
  360. package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +112 -112
  361. package/templates/skills/efcore/steps/rebase-snapshot/step-04-validate.md +157 -157
  362. package/templates/skills/efcore/steps/shared/step-00-init.md +131 -131
  363. package/templates/skills/efcore/steps/squash/step-00-init.md +141 -141
  364. package/templates/skills/efcore/steps/squash/step-01-backup.md +120 -120
  365. package/templates/skills/efcore/steps/squash/step-02-fetch.md +168 -168
  366. package/templates/skills/efcore/steps/squash/step-03-create.md +184 -184
  367. package/templates/skills/efcore/steps/squash/step-04-validate.md +174 -174
  368. package/templates/skills/explore/SKILL.md +98 -98
  369. package/templates/skills/feature-full/SKILL.md +111 -111
  370. package/templates/skills/feature-full/steps/step-00-init.md +57 -57
  371. package/templates/skills/feature-full/steps/step-01-implementation.md +120 -120
  372. package/templates/skills/gitflow/SKILL.md +377 -377
  373. package/templates/skills/gitflow/_shared.md +620 -620
  374. package/templates/skills/gitflow/phases/abort.md +189 -189
  375. package/templates/skills/gitflow/phases/cleanup.md +234 -234
  376. package/templates/skills/gitflow/phases/status.md +192 -192
  377. package/templates/skills/gitflow/references/commit-message-generation.md +58 -58
  378. package/templates/skills/gitflow/references/commit-migration-validation.md +49 -49
  379. package/templates/skills/gitflow/references/finish-cleanup.md +55 -55
  380. package/templates/skills/gitflow/references/finish-version-bumping.md +45 -45
  381. package/templates/skills/gitflow/references/init-config-template.md +135 -135
  382. package/templates/skills/gitflow/references/init-environment-detection.md +41 -41
  383. package/templates/skills/gitflow/references/init-name-normalization.md +103 -103
  384. package/templates/skills/gitflow/references/init-questions.md +185 -185
  385. package/templates/skills/gitflow/references/init-structure-creation.md +75 -75
  386. package/templates/skills/gitflow/references/init-version-detection.md +21 -21
  387. package/templates/skills/gitflow/references/init-workspace-detection.md +43 -43
  388. package/templates/skills/gitflow/references/merge-ci-status.md +36 -36
  389. package/templates/skills/gitflow/references/merge-execution.md +62 -62
  390. package/templates/skills/gitflow/references/merge-pr-context.md +76 -76
  391. package/templates/skills/gitflow/references/plan-template.md +69 -69
  392. package/templates/skills/gitflow/references/pr-build-checks.md +60 -60
  393. package/templates/skills/gitflow/references/pr-generation.md +58 -58
  394. package/templates/skills/gitflow/references/start-branch-normalization.md +28 -28
  395. package/templates/skills/gitflow/references/start-efcore-preflight.md +70 -70
  396. package/templates/skills/gitflow/references/start-local-config.md +113 -113
  397. package/templates/skills/gitflow/references/start-worktree-creation.md +50 -50
  398. package/templates/skills/gitflow/references/sync-push-verify.md +44 -44
  399. package/templates/skills/gitflow/references/sync-rebase-conflicts.md +38 -38
  400. package/templates/skills/gitflow/steps/step-commit.md +199 -199
  401. package/templates/skills/gitflow/steps/step-finish.md +147 -147
  402. package/templates/skills/gitflow/steps/step-init.md +190 -190
  403. package/templates/skills/gitflow/steps/step-merge.md +85 -85
  404. package/templates/skills/gitflow/steps/step-plan.md +151 -151
  405. package/templates/skills/gitflow/steps/step-pr.md +199 -199
  406. package/templates/skills/gitflow/steps/step-start.md +195 -195
  407. package/templates/skills/gitflow/steps/step-sync.md +161 -161
  408. package/templates/skills/gitflow/templates/config.json +72 -72
  409. package/templates/skills/mcp/SKILL.md +62 -62
  410. package/templates/skills/mcp/steps/step-01-healthcheck.md +108 -108
  411. package/templates/skills/mcp/steps/step-02-tools.md +73 -73
  412. package/templates/skills/notification/SKILL.md +173 -173
  413. package/templates/skills/quick-search/SKILL.md +99 -99
  414. package/templates/skills/ralph-loop/SKILL.md +234 -234
  415. package/templates/skills/ralph-loop/references/category-completeness.md +185 -185
  416. package/templates/skills/ralph-loop/references/category-rules.md +96 -96
  417. package/templates/skills/ralph-loop/references/compact-loop.md +300 -300
  418. package/templates/skills/ralph-loop/references/init-resume-recovery.md +127 -127
  419. package/templates/skills/ralph-loop/references/module-transition.md +151 -151
  420. package/templates/skills/ralph-loop/references/multi-module-queue.md +171 -171
  421. package/templates/skills/ralph-loop/references/parallel-execution.md +246 -246
  422. package/templates/skills/ralph-loop/references/section-splitting.md +439 -439
  423. package/templates/skills/ralph-loop/references/task-transform-legacy.md +256 -256
  424. package/templates/skills/ralph-loop/references/team-orchestration.md +547 -547
  425. package/templates/skills/ralph-loop/steps/step-00-init.md +150 -150
  426. package/templates/skills/ralph-loop/steps/step-01-task.md +174 -174
  427. package/templates/skills/ralph-loop/steps/step-02-execute.md +177 -177
  428. package/templates/skills/ralph-loop/steps/step-03-commit.md +92 -92
  429. package/templates/skills/ralph-loop/steps/step-04-check.md +207 -207
  430. package/templates/skills/ralph-loop/steps/step-05-report.md +175 -175
  431. package/templates/skills/refactor/SKILL.md +56 -56
  432. package/templates/skills/refactor/steps/step-01-discover.md +60 -60
  433. package/templates/skills/refactor/steps/step-02-execute.md +67 -67
  434. package/templates/skills/review-code/SKILL.md +94 -94
  435. package/templates/skills/review-code/references/clean-code-principles.md +292 -292
  436. package/templates/skills/review-code/references/code-quality-metrics.md +174 -174
  437. package/templates/skills/review-code/references/feedback-patterns.md +149 -149
  438. package/templates/skills/review-code/references/owasp-api-top10.md +243 -243
  439. package/templates/skills/review-code/references/security-checklist.md +212 -212
  440. package/templates/skills/review-code/steps/step-01-smartstack.md +96 -96
  441. package/templates/skills/review-code/steps/step-02-detailed-review.md +80 -80
  442. package/templates/skills/review-code/steps/step-03-react.md +44 -44
  443. package/templates/skills/ui-components/SKILL.md +137 -137
  444. package/templates/skills/ui-components/accessibility.md +170 -170
  445. package/templates/skills/ui-components/patterns/dashboard-chart.md +327 -327
  446. package/templates/skills/ui-components/patterns/data-table.md +39 -39
  447. package/templates/skills/ui-components/patterns/entity-card.md +77 -77
  448. package/templates/skills/ui-components/patterns/grid-layout.md +91 -91
  449. package/templates/skills/ui-components/patterns/kanban.md +43 -43
  450. package/templates/skills/ui-components/responsive-guidelines.md +278 -278
  451. package/templates/skills/ui-components/style-guide.md +113 -113
  452. package/templates/skills/utils/SKILL.md +44 -44
  453. package/templates/skills/utils/subcommands/test-web-config.md +152 -152
  454. package/templates/skills/utils/subcommands/test-web.md +123 -123
  455. package/templates/skills/validate/SKILL.md +181 -181
  456. package/templates/skills/validate-feature/SKILL.md +101 -101
  457. package/templates/skills/validate-feature/references/api-smoke-tests.md +140 -140
  458. package/templates/skills/validate-feature/references/db-validation-checks.md +180 -180
  459. package/templates/skills/validate-feature/steps/step-00-dependencies.md +121 -121
  460. package/templates/skills/validate-feature/steps/step-01-compile.md +39 -39
  461. package/templates/skills/validate-feature/steps/step-02-unit-tests.md +45 -45
  462. package/templates/skills/validate-feature/steps/step-03-integration-tests.md +53 -53
  463. package/templates/skills/validate-feature/steps/step-04-api-smoke.md +94 -94
  464. package/templates/skills/validate-feature/steps/step-05-db-validation.md +149 -149
  465. package/templates/skills/workflow/SKILL.md +127 -127
  466. package/templates/skills/workflow/steps/step-00-init.md +57 -57
  467. package/templates/skills/workflow/steps/step-01-implementation.md +84 -84
  468. package/templates/test-web/api-health.json +38 -38
  469. package/templates/test-web/minimal.json +19 -19
  470. package/templates/test-web/npm-package.json +46 -46
  471. package/templates/test-web/seo-check.json +54 -54
@@ -1,1584 +1,1584 @@
1
- # BLOCKING POST-CHECKs
2
-
3
- > **Referenced by:** step-04-examine.md (section 6b)
4
- > These checks run on the actual generated files. Model-interpreted checks are unreliable.
5
-
6
- ### POST-CHECK 1: Navigation routes must be full paths starting with /
7
-
8
- ```bash
9
- # Find all seed data files and check route values
10
- SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
11
- if [ -n "$SEED_FILES" ]; then
12
- # Check for short routes (no leading /) in Create() calls for navigation entities
13
- BAD_ROUTES=$(grep -Pn 'NavigationApplication\.Create\(|NavigationModule\.Create\(|NavigationSection\.Create\(|NavigationResource\.Create\(' $SEED_FILES | grep -v '"/[a-z]')
14
- if [ -n "$BAD_ROUTES" ]; then
15
- echo "BLOCKING: Navigation routes must be full paths starting with /"
16
- echo "$BAD_ROUTES"
17
- echo "Expected: \"/human-resources\" NOT \"humanresources\""
18
- exit 1
19
- fi
20
- fi
21
- ```
22
-
23
- ### POST-CHECK 2: All services must filter by TenantId (OWASP A01)
24
-
25
- ```bash
26
- # Find all service implementation files
27
- SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
28
- if [ -n "$SERVICE_FILES" ]; then
29
- # Check each service file has TenantId reference (either _currentUser.TenantId or TenantId filter)
30
- for f in $SERVICE_FILES; do
31
- # Accept either _currentTenant.TenantId (strict guard clause or nullable usage)
32
- # or entities with IOptionalTenantEntity/IScopedTenantEntity (optional tenant pattern)
33
- HAS_TENANT_FILTER=$(grep -c "TenantId" "$f")
34
- HAS_OPTIONAL_ENTITY=false
35
- if grep -q "IOptionalTenantEntity\|IScopedTenantEntity" "$f"; then
36
- HAS_OPTIONAL_ENTITY=true
37
- fi
38
-
39
- if [ "$HAS_TENANT_FILTER" -eq 0 ] && [ "$HAS_OPTIONAL_ENTITY" = false ]; then
40
- echo "BLOCKING (OWASP A01): Service missing TenantId filter or optional tenant entity: $f"
41
- echo "Every service query MUST filter by _currentTenant.TenantId"
42
- echo "OR work with entities that implement IOptionalTenantEntity/IScopedTenantEntity"
43
- exit 1
44
- fi
45
- if grep -q "Guid.Empty" "$f"; then
46
- echo "BLOCKING (OWASP A01): Service uses Guid.Empty instead of _currentTenant.TenantId: $f"
47
- exit 1
48
- fi
49
- done
50
- fi
51
- ```
52
-
53
- ### POST-CHECK 3: Controllers must use [RequirePermission], not just [Authorize] (BLOCKING)
54
-
55
- ```bash
56
- # Find all controller files
57
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
58
- if [ -n "$CTRL_FILES" ]; then
59
- for f in $CTRL_FILES; do
60
- # Check controller has at least one RequirePermission attribute
61
- if grep -q "\[Authorize\]" "$f" && ! grep -q "\[RequirePermission" "$f"; then
62
- echo "BLOCKING: Controller uses [Authorize] without [RequirePermission]: $f"
63
- echo "[Authorize] alone provides NO RBAC enforcement — any authenticated user has access"
64
- echo "Fix: Add [RequirePermission(Permissions.{Module}.{Action})] on each endpoint"
65
- exit 1
66
- fi
67
- done
68
- fi
69
- ```
70
-
71
- ### POST-CHECK 4: Seed data must not use Guid.NewGuid()
72
-
73
- ```bash
74
- SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
75
- if [ -n "$SEED_FILES" ]; then
76
- BAD_GUIDS=$(grep -n "Guid.NewGuid()" $SEED_FILES 2>/dev/null)
77
- if [ -n "$BAD_GUIDS" ]; then
78
- echo "BLOCKING: Seed data must use deterministic GUIDs (SHA256), not Guid.NewGuid()"
79
- echo "$BAD_GUIDS"
80
- exit 1
81
- fi
82
- fi
83
- ```
84
-
85
- ### POST-CHECK 5: Services must inject ICurrentTenantService (tenant isolation)
86
-
87
- ```bash
88
- SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
89
- if [ -n "$SERVICE_FILES" ]; then
90
- for f in $SERVICE_FILES; do
91
- # Accept either ICurrentTenantService or ICurrentUser (legacy) for tenant context
92
- if ! grep -qE "ICurrentTenantService|ICurrentUser" "$f"; then
93
- echo "BLOCKING: Service missing tenant context injection: $f"
94
- echo "All services MUST inject ICurrentTenantService for tenant isolation"
95
- echo "Pattern: private readonly ICurrentTenantService _currentTenant;"
96
- exit 1
97
- fi
98
- done
99
- fi
100
- ```
101
-
102
- ### POST-CHECK 6: Translation files must exist for all 4 languages (if frontend)
103
-
104
- ```bash
105
- # Find all i18n namespaces used in tsx files
106
- TSX_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
107
- if [ -n "$TSX_FILES" ]; then
108
- NAMESPACES=$(grep -ohP "useTranslation\(\[?'([^']+)" $TSX_FILES | sed "s/.*'//" | sort -u)
109
- for NS in $NAMESPACES; do
110
- for LANG in fr en it de; do
111
- if [ ! -f "src/i18n/locales/$LANG/$NS.json" ]; then
112
- echo "BLOCKING: Missing translation file: src/i18n/locales/$LANG/$NS.json"
113
- exit 1
114
- fi
115
- done
116
- done
117
- fi
118
- ```
119
-
120
- ### POST-CHECK 7: Pages must use lazy loading (no static page imports in routes)
121
-
122
- ```bash
123
- ROUTE_FILES=$(find src/routes/ -name "*.tsx" -o -name "*.ts" 2>/dev/null)
124
- if [ -n "$ROUTE_FILES" ]; then
125
- STATIC_PAGE_IMPORTS=$(grep -Pn "^import .+ from '@/pages/" $ROUTE_FILES 2>/dev/null)
126
- if [ -n "$STATIC_PAGE_IMPORTS" ]; then
127
- echo "BLOCKING: Route files must use React.lazy() for page imports, not static imports"
128
- echo "$STATIC_PAGE_IMPORTS"
129
- echo "Fix: const Page = lazy(() => import('@/pages/...').then(m => ({ default: m.Page })));"
130
- exit 1
131
- fi
132
- fi
133
- ```
134
-
135
- ### POST-CHECK 8: Forms must be full pages with routes — ZERO modals/popups/drawers/slide-overs
136
-
137
- ```bash
138
- # Check for modal/dialog/drawer/slide-over imports AND inline form state in page files
139
- PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
140
- if [ -n "$PAGE_FILES" ]; then
141
- FAIL=false
142
-
143
- # 8a. Component imports (Modal, Dialog, Drawer, etc.)
144
- MODAL_IMPORTS=$(grep -Pn "import.*(?:Modal|Dialog|Drawer|Popup|Sheet|SlideOver|Overlay)" $PAGE_FILES 2>/dev/null)
145
- if [ -n "$MODAL_IMPORTS" ]; then
146
- echo "BLOCKING: Form pages must NOT use Modal/Dialog/Drawer/Popup/SlideOver components"
147
- echo "$MODAL_IMPORTS"
148
- FAIL=true
149
- fi
150
-
151
- # 8b. Inline form state (catches drawers/slide-overs built without external components)
152
- FORM_STATE=$(grep -Pn "useState.*(?:isOpen|showModal|showDialog|showCreate|showEdit|showForm|isCreating|isEditing|showDrawer|showPanel|showSlideOver|selectedEntity|editingEntity)" $PAGE_FILES 2>/dev/null)
153
- if [ -n "$FORM_STATE" ]; then
154
- echo "BLOCKING: Inline form state detected — forms embedded in ListPage as drawers/panels"
155
- echo "Create/Edit forms MUST be separate page components with their own URL routes"
156
- echo "$FORM_STATE"
157
- FAIL=true
158
- fi
159
-
160
- if [ "$FAIL" = true ]; then
161
- echo ""
162
- echo "Fix: Create EntityCreatePage.tsx with route /create and EntityEditPage.tsx with route /:id/edit"
163
- echo "NEVER embed create/edit forms as inline drawers, panels, or slide-overs in ListPage"
164
- exit 1
165
- fi
166
- fi
167
- ```
168
-
169
- ### POST-CHECK 9: Create/Edit pages must exist as separate route pages (BLOCKING)
170
-
171
- ```bash
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
174
- LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test)
175
- FAIL=false
176
- if [ -n "$LIST_PAGES" ]; then
177
- for LIST_PAGE in $LIST_PAGES; do
178
- PAGE_DIR=$(dirname "$LIST_PAGE")
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
-
185
- # Check for create page
186
- CREATE_PAGE=$(find "$PAGE_DIR" -name "*CreatePage.tsx" 2>/dev/null)
187
- if [ -z "$CREATE_PAGE" ]; then
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
196
- fi
197
-
198
- # Check for edit page
199
- EDIT_PAGE=$(find "$PAGE_DIR" -name "*EditPage.tsx" 2>/dev/null)
200
- if [ -z "$EDIT_PAGE" ]; then
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
209
- fi
210
- done
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
220
- ```
221
-
222
- ### POST-CHECK 10: Form pages must have companion test files
223
-
224
- ```bash
225
- # Minimum requirement: if frontend pages exist, at least 1 test file must be present
226
- PAGE_FILES=$(find src/pages/ -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null)
227
- if [ -n "$PAGE_FILES" ]; then
228
- ALL_TESTS=$(find src/pages/ -name "*.test.tsx" 2>/dev/null)
229
- if [ -z "$ALL_TESTS" ]; then
230
- echo "BLOCKING: No frontend test files found in src/pages/"
231
- echo "Every form page MUST have a companion .test.tsx file"
232
- exit 1
233
- fi
234
- fi
235
-
236
- # Every CreatePage and EditPage must have a .test.tsx file
237
- FORM_PAGES=$(find src/pages/ -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test)
238
- if [ -n "$FORM_PAGES" ]; then
239
- for FORM_PAGE in $FORM_PAGES; do
240
- TEST_FILE="${FORM_PAGE%.tsx}.test.tsx"
241
- if [ ! -f "$TEST_FILE" ]; then
242
- echo "BLOCKING: Form page missing test file: $FORM_PAGE"
243
- echo "Expected: $TEST_FILE"
244
- echo "All form pages MUST have companion test files (rendering, validation, submit, navigation)"
245
- exit 1
246
- fi
247
- done
248
- fi
249
- ```
250
-
251
- ### POST-CHECK 11: FK fields must use EntityLookup — NO `<input>`, NO `<select>` (BLOCKING)
252
-
253
- ```bash
254
- # Check ALL page files for FK fields rendered as <input> or <select> instead of EntityLookup
255
- # Scans ALL .tsx files (not just CreatePage/EditPage — forms may be embedded in ListPage drawers)
256
- ALL_PAGES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
257
- if [ -n "$ALL_PAGES" ]; then
258
- FAIL=false
259
-
260
- # 1. Detect <input> with name/value binding to FK fields (fields ending in "Id")
261
- FK_INPUTS=$(grep -Pn '<input[^>]*(?:name|value)=["\x27{][^>]*[a-zA-Z]Id["\x27}]' $ALL_PAGES 2>/dev/null | grep -Pv 'type=["\x27]hidden["\x27]')
262
- if [ -n "$FK_INPUTS" ]; then
263
- echo "BLOCKING: FK fields rendered as <input> — MUST use EntityLookup component"
264
- echo "$FK_INPUTS"
265
- FAIL=true
266
- fi
267
-
268
- # 2. Detect <select> with value binding to FK fields (e.g., value={formData.departmentId})
269
- FK_SELECTS=$(grep -Pn '<select[^>]*value=\{[^}]*[a-zA-Z]Id\b' $ALL_PAGES 2>/dev/null)
270
- if [ -n "$FK_SELECTS" ]; then
271
- echo "BLOCKING: FK fields rendered as <select> dropdown — MUST use EntityLookup component"
272
- echo "A <select> loaded from API state is NOT a valid substitute for EntityLookup."
273
- echo "EntityLookup provides: debounced search, paginated results, display name resolution."
274
- echo "$FK_SELECTS"
275
- FAIL=true
276
- fi
277
-
278
- # 3. Detect onChange handlers setting FK fields from <select> (e.g., setFormData({...formData, departmentId: e.target.value}))
279
- FK_SELECT_ONCHANGE=$(grep -Pn 'onChange=.*[a-zA-Z]Id[^a-zA-Z].*e\.target\.value' $ALL_PAGES 2>/dev/null)
280
- if [ -n "$FK_SELECT_ONCHANGE" ]; then
281
- echo "BLOCKING: FK field set via e.target.value (select/input pattern) — use EntityLookup onChange(id)"
282
- echo "$FK_SELECT_ONCHANGE"
283
- FAIL=true
284
- fi
285
-
286
- # 4. Check for placeholders mentioning "ID", "GUID", or "Select..." for FK fields
287
- FK_PLACEHOLDER=$(grep -Pn 'placeholder=["\x27].*(?:[Ee]nter.*[Ii][Dd]|[Gg][Uu][Ii][Dd]|[Ss]elect.*[Ee]mployee|[Ss]elect.*[Dd]epartment|[Ss]elect.*[Pp]osition|[Ss]elect.*[Pp]roject|[Ss]elect.*[Cc]ategory)' $ALL_PAGES 2>/dev/null)
288
- if [ -n "$FK_PLACEHOLDER" ]; then
289
- echo "BLOCKING: Form has placeholder for FK field selection — use EntityLookup search instead"
290
- echo "$FK_PLACEHOLDER"
291
- FAIL=true
292
- fi
293
-
294
- # 5. Detect <option> elements with GUID-like values (sign of FK <select>)
295
- FK_OPTIONS=$(grep -Pn '<option[^>]*value=\{[^}]*\.id\}' $ALL_PAGES 2>/dev/null)
296
- if [ -n "$FK_OPTIONS" ]; then
297
- echo "BLOCKING: <option> with entity .id value detected — this is a FK <select> anti-pattern"
298
- echo "Replace the entire <select>/<option> block with <EntityLookup />"
299
- echo "$FK_OPTIONS"
300
- FAIL=true
301
- fi
302
-
303
- if [ "$FAIL" = true ]; then
304
- echo ""
305
- echo "Fix: Replace ALL FK fields with <EntityLookup /> from @/components/ui/EntityLookup"
306
- echo "See smartstack-frontend.md section 6 for the EntityLookup pattern"
307
- echo "FORBIDDEN for FK Guid fields: <input>, <select>, <option>, e.target.value"
308
- echo "REQUIRED: <EntityLookup apiEndpoint={...} mapOption={...} value={...} onChange={...} />"
309
- exit 1
310
- fi
311
- fi
312
- ```
313
-
314
- ### POST-CHECK 12: Backend APIs must support search parameter for EntityLookup
315
-
316
- ```bash
317
- # Check that controller GetAll methods accept search parameter
318
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
319
- if [ -n "$CTRL_FILES" ]; then
320
- for f in $CTRL_FILES; do
321
- # Check if controller has GetAll but no search parameter
322
- if grep -q "\[HttpGet\]" "$f" && grep -q "GetAll" "$f"; then
323
- if ! grep -q "search" "$f"; then
324
- echo "WARNING: Controller missing search parameter on GetAll: $f"
325
- echo "GetAll endpoints MUST accept ?search= to enable EntityLookup on frontend"
326
- echo "Fix: Add [FromQuery] string? search parameter to GetAll action"
327
- fi
328
- fi
329
- done
330
- fi
331
- ```
332
-
333
- ### POST-CHECK 13: No hardcoded Tailwind colors in generated pages (BLOCKING)
334
-
335
- ```bash
336
- # Scan all page and component files directly (works for uncommitted/untracked files, Windows/WSL compatible)
337
- ALL_PAGES=$(find src/pages/ src/components/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
338
- if [ -n "$ALL_PAGES" ]; then
339
- HARDCODED=$(grep -Pn '(bg|text|border)-(?!\[)(red|blue|green|gray|white|black|slate|zinc|neutral|stone)-' $ALL_PAGES 2>/dev/null)
340
- if [ -n "$HARDCODED" ]; then
341
- echo "BLOCKING: Pages MUST use CSS variables instead of hardcoded Tailwind colors"
342
- echo "SmartStack uses a theme system — hardcoded colors break dark mode and custom themes"
343
- echo ""
344
- echo "Fix mapping:"
345
- echo " bg-white → bg-[var(--bg-card)]"
346
- echo " bg-gray-50 → bg-[var(--bg-primary)]"
347
- echo " text-gray-900 → text-[var(--text-primary)]"
348
- echo " text-gray-500 → text-[var(--text-secondary)]"
349
- echo " border-gray-200 → border-[var(--border-color)]"
350
- echo " bg-blue-600 → bg-[var(--color-accent-500)]"
351
- echo " text-blue-600 → text-[var(--color-accent-500)]"
352
- echo " text-red-500 → text-[var(--error-text)]"
353
- echo " bg-green-500 → bg-[var(--success-bg)]"
354
- echo ""
355
- echo "See references/smartstack-frontend.md section 4 for full variable reference"
356
- echo ""
357
- echo "$HARDCODED"
358
- exit 1
359
- fi
360
- fi
361
- ```
362
-
363
- ### POST-CHECK 14: Routes seed data must match frontend contextRoutes
364
-
365
- ```bash
366
- SEED_ROUTES=$(grep -Poh 'Route\s*=\s*"([^"]+)"' $(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" 2>/dev/null) 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
367
- APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
368
- if [ -n "$APP_TSX" ] && [ -n "$SEED_ROUTES" ]; then
369
- FRONTEND_PATHS=$(grep -oP "path:\s*'([^']+)'" "$APP_TSX" | grep -oP "'[^']+'" | tr -d "'" | sort -u)
370
- if [ -n "$FRONTEND_PATHS" ]; then
371
- MISMATCH_FOUND=false
372
- for SEED_ROUTE in $SEED_ROUTES; do
373
- DEPTH=$(echo "$SEED_ROUTE" | tr '/' '\n' | grep -c '.')
374
- if [ "$DEPTH" -lt 3 ]; then continue; fi
375
- SEED_SUFFIX=$(echo "$SEED_ROUTE" | sed 's|^/[^/]*/||')
376
- SEED_NORM=$(echo "$SEED_SUFFIX" | tr '[:upper:]' '[:lower:]' | tr -d '-')
377
- MATCH_FOUND=false
378
- for FE_PATH in $FRONTEND_PATHS; do
379
- # Flag FORBIDDEN /list suffix BEFORE normalization
380
- if echo "$FE_PATH" | grep -qP '/list$'; then
381
- echo "WARNING: Frontend route ends with /list — should use index route instead: $FE_PATH"
382
- fi
383
- FE_BASE=$(echo "$FE_PATH" | sed 's|/list$||;s|/new$||;s|/:id.*||;s|/create$||')
384
- FE_NORM=$(echo "$FE_BASE" | tr '[:upper:]' '[:lower:]' | tr -d '-')
385
- if [ "$SEED_NORM" = "$FE_NORM" ]; then
386
- MATCH_FOUND=true
387
- break
388
- fi
389
- done
390
- if [ "$MATCH_FOUND" = false ]; then
391
- echo "BLOCKING: Seed data route has no matching frontend route: $SEED_ROUTE"
392
- MISMATCH_FOUND=true
393
- fi
394
- done
395
- if [ "$MISMATCH_FOUND" = true ]; then
396
- echo "Fix: Ensure every NavigationSeedData route has a corresponding contextRoutes entry in App.tsx"
397
- exit 1
398
- fi
399
- fi
400
- fi
401
- ```
402
-
403
- ### POST-CHECK 14b: Frontend routes must use kebab-case (BLOCKING)
404
-
405
- ```bash
406
- # POST-CHECK 14 normalizes hyphens for existence check, but does NOT catch kebab-case mismatches.
407
- # This supplementary check detects concatenated multi-word route segments without hyphens.
408
- APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
409
- if [ -n "$APP_TSX" ]; then
410
- # Extract route path strings from App.tsx
411
- FE_PATHS=$(grep -oP "path:\s*['\"]([^'\"]+)['\"]" "$APP_TSX" | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"')
412
- for FE_PATH in $FE_PATHS; do
413
- # Split path by / and check each segment
414
- for SEG in $(echo "$FE_PATH" | tr '/' '\n'); do
415
- # Skip dynamic segments (:id, :slug) and single words (< 10 chars likely single word)
416
- echo "$SEG" | grep -qP '^:' && continue
417
- # Detect multi-word segments without hyphens: 2+ consecutive lowercase sequences
418
- # e.g., "humanresources" (human+resources), "timemanagement" (time+management)
419
- if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
420
- # Potential concatenated multi-word — cross-check with seed data
421
- SEED_MATCH=$(echo "$SEED_ROUTES" | tr '/' '\n' | grep -P "^[a-z]+-[a-z]+" | tr -d '-' | grep -x "$SEG")
422
- if [ -n "$SEED_MATCH" ]; then
423
- echo "BLOCKING: Frontend route segment '$SEG' appears to be missing hyphens"
424
- echo "Seed data uses kebab-case (e.g., 'human-resources') but frontend has '$SEG'"
425
- echo "Fix: Use kebab-case in App.tsx route paths to match seed data exactly"
426
- exit 1
427
- fi
428
- fi
429
- done
430
- done
431
- fi
432
- ```
433
-
434
- ### POST-CHECK 15: HasQueryFilter must not use Guid.Empty (OWASP A01)
435
-
436
- ```bash
437
- CONFIG_FILES=$(find src/ -path "*/Configurations/*" -name "*Configuration.cs" 2>/dev/null)
438
- if [ -n "$CONFIG_FILES" ]; then
439
- BAD_FILTERS=$(grep -Pn 'HasQueryFilter.*Guid\.Empty' $CONFIG_FILES 2>/dev/null)
440
- if [ -n "$BAD_FILTERS" ]; then
441
- echo "BLOCKING (OWASP A01): HasQueryFilter uses Guid.Empty instead of runtime tenant isolation"
442
- echo "$BAD_FILTERS"
443
- echo ""
444
- echo "Anti-pattern: .HasQueryFilter(e => e.TenantId != Guid.Empty)"
445
- echo "Fix: Remove HasQueryFilter. Tenant isolation is handled by SmartStack base DbContext"
446
- exit 1
447
- fi
448
- fi
449
- ```
450
-
451
- ### POST-CHECK 16: GetAll methods must return PaginatedResult<T>
452
-
453
- ```bash
454
- SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
455
- if [ -n "$SERVICE_FILES" ]; then
456
- BAD_RETURNS=$(grep -Pn '(Task<\s*(?:List|IEnumerable|IList|ICollection|IReadOnlyList|IReadOnlyCollection)<).*GetAll' $SERVICE_FILES 2>/dev/null)
457
- if [ -n "$BAD_RETURNS" ]; then
458
- echo "BLOCKING: GetAll methods must return PaginatedResult<T>, not List/IEnumerable"
459
- echo "$BAD_RETURNS"
460
- echo "Fix: Change return type to Task<PaginatedResult<{Entity}ResponseDto>>"
461
- exit 1
462
- fi
463
- fi
464
- ```
465
-
466
- ### POST-CHECK 17: i18n files must contain required structural keys
467
-
468
- ```bash
469
- I18N_DIR="src/i18n/locales/fr"
470
- if [ -d "$I18N_DIR" ]; then
471
- REQUIRED_KEYS="actions columns empty errors form labels messages validation"
472
- for JSON_FILE in "$I18N_DIR"/*.json; do
473
- [ ! -f "$JSON_FILE" ] && continue
474
- BASENAME=$(basename "$JSON_FILE")
475
- case "$BASENAME" in common.json|navigation.json) continue;; esac
476
- for KEY in $REQUIRED_KEYS; do
477
- if ! jq -e "has(\"$KEY\")" "$JSON_FILE" > /dev/null 2>&1; then
478
- echo "BLOCKING: i18n file missing required key '$KEY': $JSON_FILE"
479
- echo "Module i18n files MUST contain: $REQUIRED_KEYS"
480
- exit 1
481
- fi
482
- done
483
- done
484
- fi
485
- ```
486
-
487
- ### POST-CHECK 18: Entities must implement IAuditableEntity + Validators must have Create/Update pairs
488
-
489
- ```bash
490
- ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null)
491
- if [ -n "$ENTITY_FILES" ]; then
492
- for f in $ENTITY_FILES; do
493
- if grep -q "ITenantEntity" "$f" && ! grep -q "IAuditableEntity" "$f"; then
494
- echo "BLOCKING: Entity implements ITenantEntity but NOT IAuditableEntity: $f"
495
- echo "Pattern: public class Entity : BaseEntity, ITenantEntity, IAuditableEntity"
496
- exit 1
497
- fi
498
- done
499
- fi
500
- CREATE_VALIDATORS=$(find src/ -path "*/Validators/*" -name "Create*Validator.cs" 2>/dev/null)
501
- if [ -n "$CREATE_VALIDATORS" ]; then
502
- for f in $CREATE_VALIDATORS; do
503
- VALIDATOR_DIR=$(dirname "$f")
504
- ENTITY_NAME=$(basename "$f" | sed 's/^Create\(.*\)Validator\.cs$/\1/')
505
- if [ ! -f "$VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs" ]; then
506
- echo "BLOCKING: Create${ENTITY_NAME}Validator exists but Update${ENTITY_NAME}Validator is missing"
507
- echo " Found: $f"
508
- echo " Expected: $VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs"
509
- exit 1
510
- fi
511
- done
512
- fi
513
- ```
514
-
515
- ### POST-CHECK 19: (REMOVED — Context level no longer exists in SmartStack navigation hierarchy)
516
-
517
- ### POST-CHECK 20: RolePermission seed data must NOT use deterministic role GUIDs
518
-
519
- ```bash
520
- # System roles (admin, manager, contributor, viewer) are pre-seeded by SmartStack core.
521
- # RolePermission mappings MUST look up roles by Code at runtime, NEVER use deterministic GUIDs.
522
- SEED_ALL_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
523
- SEED_CONST_FILES=$(find src/ -path "*/Seeding/*" -name "SeedConstants.cs" 2>/dev/null)
524
- if [ -n "$SEED_ALL_FILES" ]; then
525
- BAD_ROLE_GUID=$(grep -Pn 'DeterministicGuid\("role:' $SEED_ALL_FILES $SEED_CONST_FILES 2>/dev/null)
526
- if [ -n "$BAD_ROLE_GUID" ]; then
527
- echo "BLOCKING: Deterministic GUID for role detected (e.g., DeterministicGuid(\"role:admin\"))"
528
- echo "System roles are pre-seeded by SmartStack core with their own IDs"
529
- echo "Fix: In SeedRolePermissionsAsync(), look up roles by Code:"
530
- echo " var roles = await context.Roles.Where(r => r.IsSystem || r.ApplicationId != null).ToListAsync(ct);"
531
- echo " var role = roles.FirstOrDefault(r => r.Code == mapping.RoleCode);"
532
- echo "$BAD_ROLE_GUID"
533
- exit 1
534
- fi
535
- fi
536
- # Also check for GenerateRoleGuid usage in RolePermission mapping files (not in ApplicationRolesSeedData itself)
537
- ROLE_PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" 2>/dev/null)
538
- if [ -n "$ROLE_PERM_FILES" ]; then
539
- BAD_ROLE_REF=$(grep -Pn 'GenerateRoleGuid|GetAdminRoleId|GetManagerRoleId|GetViewerRoleId|GetContributorRoleId' $ROLE_PERM_FILES 2>/dev/null)
540
- if [ -n "$BAD_ROLE_REF" ]; then
541
- echo "WARNING: RolesSeedData uses hardcoded role GUID helpers instead of Code-based lookup"
542
- echo "Fix: Use RoleCode string (e.g., 'admin') and resolve in SeedRolePermissionsAsync()"
543
- echo "$BAD_ROLE_REF"
544
- fi
545
- fi
546
- ```
547
-
548
- ### POST-CHECK 21: Services must NOT use TenantId!.Value (null-forgiving crash pattern)
549
-
550
- ```bash
551
- # The !.Value pattern on Guid? throws InvalidOperationException (500) instead of clean 401
552
- SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
553
- if [ -n "$SERVICE_FILES" ]; then
554
- BAD_PATTERN=$(grep -Pn 'TenantId!\s*\.Value|TenantId!\s*\.ToString|\.TenantId!' $SERVICE_FILES 2>/dev/null)
555
- if [ -n "$BAD_PATTERN" ]; then
556
- echo "BLOCKING: Services use TenantId!.Value — causes 500 instead of 400 when tenant context is missing"
557
- echo "$BAD_PATTERN"
558
- echo ""
559
- echo "Fix: Replace with guard clause at the start of every method:"
560
- echo " var tenantId = _currentTenant.TenantId"
561
- echo " ?? throw new TenantContextRequiredException();"
562
- echo ""
563
- echo "This produces a clean 400 Bad Request via GlobalExceptionHandlerMiddleware."
564
- echo "NEVER use UnauthorizedAccessException for tenant context — it returns 401 which clears the frontend token."
565
- exit 1
566
- fi
567
- fi
568
-
569
- # POST-CHECK: Services must NOT use UnauthorizedAccessException for tenant context (causes token clearing)
570
- if [ -n "$SERVICE_FILES" ]; then
571
- BAD_UNAUTH=$(grep -Pn 'UnauthorizedAccessException.*[Tt]enant' $SERVICE_FILES 2>/dev/null)
572
- if [ -n "$BAD_UNAUTH" ]; then
573
- echo "BLOCKING: Services use UnauthorizedAccessException for tenant context — causes 401 which clears the frontend token"
574
- echo "$BAD_UNAUTH"
575
- echo ""
576
- echo "Fix: Replace with:"
577
- echo " var tenantId = _currentTenant.TenantId"
578
- echo " ?? throw new TenantContextRequiredException();"
579
- echo ""
580
- echo "TenantContextRequiredException returns 400 Bad Request (does not clear token)."
581
- echo "UnauthorizedAccessException returns 401 Unauthorized (clears token + redirects to login)."
582
- exit 1
583
- fi
584
- fi
585
- ```
586
-
587
- ### POST-CHECK 22: Cross-tenant entities must use Guid? TenantId
588
-
589
- ```bash
590
- for entity in $(find src/ -path "*/Domain/*" -name "*.cs" ! -name "I*.cs" 2>/dev/null); do
591
- if grep -q "IOptionalTenantEntity\|IScopedTenantEntity" "$entity"; then
592
- if grep -q "public Guid TenantId" "$entity" && ! grep -q "public Guid? TenantId" "$entity"; then
593
- echo "BLOCKING: Entity with IOptionalTenantEntity/IScopedTenantEntity must use Guid? TenantId (nullable)"
594
- exit 1
595
- fi
596
- fi
597
- done
598
- echo "POST-CHECK 22: OK"
599
- ```
600
-
601
- ### POST-CHECK 23: Scoped entities must have EntityScope property
602
-
603
- ```bash
604
- for entity in $(find src/ -path "*/Domain/*" -name "*.cs" ! -name "I*.cs" 2>/dev/null); do
605
- if grep -q "IScopedTenantEntity" "$entity"; then
606
- if ! grep -q "EntityScope\|Scope" "$entity"; then
607
- echo "BLOCKING: Entity with IScopedTenantEntity must have EntityScope Scope property"
608
- exit 1
609
- fi
610
- fi
611
- done
612
- echo "POST-CHECK 23: OK"
613
- ```
614
-
615
- ### POST-CHECK 24: Permissions.cs static constants must exist (BLOCKING)
616
-
617
- ```bash
618
- # Every module with controllers MUST have a Permissions.cs with static constants
619
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
620
- if [ -n "$CTRL_FILES" ]; then
621
- PERM_REFS=$(grep -ohP 'Permissions\.\w+\.\w+' $CTRL_FILES 2>/dev/null | sed 's/Permissions\.\([^.]*\)\..*/\1/' | sort -u)
622
- for MODULE in $PERM_REFS; do
623
- PERM_FILE=$(find src/ -name "Permissions.cs" -exec grep -l "static class $MODULE" {} \; 2>/dev/null)
624
- if [ -z "$PERM_FILE" ]; then
625
- echo "BLOCKING: Controller references Permissions.${MODULE}.* but no Permissions.cs defines static class ${MODULE}"
626
- echo "Fix: Create Application/Authorization/Permissions.cs with: public static class ${MODULE} { public const string Read = \"...\"; ... }"
627
- exit 1
628
- fi
629
- done
630
- fi
631
- ```
632
-
633
- ### POST-CHECK 25: ApplicationRolesSeedData.cs must exist (BLOCKING)
634
-
635
- ```bash
636
- # If any RolesSeedData exists, ApplicationRolesSeedData MUST also exist
637
- ROLE_SEED=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" 2>/dev/null | head -1)
638
- if [ -n "$ROLE_SEED" ]; then
639
- APP_ROLE_SEED=$(find src/ -path "*/Seeding/Data/ApplicationRolesSeedData.cs" 2>/dev/null | head -1)
640
- if [ -z "$APP_ROLE_SEED" ]; then
641
- echo "BLOCKING: RolesSeedData exists but ApplicationRolesSeedData.cs NOT FOUND"
642
- echo "ApplicationRolesSeedData defines the 4 application-scoped roles (admin, manager, contributor, viewer)"
643
- echo "Without it, SeedRolesAsync() has no role entries to create → RBAC broken"
644
- echo "Fix: Create src/Infrastructure/Persistence/Seeding/Data/ApplicationRolesSeedData.cs"
645
- exit 1
646
- fi
647
- fi
648
- ```
649
-
650
- ### POST-CHECK 25b: Section route completeness (NavigationSection → frontend route + permissions)
651
-
652
- ```bash
653
- # Every NavigationSection seed data route MUST have a corresponding frontend route in App.tsx
654
- # and section-level permissions MUST exist for each section defined in seed data
655
- SECTION_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSectionSeedData.cs" 2>/dev/null)
656
- APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
657
- if [ -n "$SECTION_SEED_FILES" ] && [ -n "$APP_TSX" ]; then
658
- # Extract section routes from seed data
659
- SECTION_ROUTES=$(grep -Poh '"/[a-z][a-z0-9/-]+"' $SECTION_SEED_FILES 2>/dev/null | tr -d '"' | sort -u)
660
- for SECTION_ROUTE in $SECTION_ROUTES; do
661
- # Extract the last segment (section-kebab) for frontend route matching
662
- SECTION_SEG=$(echo "$SECTION_ROUTE" | rev | cut -d'/' -f1 | rev)
663
- if ! grep -q "'$SECTION_SEG'" "$APP_TSX" && ! grep -q "\"$SECTION_SEG\"" "$APP_TSX"; then
664
- echo "BLOCKING: NavigationSection seed data route has no matching frontend route: $SECTION_ROUTE"
665
- echo "Expected path segment '$SECTION_SEG' in App.tsx contextRoutes"
666
- echo "Fix: Add section child routes to the module's children array in App.tsx"
667
- fi
668
- done
669
- fi
670
-
671
- # Controllers with section-level [NavRoute] (4 segments) must have matching [RequirePermission]
672
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
673
- if [ -n "$CTRL_FILES" ]; then
674
- for f in $CTRL_FILES; do
675
- # Match NavRoute with 4 dot-separated segments (section-level)
676
- SECTION_NAVROUTE=$(grep -oP 'NavRoute\("[a-z]+\.[a-z]+\.[a-z]+\.[a-z]+"\)' "$f" 2>/dev/null)
677
- if [ -n "$SECTION_NAVROUTE" ] && ! grep -q "\[RequirePermission" "$f"; then
678
- echo "BLOCKING: Section controller has [NavRoute] but no [RequirePermission]: $f"
679
- echo "Fix: Add [RequirePermission(Permissions.{Section}.{Action})] on each endpoint"
680
- exit 1
681
- fi
682
- done
683
- fi
684
-
685
- # Section-level permissions must exist for each section in seed data
686
- PERM_FILE=$(find src/ -name "Permissions.cs" -path "*/Authorization/*" 2>/dev/null | head -1)
687
- if [ -n "$SECTION_SEED_FILES" ] && [ -n "$PERM_FILE" ]; then
688
- SECTION_CODES=$(grep -oP 'Code\s*=\s*"([a-z]+)"' $SECTION_SEED_FILES 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
689
- for CODE in $SECTION_CODES; do
690
- PASCAL=$(echo "$CODE" | sed 's/^./\U&/')
691
- if ! grep -q "static class $PASCAL" "$PERM_FILE" 2>/dev/null; then
692
- echo "WARNING: Section '$CODE' in seed data has no matching Permissions.$PASCAL static class"
693
- echo "Fix: Add section-level permissions via MCP generate_permissions with 4-segment navRoute"
694
- fi
695
- done
696
- fi
697
- ```
698
-
699
- ### POST-CHECK 26: FORBIDDEN route patterns — /list and /detail/:id (BLOCKING)
700
-
701
- ```bash
702
- # 1. Check seed data for FORBIDDEN suffixes
703
- SEED_NAV_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -o -name "*NavigationSectionSeedData.cs" 2>/dev/null)
704
- if [ -n "$SEED_NAV_FILES" ]; then
705
- BAD_ROUTES=$(grep -Pn 'Route\s*=\s*.*"[^"]*/(list|detail)["/]' $SEED_NAV_FILES 2>/dev/null | grep -v '//.*Route')
706
- if [ -n "$BAD_ROUTES" ]; then
707
- echo "BLOCKING: FORBIDDEN route pattern in seed data"
708
- echo " - 'list' section route = module route (NO /list suffix)"
709
- echo " - 'detail' section route = module route + /:id (NOT /detail/:id)"
710
- echo "$BAD_ROUTES"
711
- exit 1
712
- fi
713
- fi
714
-
715
- # 2. Check frontend routes for FORBIDDEN path segments
716
- APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
717
- if [ -n "$APP_TSX" ]; then
718
- BAD_FE=$(grep -Pn "path:\s*['\"](?:list|detail)" "$APP_TSX" 2>/dev/null)
719
- if [ -n "$BAD_FE" ]; then
720
- echo "BLOCKING: FORBIDDEN frontend route path"
721
- echo " - list = index: true (no 'list' path segment)"
722
- echo " - detail = ':id' (no 'detail' path segment)"
723
- echo "$BAD_FE"
724
- exit 1
725
- fi
726
- fi
727
- echo "OK: No forbidden /list or /detail route patterns found"
728
- ```
729
-
730
- ### POST-CHECK 27: Permission path segment count (WARNING)
731
-
732
- ```bash
733
- PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "PermissionsSeedData.cs" 2>/dev/null)
734
- if [ -n "$PERM_FILES" ]; then
735
- while IFS= read -r line; do
736
- PATH_VAL=$(echo "$line" | grep -oP '"[^"]*\.[^"]*"' | tr -d '"')
737
- if [ -n "$PATH_VAL" ]; then
738
- DOTS=$(echo "$PATH_VAL" | tr -cd '.' | wc -c)
739
- # Module permissions: 2 dots (app.module.action = 3 segments = 2+1)
740
- # Section permissions: 3 dots (app.module.section.action = 4 segments = 3+1)
741
- # Wildcard: ends with .* (valid at any level)
742
- if echo "$PATH_VAL" | grep -qP '\.\*$'; then
743
- continue # Wildcards are valid
744
- elif [ "$DOTS" -lt 2 ] || [ "$DOTS" -gt 4 ]; then
745
- echo "WARNING: Permission path has unexpected segment count ($((DOTS+1)) segments): $PATH_VAL"
746
- fi
747
- fi
748
- done < <(grep -n 'Path\s*=' $PERM_FILES 2>/dev/null)
749
- fi
750
- ```
751
-
752
- ### POST-CHECK 28: IClientSeedDataProvider must have 4 methods + DI registration (BLOCKING)
753
-
754
- ```bash
755
- PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
756
- if [ -n "$PROVIDER" ]; then
757
- METHODS_FOUND=0
758
- for METHOD in SeedNavigationAsync SeedRolesAsync SeedPermissionsAsync SeedRolePermissionsAsync; do
759
- if grep -q "$METHOD" "$PROVIDER"; then
760
- METHODS_FOUND=$((METHODS_FOUND + 1))
761
- else
762
- echo "BLOCKING: IClientSeedDataProvider missing method: $METHOD in $PROVIDER"
763
- fi
764
- done
765
- if [ "$METHODS_FOUND" -lt 4 ]; then
766
- echo "Fix: IClientSeedDataProvider must implement all 4 methods: SeedNavigationAsync, SeedRolesAsync, SeedPermissionsAsync, SeedRolePermissionsAsync"
767
- exit 1
768
- fi
769
-
770
- # Check DI registration
771
- DI_FILE=$(find src/ -name "DependencyInjection.cs" -path "*/Infrastructure/*" 2>/dev/null | head -1)
772
- if [ -n "$DI_FILE" ]; then
773
- if ! grep -q "IClientSeedDataProvider" "$DI_FILE"; then
774
- echo "BLOCKING: IClientSeedDataProvider not registered in DependencyInjection.cs"
775
- echo "Fix: Add services.AddScoped<IClientSeedDataProvider, {App}SeedDataProvider>()"
776
- exit 1
777
- fi
778
- fi
779
- fi
780
- ```
781
-
782
- ### POST-CHECK 29: i18n must use separate JSON files per language (not embedded in index.ts)
783
-
784
- ```bash
785
- # Translations MUST be in src/i18n/locales/{lang}/{module}.json, NOT embedded in a single .ts file
786
- TSX_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
787
- if [ -n "$TSX_FILES" ]; then
788
- # Check if i18n locales directory exists
789
- if [ ! -d "src/i18n/locales" ]; then
790
- echo "BLOCKING: Missing src/i18n/locales/ directory"
791
- echo "Translations must be in separate JSON files: src/i18n/locales/{fr,en,it,de}/{module}.json"
792
- echo "NEVER embed translations in src/i18n/index.ts or a single TypeScript file"
793
- exit 1
794
- fi
795
-
796
- # Check for embedded translations in index.ts (common anti-pattern)
797
- I18N_INDEX=$(find src/i18n/ -maxdepth 1 -name "index.ts" -o -name "index.tsx" -o -name "config.ts" 2>/dev/null)
798
- if [ -n "$I18N_INDEX" ]; then
799
- EMBEDDED=$(grep -Pn '^\s*(resources|translations)\s*[:=]\s*\{' $I18N_INDEX 2>/dev/null)
800
- if [ -n "$EMBEDDED" ]; then
801
- echo "BLOCKING: Translations embedded in i18n config file — must be in separate JSON files"
802
- echo "Found embedded translations in:"
803
- echo "$EMBEDDED"
804
- echo ""
805
- echo "Fix: Move translations to src/i18n/locales/{fr,en,it,de}/{module}.json"
806
- echo "The i18n config should import from locales/ directory, not contain inline translations"
807
- exit 1
808
- fi
809
- fi
810
-
811
- # Verify all 4 language directories exist
812
- for LANG in fr en it de; do
813
- if [ ! -d "src/i18n/locales/$LANG" ]; then
814
- echo "BLOCKING: Missing language directory: src/i18n/locales/$LANG/"
815
- echo "SmartStack requires 4 languages: fr, en, it, de"
816
- exit 1
817
- fi
818
- done
819
-
820
- # Verify at least one JSON file exists per language
821
- for LANG in fr en it de; do
822
- JSON_COUNT=$(find "src/i18n/locales/$LANG" -name "*.json" 2>/dev/null | wc -l)
823
- if [ "$JSON_COUNT" -eq 0 ]; then
824
- echo "BLOCKING: No translation JSON files in src/i18n/locales/$LANG/"
825
- echo "Each module must have a {module}.json file per language"
826
- exit 1
827
- fi
828
- done
829
- fi
830
- ```
831
-
832
- ### POST-CHECK 30: Pages must use useTranslation hook (no hardcoded user-facing strings)
833
-
834
- ```bash
835
- # Verify that page components use i18n — detect hardcoded strings in JSX
836
- PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
837
- if [ -n "$PAGE_FILES" ]; then
838
- # Check that at least 80% of pages import useTranslation
839
- TOTAL_PAGES=$(echo "$PAGE_FILES" | wc -l)
840
- I18N_PAGES=$(grep -l "useTranslation" $PAGE_FILES 2>/dev/null | wc -l)
841
- if [ "$TOTAL_PAGES" -gt 0 ] && [ "$I18N_PAGES" -eq 0 ]; then
842
- echo "BLOCKING: No page files use useTranslation — all user-facing text must be translated"
843
- echo "Found $TOTAL_PAGES page files but 0 use useTranslation"
844
- echo ""
845
- echo "Fix: Import and use useTranslation in every page component:"
846
- echo " const { t } = useTranslation(['{module}']);"
847
- echo " t('{module}:title', 'Fallback text')"
848
- exit 1
849
- fi
850
-
851
- # Check for common hardcoded English strings in JSX (heuristic)
852
- HARDCODED_TEXT=$(grep -Pn '>\s*(Create|Edit|Delete|Save|Cancel|Search|Loading|Error|No data|Not found|Submit|Back|Actions|Name|Status|Description)\s*<' $PAGE_FILES 2>/dev/null | grep -v '{t(' | head -10)
853
- if [ -n "$HARDCODED_TEXT" ]; then
854
- echo "WARNING: Possible hardcoded user-facing strings detected in JSX"
855
- echo "All user-facing text MUST use t('namespace:key', 'Fallback')"
856
- echo "$HARDCODED_TEXT"
857
- fi
858
- fi
859
- ```
860
-
861
- ### POST-CHECK 31: List/Detail pages must include DocToggleButton (documentation panel)
862
-
863
- ```bash
864
- # Every list and detail page MUST have DocToggleButton for inline documentation access
865
- LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
866
- if [ -n "$LIST_PAGES" ]; then
867
- MISSING_DOC=0
868
- for PAGE in $LIST_PAGES; do
869
- if ! grep -q "DocToggleButton" "$PAGE" 2>/dev/null; then
870
- echo "WARNING: Page missing DocToggleButton: $PAGE"
871
- echo " Import: import { DocToggleButton } from '@/components/docs/DocToggleButton';"
872
- echo " Place in header actions: <DocToggleButton />"
873
- MISSING_DOC=$((MISSING_DOC + 1))
874
- fi
875
- done
876
- if [ "$MISSING_DOC" -gt 0 ]; then
877
- echo ""
878
- echo "WARNING: $MISSING_DOC pages missing DocToggleButton — users cannot access inline documentation"
879
- echo "See smartstack-frontend.md section 7 for placement pattern"
880
- fi
881
- fi
882
- ```
883
-
884
- ### POST-CHECK 32: Module documentation must be generated (doc-data.ts)
885
-
886
- ```bash
887
- # After frontend pages exist, /documentation should have been called
888
- TSX_PAGES=$(find src/pages/ -name "*.tsx" -not -name "*.test.*" 2>/dev/null | grep -v node_modules | grep -v "docs/")
889
- DOC_DATA=$(find src/pages/docs/ -name "doc-data.ts" 2>/dev/null)
890
- if [ -n "$TSX_PAGES" ] && [ -z "$DOC_DATA" ]; then
891
- echo "WARNING: Frontend pages exist but no documentation generated"
892
- echo "Fix: Invoke /documentation {module} --type user to generate doc-data.ts"
893
- echo "The DocToggleButton in page headers will link to this documentation"
894
- fi
895
- ```
896
-
897
- ### POST-CHECK 33: Pagination type must be PaginatedResult<T> — no aliases (BLOCKING)
898
-
899
- ```bash
900
- SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
901
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
902
- ALL_FILES="$SERVICE_FILES $CTRL_FILES"
903
- if [ -n "$(echo $ALL_FILES | tr -d ' ')" ]; then
904
- BAD_NAMES=$(grep -Pn 'PagedResult<|PaginatedResultDto<|PaginatedResponse<|PageResultDto<' $ALL_FILES 2>/dev/null)
905
- if [ -n "$BAD_NAMES" ]; then
906
- echo "BLOCKING: Pagination type must be PaginatedResult<T> — found non-canonical names"
907
- echo "$BAD_NAMES"
908
- echo "FORBIDDEN type names: PagedResult, PaginatedResultDto, PaginatedResponse, PageResultDto"
909
- echo "Fix: Use PaginatedResult<T> from SmartStack.Application.Common.Models everywhere"
910
- exit 1
911
- fi
912
- fi
913
- ```
914
-
915
- ### POST-CHECK 34: Code generation — ICodeGenerator must be registered for auto-generated entities (BLOCKING)
916
-
917
- ```bash
918
- # If feature.json has entities with codePattern.strategy != "manual",
919
- # verify that ICodeGenerator<Entity> is registered in DI
920
- FEATURE_FILES=$(find docs/ -name "feature.json" 2>/dev/null)
921
- DI_FILE=$(find src/ -name "DependencyInjection.cs" -path "*/Infrastructure/*" 2>/dev/null | head -1)
922
- if [ -n "$FEATURE_FILES" ] && [ -n "$DI_FILE" ]; then
923
- for FEATURE in $FEATURE_FILES; do
924
- ENTITIES_WITH_CODE=$(python3 -c "
925
- import json, sys
926
- try:
927
- with open('$FEATURE') as f:
928
- data = json.load(f)
929
- for e in data.get('analysis', {}).get('entities', []):
930
- cp = e.get('codePattern', {})
931
- if cp.get('strategy', 'manual') != 'manual':
932
- print(e['name'])
933
- except: pass
934
- " 2>/dev/null)
935
- for ENTITY in $ENTITIES_WITH_CODE; do
936
- if ! grep -q "ICodeGenerator<$ENTITY>" "$DI_FILE" 2>/dev/null; then
937
- echo "BLOCKING: Entity $ENTITY has auto-generated code pattern but ICodeGenerator<$ENTITY> is not registered in DI"
938
- echo "Fix: Add CodeGenerator<$ENTITY> registration in DependencyInjection.cs — see references/code-generation.md"
939
- exit 1
940
- fi
941
- done
942
- done
943
- fi
944
- ```
945
-
946
- ### POST-CHECK 35: Code regex must support hyphens (BLOCKING)
947
-
948
- ```bash
949
- VALIDATOR_FILES=$(find src/ -path "*/Validators/*" -name "*Validator.cs" 2>/dev/null)
950
- if [ -n "$VALIDATOR_FILES" ]; then
951
- OLD_REGEX=$(grep -rn '\^\\[a-z0-9_\\]+\$' $VALIDATOR_FILES 2>/dev/null | grep -v '\-')
952
- if [ -n "$OLD_REGEX" ]; then
953
- echo "BLOCKING: Code validator uses old regex without hyphen support"
954
- echo "$OLD_REGEX"
955
- echo "Fix: Update regex to ^[a-z0-9_-]+$ to support auto-generated codes with hyphens"
956
- exit 1
957
- fi
958
- fi
959
- ```
960
-
961
- ### POST-CHECK 36: CreateDto must NOT have Code field when service uses ICodeGenerator (WARNING)
962
-
963
- ```bash
964
- SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
965
- if [ -n "$SERVICE_FILES" ]; then
966
- for f in $SERVICE_FILES; do
967
- if grep -q "ICodeGenerator" "$f"; then
968
- ENTITY=$(basename "$f" | sed 's/Service\.cs$//')
969
- DTO_FILE=$(find src/ -path "*/DTOs/*" -name "Create${ENTITY}Dto.cs" 2>/dev/null | head -1)
970
- if [ -n "$DTO_FILE" ] && grep -q "public string Code" "$DTO_FILE"; then
971
- echo "WARNING: Create${ENTITY}Dto has Code field but service uses ICodeGenerator (code is auto-generated)"
972
- echo "Fix: Remove Code from Create${ENTITY}Dto — it is auto-generated by ICodeGenerator<${ENTITY}>"
973
- fi
974
- fi
975
- done
976
- fi
977
- ```
978
-
979
- ### POST-CHECK 37: Translation seed data must have idempotency guard (BLOCKING)
980
-
981
- ```bash
982
- PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
983
- if [ -n "$PROVIDER" ]; then
984
- # Check if NavigationTranslations.Add is used WITHOUT a preceding AnyAsync guard
985
- # Pattern: any .Add(NavigationTranslation.Create(...)) that is NOT inside an AnyAsync check
986
- TRANSLATION_ADDS=$(grep -c "NavigationTranslations.Add" "$PROVIDER" 2>/dev/null)
987
- TRANSLATION_GUARDS=$(grep -c "NavigationTranslations.AnyAsync" "$PROVIDER" 2>/dev/null)
988
-
989
- if [ "$TRANSLATION_ADDS" -gt 0 ] && [ "$TRANSLATION_GUARDS" -eq 0 ]; then
990
- echo "BLOCKING: Translation seed data inserts without idempotency guard in $PROVIDER"
991
- echo "Fix: Before each NavigationTranslations.Add block, check existence:"
992
- echo " if (!await context.NavigationTranslations.AnyAsync("
993
- echo " t => t.EntityId == {Module}NavigationSeedData.{Module}ModuleId"
994
- echo " && t.EntityType == NavigationEntityType.Module, ct))"
995
- echo " { foreach (var t in ...) { context.NavigationTranslations.Add(...); } }"
996
- echo "The unique index IX_nav_Translations_EntityType_EntityId_LanguageCode will crash on duplicates."
997
- exit 1
998
- fi
999
- fi
1000
- ```
1001
-
1002
- ### POST-CHECK 38: Resource seed data must use actual section IDs from DB (BLOCKING)
1003
-
1004
- ```bash
1005
- PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
1006
- if [ -n "$PROVIDER" ]; then
1007
- # Check if NavigationResource.Create uses secEntry.Id or resEntry.SectionId (deterministic GUIDs)
1008
- # instead of actualSection.Id (real DB ID). This causes FK_nav_Resources_nav_Sections_SectionId violation.
1009
- if grep -Pn 'NavigationResource\.Create\(' "$PROVIDER" | grep -q 'resEntry\.SectionId\|secEntry\.Id'; then
1010
- echo "BLOCKING: Resource seed data uses deterministic GUID as SectionId in $PROVIDER"
1011
- echo "NavigationSection.Create() generates its own ID — deterministic seed GUIDs do NOT exist in nav_Sections."
1012
- echo "Fix: Query actual section from DB before creating resources:"
1013
- echo " var actualSection = await context.NavigationSections"
1014
- echo " .FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == modEntity.Id, ct);"
1015
- echo " NavigationResource.Create(actualSection.Id, ...) // NOT secEntry.Id or resEntry.SectionId"
1016
- exit 1
1017
- fi
1018
- fi
1019
- ```
1020
-
1021
- ### POST-CHECK 39: Controllers must NOT have [Route] alongside [NavRoute] (BLOCKING)
1022
-
1023
- ```bash
1024
- # [NavRoute] REPLACES [Route] — it resolves HTTP routes from the navigation DB at startup.
1025
- # Having both is redundant and may cause route conflicts.
1026
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1027
- if [ -n "$CTRL_FILES" ]; then
1028
- for f in $CTRL_FILES; do
1029
- HAS_NAVROUTE=$(grep -P '\[NavRoute\(' "$f" 2>/dev/null)
1030
- HAS_ROUTE=$(grep -P '\[Route\(' "$f" 2>/dev/null)
1031
- if [ -n "$HAS_NAVROUTE" ] && [ -n "$HAS_ROUTE" ]; then
1032
- echo "BLOCKING: Controller has both [Route] and [NavRoute] — [NavRoute] replaces [Route]: $f"
1033
- echo " Found [NavRoute]: $HAS_NAVROUTE"
1034
- echo " Found [Route]: $HAS_ROUTE"
1035
- echo "Fix: Remove the [Route(\"api/...\")] attribute. [NavRoute] resolves routes from navigation DB at startup."
1036
- exit 1
1037
- fi
1038
- done
1039
- fi
1040
- ```
1041
-
1042
- ### POST-CHECK 40: NavRoute segments must use kebab-case for multi-word codes (BLOCKING)
1043
-
1044
- ```bash
1045
- # NavRoute segments are navigation entity Codes joined by dots.
1046
- # Multi-word codes MUST use kebab-case (e.g., "human-resources", NOT "humanresources").
1047
- # Verified from SmartStack.app: "support-client.my-tickets", "administration.access-requests"
1048
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1049
- if [ -n "$CTRL_FILES" ]; then
1050
- for f in $CTRL_FILES; do
1051
- NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
1052
- if [ -n "$NAVROUTE_VAL" ]; then
1053
- # Check each segment for concatenated multi-word (10+ lowercase chars without hyphens)
1054
- for SEG in $(echo "$NAVROUTE_VAL" | tr '.' '\n'); do
1055
- if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
1056
- echo "BLOCKING: NavRoute segment '$SEG' in $f appears to be concatenated multi-word without hyphens"
1057
- echo " Full NavRoute: $NAVROUTE_VAL"
1058
- echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
1059
- echo " SmartStack convention (from SmartStack.app): 'support-client.my-tickets'"
1060
- exit 1
1061
- fi
1062
- done
1063
- fi
1064
- done
1065
- fi
1066
-
1067
- # Also check seed data Code values for navigation entities
1068
- SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -o -name "NavigationApplicationSeedData.cs" 2>/dev/null)
1069
- if [ -n "$SEED_FILES" ]; then
1070
- CODES=$(grep -oP 'Code\s*=\s*"([^"]+)"' $SEED_FILES 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
1071
- for CODE in $CODES; do
1072
- if echo "$CODE" | grep -qP '^[a-z]{10,}$'; then
1073
- echo "BLOCKING: Navigation seed data Code '$CODE' appears to be concatenated multi-word without hyphens"
1074
- echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
1075
- exit 1
1076
- fi
1077
- done
1078
- fi
1079
- ```
1080
-
1081
- ### POST-CHECK 41: Permission codes must use kebab-case matching NavRoute codes (BLOCKING)
1082
-
1083
- ```bash
1084
- # Permission codes in [RequirePermission] and Permissions.cs MUST use kebab-case for multi-word segments.
1085
- # SmartStack.app convention: "support-client.my-tickets.read" (kebab-case everywhere)
1086
- # FORBIDDEN: "humanresources.employees.read" — must be "human-resources.employees.read"
1087
-
1088
- # Check [RequirePermission] attributes in controllers
1089
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1090
- if [ -n "$CTRL_FILES" ]; then
1091
- for f in $CTRL_FILES; do
1092
- PERM_VALS=$(grep -oP 'RequirePermission\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
1093
- for PERM in $PERM_VALS; do
1094
- # Check each segment (except the action suffix) for concatenated multi-word without hyphens
1095
- SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1) # remove last segment (action: read/create/update/delete)
1096
- for SEG in $SEGMENTS; do
1097
- if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
1098
- echo "BLOCKING: Permission code segment '$SEG' in $f appears concatenated without hyphens"
1099
- echo " Full permission: $PERM"
1100
- echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
1101
- echo " SmartStack convention: 'support-client.my-tickets.read'"
1102
- exit 1
1103
- fi
1104
- done
1105
- done
1106
- done
1107
- fi
1108
-
1109
- # Check Permissions.cs constants
1110
- PERM_FILES=$(find src/ -path "*/Authorization/Permissions.cs" 2>/dev/null)
1111
- if [ -n "$PERM_FILES" ]; then
1112
- for f in $PERM_FILES; do
1113
- CONST_VALS=$(grep -oP '=\s*"([^"]+)"' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
1114
- for PERM in $CONST_VALS; do
1115
- SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
1116
- for SEG in $SEGMENTS; do
1117
- if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
1118
- echo "BLOCKING: Permissions.cs constant segment '$SEG' in $f appears concatenated without hyphens"
1119
- echo " Full permission: $PERM"
1120
- echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
1121
- exit 1
1122
- fi
1123
- done
1124
- done
1125
- done
1126
- fi
1127
-
1128
- # Check PermissionsSeedData.cs for mismatched paths
1129
- SEED_PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*PermissionsSeedData.cs" 2>/dev/null)
1130
- if [ -n "$SEED_PERM_FILES" ]; then
1131
- PATHS=$(grep -oP '"[a-z][a-z0-9.-]+\.(read|create|update|delete|\*)"' $SEED_PERM_FILES 2>/dev/null | tr -d '"')
1132
- for PERM in $PATHS; do
1133
- SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
1134
- for SEG in $SEGMENTS; do
1135
- if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
1136
- echo "BLOCKING: PermissionsSeedData path segment '$SEG' appears concatenated without hyphens"
1137
- echo " Full permission path: $PERM"
1138
- echo " Fix: Use kebab-case matching NavRoute: 'humanresources' → 'human-resources'"
1139
- exit 1
1140
- fi
1141
- done
1142
- done
1143
- fi
1144
- ```
1145
-
1146
- ### POST-CHECK 42: Frontend navigate() calls must have matching route definitions (BLOCKING)
1147
-
1148
- ```bash
1149
- # Detect dead links: navigate() calls to paths that don't have corresponding page components.
1150
- # Example: LeavesPage has navigate('../leave-types') but no LeaveTypesPage or route exists.
1151
- PAGE_FILES=$(find web/ -name "*.tsx" -path "*/pages/*" ! -name "*.test.tsx" 2>/dev/null)
1152
- if [ -n "$PAGE_FILES" ]; then
1153
- # Extract navigate targets (relative paths like '../leave-types', './create', etc.)
1154
- NAV_TARGETS=$(grep -oP "navigate\(['\"]([^'\"]+)['\"]" $PAGE_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u)
1155
- # Extract route paths from App.tsx or route config
1156
- APP_FILES=$(find web/ -name "App.tsx" -o -name "routes.tsx" -o -name "clientRoutes*.tsx" 2>/dev/null)
1157
- if [ -n "$APP_FILES" ] && [ -n "$NAV_TARGETS" ]; then
1158
- ROUTE_PATHS=$(grep -oP "path:\s*['\"]([^'\"]+)['\"]" $APP_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u)
1159
- for TARGET in $NAV_TARGETS; do
1160
- # Skip dynamic segments (:id), back navigation (-1), and absolute URLs
1161
- if echo "$TARGET" | grep -qP '^(:|/api|http|-[0-9])'; then continue; fi
1162
- # Extract the last path segment for matching (e.g., '../leave-types' → 'leave-types')
1163
- LAST_SEG=$(echo "$TARGET" | grep -oP '[a-z][-a-z0-9]*$')
1164
- if [ -z "$LAST_SEG" ]; then continue; fi
1165
- # Check if any route path contains this segment
1166
- FOUND=$(echo "$ROUTE_PATHS" | grep -F "$LAST_SEG" 2>/dev/null)
1167
- if [ -z "$FOUND" ]; then
1168
- # Verify no page component exists for this path
1169
- SEG_PASCAL=$(echo "$LAST_SEG" | sed -r 's/(^|-)([a-z])/\U\2/g')
1170
- PAGE_EXISTS=$(find web/ -name "${SEG_PASCAL}Page.tsx" -o -name "${SEG_PASCAL}ListPage.tsx" -o -name "${SEG_PASCAL}sPage.tsx" 2>/dev/null)
1171
- if [ -z "$PAGE_EXISTS" ]; then
1172
- # Find which file has this navigate call
1173
- SOURCE_FILE=$(grep -rl "navigate(['\"].*${LAST_SEG}" $PAGE_FILES 2>/dev/null | head -1)
1174
- echo "BLOCKING: Dead link detected — navigate('$TARGET') in $SOURCE_FILE"
1175
- echo " Route segment '$LAST_SEG' has no matching route in App.tsx and no page component"
1176
- echo " Fix: Either create the page component + route, or remove the navigate() button"
1177
- exit 1
1178
- fi
1179
- fi
1180
- done
1181
- fi
1182
- fi
1183
- ```
1184
-
1185
- ### POST-CHECK 43: Detail page tabs must NOT navigate() — content switches locally (BLOCKING)
1186
-
1187
- ```bash
1188
- # Tabs on detail pages MUST use local state (setActiveTab) — NEVER navigate() to other pages.
1189
- # Root cause (test-apex-006): EmployeeDetailPage tabs navigated to ../leaves and ../time-tracking
1190
- # instead of rendering sub-resource content inline. Users lost detail page context.
1191
- DETAIL_PAGES=$(find src/ web/ -name "*DetailPage.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
1192
- if [ -n "$DETAIL_PAGES" ]; then
1193
- FAIL=false
1194
- for DP in $DETAIL_PAGES; do
1195
- # Check if the page has tabs (activeTab state)
1196
- HAS_TABS=$(grep -P "useState.*activeTab|setActiveTab" "$DP" 2>/dev/null)
1197
- if [ -z "$HAS_TABS" ]; then continue; fi
1198
-
1199
- # Check if any tab click handler calls navigate()
1200
- # Pattern: function that both references setActiveTab AND navigate()
1201
- # Look for navigate() calls inside handlers that also set tab state
1202
- 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 "//")
1203
- if [ -n "$TAB_NAVIGATE" ]; then
1204
- # Verify this navigate is in a tab handler context (near setActiveTab usage)
1205
- # Simple heuristic: if file has both setActiveTab AND navigate() to relative paths
1206
- RELATIVE_NAV=$(echo "$TAB_NAVIGATE" | grep -P "navigate\(['\"\`]\.\./" 2>/dev/null)
1207
- if [ -n "$RELATIVE_NAV" ]; then
1208
- echo "BLOCKING: Detail page tabs use navigate() instead of local content switching: $DP"
1209
- echo " Tab click handlers MUST only call setActiveTab() — render content inline"
1210
- echo " Found navigate() calls (likely in tab handlers):"
1211
- echo "$RELATIVE_NAV"
1212
- echo ""
1213
- echo " Fix: Remove navigate() from tab handlers. Render sub-resource content inline:"
1214
- echo " {activeTab === 'leaves' && <LeaveRequestsTable employeeId={entity.id} />}"
1215
- echo " See smartstack-frontend.md section 3 'Tab Behavior Rules' for the correct pattern."
1216
- FAIL=true
1217
- fi
1218
- fi
1219
- done
1220
- if [ "$FAIL" = true ]; then
1221
- exit 1
1222
- fi
1223
- fi
1224
- ```
1225
-
1226
- ### POST-CHECK 44: Migration ModelSnapshot must contain ALL entities registered in DbContext (BLOCKING)
1227
-
1228
- ```bash
1229
- # Root cause (test-apex-007): 7 entities registered in DbContext but migration only covered 3.
1230
- # Happens when migration is created ONCE in Layer 0 for the first batch, then additional entities
1231
- # are added in subsequent iterations without re-running migration.
1232
- SNAPSHOT=$(find src/ -name "*ModelSnapshot.cs" -path "*/Migrations/*" 2>/dev/null | head -1)
1233
- DBCONTEXT=$(find src/ -name "*DbContext.cs" -path "*/Persistence/*" ! -name "*DesignTime*" 2>/dev/null | head -1)
1234
- if [ -n "$SNAPSHOT" ] && [ -n "$DBCONTEXT" ]; then
1235
- # Extract DbSet entity names from DbContext (DbSet<EntityName>)
1236
- DBSET_ENTITIES=$(grep -oP 'DbSet<(\w+)>' "$DBCONTEXT" 2>/dev/null | grep -oP '<\K\w+(?=>)' | sort -u)
1237
- FAIL=false
1238
- for ENTITY in $DBSET_ENTITIES; do
1239
- # Skip base SmartStack entities (handled by core migrations)
1240
- if echo "$ENTITY" | grep -qP '^(Navigation|Tenant|User|Role|Permission|AuditLog|ApplicationTracking)'; then
1241
- continue
1242
- fi
1243
- # Check if the entity appears in ModelSnapshot (builder.Entity<EntityName>)
1244
- if ! grep -q "Entity<$ENTITY>" "$SNAPSHOT" 2>/dev/null; then
1245
- echo "BLOCKING: Entity '$ENTITY' is registered as DbSet in $DBCONTEXT but MISSING from ModelSnapshot"
1246
- echo " This means no migration was created for this entity — it will not exist in the database."
1247
- echo " Fix: Run 'dotnet ef migrations add' to include all new entities"
1248
- FAIL=true
1249
- fi
1250
- done
1251
- if [ "$FAIL" = true ]; then
1252
- echo ""
1253
- echo " Root cause: Migration was likely created once for the first batch of entities,"
1254
- echo " but additional entities were added later without regenerating the migration."
1255
- echo " Fix: Create a new migration that covers ALL missing entities."
1256
- exit 1
1257
- fi
1258
- fi
1259
- ```
1260
-
1261
- ### POST-CHECK 45: I18n namespace files must be registered in i18n config (BLOCKING)
1262
-
1263
- ```bash
1264
- # Root cause (test-apex-007): i18n JSON files existed in src/i18n/locales/ but were never
1265
- # registered in the i18n config (config.ts or index.ts). Pages calling useTranslation(['module'])
1266
- # got empty translations at runtime.
1267
- 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)
1268
- if [ -n "$I18N_CONFIG" ]; then
1269
- # Find all module JSON files in the primary language (fr)
1270
- 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)
1271
- if [ -n "$FR_FILES" ]; then
1272
- FAIL=false
1273
- for JSON_FILE in $FR_FILES; do
1274
- NS=$(basename "$JSON_FILE" .json)
1275
- # Check if namespace is referenced in config (import or resource key)
1276
- if ! grep -q "$NS" "$I18N_CONFIG" 2>/dev/null; then
1277
- echo "BLOCKING: i18n namespace '$NS' (from $JSON_FILE) is not registered in $I18N_CONFIG"
1278
- echo " Pages using useTranslation(['$NS']) will get empty translations at runtime"
1279
- echo " Fix: Add '$NS' to the resources/ns configuration in $I18N_CONFIG"
1280
- FAIL=true
1281
- fi
1282
- done
1283
- if [ "$FAIL" = true ]; then
1284
- exit 1
1285
- fi
1286
- fi
1287
- fi
1288
- ```
1289
-
1290
- ### POST-CHECK 46: FluentValidation validators must be registered via DI (BLOCKING)
1291
-
1292
- ```bash
1293
- # Root cause (test-apex-007): Validators existed but were never registered in DI.
1294
- # Without DI registration, [FromBody] DTOs are never validated — any data is accepted.
1295
- VALIDATOR_FILES=$(find src/ -name "*Validator.cs" -path "*/Validators/*" 2>/dev/null | grep -v test | grep -v Test)
1296
- if [ -n "$VALIDATOR_FILES" ]; then
1297
- # Check DI registration file exists
1298
- DI_FILE=$(find src/ -name "DependencyInjection.cs" -o -name "ServiceCollectionExtensions.cs" 2>/dev/null | grep -v test | head -1)
1299
- if [ -z "$DI_FILE" ]; then
1300
- echo "BLOCKING: Validators exist but no DependencyInjection.cs found for DI registration"
1301
- exit 1
1302
- fi
1303
- # Check for AddValidatorsFromAssembly or individual validator registration
1304
- HAS_ASSEMBLY_REG=$(grep -c "AddValidatorsFromAssembly\|AddValidatorsFromAssemblyContaining" "$DI_FILE" 2>/dev/null)
1305
- if [ "$HAS_ASSEMBLY_REG" -eq 0 ]; then
1306
- # Check individual registrations as fallback
1307
- VALIDATOR_COUNT=$(echo "$VALIDATOR_FILES" | wc -l)
1308
- REGISTERED_COUNT=0
1309
- for VF in $VALIDATOR_FILES; do
1310
- VN=$(basename "$VF" .cs)
1311
- if grep -q "$VN" "$DI_FILE" 2>/dev/null; then
1312
- REGISTERED_COUNT=$((REGISTERED_COUNT + 1))
1313
- fi
1314
- done
1315
- if [ "$REGISTERED_COUNT" -eq 0 ]; then
1316
- echo "BLOCKING: $VALIDATOR_COUNT validators exist but NONE are registered in DI ($DI_FILE)"
1317
- echo " Fix: Add 'services.AddValidatorsFromAssemblyContaining<Create{Entity}DtoValidator>();' to $DI_FILE"
1318
- echo " Or use 'services.AddValidatorsFromAssembly(typeof(Create{Entity}DtoValidator).Assembly);'"
1319
- exit 1
1320
- fi
1321
- fi
1322
- fi
1323
- ```
1324
-
1325
- ### POST-CHECK 47: Date/date properties in DTOs must use DateOnly, not string (BLOCKING)
1326
-
1327
- ```bash
1328
- # Root cause (test-apex-007): WorkLog DTO had Date property typed as string instead of DateOnly.
1329
- # This causes: invalid date parsing, no date validation, inconsistent formats across clients.
1330
- DTO_FILES=$(find src/ -name "*Dto.cs" -path "*/DTOs/*" 2>/dev/null)
1331
- if [ -n "$DTO_FILES" ]; then
1332
- FAIL=false
1333
- for f in $DTO_FILES; do
1334
- # Find string properties whose name contains "Date" (case-insensitive)
1335
- BAD_DATES=$(grep -Pn 'string\??\s+\w*[Dd]ate\w*\s*[{;,]' "$f" 2>/dev/null | grep -vi "Updated\|Created\|format\|pattern\|string\|parse")
1336
- if [ -n "$BAD_DATES" ]; then
1337
- echo "BLOCKING: DTO has string type for date field — must use DateOnly: $f"
1338
- echo "$BAD_DATES"
1339
- echo " Fix: Change 'string Date' to 'DateOnly Date' (or 'DateOnly? Date' if nullable)"
1340
- echo " DateOnly is the correct .NET type for date-only values (no time component)"
1341
- FAIL=true
1342
- fi
1343
- done
1344
- if [ "$FAIL" = true ]; then
1345
- exit 1
1346
- fi
1347
- fi
1348
- ```
1349
-
1350
- ### POST-CHECK 48: NavRoute attribute values must use kebab-case (BLOCKING)
1351
-
1352
- ```bash
1353
- # Root cause (test-apex-007): Controllers had [NavRoute("humanresources.employees")]
1354
- # instead of [NavRoute("human-resources.employees")]. This causes route mismatch with
1355
- # seed data and permission codes, resulting in 404s at runtime.
1356
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1357
- if [ -n "$CTRL_FILES" ]; then
1358
- FAIL=false
1359
- for f in $CTRL_FILES; do
1360
- NAVROUTE_VALS=$(grep -oP 'NavRoute\("([^"]+)"' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
1361
- for NR in $NAVROUTE_VALS; do
1362
- # Check each segment for concatenated multi-word without hyphens
1363
- SEGMENTS=$(echo "$NR" | tr '.' '\n')
1364
- for SEG in $SEGMENTS; do
1365
- # Detect segments that look like concatenated words (lowercase, 8+ chars, no hyphens)
1366
- # Use a simpler heuristic: lowercase-only segment with known multi-word patterns
1367
- if echo "$SEG" | grep -qP '^[a-z]{8,}$'; then
1368
- # Additional check: does it contain a known multi-word pattern?
1369
- if echo "$SEG" | grep -qP '(human|project|leave|client|support|email|time|work|resource)'; then
1370
- echo "BLOCKING: NavRoute segment '$SEG' in $f appears to be concatenated multi-word without hyphens"
1371
- echo " Full NavRoute: $NR"
1372
- echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources', 'projectmanagement' → 'project-management'"
1373
- FAIL=true
1374
- fi
1375
- fi
1376
- done
1377
- done
1378
- done
1379
- if [ "$FAIL" = true ]; then
1380
- exit 1
1381
- fi
1382
- fi
1383
- ```
1384
-
1385
- ### POST-CHECK 49: Every module with entities must have a migration covering them (BLOCKING)
1386
-
1387
- ```bash
1388
- # Complementary to POST-CHECK 44 — checks from the entity side.
1389
- # Finds entity .cs files in Domain/ and verifies they appear in at least one migration file.
1390
- ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null | grep -v test)
1391
- MIGRATION_DIR=$(find src/ -path "*/Migrations" -type d 2>/dev/null | head -1)
1392
- if [ -n "$ENTITY_FILES" ] && [ -n "$MIGRATION_DIR" ]; then
1393
- MIGRATION_FILES=$(find "$MIGRATION_DIR" -name "*.cs" ! -name "*ModelSnapshot*" ! -name "*DesignTime*" 2>/dev/null)
1394
- if [ -z "$MIGRATION_FILES" ]; then
1395
- echo "BLOCKING: Entity files exist in Domain/Entities but NO migration files found in $MIGRATION_DIR"
1396
- exit 1
1397
- fi
1398
- FAIL=false
1399
- for EF in $ENTITY_FILES; do
1400
- ENTITY_NAME=$(basename "$EF" .cs)
1401
- # Skip abstract base classes and interfaces
1402
- if grep -qP '^\s*(public\s+)?(abstract|interface)\s' "$EF" 2>/dev/null; then continue; fi
1403
- # Check if entity appears in any migration (CreateTable or AddColumn or entity reference)
1404
- FOUND=$(grep -l "$ENTITY_NAME" $MIGRATION_FILES 2>/dev/null)
1405
- if [ -z "$FOUND" ]; then
1406
- echo "BLOCKING: Entity '$ENTITY_NAME' ($EF) not found in any migration file"
1407
- echo " This entity will NOT have a database table."
1408
- echo " Fix: Run 'dotnet ef migrations add' to create a migration covering this entity"
1409
- FAIL=true
1410
- fi
1411
- done
1412
- if [ "$FAIL" = true ]; then
1413
- exit 1
1414
- fi
1415
- fi
1416
- ```
1417
-
1418
- ### POST-CHECK 50: Controllers must NOT have both [Route] and [NavRoute] attributes (BLOCKING)
1419
-
1420
- ```bash
1421
- # Root cause (test-apex-007): All 7 controllers had BOTH [Route("api/...")] and [NavRoute("...")].
1422
- # In SmartStack, [NavRoute] resolves routes dynamically from Navigation entities at startup.
1423
- # [Route] is standard ASP.NET Core static routing. When both exist:
1424
- # - NavRoute middleware tries to resolve from DB → fails if seed data not applied → no route
1425
- # - [Route] may or may not take over depending on middleware order
1426
- # - Result: 404 on ALL endpoints
1427
- # The MCP validate_conventions previously ENCOURAGED adding [Route] with [NavRoute] — this was a bug.
1428
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1429
- if [ -n "$CTRL_FILES" ]; then
1430
- FAIL=false
1431
- for f in $CTRL_FILES; do
1432
- HAS_NAVROUTE=$(grep -c '\[NavRoute(' "$f" 2>/dev/null)
1433
- HAS_ROUTE=$(grep -c '\[Route(' "$f" 2>/dev/null)
1434
- if [ "$HAS_NAVROUTE" -gt 0 ] && [ "$HAS_ROUTE" -gt 0 ]; then
1435
- NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"' "$f" 2>/dev/null | head -1)
1436
- ROUTE_VAL=$(grep -oP 'Route\("([^"]+)"' "$f" 2>/dev/null | head -1)
1437
- echo "BLOCKING: Controller has BOTH [Route] and [NavRoute] — remove [Route]: $f"
1438
- echo " Found: [$ROUTE_VAL] + [$NAVROUTE_VAL]"
1439
- echo " In SmartStack, [NavRoute] resolves routes dynamically from the database."
1440
- echo " Having [Route] alongside it causes route conflicts and 404s."
1441
- echo " Fix: Remove the [Route(...)] attribute, keep only [NavRoute(...)]"
1442
- FAIL=true
1443
- fi
1444
- done
1445
- if [ "$FAIL" = true ]; then
1446
- exit 1
1447
- fi
1448
- fi
1449
- ```
1450
-
1451
- ### POST-CHECK 51: RolesSeedData must map standard role-permission matrix (BLOCKING)
1452
-
1453
- ```bash
1454
- # SmartStack standard role-permission matrix:
1455
- # Admin = wildcard (*) — full access
1456
- # Manager = CRU (read + create + update) — no delete
1457
- # Contributor = CR (read + create) — no update, no delete
1458
- # Viewer = R (read only)
1459
- # If RolesSeedData deviates from this matrix, the RBAC model is broken.
1460
- ROLE_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" ! -name "ApplicationRolesSeedData.cs" 2>/dev/null)
1461
- if [ -n "$ROLE_SEED_FILES" ]; then
1462
- FAIL=false
1463
- for f in $ROLE_SEED_FILES; do
1464
- # Skip ApplicationRolesSeedData (defines roles, not mappings)
1465
- BASENAME=$(basename "$f")
1466
- if [ "$BASENAME" = "ApplicationRolesSeedData.cs" ]; then continue; fi
1467
-
1468
- # Check Admin has wildcard
1469
- HAS_ADMIN_WILDCARD=$(grep -Pc '(admin|Admin).*\*' "$f" 2>/dev/null)
1470
- if [ "$HAS_ADMIN_WILDCARD" -eq 0 ]; then
1471
- # Also accept .Access or wildcard pattern
1472
- HAS_ADMIN_ACCESS=$(grep -Pc '(admin|Admin).*(Access|Wildcard|IsWildcard)' "$f" 2>/dev/null)
1473
- if [ "$HAS_ADMIN_ACCESS" -eq 0 ]; then
1474
- echo "BLOCKING: Admin role missing wildcard (*) permission in $f"
1475
- echo "Fix: Admin must map to wildcard permission (navRoute.*) or use IsWildcard=true"
1476
- FAIL=true
1477
- fi
1478
- fi
1479
-
1480
- # Check Viewer has NO delete/create/update
1481
- VIEWER_WRITE=$(grep -Pc '(viewer|Viewer).*(\.delete|\.create|\.update|Delete|Create|Update)' "$f" 2>/dev/null)
1482
- if [ "$VIEWER_WRITE" -gt 0 ]; then
1483
- echo "BLOCKING: Viewer role has write permissions (create/update/delete) in $f"
1484
- echo "Fix: Viewer must only have read permission. Remove create/update/delete mappings."
1485
- FAIL=true
1486
- fi
1487
-
1488
- # Check Manager has NO delete
1489
- MANAGER_DELETE=$(grep -Pc '(manager|Manager).*(\.delete|Delete)' "$f" 2>/dev/null)
1490
- if [ "$MANAGER_DELETE" -gt 0 ]; then
1491
- echo "WARNING: Manager role has delete permission in $f"
1492
- echo "SmartStack standard: Manager = CRU (no delete). Verify this is intentional."
1493
- fi
1494
- done
1495
- if [ "$FAIL" = true ]; then
1496
- exit 1
1497
- fi
1498
- fi
1499
- ```
1500
-
1501
- ### POST-CHECK 52: PermissionAction enum must use valid typed values only (BLOCKING)
1502
-
1503
- ```bash
1504
- # Valid PermissionAction enum values: Access(0), Read(1), Create(2), Update(3), Delete(4),
1505
- # Export(5), Import(6), Approve(7), Reject(8), Assign(9), Execute(10)
1506
- # FORBIDDEN: Enum.Parse<PermissionAction>("...") — runtime crash if value doesn't exist
1507
- # FORBIDDEN: (PermissionAction)99 or any cast beyond 0-10
1508
- SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
1509
- if [ -n "$SEED_FILES" ]; then
1510
- FAIL=false
1511
- for f in $SEED_FILES; do
1512
- # Check for Enum.Parse<PermissionAction> usage
1513
- ENUM_PARSE=$(grep -Pn 'Enum\.Parse<PermissionAction>' "$f" 2>/dev/null)
1514
- if [ -n "$ENUM_PARSE" ]; then
1515
- echo "BLOCKING: Enum.Parse<PermissionAction> detected — runtime crash risk: $f"
1516
- echo "$ENUM_PARSE"
1517
- echo "Fix: Use typed enum directly: PermissionAction.Read (NOT Enum.Parse<PermissionAction>(\"Read\"))"
1518
- FAIL=true
1519
- fi
1520
-
1521
- # Check for invalid cast values (PermissionAction)N where N > 10
1522
- INVALID_CAST=$(grep -Pn '\(PermissionAction\)\s*([1-9]\d{1,}|[2-9]\d)' "$f" 2>/dev/null)
1523
- if [ -n "$INVALID_CAST" ]; then
1524
- echo "BLOCKING: Invalid PermissionAction cast detected (value > 10): $f"
1525
- echo "$INVALID_CAST"
1526
- echo "Valid values: Access(0), Read(1), Create(2), Update(3), Delete(4), Export(5), Import(6), Approve(7), Reject(8), Assign(9), Execute(10)"
1527
- FAIL=true
1528
- fi
1529
- done
1530
- if [ "$FAIL" = true ]; then
1531
- exit 1
1532
- fi
1533
- fi
1534
- ```
1535
-
1536
- ### POST-CHECK 53: Navigation translation completeness — 4 languages per level (BLOCKING)
1537
-
1538
- ```bash
1539
- # Every navigation seed data file must provide translations for ALL 4 languages (fr, en, it, de).
1540
- # If sections exist (GetSectionEntries), GetSectionTranslationEntries MUST also exist.
1541
- # If resources exist (GetResourceEntries), resource translation entries MUST also exist.
1542
- NAV_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" ! -name "*Application*" 2>/dev/null)
1543
- if [ -n "$NAV_SEED_FILES" ]; then
1544
- FAIL=false
1545
- for f in $NAV_SEED_FILES; do
1546
- # Check module translations have all 4 languages
1547
- LANG_COUNT=$(grep -c 'LanguageCode\s*=' "$f" 2>/dev/null)
1548
- HAS_FR=$(grep -c '"fr"' "$f" 2>/dev/null)
1549
- HAS_EN=$(grep -c '"en"' "$f" 2>/dev/null)
1550
- HAS_IT=$(grep -c '"it"' "$f" 2>/dev/null)
1551
- HAS_DE=$(grep -c '"de"' "$f" 2>/dev/null)
1552
-
1553
- if [ "$HAS_FR" -eq 0 ] || [ "$HAS_EN" -eq 0 ] || [ "$HAS_IT" -eq 0 ] || [ "$HAS_DE" -eq 0 ]; then
1554
- echo "BLOCKING: Missing language(s) in navigation translations: $f"
1555
- echo " fr=$HAS_FR, en=$HAS_EN, it=$HAS_IT, de=$HAS_DE (all must be > 0)"
1556
- echo "Fix: Add NavigationTranslationSeedEntry for all 4 languages (fr, en, it, de)"
1557
- FAIL=true
1558
- fi
1559
-
1560
- # If sections exist, section translations MUST exist
1561
- HAS_SECTION_ENTRIES=$(grep -c 'GetSectionEntries' "$f" 2>/dev/null)
1562
- HAS_SECTION_TRANSLATIONS=$(grep -c 'GetSectionTranslationEntries' "$f" 2>/dev/null)
1563
- if [ "$HAS_SECTION_ENTRIES" -gt 0 ] && [ "$HAS_SECTION_TRANSLATIONS" -eq 0 ]; then
1564
- echo "BLOCKING: Sections defined but GetSectionTranslationEntries() missing: $f"
1565
- echo "Fix: Add GetSectionTranslationEntries() with 4 languages per section (ref core-seed-data.md §2b)"
1566
- FAIL=true
1567
- fi
1568
-
1569
- # If resources exist, resource translations MUST exist
1570
- HAS_RESOURCE_ENTRIES=$(grep -c 'GetResourceEntries' "$f" 2>/dev/null)
1571
- HAS_RESOURCE_TRANSLATIONS=$(grep -Pc 'ResourceTranslation|GetResourceTranslation|NavigationEntityType\.Resource.*LanguageCode' "$f" 2>/dev/null)
1572
- if [ "$HAS_RESOURCE_ENTRIES" -gt 0 ] && [ "$HAS_RESOURCE_TRANSLATIONS" -eq 0 ]; then
1573
- echo "BLOCKING: Resources defined but resource translations missing: $f"
1574
- echo "Fix: Add resource translation entries with 4 languages per resource (ref core-seed-data.md §2b)"
1575
- FAIL=true
1576
- fi
1577
- done
1578
- if [ "$FAIL" = true ]; then
1579
- exit 1
1580
- fi
1581
- fi
1582
- ```
1583
-
1584
- **If ANY POST-CHECK fails → fix in step-03, re-validate.**
1
+ # BLOCKING POST-CHECKs
2
+
3
+ > **Referenced by:** step-04-examine.md (section 6b)
4
+ > These checks run on the actual generated files. Model-interpreted checks are unreliable.
5
+
6
+ ### POST-CHECK 1: Navigation routes must be full paths starting with /
7
+
8
+ ```bash
9
+ # Find all seed data files and check route values
10
+ SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
11
+ if [ -n "$SEED_FILES" ]; then
12
+ # Check for short routes (no leading /) in Create() calls for navigation entities
13
+ BAD_ROUTES=$(grep -Pn 'NavigationApplication\.Create\(|NavigationModule\.Create\(|NavigationSection\.Create\(|NavigationResource\.Create\(' $SEED_FILES | grep -v '"/[a-z]')
14
+ if [ -n "$BAD_ROUTES" ]; then
15
+ echo "BLOCKING: Navigation routes must be full paths starting with /"
16
+ echo "$BAD_ROUTES"
17
+ echo "Expected: \"/human-resources\" NOT \"humanresources\""
18
+ exit 1
19
+ fi
20
+ fi
21
+ ```
22
+
23
+ ### POST-CHECK 2: All services must filter by TenantId (OWASP A01)
24
+
25
+ ```bash
26
+ # Find all service implementation files
27
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
28
+ if [ -n "$SERVICE_FILES" ]; then
29
+ # Check each service file has TenantId reference (either _currentUser.TenantId or TenantId filter)
30
+ for f in $SERVICE_FILES; do
31
+ # Accept either _currentTenant.TenantId (strict guard clause or nullable usage)
32
+ # or entities with IOptionalTenantEntity/IScopedTenantEntity (optional tenant pattern)
33
+ HAS_TENANT_FILTER=$(grep -c "TenantId" "$f")
34
+ HAS_OPTIONAL_ENTITY=false
35
+ if grep -q "IOptionalTenantEntity\|IScopedTenantEntity" "$f"; then
36
+ HAS_OPTIONAL_ENTITY=true
37
+ fi
38
+
39
+ if [ "$HAS_TENANT_FILTER" -eq 0 ] && [ "$HAS_OPTIONAL_ENTITY" = false ]; then
40
+ echo "BLOCKING (OWASP A01): Service missing TenantId filter or optional tenant entity: $f"
41
+ echo "Every service query MUST filter by _currentTenant.TenantId"
42
+ echo "OR work with entities that implement IOptionalTenantEntity/IScopedTenantEntity"
43
+ exit 1
44
+ fi
45
+ if grep -q "Guid.Empty" "$f"; then
46
+ echo "BLOCKING (OWASP A01): Service uses Guid.Empty instead of _currentTenant.TenantId: $f"
47
+ exit 1
48
+ fi
49
+ done
50
+ fi
51
+ ```
52
+
53
+ ### POST-CHECK 3: Controllers must use [RequirePermission], not just [Authorize] (BLOCKING)
54
+
55
+ ```bash
56
+ # Find all controller files
57
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
58
+ if [ -n "$CTRL_FILES" ]; then
59
+ for f in $CTRL_FILES; do
60
+ # Check controller has at least one RequirePermission attribute
61
+ if grep -q "\[Authorize\]" "$f" && ! grep -q "\[RequirePermission" "$f"; then
62
+ echo "BLOCKING: Controller uses [Authorize] without [RequirePermission]: $f"
63
+ echo "[Authorize] alone provides NO RBAC enforcement — any authenticated user has access"
64
+ echo "Fix: Add [RequirePermission(Permissions.{Module}.{Action})] on each endpoint"
65
+ exit 1
66
+ fi
67
+ done
68
+ fi
69
+ ```
70
+
71
+ ### POST-CHECK 4: Seed data must not use Guid.NewGuid()
72
+
73
+ ```bash
74
+ SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
75
+ if [ -n "$SEED_FILES" ]; then
76
+ BAD_GUIDS=$(grep -n "Guid.NewGuid()" $SEED_FILES 2>/dev/null)
77
+ if [ -n "$BAD_GUIDS" ]; then
78
+ echo "BLOCKING: Seed data must use deterministic GUIDs (SHA256), not Guid.NewGuid()"
79
+ echo "$BAD_GUIDS"
80
+ exit 1
81
+ fi
82
+ fi
83
+ ```
84
+
85
+ ### POST-CHECK 5: Services must inject ICurrentTenantService (tenant isolation)
86
+
87
+ ```bash
88
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
89
+ if [ -n "$SERVICE_FILES" ]; then
90
+ for f in $SERVICE_FILES; do
91
+ # Accept either ICurrentTenantService or ICurrentUser (legacy) for tenant context
92
+ if ! grep -qE "ICurrentTenantService|ICurrentUser" "$f"; then
93
+ echo "BLOCKING: Service missing tenant context injection: $f"
94
+ echo "All services MUST inject ICurrentTenantService for tenant isolation"
95
+ echo "Pattern: private readonly ICurrentTenantService _currentTenant;"
96
+ exit 1
97
+ fi
98
+ done
99
+ fi
100
+ ```
101
+
102
+ ### POST-CHECK 6: Translation files must exist for all 4 languages (if frontend)
103
+
104
+ ```bash
105
+ # Find all i18n namespaces used in tsx files
106
+ TSX_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
107
+ if [ -n "$TSX_FILES" ]; then
108
+ NAMESPACES=$(grep -ohP "useTranslation\(\[?'([^']+)" $TSX_FILES | sed "s/.*'//" | sort -u)
109
+ for NS in $NAMESPACES; do
110
+ for LANG in fr en it de; do
111
+ if [ ! -f "src/i18n/locales/$LANG/$NS.json" ]; then
112
+ echo "BLOCKING: Missing translation file: src/i18n/locales/$LANG/$NS.json"
113
+ exit 1
114
+ fi
115
+ done
116
+ done
117
+ fi
118
+ ```
119
+
120
+ ### POST-CHECK 7: Pages must use lazy loading (no static page imports in routes)
121
+
122
+ ```bash
123
+ ROUTE_FILES=$(find src/routes/ -name "*.tsx" -o -name "*.ts" 2>/dev/null)
124
+ if [ -n "$ROUTE_FILES" ]; then
125
+ STATIC_PAGE_IMPORTS=$(grep -Pn "^import .+ from '@/pages/" $ROUTE_FILES 2>/dev/null)
126
+ if [ -n "$STATIC_PAGE_IMPORTS" ]; then
127
+ echo "BLOCKING: Route files must use React.lazy() for page imports, not static imports"
128
+ echo "$STATIC_PAGE_IMPORTS"
129
+ echo "Fix: const Page = lazy(() => import('@/pages/...').then(m => ({ default: m.Page })));"
130
+ exit 1
131
+ fi
132
+ fi
133
+ ```
134
+
135
+ ### POST-CHECK 8: Forms must be full pages with routes — ZERO modals/popups/drawers/slide-overs
136
+
137
+ ```bash
138
+ # Check for modal/dialog/drawer/slide-over imports AND inline form state in page files
139
+ PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
140
+ if [ -n "$PAGE_FILES" ]; then
141
+ FAIL=false
142
+
143
+ # 8a. Component imports (Modal, Dialog, Drawer, etc.)
144
+ MODAL_IMPORTS=$(grep -Pn "import.*(?:Modal|Dialog|Drawer|Popup|Sheet|SlideOver|Overlay)" $PAGE_FILES 2>/dev/null)
145
+ if [ -n "$MODAL_IMPORTS" ]; then
146
+ echo "BLOCKING: Form pages must NOT use Modal/Dialog/Drawer/Popup/SlideOver components"
147
+ echo "$MODAL_IMPORTS"
148
+ FAIL=true
149
+ fi
150
+
151
+ # 8b. Inline form state (catches drawers/slide-overs built without external components)
152
+ FORM_STATE=$(grep -Pn "useState.*(?:isOpen|showModal|showDialog|showCreate|showEdit|showForm|isCreating|isEditing|showDrawer|showPanel|showSlideOver|selectedEntity|editingEntity)" $PAGE_FILES 2>/dev/null)
153
+ if [ -n "$FORM_STATE" ]; then
154
+ echo "BLOCKING: Inline form state detected — forms embedded in ListPage as drawers/panels"
155
+ echo "Create/Edit forms MUST be separate page components with their own URL routes"
156
+ echo "$FORM_STATE"
157
+ FAIL=true
158
+ fi
159
+
160
+ if [ "$FAIL" = true ]; then
161
+ echo ""
162
+ echo "Fix: Create EntityCreatePage.tsx with route /create and EntityEditPage.tsx with route /:id/edit"
163
+ echo "NEVER embed create/edit forms as inline drawers, panels, or slide-overs in ListPage"
164
+ exit 1
165
+ fi
166
+ fi
167
+ ```
168
+
169
+ ### POST-CHECK 9: Create/Edit pages must exist as separate route pages (BLOCKING)
170
+
171
+ ```bash
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
174
+ LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test)
175
+ FAIL=false
176
+ if [ -n "$LIST_PAGES" ]; then
177
+ for LIST_PAGE in $LIST_PAGES; do
178
+ PAGE_DIR=$(dirname "$LIST_PAGE")
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
+
185
+ # Check for create page
186
+ CREATE_PAGE=$(find "$PAGE_DIR" -name "*CreatePage.tsx" 2>/dev/null)
187
+ if [ -z "$CREATE_PAGE" ]; then
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
196
+ fi
197
+
198
+ # Check for edit page
199
+ EDIT_PAGE=$(find "$PAGE_DIR" -name "*EditPage.tsx" 2>/dev/null)
200
+ if [ -z "$EDIT_PAGE" ]; then
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
209
+ fi
210
+ done
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
220
+ ```
221
+
222
+ ### POST-CHECK 10: Form pages must have companion test files
223
+
224
+ ```bash
225
+ # Minimum requirement: if frontend pages exist, at least 1 test file must be present
226
+ PAGE_FILES=$(find src/pages/ -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null)
227
+ if [ -n "$PAGE_FILES" ]; then
228
+ ALL_TESTS=$(find src/pages/ -name "*.test.tsx" 2>/dev/null)
229
+ if [ -z "$ALL_TESTS" ]; then
230
+ echo "BLOCKING: No frontend test files found in src/pages/"
231
+ echo "Every form page MUST have a companion .test.tsx file"
232
+ exit 1
233
+ fi
234
+ fi
235
+
236
+ # Every CreatePage and EditPage must have a .test.tsx file
237
+ FORM_PAGES=$(find src/pages/ -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test)
238
+ if [ -n "$FORM_PAGES" ]; then
239
+ for FORM_PAGE in $FORM_PAGES; do
240
+ TEST_FILE="${FORM_PAGE%.tsx}.test.tsx"
241
+ if [ ! -f "$TEST_FILE" ]; then
242
+ echo "BLOCKING: Form page missing test file: $FORM_PAGE"
243
+ echo "Expected: $TEST_FILE"
244
+ echo "All form pages MUST have companion test files (rendering, validation, submit, navigation)"
245
+ exit 1
246
+ fi
247
+ done
248
+ fi
249
+ ```
250
+
251
+ ### POST-CHECK 11: FK fields must use EntityLookup — NO `<input>`, NO `<select>` (BLOCKING)
252
+
253
+ ```bash
254
+ # Check ALL page files for FK fields rendered as <input> or <select> instead of EntityLookup
255
+ # Scans ALL .tsx files (not just CreatePage/EditPage — forms may be embedded in ListPage drawers)
256
+ ALL_PAGES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
257
+ if [ -n "$ALL_PAGES" ]; then
258
+ FAIL=false
259
+
260
+ # 1. Detect <input> with name/value binding to FK fields (fields ending in "Id")
261
+ FK_INPUTS=$(grep -Pn '<input[^>]*(?:name|value)=["\x27{][^>]*[a-zA-Z]Id["\x27}]' $ALL_PAGES 2>/dev/null | grep -Pv 'type=["\x27]hidden["\x27]')
262
+ if [ -n "$FK_INPUTS" ]; then
263
+ echo "BLOCKING: FK fields rendered as <input> — MUST use EntityLookup component"
264
+ echo "$FK_INPUTS"
265
+ FAIL=true
266
+ fi
267
+
268
+ # 2. Detect <select> with value binding to FK fields (e.g., value={formData.departmentId})
269
+ FK_SELECTS=$(grep -Pn '<select[^>]*value=\{[^}]*[a-zA-Z]Id\b' $ALL_PAGES 2>/dev/null)
270
+ if [ -n "$FK_SELECTS" ]; then
271
+ echo "BLOCKING: FK fields rendered as <select> dropdown — MUST use EntityLookup component"
272
+ echo "A <select> loaded from API state is NOT a valid substitute for EntityLookup."
273
+ echo "EntityLookup provides: debounced search, paginated results, display name resolution."
274
+ echo "$FK_SELECTS"
275
+ FAIL=true
276
+ fi
277
+
278
+ # 3. Detect onChange handlers setting FK fields from <select> (e.g., setFormData({...formData, departmentId: e.target.value}))
279
+ FK_SELECT_ONCHANGE=$(grep -Pn 'onChange=.*[a-zA-Z]Id[^a-zA-Z].*e\.target\.value' $ALL_PAGES 2>/dev/null)
280
+ if [ -n "$FK_SELECT_ONCHANGE" ]; then
281
+ echo "BLOCKING: FK field set via e.target.value (select/input pattern) — use EntityLookup onChange(id)"
282
+ echo "$FK_SELECT_ONCHANGE"
283
+ FAIL=true
284
+ fi
285
+
286
+ # 4. Check for placeholders mentioning "ID", "GUID", or "Select..." for FK fields
287
+ FK_PLACEHOLDER=$(grep -Pn 'placeholder=["\x27].*(?:[Ee]nter.*[Ii][Dd]|[Gg][Uu][Ii][Dd]|[Ss]elect.*[Ee]mployee|[Ss]elect.*[Dd]epartment|[Ss]elect.*[Pp]osition|[Ss]elect.*[Pp]roject|[Ss]elect.*[Cc]ategory)' $ALL_PAGES 2>/dev/null)
288
+ if [ -n "$FK_PLACEHOLDER" ]; then
289
+ echo "BLOCKING: Form has placeholder for FK field selection — use EntityLookup search instead"
290
+ echo "$FK_PLACEHOLDER"
291
+ FAIL=true
292
+ fi
293
+
294
+ # 5. Detect <option> elements with GUID-like values (sign of FK <select>)
295
+ FK_OPTIONS=$(grep -Pn '<option[^>]*value=\{[^}]*\.id\}' $ALL_PAGES 2>/dev/null)
296
+ if [ -n "$FK_OPTIONS" ]; then
297
+ echo "BLOCKING: <option> with entity .id value detected — this is a FK <select> anti-pattern"
298
+ echo "Replace the entire <select>/<option> block with <EntityLookup />"
299
+ echo "$FK_OPTIONS"
300
+ FAIL=true
301
+ fi
302
+
303
+ if [ "$FAIL" = true ]; then
304
+ echo ""
305
+ echo "Fix: Replace ALL FK fields with <EntityLookup /> from @/components/ui/EntityLookup"
306
+ echo "See smartstack-frontend.md section 6 for the EntityLookup pattern"
307
+ echo "FORBIDDEN for FK Guid fields: <input>, <select>, <option>, e.target.value"
308
+ echo "REQUIRED: <EntityLookup apiEndpoint={...} mapOption={...} value={...} onChange={...} />"
309
+ exit 1
310
+ fi
311
+ fi
312
+ ```
313
+
314
+ ### POST-CHECK 12: Backend APIs must support search parameter for EntityLookup
315
+
316
+ ```bash
317
+ # Check that controller GetAll methods accept search parameter
318
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
319
+ if [ -n "$CTRL_FILES" ]; then
320
+ for f in $CTRL_FILES; do
321
+ # Check if controller has GetAll but no search parameter
322
+ if grep -q "\[HttpGet\]" "$f" && grep -q "GetAll" "$f"; then
323
+ if ! grep -q "search" "$f"; then
324
+ echo "WARNING: Controller missing search parameter on GetAll: $f"
325
+ echo "GetAll endpoints MUST accept ?search= to enable EntityLookup on frontend"
326
+ echo "Fix: Add [FromQuery] string? search parameter to GetAll action"
327
+ fi
328
+ fi
329
+ done
330
+ fi
331
+ ```
332
+
333
+ ### POST-CHECK 13: No hardcoded Tailwind colors in generated pages (BLOCKING)
334
+
335
+ ```bash
336
+ # Scan all page and component files directly (works for uncommitted/untracked files, Windows/WSL compatible)
337
+ ALL_PAGES=$(find src/pages/ src/components/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
338
+ if [ -n "$ALL_PAGES" ]; then
339
+ HARDCODED=$(grep -Pn '(bg|text|border)-(?!\[)(red|blue|green|gray|white|black|slate|zinc|neutral|stone)-' $ALL_PAGES 2>/dev/null)
340
+ if [ -n "$HARDCODED" ]; then
341
+ echo "BLOCKING: Pages MUST use CSS variables instead of hardcoded Tailwind colors"
342
+ echo "SmartStack uses a theme system — hardcoded colors break dark mode and custom themes"
343
+ echo ""
344
+ echo "Fix mapping:"
345
+ echo " bg-white → bg-[var(--bg-card)]"
346
+ echo " bg-gray-50 → bg-[var(--bg-primary)]"
347
+ echo " text-gray-900 → text-[var(--text-primary)]"
348
+ echo " text-gray-500 → text-[var(--text-secondary)]"
349
+ echo " border-gray-200 → border-[var(--border-color)]"
350
+ echo " bg-blue-600 → bg-[var(--color-accent-500)]"
351
+ echo " text-blue-600 → text-[var(--color-accent-500)]"
352
+ echo " text-red-500 → text-[var(--error-text)]"
353
+ echo " bg-green-500 → bg-[var(--success-bg)]"
354
+ echo ""
355
+ echo "See references/smartstack-frontend.md section 4 for full variable reference"
356
+ echo ""
357
+ echo "$HARDCODED"
358
+ exit 1
359
+ fi
360
+ fi
361
+ ```
362
+
363
+ ### POST-CHECK 14: Routes seed data must match frontend application routes
364
+
365
+ ```bash
366
+ SEED_ROUTES=$(grep -Poh 'Route\s*=\s*"([^"]+)"' $(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" 2>/dev/null) 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
367
+ APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
368
+ if [ -n "$APP_TSX" ] && [ -n "$SEED_ROUTES" ]; then
369
+ FRONTEND_PATHS=$(grep -oP "path:\s*'([^']+)'" "$APP_TSX" | grep -oP "'[^']+'" | tr -d "'" | sort -u)
370
+ if [ -n "$FRONTEND_PATHS" ]; then
371
+ MISMATCH_FOUND=false
372
+ for SEED_ROUTE in $SEED_ROUTES; do
373
+ DEPTH=$(echo "$SEED_ROUTE" | tr '/' '\n' | grep -c '.')
374
+ if [ "$DEPTH" -lt 3 ]; then continue; fi
375
+ SEED_SUFFIX=$(echo "$SEED_ROUTE" | sed 's|^/[^/]*/||')
376
+ SEED_NORM=$(echo "$SEED_SUFFIX" | tr '[:upper:]' '[:lower:]' | tr -d '-')
377
+ MATCH_FOUND=false
378
+ for FE_PATH in $FRONTEND_PATHS; do
379
+ # Flag FORBIDDEN /list suffix BEFORE normalization
380
+ if echo "$FE_PATH" | grep -qP '/list$'; then
381
+ echo "WARNING: Frontend route ends with /list — should use index route instead: $FE_PATH"
382
+ fi
383
+ FE_BASE=$(echo "$FE_PATH" | sed 's|/list$||;s|/new$||;s|/:id.*||;s|/create$||')
384
+ FE_NORM=$(echo "$FE_BASE" | tr '[:upper:]' '[:lower:]' | tr -d '-')
385
+ if [ "$SEED_NORM" = "$FE_NORM" ]; then
386
+ MATCH_FOUND=true
387
+ break
388
+ fi
389
+ done
390
+ if [ "$MATCH_FOUND" = false ]; then
391
+ echo "BLOCKING: Seed data route has no matching frontend route: $SEED_ROUTE"
392
+ MISMATCH_FOUND=true
393
+ fi
394
+ done
395
+ if [ "$MISMATCH_FOUND" = true ]; then
396
+ echo "Fix: Ensure every NavigationSeedData route has a corresponding route entry in App.tsx (applicationRoutes or JSX Route wrappers)"
397
+ exit 1
398
+ fi
399
+ fi
400
+ fi
401
+ ```
402
+
403
+ ### POST-CHECK 14b: Frontend routes must use kebab-case (BLOCKING)
404
+
405
+ ```bash
406
+ # POST-CHECK 14 normalizes hyphens for existence check, but does NOT catch kebab-case mismatches.
407
+ # This supplementary check detects concatenated multi-word route segments without hyphens.
408
+ APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
409
+ if [ -n "$APP_TSX" ]; then
410
+ # Extract route path strings from App.tsx
411
+ FE_PATHS=$(grep -oP "path:\s*['\"]([^'\"]+)['\"]" "$APP_TSX" | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"')
412
+ for FE_PATH in $FE_PATHS; do
413
+ # Split path by / and check each segment
414
+ for SEG in $(echo "$FE_PATH" | tr '/' '\n'); do
415
+ # Skip dynamic segments (:id, :slug) and single words (< 10 chars likely single word)
416
+ echo "$SEG" | grep -qP '^:' && continue
417
+ # Detect multi-word segments without hyphens: 2+ consecutive lowercase sequences
418
+ # e.g., "humanresources" (human+resources), "timemanagement" (time+management)
419
+ if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
420
+ # Potential concatenated multi-word — cross-check with seed data
421
+ SEED_MATCH=$(echo "$SEED_ROUTES" | tr '/' '\n' | grep -P "^[a-z]+-[a-z]+" | tr -d '-' | grep -x "$SEG")
422
+ if [ -n "$SEED_MATCH" ]; then
423
+ echo "BLOCKING: Frontend route segment '$SEG' appears to be missing hyphens"
424
+ echo "Seed data uses kebab-case (e.g., 'human-resources') but frontend has '$SEG'"
425
+ echo "Fix: Use kebab-case in App.tsx route paths to match seed data exactly"
426
+ exit 1
427
+ fi
428
+ fi
429
+ done
430
+ done
431
+ fi
432
+ ```
433
+
434
+ ### POST-CHECK 15: HasQueryFilter must not use Guid.Empty (OWASP A01)
435
+
436
+ ```bash
437
+ CONFIG_FILES=$(find src/ -path "*/Configurations/*" -name "*Configuration.cs" 2>/dev/null)
438
+ if [ -n "$CONFIG_FILES" ]; then
439
+ BAD_FILTERS=$(grep -Pn 'HasQueryFilter.*Guid\.Empty' $CONFIG_FILES 2>/dev/null)
440
+ if [ -n "$BAD_FILTERS" ]; then
441
+ echo "BLOCKING (OWASP A01): HasQueryFilter uses Guid.Empty instead of runtime tenant isolation"
442
+ echo "$BAD_FILTERS"
443
+ echo ""
444
+ echo "Anti-pattern: .HasQueryFilter(e => e.TenantId != Guid.Empty)"
445
+ echo "Fix: Remove HasQueryFilter. Tenant isolation is handled by SmartStack base DbContext"
446
+ exit 1
447
+ fi
448
+ fi
449
+ ```
450
+
451
+ ### POST-CHECK 16: GetAll methods must return PaginatedResult<T>
452
+
453
+ ```bash
454
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
455
+ if [ -n "$SERVICE_FILES" ]; then
456
+ BAD_RETURNS=$(grep -Pn '(Task<\s*(?:List|IEnumerable|IList|ICollection|IReadOnlyList|IReadOnlyCollection)<).*GetAll' $SERVICE_FILES 2>/dev/null)
457
+ if [ -n "$BAD_RETURNS" ]; then
458
+ echo "BLOCKING: GetAll methods must return PaginatedResult<T>, not List/IEnumerable"
459
+ echo "$BAD_RETURNS"
460
+ echo "Fix: Change return type to Task<PaginatedResult<{Entity}ResponseDto>>"
461
+ exit 1
462
+ fi
463
+ fi
464
+ ```
465
+
466
+ ### POST-CHECK 17: i18n files must contain required structural keys
467
+
468
+ ```bash
469
+ I18N_DIR="src/i18n/locales/fr"
470
+ if [ -d "$I18N_DIR" ]; then
471
+ REQUIRED_KEYS="actions columns empty errors form labels messages validation"
472
+ for JSON_FILE in "$I18N_DIR"/*.json; do
473
+ [ ! -f "$JSON_FILE" ] && continue
474
+ BASENAME=$(basename "$JSON_FILE")
475
+ case "$BASENAME" in common.json|navigation.json) continue;; esac
476
+ for KEY in $REQUIRED_KEYS; do
477
+ if ! jq -e "has(\"$KEY\")" "$JSON_FILE" > /dev/null 2>&1; then
478
+ echo "BLOCKING: i18n file missing required key '$KEY': $JSON_FILE"
479
+ echo "Module i18n files MUST contain: $REQUIRED_KEYS"
480
+ exit 1
481
+ fi
482
+ done
483
+ done
484
+ fi
485
+ ```
486
+
487
+ ### POST-CHECK 18: Entities must implement IAuditableEntity + Validators must have Create/Update pairs
488
+
489
+ ```bash
490
+ ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null)
491
+ if [ -n "$ENTITY_FILES" ]; then
492
+ for f in $ENTITY_FILES; do
493
+ if grep -q "ITenantEntity" "$f" && ! grep -q "IAuditableEntity" "$f"; then
494
+ echo "BLOCKING: Entity implements ITenantEntity but NOT IAuditableEntity: $f"
495
+ echo "Pattern: public class Entity : BaseEntity, ITenantEntity, IAuditableEntity"
496
+ exit 1
497
+ fi
498
+ done
499
+ fi
500
+ CREATE_VALIDATORS=$(find src/ -path "*/Validators/*" -name "Create*Validator.cs" 2>/dev/null)
501
+ if [ -n "$CREATE_VALIDATORS" ]; then
502
+ for f in $CREATE_VALIDATORS; do
503
+ VALIDATOR_DIR=$(dirname "$f")
504
+ ENTITY_NAME=$(basename "$f" | sed 's/^Create\(.*\)Validator\.cs$/\1/')
505
+ if [ ! -f "$VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs" ]; then
506
+ echo "BLOCKING: Create${ENTITY_NAME}Validator exists but Update${ENTITY_NAME}Validator is missing"
507
+ echo " Found: $f"
508
+ echo " Expected: $VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs"
509
+ exit 1
510
+ fi
511
+ done
512
+ fi
513
+ ```
514
+
515
+ ### POST-CHECK 19: (REMOVED — Context level no longer exists in SmartStack navigation hierarchy)
516
+
517
+ ### POST-CHECK 20: RolePermission seed data must NOT use deterministic role GUIDs
518
+
519
+ ```bash
520
+ # System roles (admin, manager, contributor, viewer) are pre-seeded by SmartStack core.
521
+ # RolePermission mappings MUST look up roles by Code at runtime, NEVER use deterministic GUIDs.
522
+ SEED_ALL_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
523
+ SEED_CONST_FILES=$(find src/ -path "*/Seeding/*" -name "SeedConstants.cs" 2>/dev/null)
524
+ if [ -n "$SEED_ALL_FILES" ]; then
525
+ BAD_ROLE_GUID=$(grep -Pn 'DeterministicGuid\("role:' $SEED_ALL_FILES $SEED_CONST_FILES 2>/dev/null)
526
+ if [ -n "$BAD_ROLE_GUID" ]; then
527
+ echo "BLOCKING: Deterministic GUID for role detected (e.g., DeterministicGuid(\"role:admin\"))"
528
+ echo "System roles are pre-seeded by SmartStack core with their own IDs"
529
+ echo "Fix: In SeedRolePermissionsAsync(), look up roles by Code:"
530
+ echo " var roles = await context.Roles.Where(r => r.IsSystem || r.ApplicationId != null).ToListAsync(ct);"
531
+ echo " var role = roles.FirstOrDefault(r => r.Code == mapping.RoleCode);"
532
+ echo "$BAD_ROLE_GUID"
533
+ exit 1
534
+ fi
535
+ fi
536
+ # Also check for GenerateRoleGuid usage in RolePermission mapping files (not in ApplicationRolesSeedData itself)
537
+ ROLE_PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" 2>/dev/null)
538
+ if [ -n "$ROLE_PERM_FILES" ]; then
539
+ BAD_ROLE_REF=$(grep -Pn 'GenerateRoleGuid|GetAdminRoleId|GetManagerRoleId|GetViewerRoleId|GetContributorRoleId' $ROLE_PERM_FILES 2>/dev/null)
540
+ if [ -n "$BAD_ROLE_REF" ]; then
541
+ echo "WARNING: RolesSeedData uses hardcoded role GUID helpers instead of Code-based lookup"
542
+ echo "Fix: Use RoleCode string (e.g., 'admin') and resolve in SeedRolePermissionsAsync()"
543
+ echo "$BAD_ROLE_REF"
544
+ fi
545
+ fi
546
+ ```
547
+
548
+ ### POST-CHECK 21: Services must NOT use TenantId!.Value (null-forgiving crash pattern)
549
+
550
+ ```bash
551
+ # The !.Value pattern on Guid? throws InvalidOperationException (500) instead of clean 401
552
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
553
+ if [ -n "$SERVICE_FILES" ]; then
554
+ BAD_PATTERN=$(grep -Pn 'TenantId!\s*\.Value|TenantId!\s*\.ToString|\.TenantId!' $SERVICE_FILES 2>/dev/null)
555
+ if [ -n "$BAD_PATTERN" ]; then
556
+ echo "BLOCKING: Services use TenantId!.Value — causes 500 instead of 400 when tenant context is missing"
557
+ echo "$BAD_PATTERN"
558
+ echo ""
559
+ echo "Fix: Replace with guard clause at the start of every method:"
560
+ echo " var tenantId = _currentTenant.TenantId"
561
+ echo " ?? throw new TenantContextRequiredException();"
562
+ echo ""
563
+ echo "This produces a clean 400 Bad Request via GlobalExceptionHandlerMiddleware."
564
+ echo "NEVER use UnauthorizedAccessException for tenant context — it returns 401 which clears the frontend token."
565
+ exit 1
566
+ fi
567
+ fi
568
+
569
+ # POST-CHECK: Services must NOT use UnauthorizedAccessException for tenant context (causes token clearing)
570
+ if [ -n "$SERVICE_FILES" ]; then
571
+ BAD_UNAUTH=$(grep -Pn 'UnauthorizedAccessException.*[Tt]enant' $SERVICE_FILES 2>/dev/null)
572
+ if [ -n "$BAD_UNAUTH" ]; then
573
+ echo "BLOCKING: Services use UnauthorizedAccessException for tenant context — causes 401 which clears the frontend token"
574
+ echo "$BAD_UNAUTH"
575
+ echo ""
576
+ echo "Fix: Replace with:"
577
+ echo " var tenantId = _currentTenant.TenantId"
578
+ echo " ?? throw new TenantContextRequiredException();"
579
+ echo ""
580
+ echo "TenantContextRequiredException returns 400 Bad Request (does not clear token)."
581
+ echo "UnauthorizedAccessException returns 401 Unauthorized (clears token + redirects to login)."
582
+ exit 1
583
+ fi
584
+ fi
585
+ ```
586
+
587
+ ### POST-CHECK 22: Cross-tenant entities must use Guid? TenantId
588
+
589
+ ```bash
590
+ for entity in $(find src/ -path "*/Domain/*" -name "*.cs" ! -name "I*.cs" 2>/dev/null); do
591
+ if grep -q "IOptionalTenantEntity\|IScopedTenantEntity" "$entity"; then
592
+ if grep -q "public Guid TenantId" "$entity" && ! grep -q "public Guid? TenantId" "$entity"; then
593
+ echo "BLOCKING: Entity with IOptionalTenantEntity/IScopedTenantEntity must use Guid? TenantId (nullable)"
594
+ exit 1
595
+ fi
596
+ fi
597
+ done
598
+ echo "POST-CHECK 22: OK"
599
+ ```
600
+
601
+ ### POST-CHECK 23: Scoped entities must have EntityScope property
602
+
603
+ ```bash
604
+ for entity in $(find src/ -path "*/Domain/*" -name "*.cs" ! -name "I*.cs" 2>/dev/null); do
605
+ if grep -q "IScopedTenantEntity" "$entity"; then
606
+ if ! grep -q "EntityScope\|Scope" "$entity"; then
607
+ echo "BLOCKING: Entity with IScopedTenantEntity must have EntityScope Scope property"
608
+ exit 1
609
+ fi
610
+ fi
611
+ done
612
+ echo "POST-CHECK 23: OK"
613
+ ```
614
+
615
+ ### POST-CHECK 24: Permissions.cs static constants must exist (BLOCKING)
616
+
617
+ ```bash
618
+ # Every module with controllers MUST have a Permissions.cs with static constants
619
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
620
+ if [ -n "$CTRL_FILES" ]; then
621
+ PERM_REFS=$(grep -ohP 'Permissions\.\w+\.\w+' $CTRL_FILES 2>/dev/null | sed 's/Permissions\.\([^.]*\)\..*/\1/' | sort -u)
622
+ for MODULE in $PERM_REFS; do
623
+ PERM_FILE=$(find src/ -name "Permissions.cs" -exec grep -l "static class $MODULE" {} \; 2>/dev/null)
624
+ if [ -z "$PERM_FILE" ]; then
625
+ echo "BLOCKING: Controller references Permissions.${MODULE}.* but no Permissions.cs defines static class ${MODULE}"
626
+ echo "Fix: Create Application/Authorization/Permissions.cs with: public static class ${MODULE} { public const string Read = \"...\"; ... }"
627
+ exit 1
628
+ fi
629
+ done
630
+ fi
631
+ ```
632
+
633
+ ### POST-CHECK 25: ApplicationRolesSeedData.cs must exist (BLOCKING)
634
+
635
+ ```bash
636
+ # If any RolesSeedData exists, ApplicationRolesSeedData MUST also exist
637
+ ROLE_SEED=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" 2>/dev/null | head -1)
638
+ if [ -n "$ROLE_SEED" ]; then
639
+ APP_ROLE_SEED=$(find src/ -path "*/Seeding/Data/ApplicationRolesSeedData.cs" 2>/dev/null | head -1)
640
+ if [ -z "$APP_ROLE_SEED" ]; then
641
+ echo "BLOCKING: RolesSeedData exists but ApplicationRolesSeedData.cs NOT FOUND"
642
+ echo "ApplicationRolesSeedData defines the 4 application-scoped roles (admin, manager, contributor, viewer)"
643
+ echo "Without it, SeedRolesAsync() has no role entries to create → RBAC broken"
644
+ echo "Fix: Create src/Infrastructure/Persistence/Seeding/Data/ApplicationRolesSeedData.cs"
645
+ exit 1
646
+ fi
647
+ fi
648
+ ```
649
+
650
+ ### POST-CHECK 25b: Section route completeness (NavigationSection → frontend route + permissions)
651
+
652
+ ```bash
653
+ # Every NavigationSection seed data route MUST have a corresponding frontend route in App.tsx
654
+ # and section-level permissions MUST exist for each section defined in seed data
655
+ SECTION_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSectionSeedData.cs" 2>/dev/null)
656
+ APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
657
+ if [ -n "$SECTION_SEED_FILES" ] && [ -n "$APP_TSX" ]; then
658
+ # Extract section routes from seed data
659
+ SECTION_ROUTES=$(grep -Poh '"/[a-z][a-z0-9/-]+"' $SECTION_SEED_FILES 2>/dev/null | tr -d '"' | sort -u)
660
+ for SECTION_ROUTE in $SECTION_ROUTES; do
661
+ # Extract the last segment (section-kebab) for frontend route matching
662
+ SECTION_SEG=$(echo "$SECTION_ROUTE" | rev | cut -d'/' -f1 | rev)
663
+ if ! grep -q "'$SECTION_SEG'" "$APP_TSX" && ! grep -q "\"$SECTION_SEG\"" "$APP_TSX"; then
664
+ echo "BLOCKING: NavigationSection seed data route has no matching frontend route: $SECTION_ROUTE"
665
+ echo "Expected path segment '$SECTION_SEG' in App.tsx application route block"
666
+ echo "Fix: Add section child routes to the module's children array in App.tsx"
667
+ fi
668
+ done
669
+ fi
670
+
671
+ # Controllers with section-level [NavRoute] (4 segments) must have matching [RequirePermission]
672
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
673
+ if [ -n "$CTRL_FILES" ]; then
674
+ for f in $CTRL_FILES; do
675
+ # Match NavRoute with 4 dot-separated segments (section-level)
676
+ SECTION_NAVROUTE=$(grep -oP 'NavRoute\("[a-z]+\.[a-z]+\.[a-z]+\.[a-z]+"\)' "$f" 2>/dev/null)
677
+ if [ -n "$SECTION_NAVROUTE" ] && ! grep -q "\[RequirePermission" "$f"; then
678
+ echo "BLOCKING: Section controller has [NavRoute] but no [RequirePermission]: $f"
679
+ echo "Fix: Add [RequirePermission(Permissions.{Section}.{Action})] on each endpoint"
680
+ exit 1
681
+ fi
682
+ done
683
+ fi
684
+
685
+ # Section-level permissions must exist for each section in seed data
686
+ PERM_FILE=$(find src/ -name "Permissions.cs" -path "*/Authorization/*" 2>/dev/null | head -1)
687
+ if [ -n "$SECTION_SEED_FILES" ] && [ -n "$PERM_FILE" ]; then
688
+ SECTION_CODES=$(grep -oP 'Code\s*=\s*"([a-z]+)"' $SECTION_SEED_FILES 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
689
+ for CODE in $SECTION_CODES; do
690
+ PASCAL=$(echo "$CODE" | sed 's/^./\U&/')
691
+ if ! grep -q "static class $PASCAL" "$PERM_FILE" 2>/dev/null; then
692
+ echo "WARNING: Section '$CODE' in seed data has no matching Permissions.$PASCAL static class"
693
+ echo "Fix: Add section-level permissions via MCP generate_permissions with 4-segment navRoute"
694
+ fi
695
+ done
696
+ fi
697
+ ```
698
+
699
+ ### POST-CHECK 26: FORBIDDEN route patterns — /list and /detail/:id (BLOCKING)
700
+
701
+ ```bash
702
+ # 1. Check seed data for FORBIDDEN suffixes
703
+ SEED_NAV_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -o -name "*NavigationSectionSeedData.cs" 2>/dev/null)
704
+ if [ -n "$SEED_NAV_FILES" ]; then
705
+ BAD_ROUTES=$(grep -Pn 'Route\s*=\s*.*"[^"]*/(list|detail)["/]' $SEED_NAV_FILES 2>/dev/null | grep -v '//.*Route')
706
+ if [ -n "$BAD_ROUTES" ]; then
707
+ echo "BLOCKING: FORBIDDEN route pattern in seed data"
708
+ echo " - 'list' section route = module route (NO /list suffix)"
709
+ echo " - 'detail' section route = module route + /:id (NOT /detail/:id)"
710
+ echo "$BAD_ROUTES"
711
+ exit 1
712
+ fi
713
+ fi
714
+
715
+ # 2. Check frontend routes for FORBIDDEN path segments
716
+ APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
717
+ if [ -n "$APP_TSX" ]; then
718
+ BAD_FE=$(grep -Pn "path:\s*['\"](?:list|detail)" "$APP_TSX" 2>/dev/null)
719
+ if [ -n "$BAD_FE" ]; then
720
+ echo "BLOCKING: FORBIDDEN frontend route path"
721
+ echo " - list = index: true (no 'list' path segment)"
722
+ echo " - detail = ':id' (no 'detail' path segment)"
723
+ echo "$BAD_FE"
724
+ exit 1
725
+ fi
726
+ fi
727
+ echo "OK: No forbidden /list or /detail route patterns found"
728
+ ```
729
+
730
+ ### POST-CHECK 27: Permission path segment count (WARNING)
731
+
732
+ ```bash
733
+ PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "PermissionsSeedData.cs" 2>/dev/null)
734
+ if [ -n "$PERM_FILES" ]; then
735
+ while IFS= read -r line; do
736
+ PATH_VAL=$(echo "$line" | grep -oP '"[^"]*\.[^"]*"' | tr -d '"')
737
+ if [ -n "$PATH_VAL" ]; then
738
+ DOTS=$(echo "$PATH_VAL" | tr -cd '.' | wc -c)
739
+ # Module permissions: 2 dots (app.module.action = 3 segments = 2+1)
740
+ # Section permissions: 3 dots (app.module.section.action = 4 segments = 3+1)
741
+ # Wildcard: ends with .* (valid at any level)
742
+ if echo "$PATH_VAL" | grep -qP '\.\*$'; then
743
+ continue # Wildcards are valid
744
+ elif [ "$DOTS" -lt 2 ] || [ "$DOTS" -gt 4 ]; then
745
+ echo "WARNING: Permission path has unexpected segment count ($((DOTS+1)) segments): $PATH_VAL"
746
+ fi
747
+ fi
748
+ done < <(grep -n 'Path\s*=' $PERM_FILES 2>/dev/null)
749
+ fi
750
+ ```
751
+
752
+ ### POST-CHECK 28: IClientSeedDataProvider must have 4 methods + DI registration (BLOCKING)
753
+
754
+ ```bash
755
+ PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
756
+ if [ -n "$PROVIDER" ]; then
757
+ METHODS_FOUND=0
758
+ for METHOD in SeedNavigationAsync SeedRolesAsync SeedPermissionsAsync SeedRolePermissionsAsync; do
759
+ if grep -q "$METHOD" "$PROVIDER"; then
760
+ METHODS_FOUND=$((METHODS_FOUND + 1))
761
+ else
762
+ echo "BLOCKING: IClientSeedDataProvider missing method: $METHOD in $PROVIDER"
763
+ fi
764
+ done
765
+ if [ "$METHODS_FOUND" -lt 4 ]; then
766
+ echo "Fix: IClientSeedDataProvider must implement all 4 methods: SeedNavigationAsync, SeedRolesAsync, SeedPermissionsAsync, SeedRolePermissionsAsync"
767
+ exit 1
768
+ fi
769
+
770
+ # Check DI registration
771
+ DI_FILE=$(find src/ -name "DependencyInjection.cs" -path "*/Infrastructure/*" 2>/dev/null | head -1)
772
+ if [ -n "$DI_FILE" ]; then
773
+ if ! grep -q "IClientSeedDataProvider" "$DI_FILE"; then
774
+ echo "BLOCKING: IClientSeedDataProvider not registered in DependencyInjection.cs"
775
+ echo "Fix: Add services.AddScoped<IClientSeedDataProvider, {App}SeedDataProvider>()"
776
+ exit 1
777
+ fi
778
+ fi
779
+ fi
780
+ ```
781
+
782
+ ### POST-CHECK 29: i18n must use separate JSON files per language (not embedded in index.ts)
783
+
784
+ ```bash
785
+ # Translations MUST be in src/i18n/locales/{lang}/{module}.json, NOT embedded in a single .ts file
786
+ TSX_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
787
+ if [ -n "$TSX_FILES" ]; then
788
+ # Check if i18n locales directory exists
789
+ if [ ! -d "src/i18n/locales" ]; then
790
+ echo "BLOCKING: Missing src/i18n/locales/ directory"
791
+ echo "Translations must be in separate JSON files: src/i18n/locales/{fr,en,it,de}/{module}.json"
792
+ echo "NEVER embed translations in src/i18n/index.ts or a single TypeScript file"
793
+ exit 1
794
+ fi
795
+
796
+ # Check for embedded translations in index.ts (common anti-pattern)
797
+ I18N_INDEX=$(find src/i18n/ -maxdepth 1 -name "index.ts" -o -name "index.tsx" -o -name "config.ts" 2>/dev/null)
798
+ if [ -n "$I18N_INDEX" ]; then
799
+ EMBEDDED=$(grep -Pn '^\s*(resources|translations)\s*[:=]\s*\{' $I18N_INDEX 2>/dev/null)
800
+ if [ -n "$EMBEDDED" ]; then
801
+ echo "BLOCKING: Translations embedded in i18n config file — must be in separate JSON files"
802
+ echo "Found embedded translations in:"
803
+ echo "$EMBEDDED"
804
+ echo ""
805
+ echo "Fix: Move translations to src/i18n/locales/{fr,en,it,de}/{module}.json"
806
+ echo "The i18n config should import from locales/ directory, not contain inline translations"
807
+ exit 1
808
+ fi
809
+ fi
810
+
811
+ # Verify all 4 language directories exist
812
+ for LANG in fr en it de; do
813
+ if [ ! -d "src/i18n/locales/$LANG" ]; then
814
+ echo "BLOCKING: Missing language directory: src/i18n/locales/$LANG/"
815
+ echo "SmartStack requires 4 languages: fr, en, it, de"
816
+ exit 1
817
+ fi
818
+ done
819
+
820
+ # Verify at least one JSON file exists per language
821
+ for LANG in fr en it de; do
822
+ JSON_COUNT=$(find "src/i18n/locales/$LANG" -name "*.json" 2>/dev/null | wc -l)
823
+ if [ "$JSON_COUNT" -eq 0 ]; then
824
+ echo "BLOCKING: No translation JSON files in src/i18n/locales/$LANG/"
825
+ echo "Each module must have a {module}.json file per language"
826
+ exit 1
827
+ fi
828
+ done
829
+ fi
830
+ ```
831
+
832
+ ### POST-CHECK 30: Pages must use useTranslation hook (no hardcoded user-facing strings)
833
+
834
+ ```bash
835
+ # Verify that page components use i18n — detect hardcoded strings in JSX
836
+ PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
837
+ if [ -n "$PAGE_FILES" ]; then
838
+ # Check that at least 80% of pages import useTranslation
839
+ TOTAL_PAGES=$(echo "$PAGE_FILES" | wc -l)
840
+ I18N_PAGES=$(grep -l "useTranslation" $PAGE_FILES 2>/dev/null | wc -l)
841
+ if [ "$TOTAL_PAGES" -gt 0 ] && [ "$I18N_PAGES" -eq 0 ]; then
842
+ echo "BLOCKING: No page files use useTranslation — all user-facing text must be translated"
843
+ echo "Found $TOTAL_PAGES page files but 0 use useTranslation"
844
+ echo ""
845
+ echo "Fix: Import and use useTranslation in every page component:"
846
+ echo " const { t } = useTranslation(['{module}']);"
847
+ echo " t('{module}:title', 'Fallback text')"
848
+ exit 1
849
+ fi
850
+
851
+ # Check for common hardcoded English strings in JSX (heuristic)
852
+ HARDCODED_TEXT=$(grep -Pn '>\s*(Create|Edit|Delete|Save|Cancel|Search|Loading|Error|No data|Not found|Submit|Back|Actions|Name|Status|Description)\s*<' $PAGE_FILES 2>/dev/null | grep -v '{t(' | head -10)
853
+ if [ -n "$HARDCODED_TEXT" ]; then
854
+ echo "WARNING: Possible hardcoded user-facing strings detected in JSX"
855
+ echo "All user-facing text MUST use t('namespace:key', 'Fallback')"
856
+ echo "$HARDCODED_TEXT"
857
+ fi
858
+ fi
859
+ ```
860
+
861
+ ### POST-CHECK 31: List/Detail pages must include DocToggleButton (documentation panel)
862
+
863
+ ```bash
864
+ # Every list and detail page MUST have DocToggleButton for inline documentation access
865
+ LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
866
+ if [ -n "$LIST_PAGES" ]; then
867
+ MISSING_DOC=0
868
+ for PAGE in $LIST_PAGES; do
869
+ if ! grep -q "DocToggleButton" "$PAGE" 2>/dev/null; then
870
+ echo "WARNING: Page missing DocToggleButton: $PAGE"
871
+ echo " Import: import { DocToggleButton } from '@/components/docs/DocToggleButton';"
872
+ echo " Place in header actions: <DocToggleButton />"
873
+ MISSING_DOC=$((MISSING_DOC + 1))
874
+ fi
875
+ done
876
+ if [ "$MISSING_DOC" -gt 0 ]; then
877
+ echo ""
878
+ echo "WARNING: $MISSING_DOC pages missing DocToggleButton — users cannot access inline documentation"
879
+ echo "See smartstack-frontend.md section 7 for placement pattern"
880
+ fi
881
+ fi
882
+ ```
883
+
884
+ ### POST-CHECK 32: Module documentation must be generated (doc-data.ts)
885
+
886
+ ```bash
887
+ # After frontend pages exist, /documentation should have been called
888
+ TSX_PAGES=$(find src/pages/ -name "*.tsx" -not -name "*.test.*" 2>/dev/null | grep -v node_modules | grep -v "docs/")
889
+ DOC_DATA=$(find src/pages/docs/ -name "doc-data.ts" 2>/dev/null)
890
+ if [ -n "$TSX_PAGES" ] && [ -z "$DOC_DATA" ]; then
891
+ echo "WARNING: Frontend pages exist but no documentation generated"
892
+ echo "Fix: Invoke /documentation {module} --type user to generate doc-data.ts"
893
+ echo "The DocToggleButton in page headers will link to this documentation"
894
+ fi
895
+ ```
896
+
897
+ ### POST-CHECK 33: Pagination type must be PaginatedResult<T> — no aliases (BLOCKING)
898
+
899
+ ```bash
900
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
901
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
902
+ ALL_FILES="$SERVICE_FILES $CTRL_FILES"
903
+ if [ -n "$(echo $ALL_FILES | tr -d ' ')" ]; then
904
+ BAD_NAMES=$(grep -Pn 'PagedResult<|PaginatedResultDto<|PaginatedResponse<|PageResultDto<' $ALL_FILES 2>/dev/null)
905
+ if [ -n "$BAD_NAMES" ]; then
906
+ echo "BLOCKING: Pagination type must be PaginatedResult<T> — found non-canonical names"
907
+ echo "$BAD_NAMES"
908
+ echo "FORBIDDEN type names: PagedResult, PaginatedResultDto, PaginatedResponse, PageResultDto"
909
+ echo "Fix: Use PaginatedResult<T> from SmartStack.Application.Common.Models everywhere"
910
+ exit 1
911
+ fi
912
+ fi
913
+ ```
914
+
915
+ ### POST-CHECK 34: Code generation — ICodeGenerator must be registered for auto-generated entities (BLOCKING)
916
+
917
+ ```bash
918
+ # If feature.json has entities with codePattern.strategy != "manual",
919
+ # verify that ICodeGenerator<Entity> is registered in DI
920
+ FEATURE_FILES=$(find docs/ -name "feature.json" 2>/dev/null)
921
+ DI_FILE=$(find src/ -name "DependencyInjection.cs" -path "*/Infrastructure/*" 2>/dev/null | head -1)
922
+ if [ -n "$FEATURE_FILES" ] && [ -n "$DI_FILE" ]; then
923
+ for FEATURE in $FEATURE_FILES; do
924
+ ENTITIES_WITH_CODE=$(python3 -c "
925
+ import json, sys
926
+ try:
927
+ with open('$FEATURE') as f:
928
+ data = json.load(f)
929
+ for e in data.get('analysis', {}).get('entities', []):
930
+ cp = e.get('codePattern', {})
931
+ if cp.get('strategy', 'manual') != 'manual':
932
+ print(e['name'])
933
+ except: pass
934
+ " 2>/dev/null)
935
+ for ENTITY in $ENTITIES_WITH_CODE; do
936
+ if ! grep -q "ICodeGenerator<$ENTITY>" "$DI_FILE" 2>/dev/null; then
937
+ echo "BLOCKING: Entity $ENTITY has auto-generated code pattern but ICodeGenerator<$ENTITY> is not registered in DI"
938
+ echo "Fix: Add CodeGenerator<$ENTITY> registration in DependencyInjection.cs — see references/code-generation.md"
939
+ exit 1
940
+ fi
941
+ done
942
+ done
943
+ fi
944
+ ```
945
+
946
+ ### POST-CHECK 35: Code regex must support hyphens (BLOCKING)
947
+
948
+ ```bash
949
+ VALIDATOR_FILES=$(find src/ -path "*/Validators/*" -name "*Validator.cs" 2>/dev/null)
950
+ if [ -n "$VALIDATOR_FILES" ]; then
951
+ OLD_REGEX=$(grep -rn '\^\\[a-z0-9_\\]+\$' $VALIDATOR_FILES 2>/dev/null | grep -v '\-')
952
+ if [ -n "$OLD_REGEX" ]; then
953
+ echo "BLOCKING: Code validator uses old regex without hyphen support"
954
+ echo "$OLD_REGEX"
955
+ echo "Fix: Update regex to ^[a-z0-9_-]+$ to support auto-generated codes with hyphens"
956
+ exit 1
957
+ fi
958
+ fi
959
+ ```
960
+
961
+ ### POST-CHECK 36: CreateDto must NOT have Code field when service uses ICodeGenerator (WARNING)
962
+
963
+ ```bash
964
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
965
+ if [ -n "$SERVICE_FILES" ]; then
966
+ for f in $SERVICE_FILES; do
967
+ if grep -q "ICodeGenerator" "$f"; then
968
+ ENTITY=$(basename "$f" | sed 's/Service\.cs$//')
969
+ DTO_FILE=$(find src/ -path "*/DTOs/*" -name "Create${ENTITY}Dto.cs" 2>/dev/null | head -1)
970
+ if [ -n "$DTO_FILE" ] && grep -q "public string Code" "$DTO_FILE"; then
971
+ echo "WARNING: Create${ENTITY}Dto has Code field but service uses ICodeGenerator (code is auto-generated)"
972
+ echo "Fix: Remove Code from Create${ENTITY}Dto — it is auto-generated by ICodeGenerator<${ENTITY}>"
973
+ fi
974
+ fi
975
+ done
976
+ fi
977
+ ```
978
+
979
+ ### POST-CHECK 37: Translation seed data must have idempotency guard (BLOCKING)
980
+
981
+ ```bash
982
+ PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
983
+ if [ -n "$PROVIDER" ]; then
984
+ # Check if NavigationTranslations.Add is used WITHOUT a preceding AnyAsync guard
985
+ # Pattern: any .Add(NavigationTranslation.Create(...)) that is NOT inside an AnyAsync check
986
+ TRANSLATION_ADDS=$(grep -c "NavigationTranslations.Add" "$PROVIDER" 2>/dev/null)
987
+ TRANSLATION_GUARDS=$(grep -c "NavigationTranslations.AnyAsync" "$PROVIDER" 2>/dev/null)
988
+
989
+ if [ "$TRANSLATION_ADDS" -gt 0 ] && [ "$TRANSLATION_GUARDS" -eq 0 ]; then
990
+ echo "BLOCKING: Translation seed data inserts without idempotency guard in $PROVIDER"
991
+ echo "Fix: Before each NavigationTranslations.Add block, check existence:"
992
+ echo " if (!await context.NavigationTranslations.AnyAsync("
993
+ echo " t => t.EntityId == {Module}NavigationSeedData.{Module}ModuleId"
994
+ echo " && t.EntityType == NavigationEntityType.Module, ct))"
995
+ echo " { foreach (var t in ...) { context.NavigationTranslations.Add(...); } }"
996
+ echo "The unique index IX_nav_Translations_EntityType_EntityId_LanguageCode will crash on duplicates."
997
+ exit 1
998
+ fi
999
+ fi
1000
+ ```
1001
+
1002
+ ### POST-CHECK 38: Resource seed data must use actual section IDs from DB (BLOCKING)
1003
+
1004
+ ```bash
1005
+ PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
1006
+ if [ -n "$PROVIDER" ]; then
1007
+ # Check if NavigationResource.Create uses secEntry.Id or resEntry.SectionId (deterministic GUIDs)
1008
+ # instead of actualSection.Id (real DB ID). This causes FK_nav_Resources_nav_Sections_SectionId violation.
1009
+ if grep -Pn 'NavigationResource\.Create\(' "$PROVIDER" | grep -q 'resEntry\.SectionId\|secEntry\.Id'; then
1010
+ echo "BLOCKING: Resource seed data uses deterministic GUID as SectionId in $PROVIDER"
1011
+ echo "NavigationSection.Create() generates its own ID — deterministic seed GUIDs do NOT exist in nav_Sections."
1012
+ echo "Fix: Query actual section from DB before creating resources:"
1013
+ echo " var actualSection = await context.NavigationSections"
1014
+ echo " .FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == modEntity.Id, ct);"
1015
+ echo " NavigationResource.Create(actualSection.Id, ...) // NOT secEntry.Id or resEntry.SectionId"
1016
+ exit 1
1017
+ fi
1018
+ fi
1019
+ ```
1020
+
1021
+ ### POST-CHECK 39: Controllers must NOT have [Route] alongside [NavRoute] (BLOCKING)
1022
+
1023
+ ```bash
1024
+ # [NavRoute] REPLACES [Route] — it resolves HTTP routes from the navigation DB at startup.
1025
+ # Having both is redundant and may cause route conflicts.
1026
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1027
+ if [ -n "$CTRL_FILES" ]; then
1028
+ for f in $CTRL_FILES; do
1029
+ HAS_NAVROUTE=$(grep -P '\[NavRoute\(' "$f" 2>/dev/null)
1030
+ HAS_ROUTE=$(grep -P '\[Route\(' "$f" 2>/dev/null)
1031
+ if [ -n "$HAS_NAVROUTE" ] && [ -n "$HAS_ROUTE" ]; then
1032
+ echo "BLOCKING: Controller has both [Route] and [NavRoute] — [NavRoute] replaces [Route]: $f"
1033
+ echo " Found [NavRoute]: $HAS_NAVROUTE"
1034
+ echo " Found [Route]: $HAS_ROUTE"
1035
+ echo "Fix: Remove the [Route(\"api/...\")] attribute. [NavRoute] resolves routes from navigation DB at startup."
1036
+ exit 1
1037
+ fi
1038
+ done
1039
+ fi
1040
+ ```
1041
+
1042
+ ### POST-CHECK 40: NavRoute segments must use kebab-case for multi-word codes (BLOCKING)
1043
+
1044
+ ```bash
1045
+ # NavRoute segments are navigation entity Codes joined by dots.
1046
+ # Multi-word codes MUST use kebab-case (e.g., "human-resources", NOT "humanresources").
1047
+ # Verified from SmartStack.app: "support-client.my-tickets", "administration.access-requests"
1048
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1049
+ if [ -n "$CTRL_FILES" ]; then
1050
+ for f in $CTRL_FILES; do
1051
+ NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
1052
+ if [ -n "$NAVROUTE_VAL" ]; then
1053
+ # Check each segment for concatenated multi-word (10+ lowercase chars without hyphens)
1054
+ for SEG in $(echo "$NAVROUTE_VAL" | tr '.' '\n'); do
1055
+ if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
1056
+ echo "BLOCKING: NavRoute segment '$SEG' in $f appears to be concatenated multi-word without hyphens"
1057
+ echo " Full NavRoute: $NAVROUTE_VAL"
1058
+ echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
1059
+ echo " SmartStack convention (from SmartStack.app): 'support-client.my-tickets'"
1060
+ exit 1
1061
+ fi
1062
+ done
1063
+ fi
1064
+ done
1065
+ fi
1066
+
1067
+ # Also check seed data Code values for navigation entities
1068
+ SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -o -name "NavigationApplicationSeedData.cs" 2>/dev/null)
1069
+ if [ -n "$SEED_FILES" ]; then
1070
+ CODES=$(grep -oP 'Code\s*=\s*"([^"]+)"' $SEED_FILES 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
1071
+ for CODE in $CODES; do
1072
+ if echo "$CODE" | grep -qP '^[a-z]{10,}$'; then
1073
+ echo "BLOCKING: Navigation seed data Code '$CODE' appears to be concatenated multi-word without hyphens"
1074
+ echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
1075
+ exit 1
1076
+ fi
1077
+ done
1078
+ fi
1079
+ ```
1080
+
1081
+ ### POST-CHECK 41: Permission codes must use kebab-case matching NavRoute codes (BLOCKING)
1082
+
1083
+ ```bash
1084
+ # Permission codes in [RequirePermission] and Permissions.cs MUST use kebab-case for multi-word segments.
1085
+ # SmartStack.app convention: "support-client.my-tickets.read" (kebab-case everywhere)
1086
+ # FORBIDDEN: "humanresources.employees.read" — must be "human-resources.employees.read"
1087
+
1088
+ # Check [RequirePermission] attributes in controllers
1089
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1090
+ if [ -n "$CTRL_FILES" ]; then
1091
+ for f in $CTRL_FILES; do
1092
+ PERM_VALS=$(grep -oP 'RequirePermission\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
1093
+ for PERM in $PERM_VALS; do
1094
+ # Check each segment (except the action suffix) for concatenated multi-word without hyphens
1095
+ SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1) # remove last segment (action: read/create/update/delete)
1096
+ for SEG in $SEGMENTS; do
1097
+ if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
1098
+ echo "BLOCKING: Permission code segment '$SEG' in $f appears concatenated without hyphens"
1099
+ echo " Full permission: $PERM"
1100
+ echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
1101
+ echo " SmartStack convention: 'support-client.my-tickets.read'"
1102
+ exit 1
1103
+ fi
1104
+ done
1105
+ done
1106
+ done
1107
+ fi
1108
+
1109
+ # Check Permissions.cs constants
1110
+ PERM_FILES=$(find src/ -path "*/Authorization/Permissions.cs" 2>/dev/null)
1111
+ if [ -n "$PERM_FILES" ]; then
1112
+ for f in $PERM_FILES; do
1113
+ CONST_VALS=$(grep -oP '=\s*"([^"]+)"' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
1114
+ for PERM in $CONST_VALS; do
1115
+ SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
1116
+ for SEG in $SEGMENTS; do
1117
+ if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
1118
+ echo "BLOCKING: Permissions.cs constant segment '$SEG' in $f appears concatenated without hyphens"
1119
+ echo " Full permission: $PERM"
1120
+ echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
1121
+ exit 1
1122
+ fi
1123
+ done
1124
+ done
1125
+ done
1126
+ fi
1127
+
1128
+ # Check PermissionsSeedData.cs for mismatched paths
1129
+ SEED_PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*PermissionsSeedData.cs" 2>/dev/null)
1130
+ if [ -n "$SEED_PERM_FILES" ]; then
1131
+ PATHS=$(grep -oP '"[a-z][a-z0-9.-]+\.(read|create|update|delete|\*)"' $SEED_PERM_FILES 2>/dev/null | tr -d '"')
1132
+ for PERM in $PATHS; do
1133
+ SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
1134
+ for SEG in $SEGMENTS; do
1135
+ if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
1136
+ echo "BLOCKING: PermissionsSeedData path segment '$SEG' appears concatenated without hyphens"
1137
+ echo " Full permission path: $PERM"
1138
+ echo " Fix: Use kebab-case matching NavRoute: 'humanresources' → 'human-resources'"
1139
+ exit 1
1140
+ fi
1141
+ done
1142
+ done
1143
+ fi
1144
+ ```
1145
+
1146
+ ### POST-CHECK 42: Frontend navigate() calls must have matching route definitions (BLOCKING)
1147
+
1148
+ ```bash
1149
+ # Detect dead links: navigate() calls to paths that don't have corresponding page components.
1150
+ # Example: LeavesPage has navigate('../leave-types') but no LeaveTypesPage or route exists.
1151
+ PAGE_FILES=$(find web/ -name "*.tsx" -path "*/pages/*" ! -name "*.test.tsx" 2>/dev/null)
1152
+ if [ -n "$PAGE_FILES" ]; then
1153
+ # Extract navigate targets (relative paths like '../leave-types', './create', etc.)
1154
+ NAV_TARGETS=$(grep -oP "navigate\(['\"]([^'\"]+)['\"]" $PAGE_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u)
1155
+ # Extract route paths from App.tsx or route config
1156
+ APP_FILES=$(find web/ -name "App.tsx" -o -name "routes.tsx" -o -name "clientRoutes*.tsx" 2>/dev/null)
1157
+ if [ -n "$APP_FILES" ] && [ -n "$NAV_TARGETS" ]; then
1158
+ ROUTE_PATHS=$(grep -oP "path:\s*['\"]([^'\"]+)['\"]" $APP_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u)
1159
+ for TARGET in $NAV_TARGETS; do
1160
+ # Skip dynamic segments (:id), back navigation (-1), and absolute URLs
1161
+ if echo "$TARGET" | grep -qP '^(:|/api|http|-[0-9])'; then continue; fi
1162
+ # Extract the last path segment for matching (e.g., '../leave-types' → 'leave-types')
1163
+ LAST_SEG=$(echo "$TARGET" | grep -oP '[a-z][-a-z0-9]*$')
1164
+ if [ -z "$LAST_SEG" ]; then continue; fi
1165
+ # Check if any route path contains this segment
1166
+ FOUND=$(echo "$ROUTE_PATHS" | grep -F "$LAST_SEG" 2>/dev/null)
1167
+ if [ -z "$FOUND" ]; then
1168
+ # Verify no page component exists for this path
1169
+ SEG_PASCAL=$(echo "$LAST_SEG" | sed -r 's/(^|-)([a-z])/\U\2/g')
1170
+ PAGE_EXISTS=$(find web/ -name "${SEG_PASCAL}Page.tsx" -o -name "${SEG_PASCAL}ListPage.tsx" -o -name "${SEG_PASCAL}sPage.tsx" 2>/dev/null)
1171
+ if [ -z "$PAGE_EXISTS" ]; then
1172
+ # Find which file has this navigate call
1173
+ SOURCE_FILE=$(grep -rl "navigate(['\"].*${LAST_SEG}" $PAGE_FILES 2>/dev/null | head -1)
1174
+ echo "BLOCKING: Dead link detected — navigate('$TARGET') in $SOURCE_FILE"
1175
+ echo " Route segment '$LAST_SEG' has no matching route in App.tsx and no page component"
1176
+ echo " Fix: Either create the page component + route, or remove the navigate() button"
1177
+ exit 1
1178
+ fi
1179
+ fi
1180
+ done
1181
+ fi
1182
+ fi
1183
+ ```
1184
+
1185
+ ### POST-CHECK 43: Detail page tabs must NOT navigate() — content switches locally (BLOCKING)
1186
+
1187
+ ```bash
1188
+ # Tabs on detail pages MUST use local state (setActiveTab) — NEVER navigate() to other pages.
1189
+ # Root cause (test-apex-006): EmployeeDetailPage tabs navigated to ../leaves and ../time-tracking
1190
+ # instead of rendering sub-resource content inline. Users lost detail page context.
1191
+ DETAIL_PAGES=$(find src/ web/ -name "*DetailPage.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
1192
+ if [ -n "$DETAIL_PAGES" ]; then
1193
+ FAIL=false
1194
+ for DP in $DETAIL_PAGES; do
1195
+ # Check if the page has tabs (activeTab state)
1196
+ HAS_TABS=$(grep -P "useState.*activeTab|setActiveTab" "$DP" 2>/dev/null)
1197
+ if [ -z "$HAS_TABS" ]; then continue; fi
1198
+
1199
+ # Check if any tab click handler calls navigate()
1200
+ # Pattern: function that both references setActiveTab AND navigate()
1201
+ # Look for navigate() calls inside handlers that also set tab state
1202
+ 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 "//")
1203
+ if [ -n "$TAB_NAVIGATE" ]; then
1204
+ # Verify this navigate is in a tab handler context (near setActiveTab usage)
1205
+ # Simple heuristic: if file has both setActiveTab AND navigate() to relative paths
1206
+ RELATIVE_NAV=$(echo "$TAB_NAVIGATE" | grep -P "navigate\(['\"\`]\.\./" 2>/dev/null)
1207
+ if [ -n "$RELATIVE_NAV" ]; then
1208
+ echo "BLOCKING: Detail page tabs use navigate() instead of local content switching: $DP"
1209
+ echo " Tab click handlers MUST only call setActiveTab() — render content inline"
1210
+ echo " Found navigate() calls (likely in tab handlers):"
1211
+ echo "$RELATIVE_NAV"
1212
+ echo ""
1213
+ echo " Fix: Remove navigate() from tab handlers. Render sub-resource content inline:"
1214
+ echo " {activeTab === 'leaves' && <LeaveRequestsTable employeeId={entity.id} />}"
1215
+ echo " See smartstack-frontend.md section 3 'Tab Behavior Rules' for the correct pattern."
1216
+ FAIL=true
1217
+ fi
1218
+ fi
1219
+ done
1220
+ if [ "$FAIL" = true ]; then
1221
+ exit 1
1222
+ fi
1223
+ fi
1224
+ ```
1225
+
1226
+ ### POST-CHECK 44: Migration ModelSnapshot must contain ALL entities registered in DbContext (BLOCKING)
1227
+
1228
+ ```bash
1229
+ # Root cause (test-apex-007): 7 entities registered in DbContext but migration only covered 3.
1230
+ # Happens when migration is created ONCE in Layer 0 for the first batch, then additional entities
1231
+ # are added in subsequent iterations without re-running migration.
1232
+ SNAPSHOT=$(find src/ -name "*ModelSnapshot.cs" -path "*/Migrations/*" 2>/dev/null | head -1)
1233
+ DBCONTEXT=$(find src/ -name "*DbContext.cs" -path "*/Persistence/*" ! -name "*DesignTime*" 2>/dev/null | head -1)
1234
+ if [ -n "$SNAPSHOT" ] && [ -n "$DBCONTEXT" ]; then
1235
+ # Extract DbSet entity names from DbContext (DbSet<EntityName>)
1236
+ DBSET_ENTITIES=$(grep -oP 'DbSet<(\w+)>' "$DBCONTEXT" 2>/dev/null | grep -oP '<\K\w+(?=>)' | sort -u)
1237
+ FAIL=false
1238
+ for ENTITY in $DBSET_ENTITIES; do
1239
+ # Skip base SmartStack entities (handled by core migrations)
1240
+ if echo "$ENTITY" | grep -qP '^(Navigation|Tenant|User|Role|Permission|AuditLog|ApplicationTracking)'; then
1241
+ continue
1242
+ fi
1243
+ # Check if the entity appears in ModelSnapshot (builder.Entity<EntityName>)
1244
+ if ! grep -q "Entity<$ENTITY>" "$SNAPSHOT" 2>/dev/null; then
1245
+ echo "BLOCKING: Entity '$ENTITY' is registered as DbSet in $DBCONTEXT but MISSING from ModelSnapshot"
1246
+ echo " This means no migration was created for this entity — it will not exist in the database."
1247
+ echo " Fix: Run 'dotnet ef migrations add' to include all new entities"
1248
+ FAIL=true
1249
+ fi
1250
+ done
1251
+ if [ "$FAIL" = true ]; then
1252
+ echo ""
1253
+ echo " Root cause: Migration was likely created once for the first batch of entities,"
1254
+ echo " but additional entities were added later without regenerating the migration."
1255
+ echo " Fix: Create a new migration that covers ALL missing entities."
1256
+ exit 1
1257
+ fi
1258
+ fi
1259
+ ```
1260
+
1261
+ ### POST-CHECK 45: I18n namespace files must be registered in i18n config (BLOCKING)
1262
+
1263
+ ```bash
1264
+ # Root cause (test-apex-007): i18n JSON files existed in src/i18n/locales/ but were never
1265
+ # registered in the i18n config (config.ts or index.ts). Pages calling useTranslation(['module'])
1266
+ # got empty translations at runtime.
1267
+ 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)
1268
+ if [ -n "$I18N_CONFIG" ]; then
1269
+ # Find all module JSON files in the primary language (fr)
1270
+ 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)
1271
+ if [ -n "$FR_FILES" ]; then
1272
+ FAIL=false
1273
+ for JSON_FILE in $FR_FILES; do
1274
+ NS=$(basename "$JSON_FILE" .json)
1275
+ # Check if namespace is referenced in config (import or resource key)
1276
+ if ! grep -q "$NS" "$I18N_CONFIG" 2>/dev/null; then
1277
+ echo "BLOCKING: i18n namespace '$NS' (from $JSON_FILE) is not registered in $I18N_CONFIG"
1278
+ echo " Pages using useTranslation(['$NS']) will get empty translations at runtime"
1279
+ echo " Fix: Add '$NS' to the resources/ns configuration in $I18N_CONFIG"
1280
+ FAIL=true
1281
+ fi
1282
+ done
1283
+ if [ "$FAIL" = true ]; then
1284
+ exit 1
1285
+ fi
1286
+ fi
1287
+ fi
1288
+ ```
1289
+
1290
+ ### POST-CHECK 46: FluentValidation validators must be registered via DI (BLOCKING)
1291
+
1292
+ ```bash
1293
+ # Root cause (test-apex-007): Validators existed but were never registered in DI.
1294
+ # Without DI registration, [FromBody] DTOs are never validated — any data is accepted.
1295
+ VALIDATOR_FILES=$(find src/ -name "*Validator.cs" -path "*/Validators/*" 2>/dev/null | grep -v test | grep -v Test)
1296
+ if [ -n "$VALIDATOR_FILES" ]; then
1297
+ # Check DI registration file exists
1298
+ DI_FILE=$(find src/ -name "DependencyInjection.cs" -o -name "ServiceCollectionExtensions.cs" 2>/dev/null | grep -v test | head -1)
1299
+ if [ -z "$DI_FILE" ]; then
1300
+ echo "BLOCKING: Validators exist but no DependencyInjection.cs found for DI registration"
1301
+ exit 1
1302
+ fi
1303
+ # Check for AddValidatorsFromAssembly or individual validator registration
1304
+ HAS_ASSEMBLY_REG=$(grep -c "AddValidatorsFromAssembly\|AddValidatorsFromAssemblyContaining" "$DI_FILE" 2>/dev/null)
1305
+ if [ "$HAS_ASSEMBLY_REG" -eq 0 ]; then
1306
+ # Check individual registrations as fallback
1307
+ VALIDATOR_COUNT=$(echo "$VALIDATOR_FILES" | wc -l)
1308
+ REGISTERED_COUNT=0
1309
+ for VF in $VALIDATOR_FILES; do
1310
+ VN=$(basename "$VF" .cs)
1311
+ if grep -q "$VN" "$DI_FILE" 2>/dev/null; then
1312
+ REGISTERED_COUNT=$((REGISTERED_COUNT + 1))
1313
+ fi
1314
+ done
1315
+ if [ "$REGISTERED_COUNT" -eq 0 ]; then
1316
+ echo "BLOCKING: $VALIDATOR_COUNT validators exist but NONE are registered in DI ($DI_FILE)"
1317
+ echo " Fix: Add 'services.AddValidatorsFromAssemblyContaining<Create{Entity}DtoValidator>();' to $DI_FILE"
1318
+ echo " Or use 'services.AddValidatorsFromAssembly(typeof(Create{Entity}DtoValidator).Assembly);'"
1319
+ exit 1
1320
+ fi
1321
+ fi
1322
+ fi
1323
+ ```
1324
+
1325
+ ### POST-CHECK 47: Date/date properties in DTOs must use DateOnly, not string (BLOCKING)
1326
+
1327
+ ```bash
1328
+ # Root cause (test-apex-007): WorkLog DTO had Date property typed as string instead of DateOnly.
1329
+ # This causes: invalid date parsing, no date validation, inconsistent formats across clients.
1330
+ DTO_FILES=$(find src/ -name "*Dto.cs" -path "*/DTOs/*" 2>/dev/null)
1331
+ if [ -n "$DTO_FILES" ]; then
1332
+ FAIL=false
1333
+ for f in $DTO_FILES; do
1334
+ # Find string properties whose name contains "Date" (case-insensitive)
1335
+ BAD_DATES=$(grep -Pn 'string\??\s+\w*[Dd]ate\w*\s*[{;,]' "$f" 2>/dev/null | grep -vi "Updated\|Created\|format\|pattern\|string\|parse")
1336
+ if [ -n "$BAD_DATES" ]; then
1337
+ echo "BLOCKING: DTO has string type for date field — must use DateOnly: $f"
1338
+ echo "$BAD_DATES"
1339
+ echo " Fix: Change 'string Date' to 'DateOnly Date' (or 'DateOnly? Date' if nullable)"
1340
+ echo " DateOnly is the correct .NET type for date-only values (no time component)"
1341
+ FAIL=true
1342
+ fi
1343
+ done
1344
+ if [ "$FAIL" = true ]; then
1345
+ exit 1
1346
+ fi
1347
+ fi
1348
+ ```
1349
+
1350
+ ### POST-CHECK 48: NavRoute attribute values must use kebab-case (BLOCKING)
1351
+
1352
+ ```bash
1353
+ # Root cause (test-apex-007): Controllers had [NavRoute("humanresources.employees")]
1354
+ # instead of [NavRoute("human-resources.employees")]. This causes route mismatch with
1355
+ # seed data and permission codes, resulting in 404s at runtime.
1356
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1357
+ if [ -n "$CTRL_FILES" ]; then
1358
+ FAIL=false
1359
+ for f in $CTRL_FILES; do
1360
+ NAVROUTE_VALS=$(grep -oP 'NavRoute\("([^"]+)"' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
1361
+ for NR in $NAVROUTE_VALS; do
1362
+ # Check each segment for concatenated multi-word without hyphens
1363
+ SEGMENTS=$(echo "$NR" | tr '.' '\n')
1364
+ for SEG in $SEGMENTS; do
1365
+ # Detect segments that look like concatenated words (lowercase, 8+ chars, no hyphens)
1366
+ # Use a simpler heuristic: lowercase-only segment with known multi-word patterns
1367
+ if echo "$SEG" | grep -qP '^[a-z]{8,}$'; then
1368
+ # Additional check: does it contain a known multi-word pattern?
1369
+ if echo "$SEG" | grep -qP '(human|project|leave|client|support|email|time|work|resource)'; then
1370
+ echo "BLOCKING: NavRoute segment '$SEG' in $f appears to be concatenated multi-word without hyphens"
1371
+ echo " Full NavRoute: $NR"
1372
+ echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources', 'projectmanagement' → 'project-management'"
1373
+ FAIL=true
1374
+ fi
1375
+ fi
1376
+ done
1377
+ done
1378
+ done
1379
+ if [ "$FAIL" = true ]; then
1380
+ exit 1
1381
+ fi
1382
+ fi
1383
+ ```
1384
+
1385
+ ### POST-CHECK 49: Every module with entities must have a migration covering them (BLOCKING)
1386
+
1387
+ ```bash
1388
+ # Complementary to POST-CHECK 44 — checks from the entity side.
1389
+ # Finds entity .cs files in Domain/ and verifies they appear in at least one migration file.
1390
+ ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null | grep -v test)
1391
+ MIGRATION_DIR=$(find src/ -path "*/Migrations" -type d 2>/dev/null | head -1)
1392
+ if [ -n "$ENTITY_FILES" ] && [ -n "$MIGRATION_DIR" ]; then
1393
+ MIGRATION_FILES=$(find "$MIGRATION_DIR" -name "*.cs" ! -name "*ModelSnapshot*" ! -name "*DesignTime*" 2>/dev/null)
1394
+ if [ -z "$MIGRATION_FILES" ]; then
1395
+ echo "BLOCKING: Entity files exist in Domain/Entities but NO migration files found in $MIGRATION_DIR"
1396
+ exit 1
1397
+ fi
1398
+ FAIL=false
1399
+ for EF in $ENTITY_FILES; do
1400
+ ENTITY_NAME=$(basename "$EF" .cs)
1401
+ # Skip abstract base classes and interfaces
1402
+ if grep -qP '^\s*(public\s+)?(abstract|interface)\s' "$EF" 2>/dev/null; then continue; fi
1403
+ # Check if entity appears in any migration (CreateTable or AddColumn or entity reference)
1404
+ FOUND=$(grep -l "$ENTITY_NAME" $MIGRATION_FILES 2>/dev/null)
1405
+ if [ -z "$FOUND" ]; then
1406
+ echo "BLOCKING: Entity '$ENTITY_NAME' ($EF) not found in any migration file"
1407
+ echo " This entity will NOT have a database table."
1408
+ echo " Fix: Run 'dotnet ef migrations add' to create a migration covering this entity"
1409
+ FAIL=true
1410
+ fi
1411
+ done
1412
+ if [ "$FAIL" = true ]; then
1413
+ exit 1
1414
+ fi
1415
+ fi
1416
+ ```
1417
+
1418
+ ### POST-CHECK 50: Controllers must NOT have both [Route] and [NavRoute] attributes (BLOCKING)
1419
+
1420
+ ```bash
1421
+ # Root cause (test-apex-007): All 7 controllers had BOTH [Route("api/...")] and [NavRoute("...")].
1422
+ # In SmartStack, [NavRoute] resolves routes dynamically from Navigation entities at startup.
1423
+ # [Route] is standard ASP.NET Core static routing. When both exist:
1424
+ # - NavRoute middleware tries to resolve from DB → fails if seed data not applied → no route
1425
+ # - [Route] may or may not take over depending on middleware order
1426
+ # - Result: 404 on ALL endpoints
1427
+ # The MCP validate_conventions previously ENCOURAGED adding [Route] with [NavRoute] — this was a bug.
1428
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1429
+ if [ -n "$CTRL_FILES" ]; then
1430
+ FAIL=false
1431
+ for f in $CTRL_FILES; do
1432
+ HAS_NAVROUTE=$(grep -c '\[NavRoute(' "$f" 2>/dev/null)
1433
+ HAS_ROUTE=$(grep -c '\[Route(' "$f" 2>/dev/null)
1434
+ if [ "$HAS_NAVROUTE" -gt 0 ] && [ "$HAS_ROUTE" -gt 0 ]; then
1435
+ NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"' "$f" 2>/dev/null | head -1)
1436
+ ROUTE_VAL=$(grep -oP 'Route\("([^"]+)"' "$f" 2>/dev/null | head -1)
1437
+ echo "BLOCKING: Controller has BOTH [Route] and [NavRoute] — remove [Route]: $f"
1438
+ echo " Found: [$ROUTE_VAL] + [$NAVROUTE_VAL]"
1439
+ echo " In SmartStack, [NavRoute] resolves routes dynamically from the database."
1440
+ echo " Having [Route] alongside it causes route conflicts and 404s."
1441
+ echo " Fix: Remove the [Route(...)] attribute, keep only [NavRoute(...)]"
1442
+ FAIL=true
1443
+ fi
1444
+ done
1445
+ if [ "$FAIL" = true ]; then
1446
+ exit 1
1447
+ fi
1448
+ fi
1449
+ ```
1450
+
1451
+ ### POST-CHECK 51: RolesSeedData must map standard role-permission matrix (BLOCKING)
1452
+
1453
+ ```bash
1454
+ # SmartStack standard role-permission matrix:
1455
+ # Admin = wildcard (*) — full access
1456
+ # Manager = CRU (read + create + update) — no delete
1457
+ # Contributor = CR (read + create) — no update, no delete
1458
+ # Viewer = R (read only)
1459
+ # If RolesSeedData deviates from this matrix, the RBAC model is broken.
1460
+ ROLE_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" ! -name "ApplicationRolesSeedData.cs" 2>/dev/null)
1461
+ if [ -n "$ROLE_SEED_FILES" ]; then
1462
+ FAIL=false
1463
+ for f in $ROLE_SEED_FILES; do
1464
+ # Skip ApplicationRolesSeedData (defines roles, not mappings)
1465
+ BASENAME=$(basename "$f")
1466
+ if [ "$BASENAME" = "ApplicationRolesSeedData.cs" ]; then continue; fi
1467
+
1468
+ # Check Admin has wildcard
1469
+ HAS_ADMIN_WILDCARD=$(grep -Pc '(admin|Admin).*\*' "$f" 2>/dev/null)
1470
+ if [ "$HAS_ADMIN_WILDCARD" -eq 0 ]; then
1471
+ # Also accept .Access or wildcard pattern
1472
+ HAS_ADMIN_ACCESS=$(grep -Pc '(admin|Admin).*(Access|Wildcard|IsWildcard)' "$f" 2>/dev/null)
1473
+ if [ "$HAS_ADMIN_ACCESS" -eq 0 ]; then
1474
+ echo "BLOCKING: Admin role missing wildcard (*) permission in $f"
1475
+ echo "Fix: Admin must map to wildcard permission (navRoute.*) or use IsWildcard=true"
1476
+ FAIL=true
1477
+ fi
1478
+ fi
1479
+
1480
+ # Check Viewer has NO delete/create/update
1481
+ VIEWER_WRITE=$(grep -Pc '(viewer|Viewer).*(\.delete|\.create|\.update|Delete|Create|Update)' "$f" 2>/dev/null)
1482
+ if [ "$VIEWER_WRITE" -gt 0 ]; then
1483
+ echo "BLOCKING: Viewer role has write permissions (create/update/delete) in $f"
1484
+ echo "Fix: Viewer must only have read permission. Remove create/update/delete mappings."
1485
+ FAIL=true
1486
+ fi
1487
+
1488
+ # Check Manager has NO delete
1489
+ MANAGER_DELETE=$(grep -Pc '(manager|Manager).*(\.delete|Delete)' "$f" 2>/dev/null)
1490
+ if [ "$MANAGER_DELETE" -gt 0 ]; then
1491
+ echo "WARNING: Manager role has delete permission in $f"
1492
+ echo "SmartStack standard: Manager = CRU (no delete). Verify this is intentional."
1493
+ fi
1494
+ done
1495
+ if [ "$FAIL" = true ]; then
1496
+ exit 1
1497
+ fi
1498
+ fi
1499
+ ```
1500
+
1501
+ ### POST-CHECK 52: PermissionAction enum must use valid typed values only (BLOCKING)
1502
+
1503
+ ```bash
1504
+ # Valid PermissionAction enum values: Access(0), Read(1), Create(2), Update(3), Delete(4),
1505
+ # Export(5), Import(6), Approve(7), Reject(8), Assign(9), Execute(10)
1506
+ # FORBIDDEN: Enum.Parse<PermissionAction>("...") — runtime crash if value doesn't exist
1507
+ # FORBIDDEN: (PermissionAction)99 or any cast beyond 0-10
1508
+ SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
1509
+ if [ -n "$SEED_FILES" ]; then
1510
+ FAIL=false
1511
+ for f in $SEED_FILES; do
1512
+ # Check for Enum.Parse<PermissionAction> usage
1513
+ ENUM_PARSE=$(grep -Pn 'Enum\.Parse<PermissionAction>' "$f" 2>/dev/null)
1514
+ if [ -n "$ENUM_PARSE" ]; then
1515
+ echo "BLOCKING: Enum.Parse<PermissionAction> detected — runtime crash risk: $f"
1516
+ echo "$ENUM_PARSE"
1517
+ echo "Fix: Use typed enum directly: PermissionAction.Read (NOT Enum.Parse<PermissionAction>(\"Read\"))"
1518
+ FAIL=true
1519
+ fi
1520
+
1521
+ # Check for invalid cast values (PermissionAction)N where N > 10
1522
+ INVALID_CAST=$(grep -Pn '\(PermissionAction\)\s*([1-9]\d{1,}|[2-9]\d)' "$f" 2>/dev/null)
1523
+ if [ -n "$INVALID_CAST" ]; then
1524
+ echo "BLOCKING: Invalid PermissionAction cast detected (value > 10): $f"
1525
+ echo "$INVALID_CAST"
1526
+ echo "Valid values: Access(0), Read(1), Create(2), Update(3), Delete(4), Export(5), Import(6), Approve(7), Reject(8), Assign(9), Execute(10)"
1527
+ FAIL=true
1528
+ fi
1529
+ done
1530
+ if [ "$FAIL" = true ]; then
1531
+ exit 1
1532
+ fi
1533
+ fi
1534
+ ```
1535
+
1536
+ ### POST-CHECK 53: Navigation translation completeness — 4 languages per level (BLOCKING)
1537
+
1538
+ ```bash
1539
+ # Every navigation seed data file must provide translations for ALL 4 languages (fr, en, it, de).
1540
+ # If sections exist (GetSectionEntries), GetSectionTranslationEntries MUST also exist.
1541
+ # If resources exist (GetResourceEntries), resource translation entries MUST also exist.
1542
+ NAV_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" ! -name "*Application*" 2>/dev/null)
1543
+ if [ -n "$NAV_SEED_FILES" ]; then
1544
+ FAIL=false
1545
+ for f in $NAV_SEED_FILES; do
1546
+ # Check module translations have all 4 languages
1547
+ LANG_COUNT=$(grep -c 'LanguageCode\s*=' "$f" 2>/dev/null)
1548
+ HAS_FR=$(grep -c '"fr"' "$f" 2>/dev/null)
1549
+ HAS_EN=$(grep -c '"en"' "$f" 2>/dev/null)
1550
+ HAS_IT=$(grep -c '"it"' "$f" 2>/dev/null)
1551
+ HAS_DE=$(grep -c '"de"' "$f" 2>/dev/null)
1552
+
1553
+ if [ "$HAS_FR" -eq 0 ] || [ "$HAS_EN" -eq 0 ] || [ "$HAS_IT" -eq 0 ] || [ "$HAS_DE" -eq 0 ]; then
1554
+ echo "BLOCKING: Missing language(s) in navigation translations: $f"
1555
+ echo " fr=$HAS_FR, en=$HAS_EN, it=$HAS_IT, de=$HAS_DE (all must be > 0)"
1556
+ echo "Fix: Add NavigationTranslationSeedEntry for all 4 languages (fr, en, it, de)"
1557
+ FAIL=true
1558
+ fi
1559
+
1560
+ # If sections exist, section translations MUST exist
1561
+ HAS_SECTION_ENTRIES=$(grep -c 'GetSectionEntries' "$f" 2>/dev/null)
1562
+ HAS_SECTION_TRANSLATIONS=$(grep -c 'GetSectionTranslationEntries' "$f" 2>/dev/null)
1563
+ if [ "$HAS_SECTION_ENTRIES" -gt 0 ] && [ "$HAS_SECTION_TRANSLATIONS" -eq 0 ]; then
1564
+ echo "BLOCKING: Sections defined but GetSectionTranslationEntries() missing: $f"
1565
+ echo "Fix: Add GetSectionTranslationEntries() with 4 languages per section (ref core-seed-data.md §2b)"
1566
+ FAIL=true
1567
+ fi
1568
+
1569
+ # If resources exist, resource translations MUST exist
1570
+ HAS_RESOURCE_ENTRIES=$(grep -c 'GetResourceEntries' "$f" 2>/dev/null)
1571
+ HAS_RESOURCE_TRANSLATIONS=$(grep -Pc 'ResourceTranslation|GetResourceTranslation|NavigationEntityType\.Resource.*LanguageCode' "$f" 2>/dev/null)
1572
+ if [ "$HAS_RESOURCE_ENTRIES" -gt 0 ] && [ "$HAS_RESOURCE_TRANSLATIONS" -eq 0 ]; then
1573
+ echo "BLOCKING: Resources defined but resource translations missing: $f"
1574
+ echo "Fix: Add resource translation entries with 4 languages per resource (ref core-seed-data.md §2b)"
1575
+ FAIL=true
1576
+ fi
1577
+ done
1578
+ if [ "$FAIL" = true ]; then
1579
+ exit 1
1580
+ fi
1581
+ fi
1582
+ ```
1583
+
1584
+ **If ANY POST-CHECK fails → fix in step-03, re-validate.**