@atlashub/smartstack-cli 3.39.0 → 3.41.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 (472) 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 +6 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp-entry.mjs +6 -4
  14. package/dist/mcp-entry.mjs.map +1 -1
  15. package/package.json +115 -115
  16. package/scripts/extract-api-endpoints.ts +325 -325
  17. package/scripts/extract-business-rules.ts +440 -440
  18. package/scripts/generate-doc-with-mock-ui.ts +804 -804
  19. package/scripts/health-check.sh +168 -168
  20. package/scripts/postinstall.js +18 -18
  21. package/templates/agents/action.md +37 -37
  22. package/templates/agents/ba-reader.md +378 -378
  23. package/templates/agents/ba-writer.md +861 -861
  24. package/templates/agents/code-reviewer.md +163 -163
  25. package/templates/agents/db-reader.md +149 -149
  26. package/templates/agents/docs-context-reader.md +143 -143
  27. package/templates/agents/docs-sync-checker.md +122 -122
  28. package/templates/agents/efcore/conflicts.md +95 -84
  29. package/templates/agents/efcore/db-deploy.md +85 -74
  30. package/templates/agents/efcore/db-reset.md +96 -85
  31. package/templates/agents/efcore/db-seed.md +72 -61
  32. package/templates/agents/efcore/db-status.md +97 -86
  33. package/templates/agents/efcore/migration.md +197 -186
  34. package/templates/agents/efcore/rebase-snapshot.md +119 -108
  35. package/templates/agents/efcore/scan.md +103 -92
  36. package/templates/agents/efcore/squash.md +172 -161
  37. package/templates/agents/explore-codebase.md +66 -66
  38. package/templates/agents/explore-docs.md +98 -98
  39. package/templates/agents/fix-grammar.md +50 -50
  40. package/templates/agents/gitflow/abort.md +45 -45
  41. package/templates/agents/gitflow/cleanup.md +96 -96
  42. package/templates/agents/gitflow/commit.md +236 -236
  43. package/templates/agents/gitflow/exec.md +48 -48
  44. package/templates/agents/gitflow/finish.md +146 -146
  45. package/templates/agents/gitflow/init-clone.md +199 -199
  46. package/templates/agents/gitflow/init-detect.md +137 -137
  47. package/templates/agents/gitflow/init-validate.md +225 -225
  48. package/templates/agents/gitflow/init.md +340 -340
  49. package/templates/agents/gitflow/merge.md +145 -145
  50. package/templates/agents/gitflow/plan.md +42 -42
  51. package/templates/agents/gitflow/pr.md +191 -191
  52. package/templates/agents/gitflow/review.md +49 -49
  53. package/templates/agents/gitflow/start.md +147 -147
  54. package/templates/agents/gitflow/status.md +95 -95
  55. package/templates/agents/mcp-healthcheck.md +163 -163
  56. package/templates/agents/snipper.md +37 -37
  57. package/templates/agents/websearch.md +46 -46
  58. package/templates/hooks/appsettings-guard.sh +76 -76
  59. package/templates/hooks/docs-drift-check.md +96 -96
  60. package/templates/hooks/ef-migration-check.md +139 -139
  61. package/templates/hooks/hooks.json +58 -58
  62. package/templates/hooks/mcp-check.md +64 -64
  63. package/templates/hooks/ralph-mcp-logger.sh +46 -46
  64. package/templates/hooks/ralph-session-end.sh +69 -69
  65. package/templates/hooks/stop-hook.sh +177 -177
  66. package/templates/hooks/wsl-dotnet-cleanup.sh +24 -24
  67. package/templates/mcp-scaffolding/component.tsx.hbs +318 -318
  68. package/templates/mcp-scaffolding/controller.cs.hbs +192 -192
  69. package/templates/mcp-scaffolding/entity-extension.cs.hbs +239 -239
  70. package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +116 -116
  71. package/templates/mcp-scaffolding/frontend/nav-routes.ts.hbs +133 -133
  72. package/templates/mcp-scaffolding/frontend/routes.tsx.hbs +126 -126
  73. package/templates/mcp-scaffolding/migrations/seed-roles.cs.hbs +261 -261
  74. package/templates/mcp-scaffolding/service-extension.cs.hbs +53 -53
  75. package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +436 -436
  76. package/templates/mcp-scaffolding/tests/entity.test.cs.hbs +239 -239
  77. package/templates/mcp-scaffolding/tests/repository.test.cs.hbs +441 -441
  78. package/templates/mcp-scaffolding/tests/security.test.cs.hbs +442 -442
  79. package/templates/mcp-scaffolding/tests/service.test.cs.hbs +402 -402
  80. package/templates/mcp-scaffolding/tests/validator.test.cs.hbs +428 -428
  81. package/templates/project/DependencyInjection.Application.cs.template +25 -25
  82. package/templates/project/DependencyInjection.Infrastructure.cs.template +61 -61
  83. package/templates/project/DesignTimeExtensionsDbContextFactory.cs.template +70 -70
  84. package/templates/project/ExampleEntity.cs.template +116 -116
  85. package/templates/project/ExampleEntityConfiguration.cs.template +64 -64
  86. package/templates/project/ExampleService.cs.template +146 -146
  87. package/templates/project/ExtensionsDbContext.cs.template +41 -41
  88. package/templates/project/IExtensionsDbContext.cs.template +22 -22
  89. package/templates/project/Program.cs.template +47 -47
  90. package/templates/project/README.md +79 -79
  91. package/templates/project/api.ts.template +12 -12
  92. package/templates/project/appsettings.json.template +170 -170
  93. package/templates/project/claude-settings.json.template +5 -5
  94. package/templates/project/test-frontend/msw/handlers.ts +58 -58
  95. package/templates/project/test-frontend/msw/server.ts +25 -25
  96. package/templates/project/test-frontend/setup.ts +16 -16
  97. package/templates/project/test-frontend/test-utils.tsx +59 -59
  98. package/templates/project/test-frontend/vitest.config.ts +31 -31
  99. package/templates/ralph/README.md +93 -93
  100. package/templates/ralph/ralph.config.yaml +113 -113
  101. package/templates/scripts/setup-ralph-loop.sh +173 -173
  102. package/templates/skills/_resources/config-safety.md +61 -61
  103. package/templates/skills/_resources/context-digest-template.md +53 -53
  104. package/templates/skills/_resources/doc-context-cache.md +60 -60
  105. package/templates/skills/_resources/docs-manifest-schema.md +155 -155
  106. package/templates/skills/_resources/formatting-guide.md +124 -124
  107. package/templates/skills/_resources/mcp-validate-documentation-spec.md +181 -181
  108. package/templates/skills/_shared.md +228 -228
  109. package/templates/skills/admin/SKILL.md +48 -48
  110. package/templates/skills/ai-prompt/SKILL.md +107 -107
  111. package/templates/skills/ai-prompt/steps/step-00-init.md +47 -47
  112. package/templates/skills/ai-prompt/steps/step-01-implementation.md +122 -122
  113. package/templates/skills/apex/SKILL.md +168 -168
  114. package/templates/skills/apex/_shared.md +141 -141
  115. package/templates/skills/apex/references/agent-teams-protocol.md +164 -164
  116. package/templates/skills/apex/references/analysis-methods.md +141 -141
  117. package/templates/skills/apex/references/challenge-questions.md +145 -145
  118. package/templates/skills/apex/references/code-generation.md +412 -412
  119. package/templates/skills/apex/references/core-seed-data.md +1437 -1437
  120. package/templates/skills/apex/references/error-classification.md +144 -144
  121. package/templates/skills/apex/references/examine-build-validation.md +82 -82
  122. package/templates/skills/apex/references/execution-frontend-gates.md +177 -177
  123. package/templates/skills/apex/references/execution-frontend-patterns.md +105 -105
  124. package/templates/skills/apex/references/execution-layer1-rules.md +96 -96
  125. package/templates/skills/apex/references/initialization-challenge-flow.md +110 -110
  126. package/templates/skills/apex/references/planning-layer-mapping.md +151 -151
  127. package/templates/skills/apex/references/post-checks.md +1584 -1584
  128. package/templates/skills/apex/references/smartstack-api.md +1053 -1053
  129. package/templates/skills/apex/references/smartstack-frontend.md +1571 -1571
  130. package/templates/skills/apex/references/smartstack-layers.md +402 -402
  131. package/templates/skills/apex/steps/step-00-init.md +307 -307
  132. package/templates/skills/apex/steps/step-01-analyze.md +165 -165
  133. package/templates/skills/apex/steps/step-02-plan.md +144 -144
  134. package/templates/skills/apex/steps/step-03-execute.md +328 -328
  135. package/templates/skills/apex/steps/step-04-examine.md +263 -263
  136. package/templates/skills/apex/steps/step-05-deep-review.md +129 -129
  137. package/templates/skills/apex/steps/step-06-resolve.md +101 -101
  138. package/templates/skills/apex/steps/step-07-tests.md +238 -238
  139. package/templates/skills/apex/steps/step-08-run-tests.md +125 -125
  140. package/templates/skills/application/SKILL.md +4 -4
  141. package/templates/skills/application/references/application-roles-template.md +227 -227
  142. package/templates/skills/application/references/backend-controller-hierarchy.md +58 -58
  143. package/templates/skills/application/references/backend-entity-seeding.md +72 -72
  144. package/templates/skills/application/references/backend-seeding-and-dto-output.md +83 -83
  145. package/templates/skills/application/references/backend-table-prefix-mapping.md +79 -79
  146. package/templates/skills/application/references/backend-verification.md +88 -88
  147. package/templates/skills/application/references/frontend-i18n-and-output.md +67 -67
  148. package/templates/skills/application/references/frontend-route-naming.md +117 -117
  149. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +107 -107
  150. package/templates/skills/application/references/frontend-verification.md +156 -156
  151. package/templates/skills/application/references/migration-checklist-troubleshooting.md +1 -1
  152. package/templates/skills/application/references/provider-template.md +177 -177
  153. package/templates/skills/application/references/roles-client-project-handling.md +55 -55
  154. package/templates/skills/application/references/roles-fallback-procedure.md +149 -149
  155. package/templates/skills/application/references/test-coverage-requirements.md +213 -213
  156. package/templates/skills/application/references/test-frontend.md +73 -73
  157. package/templates/skills/application/references/test-prerequisites.md +72 -72
  158. package/templates/skills/application/steps/step-05-frontend.md +176 -176
  159. package/templates/skills/application/steps/step-06-migration.md +193 -193
  160. package/templates/skills/application/steps/step-07-tests.md +356 -356
  161. package/templates/skills/application/steps/step-08-documentation.md +137 -137
  162. package/templates/skills/application/templates-backend.md +463 -463
  163. package/templates/skills/application/templates-frontend.md +685 -685
  164. package/templates/skills/application/templates-i18n.md +520 -520
  165. package/templates/skills/application/templates-seed.md +1096 -1096
  166. package/templates/skills/business-analyse/SKILL.md +327 -327
  167. package/templates/skills/business-analyse/_architecture.md +123 -123
  168. package/templates/skills/business-analyse/_elicitation.md +206 -206
  169. package/templates/skills/business-analyse/_module-loop.md +115 -115
  170. package/templates/skills/business-analyse/_shared.md +383 -383
  171. package/templates/skills/business-analyse/_suggestions.md +34 -34
  172. package/templates/skills/business-analyse/html/ba-interactive.html +4477 -4477
  173. package/templates/skills/business-analyse/html/build-html.js +77 -77
  174. package/templates/skills/business-analyse/html/src/scripts/01-data-init.js +150 -150
  175. package/templates/skills/business-analyse/html/src/scripts/02-navigation.js +227 -227
  176. package/templates/skills/business-analyse/html/src/scripts/03-render-cadrage.js +199 -199
  177. package/templates/skills/business-analyse/html/src/scripts/04-render-modules.js +205 -205
  178. package/templates/skills/business-analyse/html/src/scripts/05-render-specs.js +647 -647
  179. package/templates/skills/business-analyse/html/src/scripts/06-render-consolidation.js +195 -195
  180. package/templates/skills/business-analyse/html/src/scripts/07-render-handoff.js +92 -92
  181. package/templates/skills/business-analyse/html/src/scripts/08-editing.js +135 -135
  182. package/templates/skills/business-analyse/html/src/scripts/09-export.js +168 -168
  183. package/templates/skills/business-analyse/html/src/scripts/10-comments.js +171 -171
  184. package/templates/skills/business-analyse/html/src/scripts/11-review-panel.js +166 -166
  185. package/templates/skills/business-analyse/html/src/styles/01-variables.css +38 -38
  186. package/templates/skills/business-analyse/html/src/styles/02-layout.css +101 -101
  187. package/templates/skills/business-analyse/html/src/styles/03-navigation.css +120 -120
  188. package/templates/skills/business-analyse/html/src/styles/04-cards.css +196 -196
  189. package/templates/skills/business-analyse/html/src/styles/05-modules.css +454 -454
  190. package/templates/skills/business-analyse/html/src/styles/06-wireframes.css +272 -272
  191. package/templates/skills/business-analyse/html/src/styles/07-comments.css +184 -184
  192. package/templates/skills/business-analyse/html/src/styles/08-review-panel.css +241 -241
  193. package/templates/skills/business-analyse/html/src/template.html +516 -516
  194. package/templates/skills/business-analyse/patterns/suggestion-catalog.md +546 -546
  195. package/templates/skills/business-analyse/questionnaire/00-application.md +160 -160
  196. package/templates/skills/business-analyse/questionnaire/00b-project.md +85 -85
  197. package/templates/skills/business-analyse/questionnaire/01-context.md +185 -185
  198. package/templates/skills/business-analyse/questionnaire/02-stakeholders.md +189 -189
  199. package/templates/skills/business-analyse/questionnaire/03-scope.md +164 -164
  200. package/templates/skills/business-analyse/questionnaire/04-data.md +88 -88
  201. package/templates/skills/business-analyse/questionnaire/05-integrations.md +58 -58
  202. package/templates/skills/business-analyse/questionnaire/06-security.md +68 -68
  203. package/templates/skills/business-analyse/questionnaire/07-ui.md +76 -76
  204. package/templates/skills/business-analyse/questionnaire/08-performance.md +42 -42
  205. package/templates/skills/business-analyse/questionnaire/09-constraints.md +45 -45
  206. package/templates/skills/business-analyse/questionnaire/10-documentation.md +43 -43
  207. package/templates/skills/business-analyse/questionnaire/11-data-lifecycle.md +59 -59
  208. package/templates/skills/business-analyse/questionnaire/12-migration.md +58 -58
  209. package/templates/skills/business-analyse/questionnaire/13-cross-module.md +69 -69
  210. package/templates/skills/business-analyse/questionnaire/14-risk-assumptions.md +135 -135
  211. package/templates/skills/business-analyse/questionnaire/15-success-metrics.md +136 -136
  212. package/templates/skills/business-analyse/questionnaire.md +337 -337
  213. package/templates/skills/business-analyse/react/application-viewer.md +242 -242
  214. package/templates/skills/business-analyse/react/components.md +551 -551
  215. package/templates/skills/business-analyse/react/i18n-template.md +306 -306
  216. package/templates/skills/business-analyse/references/acceptance-criteria.md +169 -169
  217. package/templates/skills/business-analyse/references/agent-module-prompt.md +362 -362
  218. package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +557 -557
  219. package/templates/skills/business-analyse/references/analysis-semantic-checks.md +190 -190
  220. package/templates/skills/business-analyse/references/cache-warming-strategy.md +566 -566
  221. package/templates/skills/business-analyse/references/cadrage-challenge-patterns.md +41 -41
  222. package/templates/skills/business-analyse/references/cadrage-coverage-matrix.md +74 -74
  223. package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +115 -115
  224. package/templates/skills/business-analyse/references/cadrage-shared-modules.md +68 -69
  225. package/templates/skills/business-analyse/references/cadrage-structure-cards.md +85 -85
  226. package/templates/skills/business-analyse/references/compilation-structure-cards.md +297 -297
  227. package/templates/skills/business-analyse/references/consolidation-structural-checks.md +107 -107
  228. package/templates/skills/business-analyse/references/deploy-data-build.md +180 -180
  229. package/templates/skills/business-analyse/references/deploy-modes.md +118 -118
  230. package/templates/skills/business-analyse/references/detection-strategies.md +424 -424
  231. package/templates/skills/business-analyse/references/entity-architecture-decision.md +218 -218
  232. package/templates/skills/business-analyse/references/handoff-file-templates.md +120 -120
  233. package/templates/skills/business-analyse/references/handoff-mappings.md +81 -81
  234. package/templates/skills/business-analyse/references/handoff-seeddata-generation.md +312 -312
  235. package/templates/skills/business-analyse/references/html-data-mapping.md +299 -299
  236. package/templates/skills/business-analyse/references/init-schema-deployment.md +65 -65
  237. package/templates/skills/business-analyse/references/naming-conventions.md +243 -243
  238. package/templates/skills/business-analyse/references/prd-generation.md +258 -258
  239. package/templates/skills/business-analyse/references/review-data-mapping.md +363 -363
  240. package/templates/skills/business-analyse/references/robustness-checks.md +542 -542
  241. package/templates/skills/business-analyse/references/spec-auto-inference.md +111 -111
  242. package/templates/skills/business-analyse/references/team-orchestration.md +1022 -1022
  243. package/templates/skills/business-analyse/references/ui-dashboard-spec.md +85 -85
  244. package/templates/skills/business-analyse/references/ui-resource-cards.md +259 -259
  245. package/templates/skills/business-analyse/references/validate-incremental-html.md +121 -121
  246. package/templates/skills/business-analyse/references/validation-checklist.md +347 -347
  247. package/templates/skills/business-analyse/references/wireframe-svg-style-guide.md +335 -335
  248. package/templates/skills/business-analyse/schemas/application-schema.json +453 -453
  249. package/templates/skills/business-analyse/schemas/feature-schema.json +53 -53
  250. package/templates/skills/business-analyse/schemas/project-schema.json +485 -485
  251. package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +201 -201
  252. package/templates/skills/business-analyse/schemas/sections/discovery-schema.json +82 -82
  253. package/templates/skills/business-analyse/schemas/sections/handoff-schema.json +80 -80
  254. package/templates/skills/business-analyse/schemas/sections/metadata-schema.json +70 -70
  255. package/templates/skills/business-analyse/schemas/sections/specification-schema.json +547 -547
  256. package/templates/skills/business-analyse/schemas/sections/validation-schema.json +93 -93
  257. package/templates/skills/business-analyse/schemas/shared/common-defs.json +226 -226
  258. package/templates/skills/business-analyse/steps/step-00-init.md +575 -576
  259. package/templates/skills/business-analyse/steps/step-01-cadrage.md +767 -767
  260. package/templates/skills/business-analyse/steps/step-01b-applications.md +419 -419
  261. package/templates/skills/business-analyse/steps/step-02-decomposition.md +387 -387
  262. package/templates/skills/business-analyse/steps/step-03a-data.md +16 -16
  263. package/templates/skills/business-analyse/steps/step-03a1-setup.md +506 -506
  264. package/templates/skills/business-analyse/steps/step-03a2-analysis.md +252 -252
  265. package/templates/skills/business-analyse/steps/step-03b-ui.md +425 -425
  266. package/templates/skills/business-analyse/steps/step-03c-compile.md +611 -611
  267. package/templates/skills/business-analyse/steps/step-03d-validate.md +783 -783
  268. package/templates/skills/business-analyse/steps/step-04-consolidation.md +17 -17
  269. package/templates/skills/business-analyse/steps/step-04a-collect.md +415 -415
  270. package/templates/skills/business-analyse/steps/step-04b-analyze.md +163 -163
  271. package/templates/skills/business-analyse/steps/step-04c-decide.md +186 -186
  272. package/templates/skills/business-analyse/steps/step-05a-handoff.md +840 -840
  273. package/templates/skills/business-analyse/steps/step-05b-deploy.md +522 -522
  274. package/templates/skills/business-analyse/steps/step-05c-ralph-readiness.md +703 -703
  275. package/templates/skills/business-analyse/steps/step-06-review.md +278 -278
  276. package/templates/skills/business-analyse/templates/tpl-frd.md +168 -168
  277. package/templates/skills/business-analyse/templates/tpl-handoff.md +186 -186
  278. package/templates/skills/business-analyse/templates/tpl-launch-displays.md +59 -59
  279. package/templates/skills/business-analyse/templates/tpl-progress.md +172 -172
  280. package/templates/skills/business-analyse/templates-frd.md +476 -476
  281. package/templates/skills/business-analyse/templates-react.md +574 -574
  282. package/templates/skills/cc-agent/SKILL.md +129 -129
  283. package/templates/skills/cc-agent/references/agent-behavior-patterns.md +95 -95
  284. package/templates/skills/cc-agent/references/agent-frontmatter.md +213 -213
  285. package/templates/skills/cc-agent/references/permission-modes.md +102 -102
  286. package/templates/skills/cc-agent/references/tools-reference.md +144 -144
  287. package/templates/skills/cc-agent/steps/step-00-init.md +134 -134
  288. package/templates/skills/cc-agent/steps/step-01-design.md +186 -186
  289. package/templates/skills/cc-agent/steps/step-02-generate.md +131 -131
  290. package/templates/skills/cc-agent/steps/step-03-validate.md +130 -130
  291. package/templates/skills/cc-agent/templates/agent-categorized.md +67 -67
  292. package/templates/skills/cc-agent/templates/agent-standalone.md +56 -56
  293. package/templates/skills/cc-agent/templates/agent-with-skills.md +94 -94
  294. package/templates/skills/cc-audit/SKILL.md +108 -108
  295. package/templates/skills/cc-audit/references/agent-checklist.md +91 -91
  296. package/templates/skills/cc-audit/references/hook-checklist.md +110 -110
  297. package/templates/skills/cc-audit/references/skill-checklist.md +70 -70
  298. package/templates/skills/cc-audit/steps/step-00-init.md +98 -98
  299. package/templates/skills/cc-audit/steps/step-01-scan.md +142 -142
  300. package/templates/skills/cc-audit/steps/step-02-analyze.md +158 -158
  301. package/templates/skills/cc-audit/steps/step-03-report.md +142 -142
  302. package/templates/skills/cc-skill/SKILL.md +134 -134
  303. package/templates/skills/cc-skill/references/best-practices.md +167 -167
  304. package/templates/skills/cc-skill/references/frontmatter-reference.md +182 -182
  305. package/templates/skills/cc-skill/references/skill-patterns.md +199 -199
  306. package/templates/skills/cc-skill/steps/step-00-init.md +119 -119
  307. package/templates/skills/cc-skill/steps/step-01-design.md +199 -199
  308. package/templates/skills/cc-skill/steps/step-02-generate.md +145 -145
  309. package/templates/skills/cc-skill/steps/step-03-steps.md +151 -151
  310. package/templates/skills/cc-skill/steps/step-04-validate.md +124 -124
  311. package/templates/skills/cc-skill/templates/skill-forked.md +85 -85
  312. package/templates/skills/cc-skill/templates/skill-progressive.md +102 -102
  313. package/templates/skills/cc-skill/templates/skill-simple.md +75 -75
  314. package/templates/skills/cc-skill/templates/step-template.md +82 -82
  315. package/templates/skills/check-version/SKILL.md +196 -196
  316. package/templates/skills/controller/SKILL.md +162 -162
  317. package/templates/skills/controller/postman-templates.md +614 -614
  318. package/templates/skills/controller/references/controller-code-templates.md +159 -159
  319. package/templates/skills/controller/references/mcp-scaffold-workflow.md +209 -209
  320. package/templates/skills/controller/references/permission-sync-templates.md +149 -149
  321. package/templates/skills/controller/steps/step-00-init.md +193 -191
  322. package/templates/skills/controller/steps/step-01-analyze.md +146 -146
  323. package/templates/skills/controller/steps/step-02-plan.md +176 -176
  324. package/templates/skills/controller/steps/step-03-generate.md +189 -189
  325. package/templates/skills/controller/steps/step-04-perms.md +80 -80
  326. package/templates/skills/controller/steps/step-05-validate.md +107 -107
  327. package/templates/skills/controller/templates.md +1555 -1555
  328. package/templates/skills/debug/SKILL.md +70 -70
  329. package/templates/skills/debug/references/team-protocol.md +232 -232
  330. package/templates/skills/debug/steps/step-00-init.md +57 -57
  331. package/templates/skills/debug/steps/step-01-analyze.md +219 -219
  332. package/templates/skills/debug/steps/step-02-resolve.md +85 -85
  333. package/templates/skills/documentation/SKILL.md +132 -132
  334. package/templates/skills/documentation/data-schema.md +227 -227
  335. package/templates/skills/documentation/steps/step-00-init.md +70 -70
  336. package/templates/skills/documentation/steps/step-01-scan.md +113 -113
  337. package/templates/skills/documentation/steps/step-02-generate.md +231 -231
  338. package/templates/skills/documentation/steps/step-03-validate.md +251 -238
  339. package/templates/skills/documentation/templates.md +662 -663
  340. package/templates/skills/efcore/SKILL.md +168 -167
  341. package/templates/skills/efcore/references/both-contexts.md +32 -32
  342. package/templates/skills/efcore/references/database-operations.md +67 -67
  343. package/templates/skills/efcore/references/destructive-operations.md +38 -38
  344. package/templates/skills/efcore/references/reset-operations.md +81 -81
  345. package/templates/skills/efcore/references/seed-methods.md +86 -86
  346. package/templates/skills/efcore/references/shared-init-functions.md +250 -250
  347. package/templates/skills/efcore/references/sql-objects-injection.md +61 -61
  348. package/templates/skills/efcore/references/troubleshooting.md +81 -81
  349. package/templates/skills/efcore/references/zero-downtime-patterns.md +227 -227
  350. package/templates/skills/efcore/steps/db/step-deploy.md +217 -217
  351. package/templates/skills/efcore/steps/db/step-reset.md +186 -186
  352. package/templates/skills/efcore/steps/db/step-seed.md +166 -166
  353. package/templates/skills/efcore/steps/db/step-status.md +173 -173
  354. package/templates/skills/efcore/steps/migration/step-00-init.md +102 -102
  355. package/templates/skills/efcore/steps/migration/step-01-check.md +164 -164
  356. package/templates/skills/efcore/steps/migration/step-02-create.md +160 -160
  357. package/templates/skills/efcore/steps/migration/step-03-validate.md +168 -168
  358. package/templates/skills/efcore/steps/rebase-snapshot/step-00-init.md +173 -173
  359. package/templates/skills/efcore/steps/rebase-snapshot/step-01-backup.md +100 -100
  360. package/templates/skills/efcore/steps/rebase-snapshot/step-02-fetch.md +115 -115
  361. package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +112 -112
  362. package/templates/skills/efcore/steps/rebase-snapshot/step-04-validate.md +157 -157
  363. package/templates/skills/efcore/steps/shared/step-00-init.md +131 -131
  364. package/templates/skills/efcore/steps/squash/step-00-init.md +141 -141
  365. package/templates/skills/efcore/steps/squash/step-01-backup.md +120 -120
  366. package/templates/skills/efcore/steps/squash/step-02-fetch.md +168 -168
  367. package/templates/skills/efcore/steps/squash/step-03-create.md +184 -184
  368. package/templates/skills/efcore/steps/squash/step-04-validate.md +174 -174
  369. package/templates/skills/explore/SKILL.md +98 -98
  370. package/templates/skills/feature-full/SKILL.md +111 -111
  371. package/templates/skills/feature-full/steps/step-00-init.md +57 -57
  372. package/templates/skills/feature-full/steps/step-01-implementation.md +120 -120
  373. package/templates/skills/gitflow/SKILL.md +377 -377
  374. package/templates/skills/gitflow/_shared.md +620 -620
  375. package/templates/skills/gitflow/phases/abort.md +189 -189
  376. package/templates/skills/gitflow/phases/cleanup.md +234 -234
  377. package/templates/skills/gitflow/phases/status.md +192 -192
  378. package/templates/skills/gitflow/references/commit-message-generation.md +58 -58
  379. package/templates/skills/gitflow/references/commit-migration-validation.md +49 -49
  380. package/templates/skills/gitflow/references/finish-cleanup.md +55 -55
  381. package/templates/skills/gitflow/references/finish-version-bumping.md +45 -45
  382. package/templates/skills/gitflow/references/init-config-template.md +135 -135
  383. package/templates/skills/gitflow/references/init-environment-detection.md +41 -41
  384. package/templates/skills/gitflow/references/init-name-normalization.md +103 -103
  385. package/templates/skills/gitflow/references/init-questions.md +185 -185
  386. package/templates/skills/gitflow/references/init-structure-creation.md +75 -75
  387. package/templates/skills/gitflow/references/init-version-detection.md +21 -21
  388. package/templates/skills/gitflow/references/init-workspace-detection.md +43 -43
  389. package/templates/skills/gitflow/references/merge-ci-status.md +36 -36
  390. package/templates/skills/gitflow/references/merge-execution.md +62 -62
  391. package/templates/skills/gitflow/references/merge-pr-context.md +76 -76
  392. package/templates/skills/gitflow/references/plan-template.md +69 -69
  393. package/templates/skills/gitflow/references/pr-build-checks.md +60 -60
  394. package/templates/skills/gitflow/references/pr-generation.md +58 -58
  395. package/templates/skills/gitflow/references/start-branch-normalization.md +28 -28
  396. package/templates/skills/gitflow/references/start-efcore-preflight.md +70 -70
  397. package/templates/skills/gitflow/references/start-local-config.md +113 -113
  398. package/templates/skills/gitflow/references/start-worktree-creation.md +50 -50
  399. package/templates/skills/gitflow/references/sync-push-verify.md +44 -44
  400. package/templates/skills/gitflow/references/sync-rebase-conflicts.md +38 -38
  401. package/templates/skills/gitflow/steps/step-commit.md +199 -199
  402. package/templates/skills/gitflow/steps/step-finish.md +147 -147
  403. package/templates/skills/gitflow/steps/step-init.md +190 -190
  404. package/templates/skills/gitflow/steps/step-merge.md +85 -85
  405. package/templates/skills/gitflow/steps/step-plan.md +151 -151
  406. package/templates/skills/gitflow/steps/step-pr.md +199 -199
  407. package/templates/skills/gitflow/steps/step-start.md +195 -195
  408. package/templates/skills/gitflow/steps/step-sync.md +161 -161
  409. package/templates/skills/gitflow/templates/config.json +72 -72
  410. package/templates/skills/mcp/SKILL.md +62 -62
  411. package/templates/skills/mcp/steps/step-01-healthcheck.md +108 -108
  412. package/templates/skills/mcp/steps/step-02-tools.md +73 -73
  413. package/templates/skills/notification/SKILL.md +173 -173
  414. package/templates/skills/quick-search/SKILL.md +99 -99
  415. package/templates/skills/ralph-loop/SKILL.md +234 -234
  416. package/templates/skills/ralph-loop/references/category-completeness.md +185 -185
  417. package/templates/skills/ralph-loop/references/category-rules.md +96 -96
  418. package/templates/skills/ralph-loop/references/compact-loop.md +300 -300
  419. package/templates/skills/ralph-loop/references/init-resume-recovery.md +127 -127
  420. package/templates/skills/ralph-loop/references/module-transition.md +151 -151
  421. package/templates/skills/ralph-loop/references/multi-module-queue.md +171 -171
  422. package/templates/skills/ralph-loop/references/parallel-execution.md +246 -246
  423. package/templates/skills/ralph-loop/references/section-splitting.md +439 -439
  424. package/templates/skills/ralph-loop/references/task-transform-legacy.md +256 -256
  425. package/templates/skills/ralph-loop/references/team-orchestration.md +547 -547
  426. package/templates/skills/ralph-loop/steps/step-00-init.md +150 -150
  427. package/templates/skills/ralph-loop/steps/step-01-task.md +174 -174
  428. package/templates/skills/ralph-loop/steps/step-02-execute.md +177 -177
  429. package/templates/skills/ralph-loop/steps/step-03-commit.md +92 -92
  430. package/templates/skills/ralph-loop/steps/step-04-check.md +207 -207
  431. package/templates/skills/ralph-loop/steps/step-05-report.md +175 -175
  432. package/templates/skills/refactor/SKILL.md +56 -56
  433. package/templates/skills/refactor/steps/step-01-discover.md +60 -60
  434. package/templates/skills/refactor/steps/step-02-execute.md +67 -67
  435. package/templates/skills/review-code/SKILL.md +95 -94
  436. package/templates/skills/review-code/references/clean-code-principles.md +292 -292
  437. package/templates/skills/review-code/references/code-quality-metrics.md +174 -174
  438. package/templates/skills/review-code/references/feedback-patterns.md +149 -149
  439. package/templates/skills/review-code/references/owasp-api-top10.md +243 -243
  440. package/templates/skills/review-code/references/security-checklist.md +212 -212
  441. package/templates/skills/review-code/steps/step-01-smartstack.md +96 -96
  442. package/templates/skills/review-code/steps/step-02-detailed-review.md +80 -80
  443. package/templates/skills/review-code/steps/step-03-react.md +44 -44
  444. package/templates/skills/ui-components/SKILL.md +137 -137
  445. package/templates/skills/ui-components/accessibility.md +170 -170
  446. package/templates/skills/ui-components/patterns/dashboard-chart.md +327 -327
  447. package/templates/skills/ui-components/patterns/data-table.md +39 -39
  448. package/templates/skills/ui-components/patterns/entity-card.md +77 -77
  449. package/templates/skills/ui-components/patterns/grid-layout.md +91 -91
  450. package/templates/skills/ui-components/patterns/kanban.md +43 -43
  451. package/templates/skills/ui-components/responsive-guidelines.md +278 -278
  452. package/templates/skills/ui-components/style-guide.md +113 -113
  453. package/templates/skills/utils/SKILL.md +44 -44
  454. package/templates/skills/utils/subcommands/test-web-config.md +152 -152
  455. package/templates/skills/utils/subcommands/test-web.md +123 -123
  456. package/templates/skills/validate/SKILL.md +181 -181
  457. package/templates/skills/validate-feature/SKILL.md +101 -101
  458. package/templates/skills/validate-feature/references/api-smoke-tests.md +140 -140
  459. package/templates/skills/validate-feature/references/db-validation-checks.md +180 -180
  460. package/templates/skills/validate-feature/steps/step-00-dependencies.md +121 -121
  461. package/templates/skills/validate-feature/steps/step-01-compile.md +39 -39
  462. package/templates/skills/validate-feature/steps/step-02-unit-tests.md +45 -45
  463. package/templates/skills/validate-feature/steps/step-03-integration-tests.md +53 -53
  464. package/templates/skills/validate-feature/steps/step-04-api-smoke.md +94 -94
  465. package/templates/skills/validate-feature/steps/step-05-db-validation.md +149 -149
  466. package/templates/skills/workflow/SKILL.md +127 -127
  467. package/templates/skills/workflow/steps/step-00-init.md +57 -57
  468. package/templates/skills/workflow/steps/step-01-implementation.md +84 -84
  469. package/templates/test-web/api-health.json +38 -38
  470. package/templates/test-web/minimal.json +19 -19
  471. package/templates/test-web/npm-package.json +46 -46
  472. 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.**