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