@atlashub/smartstack-cli 4.31.0 → 4.33.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 (41) hide show
  1. package/.documentation/commands.html +952 -116
  2. package/.documentation/index.html +2 -2
  3. package/.documentation/init.html +358 -174
  4. package/dist/mcp-entry.mjs +271 -44
  5. package/dist/mcp-entry.mjs.map +1 -1
  6. package/package.json +1 -1
  7. package/templates/mcp-scaffolding/controller.cs.hbs +54 -128
  8. package/templates/project/README.md +19 -0
  9. package/templates/skills/apex/SKILL.md +16 -10
  10. package/templates/skills/apex/_shared.md +1 -1
  11. package/templates/skills/apex/references/checks/architecture-checks.sh +154 -0
  12. package/templates/skills/apex/references/checks/backend-checks.sh +194 -0
  13. package/templates/skills/apex/references/checks/frontend-checks.sh +448 -0
  14. package/templates/skills/apex/references/checks/infrastructure-checks.sh +255 -0
  15. package/templates/skills/apex/references/checks/security-checks.sh +153 -0
  16. package/templates/skills/apex/references/checks/seed-checks.sh +536 -0
  17. package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +49 -192
  18. package/templates/skills/apex/references/parallel-execution.md +18 -5
  19. package/templates/skills/apex/references/post-checks.md +124 -2156
  20. package/templates/skills/apex/references/smartstack-api.md +160 -957
  21. package/templates/skills/apex/references/smartstack-frontend-compliance.md +23 -1
  22. package/templates/skills/apex/references/smartstack-frontend.md +134 -1022
  23. package/templates/skills/apex/references/smartstack-layers.md +12 -6
  24. package/templates/skills/apex/steps/step-00-init.md +81 -238
  25. package/templates/skills/apex/steps/step-03-execute.md +25 -751
  26. package/templates/skills/apex/steps/step-03a-layer0-domain.md +118 -0
  27. package/templates/skills/apex/steps/step-03b-layer1-seed.md +91 -0
  28. package/templates/skills/apex/steps/step-03c-layer2-backend.md +240 -0
  29. package/templates/skills/apex/steps/step-03d-layer3-frontend.md +300 -0
  30. package/templates/skills/apex/steps/step-03e-layer4-devdata.md +44 -0
  31. package/templates/skills/apex/steps/step-04-examine.md +70 -150
  32. package/templates/skills/application/references/frontend-i18n-and-output.md +2 -2
  33. package/templates/skills/application/references/frontend-route-naming.md +5 -1
  34. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +49 -198
  35. package/templates/skills/application/references/frontend-verification.md +11 -11
  36. package/templates/skills/application/steps/step-05-frontend.md +26 -15
  37. package/templates/skills/application/templates-frontend.md +4 -0
  38. package/templates/skills/cli-app-sync/SKILL.md +2 -2
  39. package/templates/skills/cli-app-sync/references/comparison-map.md +1 -1
  40. package/templates/skills/controller/references/controller-code-templates.md +70 -67
  41. package/templates/skills/controller/references/mcp-scaffold-workflow.md +5 -1
@@ -1,2165 +1,134 @@
1
- # POST-CHECKs (Security + Convention + Architecture)
1
+ # POST-CHECKs Compact Reference
2
2
 
3
3
  > **Referenced by:** step-04-examine.md (section 6b)
4
- > These checks run on the actual generated files. Model-interpreted checks are unreliable.
4
+ > **Execution:** Run .sh scripts from `references/checks/`. Each script outputs BLOCKING/WARNING/OK per check.
5
5
 
6
6
  ## Run MCP Tools FIRST
7
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
- ### POST-CHECK S7: Controllers must NOT use Guid.Empty for tenantId/userId (OWASP A01)
121
-
122
- ```bash
123
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
124
- if [ -n "$CTRL_FILES" ]; then
125
- BAD_GUID=$(grep -Pn 'Guid\.Empty' $CTRL_FILES 2>/dev/null)
126
- if [ -n "$BAD_GUID" ]; then
127
- echo "BLOCKING (OWASP A01): Controller uses Guid.Empty — tenant isolation bypassed"
128
- echo "$BAD_GUID"
129
- echo "Fix: Use _currentTenant.TenantId from ICurrentTenantService"
130
- exit 1
131
- fi
132
- fi
133
- ```
134
-
135
- ### POST-CHECK S8: Write endpoints must NOT use Read permissions (BLOCKING)
136
-
137
- > **Source:** audit ba-002 finding C1 — Cancel endpoint (POST) used `Permissions.Absences.Read`,
138
- > allowing any reader to cancel absences. Write operations MUST use write permissions.
139
-
140
- ```bash
141
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
142
- if [ -n "$CTRL_FILES" ]; then
143
- for f in $CTRL_FILES; do
144
- # Extract blocks: find each method with Http verb + RequirePermission
145
- # Check POST/PUT/DELETE/PATCH endpoints that use *.Read permission
146
- WRITE_WITH_READ=$(grep -Pn '\[(HttpPost|HttpPut|HttpDelete|HttpPatch)' "$f" | while read -r line; do
147
- LINE_NUM=$(echo "$line" | cut -d: -f1)
148
- # Look within 5 lines for RequirePermission with .Read
149
- sed -n "$((LINE_NUM-2)),$((LINE_NUM+5))p" "$f" | grep -P 'RequirePermission.*\.Read\b' | head -1
150
- done)
151
- if [ -n "$WRITE_WITH_READ" ]; then
152
- echo "BLOCKING: Write endpoint uses Read permission in $f"
153
- echo "$WRITE_WITH_READ"
154
- echo "Fix: POST/PUT/DELETE/PATCH endpoints must use Create/Update/Delete permissions, never Read"
155
- exit 1
156
- fi
157
- done
158
- fi
159
- ```
160
-
161
- ### POST-CHECK S9: FK relationships must enforce tenant isolation (BLOCKING)
162
-
163
- > **Source:** audit ba-002 finding C5 — Department.ManagerId FK referenced Employee without
164
- > tenant constraint, allowing cross-tenant data leakage through navigation properties.
165
-
166
- ```bash
167
- CONFIG_FILES=$(find src/ -path "*/Configurations/*" -name "*Configuration.cs" 2>/dev/null)
168
- if [ -n "$CONFIG_FILES" ]; then
169
- for f in $CONFIG_FILES; do
170
- # Check if entity implements ITenantEntity
171
- ENTITY_NAME=$(grep -oP 'IEntityTypeConfiguration<(\w+)>' "$f" | grep -oP '<\K[^>]+')
172
- if [ -z "$ENTITY_NAME" ]; then continue; fi
173
-
174
- # Check if the entity is tenant-scoped (has TenantId in its properties or base class)
175
- ENTITY_FILE=$(find src/ -path "*/Domain/*" -name "${ENTITY_NAME}.cs" 2>/dev/null | head -1)
176
- if [ -z "$ENTITY_FILE" ]; then continue; fi
177
-
178
- IS_TENANT=$(grep -cE 'ITenantEntity|BaseEntity|TenantId' "$ENTITY_FILE")
179
- if [ "$IS_TENANT" -eq 0 ]; then continue; fi
180
-
181
- # For tenant entities, check FK relationships point to other tenant entities
182
- # Warning: FK to non-tenant entity from tenant entity = potential cross-tenant leak
183
- FK_TARGETS=$(grep -oP 'HasOne<(\w+)>|HasOne\(e\s*=>\s*e\.(\w+)\)' "$f" | grep -oP '\w+(?=>|\))' | sort -u)
184
- for TARGET in $FK_TARGETS; do
185
- TARGET_FILE=$(find src/ -path "*/Domain/*" -name "${TARGET}.cs" 2>/dev/null | head -1)
186
- if [ -n "$TARGET_FILE" ]; then
187
- TARGET_IS_TENANT=$(grep -cE 'ITenantEntity|BaseEntity|TenantId' "$TARGET_FILE")
188
- if [ "$TARGET_IS_TENANT" -gt 0 ]; then
189
- # Both are tenant entities — verify the FK has a composite or filtered relationship
190
- # At minimum, warn that cross-tenant reference is possible without query filter
191
- HAS_QUERY_FILTER=$(grep -c 'HasQueryFilter' "$f")
192
- if [ "$HAS_QUERY_FILTER" -eq 0 ]; then
193
- echo "WARNING: $f — FK from tenant entity $ENTITY_NAME to tenant entity $TARGET without HasQueryFilter"
194
- echo "Cross-tenant data leakage possible if FK is assigned without tenant validation"
195
- echo "Fix: Add tenant validation in service layer before assigning FK, or use composite FK (TenantId + EntityId)"
196
- fi
197
- fi
198
- fi
199
- done
200
- done
201
- fi
202
- ```
203
-
204
- ---
205
-
206
- ## Backend — Entity, Service & Controller Checks
207
-
208
- ### POST-CHECK V1: Controllers with POST/PUT must have corresponding Validators (BLOCKING)
209
-
210
- ```bash
211
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
212
- if [ -n "$CTRL_FILES" ]; then
213
- for f in $CTRL_FILES; do
214
- # Check if controller has POST or PUT endpoints
215
- HAS_WRITE=$(grep -cE "\[Http(Post|Put)\]" "$f")
216
- if [ "$HAS_WRITE" -gt 0 ]; then
217
- # Extract DTO names from POST/PUT method parameters
218
- DTOS=$(grep -oP '(?:Create|Update)\w+Dto' "$f" | sort -u)
219
- for DTO in $DTOS; do
220
- VALIDATOR_NAME=$(echo "$DTO" | sed 's/Dto$/Validator/')
221
- VALIDATOR_FILE=$(find src/ -path "*/Validators/*" -name "${VALIDATOR_NAME}.cs" 2>/dev/null)
222
- if [ -z "$VALIDATOR_FILE" ]; then
223
- echo "BLOCKING: Controller $f uses $DTO but no ${VALIDATOR_NAME}.cs found"
224
- echo "Fix: Create Validator with FluentValidation rules from business rules"
225
- exit 1
226
- fi
227
- done
228
- fi
229
- done
230
- fi
231
- ```
232
-
233
- ### POST-CHECK V2: Validators must be registered in DI (WARNING)
234
-
235
- ```bash
236
- VALIDATOR_FILES=$(find src/ -path "*/Validators/*" -name "*Validator.cs" 2>/dev/null)
237
- if [ -n "$VALIDATOR_FILES" ]; then
238
- DI_FILE=$(find src/ -name "DependencyInjection.cs" -o -name "ServiceCollectionExtensions.cs" 2>/dev/null | head -1)
239
- if [ -n "$DI_FILE" ]; then
240
- for f in $VALIDATOR_FILES; do
241
- VALIDATOR_NAME=$(basename "$f" .cs)
242
- if ! grep -q "$VALIDATOR_NAME" "$DI_FILE"; then
243
- echo "WARNING: Validator $VALIDATOR_NAME not registered in DI: $DI_FILE"
244
- fi
245
- done
246
- fi
247
- fi
248
- ```
249
-
250
- ### POST-CHECK C1: Navigation routes must be full paths starting with /
251
-
252
- ```bash
253
- # Find all seed data files and check route values
254
- SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
255
- if [ -n "$SEED_FILES" ]; then
256
- # Check for short routes (no leading /) in Create() calls for navigation entities
257
- BAD_ROUTES=$(grep -Pn 'NavigationApplication\.Create\(|NavigationModule\.Create\(|NavigationSection\.Create\(|NavigationResource\.Create\(' $SEED_FILES | grep -v '"/[a-z]')
258
- if [ -n "$BAD_ROUTES" ]; then
259
- echo "WARNING: Navigation routes must be full paths starting with /"
260
- echo "$BAD_ROUTES"
261
- echo "Expected: \"/human-resources\" NOT \"humanresources\""
262
- exit 0
263
- fi
264
- fi
265
- ```
266
-
267
- ### POST-CHECK C2: Seed data must not use deterministic/sequential/fixed GUIDs
268
-
269
- ```bash
270
- SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
271
- if [ -n "$SEED_FILES" ]; then
272
- BAD_GUIDS=$(grep -Pn 'GenerateDeterministicGuid|GenerateGuid\(int|11111111-1111-1111-1111-' $SEED_FILES 2>/dev/null)
273
- if [ -n "$BAD_GUIDS" ]; then
274
- echo "WARNING: Seed data must use Guid.NewGuid(), not deterministic/sequential/fixed GUIDs"
275
- echo "$BAD_GUIDS"
276
- exit 0
277
- fi
278
- fi
279
- ```
280
-
281
- ## Frontend — CSS, Forms, Components, I18n
282
-
283
- ### POST-CHECK C3a: Frontend must not be empty if Layer 3 was planned (BLOCKING)
284
-
285
- ```bash
286
- # If foundation_mode is false AND App.tsx exists, verify frontend was generated
287
- APP_TSX=$(find web/ src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
288
- if [ -n "$APP_TSX" ]; then
289
- # Check if applicationRoutes is an empty object
290
- EMPTY_ROUTES=$(grep -P "applicationRoutes.*=\s*\{[\s/]*\}" "$APP_TSX" 2>/dev/null)
291
- if [ -n "$EMPTY_ROUTES" ]; then
292
- echo "BLOCKING: applicationRoutes in App.tsx is empty — Layer 3 frontend was NOT executed"
293
- echo "Expected: at least one application key with route definitions"
294
- echo "Fix: Run Layer 3 (scaffold_routes + scaffold_extension + route wiring)"
295
- exit 1
296
- fi
297
-
298
- # Check pages/ directory is not empty
299
- PAGE_COUNT=$(find web/ src/ -path "*/pages/*" -name "*.tsx" -not -path "*/node_modules/*" 2>/dev/null | wc -l)
300
- if [ "$PAGE_COUNT" -eq 0 ]; then
301
- echo "BLOCKING: No page components found in pages/ directory"
302
- echo "Fix: Generate pages via scaffold_extension or /ui-components"
303
- exit 1
304
- fi
305
-
306
- # Check navRoutes.generated.ts exists
307
- NAV_ROUTES=$(find web/ src/ -name "navRoutes.generated.ts" -not -path "*/node_modules/*" 2>/dev/null | head -1)
308
- if [ -z "$NAV_ROUTES" ]; then
309
- echo "BLOCKING: navRoutes.generated.ts not found — scaffold_routes was never called"
310
- echo "Fix: Run scaffold_routes(source: 'controllers', outputFormat: 'applicationRoutes')"
311
- exit 1
312
- fi
313
- fi
314
- ```
315
-
316
- ### POST-CHECK C3: Translation files must exist for all 4 languages (if frontend)
317
-
318
- ```bash
319
- # Find all i18n namespaces used in tsx files
320
- TSX_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
321
- if [ -n "$TSX_FILES" ]; then
322
- NAMESPACES=$(grep -ohP "useTranslation\(\[?'([^']+)" $TSX_FILES | sed "s/.*'//" | sort -u)
323
- for NS in $NAMESPACES; do
324
- for LANG in fr en it de; do
325
- if [ ! -f "src/i18n/locales/$LANG/$NS.json" ]; then
326
- echo "WARNING: Missing translation file: src/i18n/locales/$LANG/$NS.json"
327
- exit 1
328
- fi
329
- done
330
- done
331
- fi
332
- ```
333
-
334
- ### POST-CHECK C4: Forms must be full pages with routes — ZERO modals/popups/drawers/slide-overs
335
-
336
- ```bash
337
- # Check for modal/dialog/drawer/slide-over imports AND inline form state in page files
338
- PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
339
- if [ -n "$PAGE_FILES" ]; then
340
- FAIL=false
341
-
342
- # 8a. Component imports (Modal, Dialog, Drawer, etc.)
343
- MODAL_IMPORTS=$(grep -Pn "import.*(?:Modal|Dialog|Drawer|Popup|Sheet|SlideOver|Overlay)" $PAGE_FILES 2>/dev/null)
344
- if [ -n "$MODAL_IMPORTS" ]; then
345
- echo "WARNING: Form pages must NOT use Modal/Dialog/Drawer/Popup/SlideOver components"
346
- echo "$MODAL_IMPORTS"
347
- FAIL=true
348
- fi
349
-
350
- # 8b. Inline form state (catches drawers/slide-overs built without external components)
351
- FORM_STATE=$(grep -Pn "useState.*(?:isOpen|showModal|showDialog|showCreate|showEdit|showForm|isCreating|isEditing|showDrawer|showPanel|showSlideOver|selectedEntity|editingEntity)" $PAGE_FILES 2>/dev/null)
352
- if [ -n "$FORM_STATE" ]; then
353
- echo "WARNING: Inline form state detected — forms embedded in ListPage as drawers/panels"
354
- echo "Create/Edit forms MUST be separate page components with their own URL routes"
355
- echo "$FORM_STATE"
356
- FAIL=true
357
- fi
358
-
359
- if [ "$FAIL" = true ]; then
360
- echo ""
361
- echo "Fix: Create EntityCreatePage.tsx with route /create and EntityEditPage.tsx with route /:id/edit"
362
- echo "NEVER embed create/edit forms as inline drawers, panels, or slide-overs in ListPage"
363
- exit 1
364
- fi
365
- fi
366
- ```
367
-
368
- ### POST-CHECK C5: Create/Edit pages must exist as separate route pages (CRITICAL)
369
-
370
- ```bash
371
- # For each module with a list page, verify create and edit pages exist
372
- # If ListPage has navigate() calls to /create or /:id/edit, the target pages MUST exist
373
- LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test)
374
- FAIL=false
375
- if [ -n "$LIST_PAGES" ]; then
376
- for LIST_PAGE in $LIST_PAGES; do
377
- PAGE_DIR=$(dirname "$LIST_PAGE")
378
- MODULE_NAME=$(basename "$PAGE_DIR")
379
-
380
- # Detect if ListPage navigates to /create or /edit routes
381
- HAS_CREATE_NAV=$(grep -P "navigate\(.*['/]create" "$LIST_PAGE" 2>/dev/null)
382
- HAS_EDIT_NAV=$(grep -P "navigate\(.*['/]edit|navigate\(.*/:id/edit" "$LIST_PAGE" 2>/dev/null)
383
-
384
- # Check for create page
385
- CREATE_PAGE=$(find "$PAGE_DIR" -name "*CreatePage.tsx" 2>/dev/null)
386
- if [ -z "$CREATE_PAGE" ]; then
387
- if [ -n "$HAS_CREATE_NAV" ]; then
388
- echo "CRITICAL: Module $MODULE_NAME ListPage navigates to /create but CreatePage does NOT exist"
389
- echo " Dead link: $HAS_CREATE_NAV"
390
- echo " Fix: Create ${MODULE_NAME}CreatePage.tsx in $PAGE_DIR"
391
- FAIL=true
392
- else
393
- echo "WARNING: Module $MODULE_NAME has a list page but no CreatePage — expected EntityCreatePage.tsx"
394
- fi
395
- fi
396
-
397
- # Check for edit page
398
- EDIT_PAGE=$(find "$PAGE_DIR" -name "*EditPage.tsx" 2>/dev/null)
399
- if [ -z "$EDIT_PAGE" ]; then
400
- if [ -n "$HAS_EDIT_NAV" ]; then
401
- echo "CRITICAL: Module $MODULE_NAME ListPage navigates to /:id/edit but EditPage does NOT exist"
402
- echo " Dead link: $HAS_EDIT_NAV"
403
- echo " Fix: Create ${MODULE_NAME}EditPage.tsx in $PAGE_DIR"
404
- FAIL=true
405
- else
406
- echo "WARNING: Module $MODULE_NAME has a list page but no EditPage — expected EntityEditPage.tsx"
407
- fi
408
- fi
409
- done
410
- fi
411
-
412
- if [ "$FAIL" = true ]; then
413
- echo ""
414
- echo "CRITICAL: Create/Edit pages are MISSING but ListPage buttons link to them."
415
- echo "Users will see white screen / 404 when clicking Create or Edit buttons."
416
- echo "Fix: Generate form pages using /ui-components skill patterns (smartstack-frontend.md section 3b)"
417
- exit 1
418
- fi
419
- ```
420
-
421
- ### POST-CHECK C6: Form pages must have companion test files
422
-
423
- ```bash
424
- # Minimum requirement: if frontend pages exist, at least 1 test file must be present
425
- PAGE_FILES=$(find src/pages/ -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null)
426
- if [ -n "$PAGE_FILES" ]; then
427
- ALL_TESTS=$(find src/pages/ -name "*.test.tsx" 2>/dev/null)
428
- if [ -z "$ALL_TESTS" ]; then
429
- echo "WARNING: No frontend test files found in src/pages/"
430
- echo "Every form page MUST have a companion .test.tsx file"
431
- exit 1
432
- fi
433
- fi
434
-
435
- # Every CreatePage and EditPage must have a .test.tsx file
436
- FORM_PAGES=$(find src/pages/ -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test)
437
- if [ -n "$FORM_PAGES" ]; then
438
- for FORM_PAGE in $FORM_PAGES; do
439
- TEST_FILE="${FORM_PAGE%.tsx}.test.tsx"
440
- if [ ! -f "$TEST_FILE" ]; then
441
- echo "WARNING: Form page missing test file: $FORM_PAGE"
442
- echo "Expected: $TEST_FILE"
443
- echo "All form pages MUST have companion test files (rendering, validation, submit, navigation)"
444
- exit 1
445
- fi
446
- done
447
- fi
448
- ```
449
-
450
- ### POST-CHECK C7: FK fields must use EntityLookup — NO `<input>`, NO `<select>` (WARNING)
451
-
452
- ```bash
453
- # Check ALL page files for FK fields rendered as <input> or <select> instead of EntityLookup
454
- # Scans ALL .tsx files (not just CreatePage/EditPage — forms may be embedded in ListPage drawers)
455
- ALL_PAGES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
456
- if [ -n "$ALL_PAGES" ]; then
457
- FAIL=false
458
-
459
- # 1. Detect <input> with name/value binding to FK fields (fields ending in "Id")
460
- FK_INPUTS=$(grep -Pn '<input[^>]*(?:name|value)=["\x27{][^>]*[a-zA-Z]Id["\x27}]' $ALL_PAGES 2>/dev/null | grep -Pv 'type=["\x27]hidden["\x27]')
461
- if [ -n "$FK_INPUTS" ]; then
462
- echo "WARNING: FK fields rendered as <input> — MUST use EntityLookup component"
463
- echo "$FK_INPUTS"
464
- FAIL=true
465
- fi
466
-
467
- # 2. Detect <select> with value binding to FK fields (e.g., value={formData.departmentId})
468
- FK_SELECTS=$(grep -Pn '<select[^>]*value=\{[^}]*[a-zA-Z]Id\b' $ALL_PAGES 2>/dev/null)
469
- if [ -n "$FK_SELECTS" ]; then
470
- echo "WARNING: FK fields rendered as <select> dropdown — MUST use EntityLookup component"
471
- echo "A <select> loaded from API state is NOT a valid substitute for EntityLookup."
472
- echo "EntityLookup provides: debounced search, paginated results, display name resolution."
473
- echo "$FK_SELECTS"
474
- FAIL=true
475
- fi
476
-
477
- # 3. Detect onChange handlers setting FK fields from <select> (e.g., setFormData({...formData, departmentId: e.target.value}))
478
- FK_SELECT_ONCHANGE=$(grep -Pn 'onChange=.*[a-zA-Z]Id[^a-zA-Z].*e\.target\.value' $ALL_PAGES 2>/dev/null)
479
- if [ -n "$FK_SELECT_ONCHANGE" ]; then
480
- echo "WARNING: FK field set via e.target.value (select/input pattern) — use EntityLookup onChange(id)"
481
- echo "$FK_SELECT_ONCHANGE"
482
- FAIL=true
483
- fi
484
-
485
- # 4. Check for placeholders mentioning "ID", "GUID", or "Select..." for FK fields
486
- 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)
487
- if [ -n "$FK_PLACEHOLDER" ]; then
488
- echo "WARNING: Form has placeholder for FK field selection — use EntityLookup search instead"
489
- echo "$FK_PLACEHOLDER"
490
- FAIL=true
491
- fi
492
-
493
- # 5. Detect <option> elements with GUID-like values (sign of FK <select>)
494
- FK_OPTIONS=$(grep -Pn '<option[^>]*value=\{[^}]*\.id\}' $ALL_PAGES 2>/dev/null)
495
- if [ -n "$FK_OPTIONS" ]; then
496
- echo "WARNING: <option> with entity .id value detected — this is a FK <select> anti-pattern"
497
- echo "Replace the entire <select>/<option> block with <EntityLookup />"
498
- echo "$FK_OPTIONS"
499
- FAIL=true
500
- fi
501
-
502
- if [ "$FAIL" = true ]; then
503
- echo ""
504
- echo "Fix: Replace ALL FK fields with <EntityLookup /> from @/components/ui/EntityLookup"
505
- echo "See smartstack-frontend.md section 6 for the EntityLookup pattern"
506
- echo "FORBIDDEN for FK Guid fields: <input>, <select>, <option>, e.target.value"
507
- echo "REQUIRED: <EntityLookup apiEndpoint={...} mapOption={...} value={...} onChange={...} />"
508
- exit 0
509
- fi
510
- fi
511
- ```
512
-
513
- ### POST-CHECK C8: Backend APIs must support search parameter for EntityLookup (BLOCKING)
514
-
515
- ```bash
516
- # Part 1: Check that ALL controller GetAll methods accept search parameter
517
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
518
- if [ -n "$CTRL_FILES" ]; then
519
- for f in $CTRL_FILES; do
520
- if grep -q "\[HttpGet\]" "$f" && grep -q "GetAll" "$f"; then
521
- if ! grep -q "search" "$f"; then
522
- echo "BLOCKING: Controller missing search parameter on GetAll: $f"
523
- echo "GetAll endpoints must accept ?search= to enable EntityLookup on frontend"
524
- echo "Fix: Add [FromQuery] string? search parameter to GetAll action"
525
- exit 1
526
- fi
527
- fi
528
- done
529
- fi
530
-
531
- # Part 2: Cross-validate — every EntityLookup on frontend has a matching controller with ?search=
532
- WEB_DIR=$(find . -name "vitest.config.ts" -o -name "vite.config.ts" 2>/dev/null | head -1 | xargs dirname 2>/dev/null)
533
- if [ -n "$WEB_DIR" ]; then
534
- LOOKUP_FILES=$(grep -rl "EntityLookup" "$WEB_DIR/src/pages/" "$WEB_DIR/src/components/" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
535
- if [ -n "$LOOKUP_FILES" ]; then
536
- for f in $LOOKUP_FILES; do
537
- # Extract API endpoint paths from EntityLookup apiEndpoint prop
538
- ENDPOINTS=$(grep -oP "apiEndpoint=['\"]([^'\"]+)['\"]" "$f" 2>/dev/null | grep -oP "['\"]([^'\"]+)['\"]" | tr -d "'" | tr -d '"')
539
- for ep in $ENDPOINTS; do
540
- # Derive controller name from endpoint (e.g., /api/departments → DepartmentsController)
541
- ENTITY=$(echo "$ep" | sed 's|.*/||' | sed 's/.*/\u&/')
542
- CTRL=$(find src/ -path "*/Controllers/*${ENTITY}*Controller.cs" 2>/dev/null | head -1)
543
- if [ -n "$CTRL" ] && ! grep -q "search" "$CTRL"; then
544
- echo "BLOCKING: EntityLookup in $f calls $ep, but controller $CTRL does not support ?search="
545
- echo "Fix: Add [FromQuery] string? search parameter to GetAll in $CTRL"
546
- exit 1
547
- fi
548
- done
549
- done
550
- fi
551
- fi
552
- ```
553
-
554
- ### POST-CHECK C9: No hardcoded Tailwind colors in generated pages (WARNING)
555
-
556
- ```bash
557
- # Scan all page and component files directly (works for uncommitted/untracked files, Windows/WSL compatible)
558
- ALL_PAGES=$(find src/pages/ src/components/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
559
- if [ -n "$ALL_PAGES" ]; then
560
- HARDCODED=$(grep -Pn '(bg|text|border)-(?!\[)(red|blue|green|gray|white|black|slate|zinc|neutral|stone)-' $ALL_PAGES 2>/dev/null)
561
- if [ -n "$HARDCODED" ]; then
562
- echo "WARNING: Pages MUST use CSS variables instead of hardcoded Tailwind colors"
563
- echo "SmartStack uses a theme system — hardcoded colors break dark mode and custom themes"
564
- echo ""
565
- echo "Fix mapping:"
566
- echo " bg-white → bg-[var(--bg-card)]"
567
- echo " bg-gray-50 → bg-[var(--bg-primary)]"
568
- echo " text-gray-900 → text-[var(--text-primary)]"
569
- echo " text-gray-500 → text-[var(--text-secondary)]"
570
- echo " border-gray-200 → border-[var(--border-color)]"
571
- echo " bg-blue-600 → bg-[var(--color-accent-500)]"
572
- echo " text-blue-600 → text-[var(--color-accent-500)]"
573
- echo " text-red-500 → text-[var(--error-text)]"
574
- echo " bg-green-500 → bg-[var(--success-bg)]"
575
- echo ""
576
- echo "See references/smartstack-frontend.md section 4 for full variable reference"
577
- echo ""
578
- echo "$HARDCODED"
579
- exit 0
580
- fi
581
- fi
582
- ```
583
-
584
- ## Seed Data — Navigation, Roles, Permissions
585
-
586
- ### POST-CHECK C10: Routes seed data must match frontend application routes
587
-
588
- ```bash
589
- 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)
590
- APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
591
- if [ -n "$APP_TSX" ] && [ -n "$SEED_ROUTES" ]; then
592
- FRONTEND_PATHS=$(grep -oP "path:\s*'([^']+)'" "$APP_TSX" | grep -oP "'[^']+'" | tr -d "'" | sort -u)
593
- if [ -n "$FRONTEND_PATHS" ]; then
594
- MISMATCH_FOUND=false
595
- for SEED_ROUTE in $SEED_ROUTES; do
596
- DEPTH=$(echo "$SEED_ROUTE" | tr '/' '\n' | grep -c '.')
597
- if [ "$DEPTH" -lt 3 ]; then continue; fi
598
- SEED_SUFFIX=$(echo "$SEED_ROUTE" | sed 's|^/[^/]*/||')
599
- SEED_NORM=$(echo "$SEED_SUFFIX" | tr '[:upper:]' '[:lower:]' | tr -d '-')
600
- MATCH_FOUND=false
601
- for FE_PATH in $FRONTEND_PATHS; do
602
- # Flag FORBIDDEN /list suffix BEFORE normalization
603
- if echo "$FE_PATH" | grep -qP '/list$'; then
604
- echo "WARNING: Frontend route ends with /list — should use index route instead: $FE_PATH"
605
- fi
606
- FE_BASE=$(echo "$FE_PATH" | sed 's|/list$||;s|/new$||;s|/:id.*||;s|/create$||')
607
- FE_NORM=$(echo "$FE_BASE" | tr '[:upper:]' '[:lower:]' | tr -d '-')
608
- if [ "$SEED_NORM" = "$FE_NORM" ]; then
609
- MATCH_FOUND=true
610
- break
611
- fi
612
- done
613
- if [ "$MATCH_FOUND" = false ]; then
614
- echo "CRITICAL: Seed data route has no matching frontend route: $SEED_ROUTE"
615
- MISMATCH_FOUND=true
616
- fi
617
- done
618
- if [ "$MISMATCH_FOUND" = true ]; then
619
- echo "Fix: Ensure every NavigationSeedData route has a corresponding route entry in App.tsx (applicationRoutes or JSX Route wrappers)"
620
- exit 1
621
- fi
622
- fi
623
- fi
624
- ```
625
-
626
- ### POST-CHECK C11: Frontend routes must use kebab-case (BLOCKING)
627
-
628
- ```bash
629
- # POST-CHECK C10 normalizes hyphens for existence check, but does NOT catch kebab-case mismatches.
630
- # This supplementary check detects concatenated multi-word route segments without hyphens.
631
- APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
632
- if [ -n "$APP_TSX" ]; then
633
- # Extract route path strings from App.tsx
634
- FE_PATHS=$(grep -oP "path:\s*['\"]([^'\"]+)['\"]" "$APP_TSX" | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"')
635
- for FE_PATH in $FE_PATHS; do
636
- # Split path by / and check each segment
637
- for SEG in $(echo "$FE_PATH" | tr '/' '\n'); do
638
- # Skip dynamic segments (:id, :slug) and single words (< 10 chars likely single word)
639
- echo "$SEG" | grep -qP '^:' && continue
640
- # Detect multi-word segments without hyphens: 2+ consecutive lowercase sequences
641
- # e.g., "humanresources" (human+resources), "timemanagement" (time+management)
642
- if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
643
- # Potential concatenated multi-word — cross-check with seed data
644
- SEED_MATCH=$(echo "$SEED_ROUTES" | tr '/' '\n' | grep -P "^[a-z]+-[a-z]+" | tr -d '-' | grep -x "$SEG")
645
- if [ -n "$SEED_MATCH" ]; then
646
- echo "BLOCKING: Frontend route segment '$SEG' appears to be missing hyphens"
647
- echo "Seed data uses kebab-case (e.g., 'human-resources') but frontend has '$SEG'"
648
- echo "Fix: Use kebab-case in App.tsx route paths to match seed data exactly"
649
- exit 1
650
- fi
651
- fi
652
- done
653
- done
654
- fi
655
- ```
656
-
657
- ### POST-CHECK C12: GetAll methods must return PaginatedResult<T>
658
-
659
- ```bash
660
- SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
661
- if [ -n "$SERVICE_FILES" ]; then
662
- BAD_RETURNS=$(grep -Pn '(Task<\s*(?:List|IEnumerable|IList|ICollection|IReadOnlyList|IReadOnlyCollection)<).*GetAll' $SERVICE_FILES 2>/dev/null)
663
- if [ -n "$BAD_RETURNS" ]; then
664
- echo "WARNING: GetAll methods must return PaginatedResult<T>, not List/IEnumerable"
665
- echo "$BAD_RETURNS"
666
- echo "Fix: Change return type to Task<PaginatedResult<{Entity}ResponseDto>>"
667
- exit 1
668
- fi
669
- fi
670
- ```
671
-
672
- ### POST-CHECK C13: i18n files must contain required structural keys
673
-
674
- ```bash
675
- I18N_DIR="src/i18n/locales/fr"
676
- if [ -d "$I18N_DIR" ]; then
677
- REQUIRED_KEYS="actions columns empty errors form labels messages validation"
678
- for JSON_FILE in "$I18N_DIR"/*.json; do
679
- [ ! -f "$JSON_FILE" ] && continue
680
- BASENAME=$(basename "$JSON_FILE")
681
- case "$BASENAME" in common.json|navigation.json) continue;; esac
682
- for KEY in $REQUIRED_KEYS; do
683
- if ! jq -e "has(\"$KEY\")" "$JSON_FILE" > /dev/null 2>&1; then
684
- echo "WARNING: i18n file missing required key '$KEY': $JSON_FILE"
685
- echo "Module i18n files MUST contain: $REQUIRED_KEYS"
686
- exit 1
687
- fi
688
- done
689
- done
690
- fi
691
- ```
692
-
693
- ### POST-CHECK C14: Entities must implement IAuditableEntity + Validators must have Create/Update pairs
694
-
695
- ```bash
696
- ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null)
697
- if [ -n "$ENTITY_FILES" ]; then
698
- for f in $ENTITY_FILES; do
699
- if grep -q "ITenantEntity" "$f" && ! grep -q "IAuditableEntity" "$f"; then
700
- echo "WARNING: Entity implements ITenantEntity but NOT IAuditableEntity: $f"
701
- echo "Pattern: public class Entity : BaseEntity, ITenantEntity, IAuditableEntity"
702
- exit 1
703
- fi
704
- done
705
- fi
706
- CREATE_VALIDATORS=$(find src/ -path "*/Validators/*" -name "Create*Validator.cs" 2>/dev/null)
707
- if [ -n "$CREATE_VALIDATORS" ]; then
708
- for f in $CREATE_VALIDATORS; do
709
- VALIDATOR_DIR=$(dirname "$f")
710
- ENTITY_NAME=$(basename "$f" | sed 's/^Create\(.*\)Validator\.cs$/\1/')
711
- if [ ! -f "$VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs" ]; then
712
- echo "WARNING: Create${ENTITY_NAME}Validator exists but Update${ENTITY_NAME}Validator is missing"
713
- echo " Found: $f"
714
- echo " Expected: $VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs"
715
- exit 1
716
- fi
717
- done
718
- fi
719
- ```
720
-
721
- ### POST-CHECK C15: RolePermission seed data must NOT use deterministic role GUIDs
722
-
723
- ```bash
724
- # System roles (admin, manager, contributor, viewer) are pre-seeded by SmartStack core.
725
- # RolePermission mappings MUST look up roles by Code at runtime, NEVER use deterministic GUIDs.
726
- SEED_ALL_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
727
- SEED_CONST_FILES=$(find src/ -path "*/Seeding/*" -name "SeedConstants.cs" 2>/dev/null)
728
- if [ -n "$SEED_ALL_FILES" ]; then
729
- BAD_ROLE_GUID=$(grep -Pn 'DeterministicGuid\("role:' $SEED_ALL_FILES $SEED_CONST_FILES 2>/dev/null)
730
- if [ -n "$BAD_ROLE_GUID" ]; then
731
- echo "WARNING: Deterministic GUID for role detected (e.g., DeterministicGuid(\"role:admin\"))"
732
- echo "System roles are pre-seeded by SmartStack core with their own IDs"
733
- echo "Fix: In SeedRolePermissionsAsync(), look up roles by Code:"
734
- echo " var roles = await context.Roles.Where(r => r.IsSystem || r.ApplicationId != null).ToListAsync(ct);"
735
- echo " var role = roles.FirstOrDefault(r => r.Code == mapping.RoleCode);"
736
- echo "$BAD_ROLE_GUID"
737
- exit 1
738
- fi
739
- fi
740
- # Also check for GenerateRoleGuid usage in RolePermission mapping files (not in ApplicationRolesSeedData itself)
741
- ROLE_PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" 2>/dev/null)
742
- if [ -n "$ROLE_PERM_FILES" ]; then
743
- BAD_ROLE_REF=$(grep -Pn 'GenerateRoleGuid|GetAdminRoleId|GetManagerRoleId|GetViewerRoleId|GetContributorRoleId' $ROLE_PERM_FILES 2>/dev/null)
744
- if [ -n "$BAD_ROLE_REF" ]; then
745
- echo "WARNING: RolesSeedData uses hardcoded role GUID helpers instead of Code-based lookup"
746
- echo "Fix: Use RoleCode string (e.g., 'admin') and resolve in SeedRolePermissionsAsync()"
747
- echo "$BAD_ROLE_REF"
748
- fi
749
- fi
750
- ```
751
-
752
- ### POST-CHECK C16: Cross-tenant entities must use Guid? TenantId
753
-
754
- ```bash
755
- for entity in $(find src/ -path "*/Domain/*" -name "*.cs" ! -name "I*.cs" 2>/dev/null); do
756
- if grep -q "IOptionalTenantEntity\|IScopedTenantEntity" "$entity"; then
757
- if grep -q "public Guid TenantId" "$entity" && ! grep -q "public Guid? TenantId" "$entity"; then
758
- echo "CRITICAL: Entity with IOptionalTenantEntity/IScopedTenantEntity must use Guid? TenantId (nullable)"
759
- exit 1
760
- fi
761
- fi
762
- done
763
- echo "POST-CHECK C16: OK"
764
- ```
765
-
766
- ### POST-CHECK C17: Scoped entities must have EntityScope property
767
-
768
- ```bash
769
- for entity in $(find src/ -path "*/Domain/*" -name "*.cs" ! -name "I*.cs" 2>/dev/null); do
770
- if grep -q "IScopedTenantEntity" "$entity"; then
771
- if ! grep -q "EntityScope\|Scope" "$entity"; then
772
- echo "CRITICAL: Entity with IScopedTenantEntity must have EntityScope Scope property"
773
- exit 1
774
- fi
775
- fi
776
- done
777
- echo "POST-CHECK C17: OK"
778
- ```
779
-
780
- ### POST-CHECK C18: Permissions.cs static constants must exist (BLOCKING)
781
-
782
- ```bash
783
- # Every module with controllers MUST have a Permissions.cs with static constants
784
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
785
- if [ -n "$CTRL_FILES" ]; then
786
- PERM_REFS=$(grep -ohP 'Permissions\.\w+\.\w+' $CTRL_FILES 2>/dev/null | sed 's/Permissions\.\([^.]*\)\..*/\1/' | sort -u)
787
- for MODULE in $PERM_REFS; do
788
- PERM_FILE=$(find src/ -name "Permissions.cs" -exec grep -l "static class $MODULE" {} \; 2>/dev/null)
789
- if [ -z "$PERM_FILE" ]; then
790
- echo "BLOCKING: Controller references Permissions.${MODULE}.* but no Permissions.cs defines static class ${MODULE}"
791
- echo "Fix: Create Application/Authorization/Permissions.cs with: public static class ${MODULE} { public const string Read = \"...\"; ... }"
792
- exit 1
793
- fi
794
- done
795
- fi
796
- ```
797
-
798
- ### POST-CHECK C19: ApplicationRolesSeedData.cs must exist (BLOCKING)
799
-
800
- ```bash
801
- # If any RolesSeedData exists, ApplicationRolesSeedData MUST also exist
802
- ROLE_SEED=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" 2>/dev/null | head -1)
803
- if [ -n "$ROLE_SEED" ]; then
804
- APP_ROLE_SEED=$(find src/ -path "*/Seeding/Data/ApplicationRolesSeedData.cs" 2>/dev/null | head -1)
805
- if [ -z "$APP_ROLE_SEED" ]; then
806
- echo "BLOCKING: RolesSeedData exists but ApplicationRolesSeedData.cs NOT FOUND"
807
- echo "ApplicationRolesSeedData defines the 4 application-scoped roles (admin, manager, contributor, viewer)"
808
- echo "Without it, SeedRolesAsync() has no role entries to create → RBAC broken"
809
- echo "Fix: Create src/Infrastructure/Persistence/Seeding/Data/ApplicationRolesSeedData.cs"
810
- exit 1
811
- fi
812
- fi
813
- ```
814
-
815
- ### POST-CHECK C20: Section route completeness (NavigationSection → frontend route + permissions)
816
-
817
- ```bash
818
- # Every NavigationSection seed data route MUST have a corresponding frontend route in App.tsx
819
- # and section-level permissions MUST exist for each section defined in seed data
820
- SECTION_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSectionSeedData.cs" 2>/dev/null)
821
- APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
822
- if [ -n "$SECTION_SEED_FILES" ] && [ -n "$APP_TSX" ]; then
823
- # Extract section routes from seed data
824
- SECTION_ROUTES=$(grep -Poh '"/[a-z][a-z0-9/-]+"' $SECTION_SEED_FILES 2>/dev/null | tr -d '"' | sort -u)
825
- for SECTION_ROUTE in $SECTION_ROUTES; do
826
- # Extract the last segment (section-kebab) for frontend route matching
827
- SECTION_SEG=$(echo "$SECTION_ROUTE" | rev | cut -d'/' -f1 | rev)
828
- if ! grep -q "'$SECTION_SEG'" "$APP_TSX" && ! grep -q "\"$SECTION_SEG\"" "$APP_TSX"; then
829
- echo "WARNING: NavigationSection seed data route has no matching frontend route: $SECTION_ROUTE"
830
- echo "Expected path segment '$SECTION_SEG' in App.tsx application route block"
831
- echo "Fix: Add section child routes to the module's children array in App.tsx"
832
- fi
833
- done
834
- fi
835
-
836
- # All controllers with [NavRoute] must have matching [RequirePermission]
837
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
838
- if [ -n "$CTRL_FILES" ]; then
839
- for f in $CTRL_FILES; do
840
- # Match NavRoute with 2+ dot-separated segments (minimum: app.module)
841
- SECTION_NAVROUTE=$(grep -oP 'NavRoute\("[a-z-]+\.[a-z-]+' "$f" 2>/dev/null)
842
- if [ -n "$SECTION_NAVROUTE" ] && ! grep -q "\[RequirePermission" "$f"; then
843
- echo "CRITICAL: Controller has [NavRoute] but no [RequirePermission]: $f"
844
- echo "Fix: Add [RequirePermission(Permissions.{Section}.{Action})] on each endpoint"
845
- exit 1
846
- fi
847
- done
848
- fi
849
-
850
- # Section-level permissions must exist for each section in seed data
851
- PERM_FILE=$(find src/ -name "Permissions.cs" -path "*/Authorization/*" 2>/dev/null | head -1)
852
- if [ -n "$SECTION_SEED_FILES" ] && [ -n "$PERM_FILE" ]; then
853
- SECTION_CODES=$(grep -oP 'Code\s*=\s*"([a-z]+)"' $SECTION_SEED_FILES 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
854
- for CODE in $SECTION_CODES; do
855
- PASCAL=$(echo "$CODE" | sed 's/^./\U&/')
856
- if ! grep -q "static class $PASCAL" "$PERM_FILE" 2>/dev/null; then
857
- echo "WARNING: Section '$CODE' in seed data has no matching Permissions.$PASCAL static class"
858
- echo "Fix: Add section-level permissions via MCP generate_permissions with 3-segment navRoute (app.module.section)"
859
- fi
860
- done
861
- fi
862
- ```
863
-
864
- ### POST-CHECK C21: FORBIDDEN route patterns — /list and /detail/:id (WARNING)
865
-
866
- ```bash
867
- # 1. Check seed data for FORBIDDEN suffixes
868
- SEED_NAV_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -o -name "*NavigationSectionSeedData.cs" 2>/dev/null)
869
- if [ -n "$SEED_NAV_FILES" ]; then
870
- BAD_ROUTES=$(grep -Pn 'Route\s*=\s*.*"[^"]*/(list|detail)["/]' $SEED_NAV_FILES 2>/dev/null | grep -v '//.*Route')
871
- if [ -n "$BAD_ROUTES" ]; then
872
- echo "WARNING: FORBIDDEN route pattern in seed data"
873
- echo " - 'list' section route = module route (NO /list suffix)"
874
- echo " - 'detail' section route = module route + /:id (NOT /detail/:id)"
875
- echo "$BAD_ROUTES"
876
- exit 0
877
- fi
878
- fi
879
-
880
- # 2. Check frontend routes for FORBIDDEN path segments
881
- APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
882
- if [ -n "$APP_TSX" ]; then
883
- BAD_FE=$(grep -Pn "path:\s*['\"](?:list|detail)" "$APP_TSX" 2>/dev/null)
884
- if [ -n "$BAD_FE" ]; then
885
- echo "WARNING: FORBIDDEN frontend route path"
886
- echo " - list = index: true (no 'list' path segment)"
887
- echo " - detail = ':id' (no 'detail' path segment)"
888
- echo "$BAD_FE"
889
- exit 0
890
- fi
891
- fi
892
- echo "OK: No forbidden /list or /detail route patterns found"
893
- ```
894
-
895
- ### POST-CHECK C22: Permission path segment count (WARNING)
896
-
897
- ```bash
898
- PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "PermissionsSeedData.cs" 2>/dev/null)
899
- if [ -n "$PERM_FILES" ]; then
900
- while IFS= read -r line; do
901
- PATH_VAL=$(echo "$line" | grep -oP '"[^"]*\.[^"]*"' | tr -d '"')
902
- if [ -n "$PATH_VAL" ]; then
903
- DOTS=$(echo "$PATH_VAL" | tr -cd '.' | wc -c)
904
- # Module permissions: 2 dots (app.module.action = 3 segments = 2+1)
905
- # Section permissions: 3 dots (app.module.section.action = 4 segments = 3+1)
906
- # Wildcard: ends with .* (valid at any level)
907
- if echo "$PATH_VAL" | grep -qP '\.\*$'; then
908
- continue # Wildcards are valid
909
- elif [ "$DOTS" -lt 2 ] || [ "$DOTS" -gt 4 ]; then
910
- echo "WARNING: Permission path has unexpected segment count ($((DOTS+1)) segments): $PATH_VAL"
911
- fi
912
- fi
913
- done < <(grep -n 'Path\s*=' $PERM_FILES 2>/dev/null)
914
- fi
915
- ```
916
-
917
- ### POST-CHECK C23: IClientSeedDataProvider must have 4 methods + DI registration (BLOCKING)
918
-
919
- ```bash
920
- PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
921
- if [ -n "$PROVIDER" ]; then
922
- METHODS_FOUND=0
923
- for METHOD in SeedNavigationAsync SeedRolesAsync SeedPermissionsAsync SeedRolePermissionsAsync; do
924
- if grep -q "$METHOD" "$PROVIDER"; then
925
- METHODS_FOUND=$((METHODS_FOUND + 1))
926
- else
927
- echo "BLOCKING: IClientSeedDataProvider missing method: $METHOD in $PROVIDER"
928
- fi
929
- done
930
- if [ "$METHODS_FOUND" -lt 4 ]; then
931
- echo "Fix: IClientSeedDataProvider must implement all 4 methods: SeedNavigationAsync, SeedRolesAsync, SeedPermissionsAsync, SeedRolePermissionsAsync"
932
- exit 1
933
- fi
934
-
935
- # Check DI registration
936
- DI_FILE=$(find src/ -name "DependencyInjection.cs" -path "*/Infrastructure/*" 2>/dev/null | head -1)
937
- if [ -n "$DI_FILE" ]; then
938
- if ! grep -q "IClientSeedDataProvider" "$DI_FILE"; then
939
- echo "BLOCKING: IClientSeedDataProvider not registered in DependencyInjection.cs"
940
- echo "Fix: Add services.AddScoped<IClientSeedDataProvider, {App}SeedDataProvider>()"
941
- exit 1
942
- fi
943
- fi
944
- fi
945
- ```
946
-
947
- ### POST-CHECK C24: i18n must use separate JSON files per language (not embedded in index.ts)
948
-
949
- ```bash
950
- # Translations MUST be in src/i18n/locales/{lang}/{module}.json, NOT embedded in a single .ts file
951
- TSX_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
952
- if [ -n "$TSX_FILES" ]; then
953
- # Check if i18n locales directory exists
954
- if [ ! -d "src/i18n/locales" ]; then
955
- echo "WARNING: Missing src/i18n/locales/ directory"
956
- echo "Translations must be in separate JSON files: src/i18n/locales/{fr,en,it,de}/{module}.json"
957
- echo "NEVER embed translations in src/i18n/index.ts or a single TypeScript file"
958
- exit 1
959
- fi
960
-
961
- # Check for embedded translations in index.ts (common anti-pattern)
962
- I18N_INDEX=$(find src/i18n/ -maxdepth 1 -name "index.ts" -o -name "index.tsx" -o -name "config.ts" 2>/dev/null)
963
- if [ -n "$I18N_INDEX" ]; then
964
- EMBEDDED=$(grep -Pn '^\s*(resources|translations)\s*[:=]\s*\{' $I18N_INDEX 2>/dev/null)
965
- if [ -n "$EMBEDDED" ]; then
966
- echo "WARNING: Translations embedded in i18n config file — must be in separate JSON files"
967
- echo "Found embedded translations in:"
968
- echo "$EMBEDDED"
969
- echo ""
970
- echo "Fix: Move translations to src/i18n/locales/{fr,en,it,de}/{module}.json"
971
- echo "The i18n config should import from locales/ directory, not contain inline translations"
972
- exit 1
973
- fi
974
- fi
975
-
976
- # Verify all 4 language directories exist
977
- for LANG in fr en it de; do
978
- if [ ! -d "src/i18n/locales/$LANG" ]; then
979
- echo "WARNING: Missing language directory: src/i18n/locales/$LANG/"
980
- echo "SmartStack requires 4 languages: fr, en, it, de"
981
- exit 1
982
- fi
983
- done
984
-
985
- # Verify at least one JSON file exists per language
986
- for LANG in fr en it de; do
987
- JSON_COUNT=$(find "src/i18n/locales/$LANG" -name "*.json" 2>/dev/null | wc -l)
988
- if [ "$JSON_COUNT" -eq 0 ]; then
989
- echo "WARNING: No translation JSON files in src/i18n/locales/$LANG/"
990
- echo "Each module must have a {module}.json file per language"
991
- exit 1
992
- fi
993
- done
994
- fi
995
- ```
996
-
997
- ### POST-CHECK C25: Pages must use useTranslation hook (no hardcoded user-facing strings)
998
-
999
- ```bash
1000
- # Verify that page components use i18n — detect hardcoded strings in JSX
1001
- PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
1002
- if [ -n "$PAGE_FILES" ]; then
1003
- # Check that at least 80% of pages import useTranslation
1004
- TOTAL_PAGES=$(echo "$PAGE_FILES" | wc -l)
1005
- I18N_PAGES=$(grep -l "useTranslation" $PAGE_FILES 2>/dev/null | wc -l)
1006
- if [ "$TOTAL_PAGES" -gt 0 ] && [ "$I18N_PAGES" -eq 0 ]; then
1007
- echo "WARNING: No page files use useTranslation — all user-facing text must be translated"
1008
- echo "Found $TOTAL_PAGES page files but 0 use useTranslation"
1009
- echo ""
1010
- echo "Fix: Import and use useTranslation in every page component:"
1011
- echo " const { t } = useTranslation(['{module}']);"
1012
- echo " t('{module}:title', 'Fallback text')"
1013
- exit 1
1014
- fi
1015
-
1016
- # Check for common hardcoded English strings in JSX (heuristic)
1017
- 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)
1018
- if [ -n "$HARDCODED_TEXT" ]; then
1019
- echo "WARNING: Possible hardcoded user-facing strings detected in JSX"
1020
- echo "All user-facing text MUST use t('namespace:key', 'Fallback')"
1021
- echo "$HARDCODED_TEXT"
1022
- fi
1023
- fi
1024
- ```
1025
-
1026
- ### POST-CHECK C26: List/Detail pages must include DocToggleButton (documentation panel)
1027
-
1028
- ```bash
1029
- # Every list and detail page MUST have DocToggleButton for inline documentation access
1030
- LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
1031
- if [ -n "$LIST_PAGES" ]; then
1032
- MISSING_DOC=0
1033
- for PAGE in $LIST_PAGES; do
1034
- if ! grep -q "DocToggleButton" "$PAGE" 2>/dev/null; then
1035
- echo "WARNING: Page missing DocToggleButton: $PAGE"
1036
- echo " Import: import { DocToggleButton } from '@/components/docs/DocToggleButton';"
1037
- echo " Place in header actions: <DocToggleButton />"
1038
- MISSING_DOC=$((MISSING_DOC + 1))
1039
- fi
1040
- done
1041
- if [ "$MISSING_DOC" -gt 0 ]; then
1042
- echo ""
1043
- echo "WARNING: $MISSING_DOC pages missing DocToggleButton — users cannot access inline documentation"
1044
- echo "See smartstack-frontend.md section 7 for placement pattern"
1045
- fi
1046
- fi
1047
- ```
1048
-
1049
- ### POST-CHECK C27: Module documentation must be generated (doc-data.ts)
1050
-
1051
- ```bash
1052
- # After frontend pages exist, /documentation should have been called
1053
- TSX_PAGES=$(find src/pages/ -name "*.tsx" -not -name "*.test.*" 2>/dev/null | grep -v node_modules | grep -v "docs/")
1054
- DOC_DATA=$(find src/pages/docs/ -name "doc-data.ts" 2>/dev/null)
1055
- if [ -n "$TSX_PAGES" ] && [ -z "$DOC_DATA" ]; then
1056
- echo "WARNING: Frontend pages exist but no documentation generated"
1057
- echo "Fix: Invoke /documentation {module} --type user to generate doc-data.ts"
1058
- echo "The DocToggleButton in page headers will link to this documentation"
1059
- fi
1060
- ```
1061
-
1062
- ### POST-CHECK C28: Pagination type must be PaginatedResult<T> — no aliases (WARNING)
1063
-
1064
- ```bash
1065
- SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
1066
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1067
- ALL_FILES="$SERVICE_FILES $CTRL_FILES"
1068
- if [ -n "$(echo $ALL_FILES | tr -d ' ')" ]; then
1069
- BAD_NAMES=$(grep -Pn 'PagedResult<|PaginatedResultDto<|PaginatedResponse<|PageResultDto<' $ALL_FILES 2>/dev/null)
1070
- if [ -n "$BAD_NAMES" ]; then
1071
- echo "WARNING: Pagination type must be PaginatedResult<T> — found non-canonical names"
1072
- echo "$BAD_NAMES"
1073
- echo "FORBIDDEN type names: PagedResult, PaginatedResultDto, PaginatedResponse, PageResultDto"
1074
- echo "Fix: Use PaginatedResult<T> from SmartStack.Application.Common.Models everywhere"
1075
- exit 0
1076
- fi
1077
- fi
1078
- ```
1079
-
1080
- ## Infrastructure — Migration & Build
1081
-
1082
- ### POST-CHECK C29: Code generation — ICodeGenerator must be registered for auto-generated entities (BLOCKING)
1083
-
1084
- ```bash
1085
- # If feature.json has entities with codePattern.strategy != "manual",
1086
- # verify that ICodeGenerator<Entity> is registered in DI
1087
- FEATURE_FILES=$(find docs/ -name "feature.json" 2>/dev/null)
1088
- DI_FILE=$(find src/ -name "DependencyInjection.cs" -path "*/Infrastructure/*" 2>/dev/null | head -1)
1089
- if [ -n "$FEATURE_FILES" ] && [ -n "$DI_FILE" ]; then
1090
- for FEATURE in $FEATURE_FILES; do
1091
- ENTITIES_WITH_CODE=$(python3 -c "
1092
- import json, sys
1093
- try:
1094
- with open('$FEATURE') as f:
1095
- data = json.load(f)
1096
- for e in data.get('analysis', {}).get('entities', []):
1097
- cp = e.get('codePattern', {})
1098
- if cp.get('strategy', 'manual') != 'manual':
1099
- print(e['name'])
1100
- except: pass
1101
- " 2>/dev/null)
1102
- for ENTITY in $ENTITIES_WITH_CODE; do
1103
- if ! grep -q "ICodeGenerator<$ENTITY>" "$DI_FILE" 2>/dev/null; then
1104
- echo "BLOCKING: Entity $ENTITY has auto-generated code pattern but ICodeGenerator<$ENTITY> is not registered in DI"
1105
- echo "Fix: Add CodeGenerator<$ENTITY> registration in DependencyInjection.cs — see references/code-generation.md"
1106
- exit 1
1107
- fi
1108
- done
1109
- done
1110
- fi
1111
- ```
1112
-
1113
- ### POST-CHECK C30: Code regex must support hyphens (BLOCKING)
1114
-
1115
- ```bash
1116
- VALIDATOR_FILES=$(find src/ -path "*/Validators/*" -name "*Validator.cs" 2>/dev/null)
1117
- if [ -n "$VALIDATOR_FILES" ]; then
1118
- OLD_REGEX=$(grep -rn '\^\\[a-z0-9_\\]+\$' $VALIDATOR_FILES 2>/dev/null | grep -v '\-')
1119
- if [ -n "$OLD_REGEX" ]; then
1120
- echo "BLOCKING: Code validator uses old regex without hyphen support"
1121
- echo "$OLD_REGEX"
1122
- echo "Fix: Update regex to ^[a-z0-9_-]+$ to support auto-generated codes with hyphens"
1123
- exit 1
1124
- fi
1125
- fi
1126
- ```
1127
-
1128
- ### POST-CHECK C31: CreateDto must NOT have Code field when service uses ICodeGenerator (WARNING)
1129
-
1130
- ```bash
1131
- SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
1132
- if [ -n "$SERVICE_FILES" ]; then
1133
- for f in $SERVICE_FILES; do
1134
- if grep -q "ICodeGenerator" "$f"; then
1135
- ENTITY=$(basename "$f" | sed 's/Service\.cs$//')
1136
- DTO_FILE=$(find src/ -path "*/DTOs/*" -name "Create${ENTITY}Dto.cs" 2>/dev/null | head -1)
1137
- if [ -n "$DTO_FILE" ] && grep -q "public string Code" "$DTO_FILE"; then
1138
- echo "WARNING: Create${ENTITY}Dto has Code field but service uses ICodeGenerator (code is auto-generated)"
1139
- echo "Fix: Remove Code from Create${ENTITY}Dto — it is auto-generated by ICodeGenerator<${ENTITY}>"
1140
- fi
1141
- fi
1142
- done
1143
- fi
1144
- ```
1145
-
1146
- ### POST-CHECK C32: Translation seed data must have idempotency guard (CRITICAL)
1147
-
1148
- ```bash
1149
- PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
1150
- if [ -n "$PROVIDER" ]; then
1151
- # Check if NavigationTranslations.Add is used WITHOUT a preceding AnyAsync guard
1152
- # Pattern: any .Add(NavigationTranslation.Create(...)) that is NOT inside an AnyAsync check
1153
- TRANSLATION_ADDS=$(grep -c "NavigationTranslations.Add" "$PROVIDER" 2>/dev/null)
1154
- TRANSLATION_GUARDS=$(grep -c "NavigationTranslations.AnyAsync" "$PROVIDER" 2>/dev/null)
1155
-
1156
- if [ "$TRANSLATION_ADDS" -gt 0 ] && [ "$TRANSLATION_GUARDS" -eq 0 ]; then
1157
- echo "CRITICAL: Translation seed data inserts without idempotency guard in $PROVIDER"
1158
- echo "Fix: Before each NavigationTranslations.Add block, check existence:"
1159
- echo " if (!await context.NavigationTranslations.AnyAsync("
1160
- echo " t => t.EntityId == {Module}NavigationSeedData.{Module}ModuleId"
1161
- echo " && t.EntityType == NavigationEntityType.Module, ct))"
1162
- echo " { foreach (var t in ...) { context.NavigationTranslations.Add(...); } }"
1163
- echo "The unique index IX_nav_Translations_EntityType_EntityId_LanguageCode will crash on duplicates."
1164
- exit 1
1165
- fi
1166
- fi
1167
- ```
1168
-
1169
- ### POST-CHECK C33: Resource seed data must use actual section IDs from DB (CRITICAL)
1170
-
1171
- ```bash
1172
- PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
1173
- if [ -n "$PROVIDER" ]; then
1174
- # Check if NavigationResource.Create uses secEntry.Id or resEntry.SectionId (seed-time GUIDs)
1175
- # instead of actualSection.Id (real DB ID). This causes FK_nav_Resources_nav_Sections_SectionId violation.
1176
- if grep -Pn 'NavigationResource\.Create\(' "$PROVIDER" | grep -q 'resEntry\.SectionId\|secEntry\.Id'; then
1177
- echo "CRITICAL: Resource seed data uses seed-time GUID as SectionId in $PROVIDER"
1178
- echo "NavigationSection.Create() generates its own ID — seed-time GUIDs do NOT exist in nav_Sections."
1179
- echo "Fix: Query actual section from DB before creating resources:"
1180
- echo " var actualSection = await context.NavigationSections"
1181
- echo " .FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == modEntity.Id, ct);"
1182
- echo " NavigationResource.Create(actualSection.Id, ...) // NOT secEntry.Id or resEntry.SectionId"
1183
- exit 1
1184
- fi
1185
- fi
1186
- ```
1187
-
1188
- ### POST-CHECK C34: NavRoute segments must use kebab-case for multi-word codes (BLOCKING)
1189
-
1190
- ```bash
1191
- # NavRoute segments are navigation entity Codes joined by dots.
1192
- # Multi-word codes MUST use kebab-case (e.g., "human-resources", NOT "humanresources").
1193
- # Verified from SmartStack.app: "support-client.my-tickets", "administration.access-requests"
1194
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1195
- if [ -n "$CTRL_FILES" ]; then
1196
- for f in $CTRL_FILES; do
1197
- NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
1198
- if [ -n "$NAVROUTE_VAL" ]; then
1199
- # Check each segment for concatenated multi-word (10+ lowercase chars without hyphens)
1200
- for SEG in $(echo "$NAVROUTE_VAL" | tr '.' '\n'); do
1201
- if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
1202
- echo "BLOCKING: NavRoute segment '$SEG' in $f appears to be concatenated multi-word without hyphens"
1203
- echo " Full NavRoute: $NAVROUTE_VAL"
1204
- echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
1205
- echo " SmartStack convention (from SmartStack.app): 'support-client.my-tickets'"
1206
- exit 1
1207
- fi
1208
- done
1209
- fi
1210
- done
1211
- fi
1212
-
1213
- # Also check seed data Code values for navigation entities
1214
- SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -o -name "NavigationApplicationSeedData.cs" 2>/dev/null)
1215
- if [ -n "$SEED_FILES" ]; then
1216
- CODES=$(grep -oP 'Code\s*=\s*"([^"]+)"' $SEED_FILES 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
1217
- for CODE in $CODES; do
1218
- if echo "$CODE" | grep -qP '^[a-z]{10,}$'; then
1219
- echo "BLOCKING: Navigation seed data Code '$CODE' appears to be concatenated multi-word without hyphens"
1220
- echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
1221
- exit 1
1222
- fi
1223
- done
1224
- fi
1225
- ```
1226
-
1227
- ### POST-CHECK C35: Permission codes must use kebab-case matching NavRoute codes (BLOCKING)
1228
-
1229
- ```bash
1230
- # Permission codes in [RequirePermission] and Permissions.cs MUST use kebab-case for multi-word segments.
1231
- # SmartStack.app convention: "support-client.my-tickets.read" (kebab-case everywhere)
1232
- # FORBIDDEN: "humanresources.employees.read" — must be "human-resources.employees.read"
1233
-
1234
- # Check [RequirePermission] attributes in controllers
1235
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1236
- if [ -n "$CTRL_FILES" ]; then
1237
- for f in $CTRL_FILES; do
1238
- PERM_VALS=$(grep -oP 'RequirePermission\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
1239
- for PERM in $PERM_VALS; do
1240
- # Check each segment (except the action suffix) for concatenated multi-word without hyphens
1241
- SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1) # remove last segment (action: read/create/update/delete)
1242
- for SEG in $SEGMENTS; do
1243
- if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
1244
- echo "BLOCKING: Permission code segment '$SEG' in $f appears concatenated without hyphens"
1245
- echo " Full permission: $PERM"
1246
- echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
1247
- echo " SmartStack convention: 'support-client.my-tickets.read'"
1248
- exit 1
1249
- fi
1250
- done
1251
- done
1252
- done
1253
- fi
1254
-
1255
- # Check Permissions.cs constants
1256
- PERM_FILES=$(find src/ -path "*/Authorization/Permissions.cs" 2>/dev/null)
1257
- if [ -n "$PERM_FILES" ]; then
1258
- for f in $PERM_FILES; do
1259
- CONST_VALS=$(grep -oP '=\s*"([^"]+)"' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
1260
- for PERM in $CONST_VALS; do
1261
- SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
1262
- for SEG in $SEGMENTS; do
1263
- if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
1264
- echo "BLOCKING: Permissions.cs constant segment '$SEG' in $f appears concatenated without hyphens"
1265
- echo " Full permission: $PERM"
1266
- echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
1267
- exit 1
1268
- fi
1269
- done
1270
- done
1271
- done
1272
- fi
1273
-
1274
- # Check PermissionsSeedData.cs for mismatched paths
1275
- SEED_PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*PermissionsSeedData.cs" 2>/dev/null)
1276
- if [ -n "$SEED_PERM_FILES" ]; then
1277
- PATHS=$(grep -oP '"[a-z][a-z0-9.-]+\.(read|create|update|delete|\*)"' $SEED_PERM_FILES 2>/dev/null | tr -d '"')
1278
- for PERM in $PATHS; do
1279
- SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
1280
- for SEG in $SEGMENTS; do
1281
- if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
1282
- echo "BLOCKING: PermissionsSeedData path segment '$SEG' appears concatenated without hyphens"
1283
- echo " Full permission path: $PERM"
1284
- echo " Fix: Use kebab-case matching NavRoute: 'humanresources' → 'human-resources'"
1285
- exit 1
1286
- fi
1287
- done
1288
- done
1289
- fi
1290
- ```
1291
-
1292
- ### POST-CHECK C36: Frontend navigate() calls must have matching route definitions (CRITICAL)
1293
-
1294
- ```bash
1295
- # Detect dead links: navigate() calls to paths that don't have corresponding page components.
1296
- # Example: LeavesPage has navigate('../leave-types') but no LeaveTypesPage or route exists.
1297
- PAGE_FILES=$(find web/ -name "*.tsx" -path "*/pages/*" ! -name "*.test.tsx" 2>/dev/null)
1298
- if [ -n "$PAGE_FILES" ]; then
1299
- # Extract navigate targets (relative paths like '../leave-types', './create', etc.)
1300
- NAV_TARGETS=$(grep -oP "navigate\(['\"]([^'\"]+)['\"]" $PAGE_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u)
1301
- # Extract route paths from App.tsx or route config
1302
- APP_FILES=$(find web/ -name "App.tsx" -o -name "routes.tsx" -o -name "applicationRoutes*.tsx" -o -name "clientRoutes*.tsx" 2>/dev/null)
1303
- if [ -n "$APP_FILES" ] && [ -n "$NAV_TARGETS" ]; then
1304
- ROUTE_PATHS=$(grep -oP "path:\s*['\"]([^'\"]+)['\"]" $APP_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u)
1305
- for TARGET in $NAV_TARGETS; do
1306
- # Skip dynamic segments (:id), back navigation (-1), and absolute URLs
1307
- if echo "$TARGET" | grep -qP '^(:|/api|http|-[0-9])'; then continue; fi
1308
- # Extract the last path segment for matching (e.g., '../leave-types' → 'leave-types')
1309
- LAST_SEG=$(echo "$TARGET" | grep -oP '[a-z][-a-z0-9]*$')
1310
- if [ -z "$LAST_SEG" ]; then continue; fi
1311
- # Check if any route path contains this segment
1312
- FOUND=$(echo "$ROUTE_PATHS" | grep -F "$LAST_SEG" 2>/dev/null)
1313
- if [ -z "$FOUND" ]; then
1314
- # Verify no page component exists for this path
1315
- SEG_PASCAL=$(echo "$LAST_SEG" | sed -r 's/(^|-)([a-z])/\U\2/g')
1316
- PAGE_EXISTS=$(find web/ -name "${SEG_PASCAL}Page.tsx" -o -name "${SEG_PASCAL}ListPage.tsx" -o -name "${SEG_PASCAL}sPage.tsx" 2>/dev/null)
1317
- if [ -z "$PAGE_EXISTS" ]; then
1318
- # Find which file has this navigate call
1319
- SOURCE_FILE=$(grep -rl "navigate(['\"].*${LAST_SEG}" $PAGE_FILES 2>/dev/null | head -1)
1320
- echo "CRITICAL: Dead link detected — navigate('$TARGET') in $SOURCE_FILE"
1321
- echo " Route segment '$LAST_SEG' has no matching route in App.tsx and no page component"
1322
- echo " Fix: Either create the page component + route, or remove the navigate() button"
1323
- exit 1
1324
- fi
1325
- fi
1326
- done
1327
- fi
1328
- fi
1329
- ```
1330
-
1331
- ### POST-CHECK C37: Detail page tabs must NOT navigate() — content switches locally (CRITICAL)
1332
-
1333
- ```bash
1334
- # Tabs on detail pages MUST use local state (setActiveTab) — NEVER navigate() to other pages.
1335
- # Root cause (test-apex-006): EmployeeDetailPage tabs navigated to ../leaves and ../time-tracking
1336
- # instead of rendering sub-resource content inline. Users lost detail page context.
1337
- DETAIL_PAGES=$(find src/ web/ -name "*DetailPage.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
1338
- if [ -n "$DETAIL_PAGES" ]; then
1339
- FAIL=false
1340
- for DP in $DETAIL_PAGES; do
1341
- # Check if the page has tabs (activeTab state)
1342
- HAS_TABS=$(grep -P "useState.*activeTab|setActiveTab" "$DP" 2>/dev/null)
1343
- if [ -z "$HAS_TABS" ]; then continue; fi
1344
-
1345
- # Check if any tab click handler calls navigate()
1346
- # Pattern: function that both references setActiveTab AND navigate()
1347
- # Look for navigate() calls inside handlers that also set tab state
1348
- 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 "//")
1349
- if [ -n "$TAB_NAVIGATE" ]; then
1350
- # Verify this navigate is in a tab handler context (near setActiveTab usage)
1351
- # Simple heuristic: if file has both setActiveTab AND navigate() to relative paths
1352
- RELATIVE_NAV=$(echo "$TAB_NAVIGATE" | grep -P "navigate\(['\"\`]\.\./" 2>/dev/null)
1353
- if [ -n "$RELATIVE_NAV" ]; then
1354
- echo "CRITICAL: Detail page tabs use navigate() instead of local content switching: $DP"
1355
- echo " Tab click handlers MUST only call setActiveTab() — render content inline"
1356
- echo " Found navigate() calls (likely in tab handlers):"
1357
- echo "$RELATIVE_NAV"
1358
- echo ""
1359
- echo " Fix: Remove navigate() from tab handlers. Render sub-resource content inline:"
1360
- echo " {activeTab === 'leaves' && <LeaveRequestsTable employeeId={entity.id} />}"
1361
- echo " See smartstack-frontend.md section 3 'Tab Behavior Rules' for the correct pattern."
1362
- FAIL=true
1363
- fi
1364
- fi
1365
- done
1366
- if [ "$FAIL" = true ]; then
1367
- exit 1
1368
- fi
1369
- fi
1370
- ```
1371
-
1372
- ### POST-CHECK C38: Migration ModelSnapshot must contain ALL entities registered in DbContext (BLOCKING)
1373
-
1374
- ```bash
1375
- # Root cause (test-apex-007): 7 entities registered in DbContext but migration only covered 3.
1376
- # Happens when migration is created ONCE in Layer 0 for the first batch, then additional entities
1377
- # are added in subsequent iterations without re-running migration.
1378
- SNAPSHOT=$(find src/ -name "*ModelSnapshot.cs" -path "*/Migrations/*" 2>/dev/null | head -1)
1379
- DBCONTEXT=$(find src/ -name "*DbContext.cs" -path "*/Persistence/*" ! -name "*DesignTime*" 2>/dev/null | head -1)
1380
- if [ -n "$SNAPSHOT" ] && [ -n "$DBCONTEXT" ]; then
1381
- # Extract DbSet entity names from DbContext (DbSet<EntityName>)
1382
- DBSET_ENTITIES=$(grep -oP 'DbSet<(\w+)>' "$DBCONTEXT" 2>/dev/null | grep -oP '<\K\w+(?=>)' | sort -u)
1383
- FAIL=false
1384
- for ENTITY in $DBSET_ENTITIES; do
1385
- # Skip base SmartStack entities (handled by core migrations)
1386
- if echo "$ENTITY" | grep -qP '^(Navigation|Tenant|User|Role|Permission|AuditLog|ApplicationTracking)'; then
1387
- continue
1388
- fi
1389
- # Check if the entity appears in ModelSnapshot (builder.Entity<EntityName>)
1390
- if ! grep -q "Entity<$ENTITY>" "$SNAPSHOT" 2>/dev/null; then
1391
- echo "BLOCKING: Entity '$ENTITY' is registered as DbSet in $DBCONTEXT but MISSING from ModelSnapshot"
1392
- echo " This means no migration was created for this entity — it will not exist in the database."
1393
- echo " Fix: Run 'dotnet ef migrations add' to include all new entities"
1394
- FAIL=true
1395
- fi
1396
- done
1397
- if [ "$FAIL" = true ]; then
1398
- echo ""
1399
- echo " Root cause: Migration was likely created once for the first batch of entities,"
1400
- echo " but additional entities were added later without regenerating the migration."
1401
- echo " Fix: Create a new migration that covers ALL missing entities."
1402
- exit 1
1403
- fi
1404
- fi
1405
- ```
1406
-
1407
- ### POST-CHECK C39: I18n namespace files must be registered in i18n config (CRITICAL)
1408
-
1409
- ```bash
1410
- # Root cause (test-apex-007): i18n JSON files existed in src/i18n/locales/ but were never
1411
- # registered in the i18n config (config.ts or index.ts). Pages calling useTranslation(['module'])
1412
- # got empty translations at runtime.
1413
- 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)
1414
- if [ -n "$I18N_CONFIG" ]; then
1415
- # Find all module JSON files in the primary language (fr)
1416
- 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)
1417
- if [ -n "$FR_FILES" ]; then
1418
- FAIL=false
1419
- for JSON_FILE in $FR_FILES; do
1420
- NS=$(basename "$JSON_FILE" .json)
1421
- # Check if namespace is referenced in config (import or resource key)
1422
- if ! grep -q "$NS" "$I18N_CONFIG" 2>/dev/null; then
1423
- echo "CRITICAL: i18n namespace '$NS' (from $JSON_FILE) is not registered in $I18N_CONFIG"
1424
- echo " Pages using useTranslation(['$NS']) will get empty translations at runtime"
1425
- echo " Fix: Add '$NS' to the resources/ns configuration in $I18N_CONFIG"
1426
- FAIL=true
1427
- fi
1428
- done
1429
- if [ "$FAIL" = true ]; then
1430
- exit 1
1431
- fi
1432
- fi
1433
- fi
1434
- ```
1435
-
1436
- ### POST-CHECK C40: FluentValidation validators must be registered via DI (BLOCKING)
1437
-
1438
- ```bash
1439
- # Root cause (test-apex-007): Validators existed but were never registered in DI.
1440
- # Without DI registration, [FromBody] DTOs are never validated — any data is accepted.
1441
- VALIDATOR_FILES=$(find src/ -name "*Validator.cs" -path "*/Validators/*" 2>/dev/null | grep -v test | grep -v Test)
1442
- if [ -n "$VALIDATOR_FILES" ]; then
1443
- # Check DI registration file exists
1444
- DI_FILE=$(find src/ -name "DependencyInjection.cs" -o -name "ServiceCollectionExtensions.cs" 2>/dev/null | grep -v test | head -1)
1445
- if [ -z "$DI_FILE" ]; then
1446
- echo "BLOCKING: Validators exist but no DependencyInjection.cs found for DI registration"
1447
- exit 1
1448
- fi
1449
- # Check for AddValidatorsFromAssembly or individual validator registration
1450
- HAS_ASSEMBLY_REG=$(grep -c "AddValidatorsFromAssembly\|AddValidatorsFromAssemblyContaining" "$DI_FILE" 2>/dev/null)
1451
- if [ "$HAS_ASSEMBLY_REG" -eq 0 ]; then
1452
- # Check individual registrations as fallback
1453
- VALIDATOR_COUNT=$(echo "$VALIDATOR_FILES" | wc -l)
1454
- REGISTERED_COUNT=0
1455
- for VF in $VALIDATOR_FILES; do
1456
- VN=$(basename "$VF" .cs)
1457
- if grep -q "$VN" "$DI_FILE" 2>/dev/null; then
1458
- REGISTERED_COUNT=$((REGISTERED_COUNT + 1))
1459
- fi
1460
- done
1461
- if [ "$REGISTERED_COUNT" -eq 0 ]; then
1462
- echo "BLOCKING: $VALIDATOR_COUNT validators exist but NONE are registered in DI ($DI_FILE)"
1463
- echo " Fix: Add 'services.AddValidatorsFromAssemblyContaining<Create{Entity}DtoValidator>();' to $DI_FILE"
1464
- echo " Or use 'services.AddValidatorsFromAssembly(typeof(Create{Entity}DtoValidator).Assembly);'"
1465
- exit 1
1466
- fi
1467
- fi
1468
- fi
1469
- ```
1470
-
1471
- ### POST-CHECK C41: Date/date properties in DTOs must use DateOnly, not string (WARNING)
1472
-
1473
- ```bash
1474
- # Root cause (test-apex-007): WorkLog DTO had Date property typed as string instead of DateOnly.
1475
- # This causes: invalid date parsing, no date validation, inconsistent formats across clients.
1476
- DTO_FILES=$(find src/ -name "*Dto.cs" -path "*/DTOs/*" 2>/dev/null)
1477
- if [ -n "$DTO_FILES" ]; then
1478
- FAIL=false
1479
- for f in $DTO_FILES; do
1480
- # Find string properties whose name contains "Date" (case-insensitive)
1481
- BAD_DATES=$(grep -Pn 'string\??\s+\w*[Dd]ate\w*\s*[{;,]' "$f" 2>/dev/null | grep -vi "Updated\|Created\|format\|pattern\|string\|parse")
1482
- if [ -n "$BAD_DATES" ]; then
1483
- echo "WARNING: DTO has string type for date field — must use DateOnly: $f"
1484
- echo "$BAD_DATES"
1485
- echo " Fix: Change 'string Date' to 'DateOnly Date' (or 'DateOnly? Date' if nullable)"
1486
- echo " DateOnly is the correct .NET type for date-only values (no time component)"
1487
- FAIL=true
1488
- fi
1489
- done
1490
- if [ "$FAIL" = true ]; then
1491
- exit 0
1492
- fi
1493
- fi
1494
- ```
1495
-
1496
- ### POST-CHECK C42: Every module with entities must have a migration covering them (BLOCKING)
1497
-
1498
- ```bash
1499
- # Complementary to POST-CHECK C38 — checks from the entity side.
1500
- # Finds entity .cs files in Domain/ and verifies they appear in at least one migration file.
1501
- ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null | grep -v test)
1502
- MIGRATION_DIR=$(find src/ -path "*/Migrations" -type d 2>/dev/null | head -1)
1503
- if [ -n "$ENTITY_FILES" ] && [ -n "$MIGRATION_DIR" ]; then
1504
- MIGRATION_FILES=$(find "$MIGRATION_DIR" -name "*.cs" ! -name "*ModelSnapshot*" ! -name "*DesignTime*" 2>/dev/null)
1505
- if [ -z "$MIGRATION_FILES" ]; then
1506
- echo "BLOCKING: Entity files exist in Domain/Entities but NO migration files found in $MIGRATION_DIR"
1507
- exit 1
1508
- fi
1509
- FAIL=false
1510
- for EF in $ENTITY_FILES; do
1511
- ENTITY_NAME=$(basename "$EF" .cs)
1512
- # Skip abstract base classes and interfaces
1513
- if grep -qP '^\s*(public\s+)?(abstract|interface)\s' "$EF" 2>/dev/null; then continue; fi
1514
- # Check if entity appears in any migration (CreateTable or AddColumn or entity reference)
1515
- FOUND=$(grep -l "$ENTITY_NAME" $MIGRATION_FILES 2>/dev/null)
1516
- if [ -z "$FOUND" ]; then
1517
- echo "BLOCKING: Entity '$ENTITY_NAME' ($EF) not found in any migration file"
1518
- echo " This entity will NOT have a database table."
1519
- echo " Fix: Run 'dotnet ef migrations add' to create a migration covering this entity"
1520
- FAIL=true
1521
- fi
1522
- done
1523
- if [ "$FAIL" = true ]; then
1524
- exit 1
1525
- fi
1526
- fi
1527
- ```
1528
-
1529
- ### POST-CHECK C43: Controllers must NOT have both [Route] and [NavRoute] attributes (BLOCKING)
1530
-
1531
- ```bash
1532
- # Root cause (test-apex-007): All 7 controllers had BOTH [Route("api/...")] and [NavRoute("...")].
1533
- # In SmartStack, [NavRoute] resolves routes dynamically from Navigation entities at startup.
1534
- # [Route] is standard ASP.NET Core static routing. When both exist:
1535
- # - NavRoute middleware tries to resolve from DB → fails if seed data not applied → no route
1536
- # - [Route] may or may not take over depending on middleware order
1537
- # - Result: 404 on ALL endpoints
1538
- # The MCP validate_conventions previously ENCOURAGED adding [Route] with [NavRoute] — this was a bug.
1539
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1540
- if [ -n "$CTRL_FILES" ]; then
1541
- FAIL=false
1542
- for f in $CTRL_FILES; do
1543
- HAS_NAVROUTE=$(grep -c '\[NavRoute(' "$f" 2>/dev/null)
1544
- HAS_ROUTE=$(grep -c '\[Route(' "$f" 2>/dev/null)
1545
- if [ "$HAS_NAVROUTE" -gt 0 ] && [ "$HAS_ROUTE" -gt 0 ]; then
1546
- NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"' "$f" 2>/dev/null | head -1)
1547
- ROUTE_VAL=$(grep -oP 'Route\("([^"]+)"' "$f" 2>/dev/null | head -1)
1548
- echo "BLOCKING: Controller has BOTH [Route] and [NavRoute] — remove [Route]: $f"
1549
- echo " Found: [$ROUTE_VAL] + [$NAVROUTE_VAL]"
1550
- echo " In SmartStack, [NavRoute] resolves routes dynamically from the database."
1551
- echo " Having [Route] alongside it causes route conflicts and 404s."
1552
- echo " Fix: Remove the [Route(...)] attribute, keep only [NavRoute(...)]"
1553
- FAIL=true
1554
- fi
1555
- done
1556
- if [ "$FAIL" = true ]; then
1557
- exit 1
1558
- fi
1559
- fi
1560
- ```
1561
-
1562
- ### POST-CHECK C44: RolesSeedData must map standard role-permission matrix (CRITICAL)
1563
-
1564
- ```bash
1565
- # SmartStack standard role-permission matrix:
1566
- # Admin = wildcard (*) — full access
1567
- # Manager = CRU (read + create + update) — no delete
1568
- # Contributor = CR (read + create) — no update, no delete
1569
- # Viewer = R (read only)
1570
- # If RolesSeedData deviates from this matrix, the RBAC model is broken.
1571
- ROLE_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" ! -name "ApplicationRolesSeedData.cs" 2>/dev/null)
1572
- if [ -n "$ROLE_SEED_FILES" ]; then
1573
- FAIL=false
1574
- for f in $ROLE_SEED_FILES; do
1575
- # Skip ApplicationRolesSeedData (defines roles, not mappings)
1576
- BASENAME=$(basename "$f")
1577
- if [ "$BASENAME" = "ApplicationRolesSeedData.cs" ]; then continue; fi
1578
-
1579
- # Check Admin has wildcard
1580
- HAS_ADMIN_WILDCARD=$(grep -Pc '(admin|Admin).*\*' "$f" 2>/dev/null)
1581
- if [ "$HAS_ADMIN_WILDCARD" -eq 0 ]; then
1582
- # Also accept .Access or wildcard pattern
1583
- HAS_ADMIN_ACCESS=$(grep -Pc '(admin|Admin).*(Access|Wildcard|IsWildcard)' "$f" 2>/dev/null)
1584
- if [ "$HAS_ADMIN_ACCESS" -eq 0 ]; then
1585
- echo "CRITICAL: Admin role missing wildcard (*) permission in $f"
1586
- echo "Fix: Admin must map to wildcard permission (navRoute.*) or use IsWildcard=true"
1587
- FAIL=true
1588
- fi
1589
- fi
1590
-
1591
- # Check Viewer has NO delete/create/update
1592
- VIEWER_WRITE=$(grep -Pc '(viewer|Viewer).*(\.delete|\.create|\.update|Delete|Create|Update)' "$f" 2>/dev/null)
1593
- if [ "$VIEWER_WRITE" -gt 0 ]; then
1594
- echo "CRITICAL: Viewer role has write permissions (create/update/delete) in $f"
1595
- echo "Fix: Viewer must only have read permission. Remove create/update/delete mappings."
1596
- FAIL=true
1597
- fi
1598
-
1599
- # Check Manager has NO delete
1600
- MANAGER_DELETE=$(grep -Pc '(manager|Manager).*(\.delete|Delete)' "$f" 2>/dev/null)
1601
- if [ "$MANAGER_DELETE" -gt 0 ]; then
1602
- echo "WARNING: Manager role has delete permission in $f"
1603
- echo "SmartStack standard: Manager = CRU (no delete). Verify this is intentional."
1604
- fi
1605
- done
1606
- if [ "$FAIL" = true ]; then
1607
- exit 1
1608
- fi
1609
- fi
1610
- ```
1611
-
1612
- ### POST-CHECK C45: PermissionAction enum must use valid typed values only (BLOCKING)
1613
-
1614
- ```bash
1615
- # Valid PermissionAction enum values: Access(0), Read(1), Create(2), Update(3), Delete(4),
1616
- # Export(5), Import(6), Approve(7), Reject(8), Assign(9), Execute(10)
1617
- # FORBIDDEN: Enum.Parse<PermissionAction>("...") — runtime crash if value doesn't exist
1618
- # FORBIDDEN: (PermissionAction)99 or any cast beyond 0-10
1619
- SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
1620
- if [ -n "$SEED_FILES" ]; then
1621
- FAIL=false
1622
- for f in $SEED_FILES; do
1623
- # Check for Enum.Parse<PermissionAction> usage
1624
- ENUM_PARSE=$(grep -Pn 'Enum\.Parse<PermissionAction>' "$f" 2>/dev/null)
1625
- if [ -n "$ENUM_PARSE" ]; then
1626
- echo "BLOCKING: Enum.Parse<PermissionAction> detected — runtime crash risk: $f"
1627
- echo "$ENUM_PARSE"
1628
- echo "Fix: Use typed enum directly: PermissionAction.Read (NOT Enum.Parse<PermissionAction>(\"Read\"))"
1629
- FAIL=true
1630
- fi
1631
-
1632
- # Check for invalid cast values (PermissionAction)N where N > 10
1633
- INVALID_CAST=$(grep -Pn '\(PermissionAction\)\s*([1-9]\d{1,}|[2-9]\d)' "$f" 2>/dev/null)
1634
- if [ -n "$INVALID_CAST" ]; then
1635
- echo "BLOCKING: Invalid PermissionAction cast detected (value > 10): $f"
1636
- echo "$INVALID_CAST"
1637
- 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)"
1638
- FAIL=true
1639
- fi
1640
- done
1641
- if [ "$FAIL" = true ]; then
1642
- exit 1
1643
- fi
1644
- fi
1645
- ```
1646
-
1647
- ### POST-CHECK C46: Navigation translation completeness — 4 languages per level (WARNING)
1648
-
1649
- ```bash
1650
- # Every navigation seed data file must provide translations for ALL 4 languages (fr, en, it, de).
1651
- # If sections exist (GetSectionEntries), GetSectionTranslationEntries MUST also exist.
1652
- # If resources exist (GetResourceEntries), resource translation entries MUST also exist.
1653
- NAV_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" ! -name "*Application*" 2>/dev/null)
1654
- if [ -n "$NAV_SEED_FILES" ]; then
1655
- FAIL=false
1656
- for f in $NAV_SEED_FILES; do
1657
- # Check module translations have all 4 languages
1658
- LANG_COUNT=$(grep -c 'LanguageCode\s*=' "$f" 2>/dev/null)
1659
- HAS_FR=$(grep -c '"fr"' "$f" 2>/dev/null)
1660
- HAS_EN=$(grep -c '"en"' "$f" 2>/dev/null)
1661
- HAS_IT=$(grep -c '"it"' "$f" 2>/dev/null)
1662
- HAS_DE=$(grep -c '"de"' "$f" 2>/dev/null)
1663
-
1664
- if [ "$HAS_FR" -eq 0 ] || [ "$HAS_EN" -eq 0 ] || [ "$HAS_IT" -eq 0 ] || [ "$HAS_DE" -eq 0 ]; then
1665
- echo "WARNING: Missing language(s) in navigation translations: $f"
1666
- echo " fr=$HAS_FR, en=$HAS_EN, it=$HAS_IT, de=$HAS_DE (all must be > 0)"
1667
- echo "Fix: Add NavigationTranslationSeedEntry for all 4 languages (fr, en, it, de)"
1668
- FAIL=true
1669
- fi
1670
-
1671
- # If sections exist, section translations MUST exist
1672
- HAS_SECTION_ENTRIES=$(grep -c 'GetSectionEntries' "$f" 2>/dev/null)
1673
- HAS_SECTION_TRANSLATIONS=$(grep -c 'GetSectionTranslationEntries' "$f" 2>/dev/null)
1674
- if [ "$HAS_SECTION_ENTRIES" -gt 0 ] && [ "$HAS_SECTION_TRANSLATIONS" -eq 0 ]; then
1675
- echo "WARNING: Sections defined but GetSectionTranslationEntries() missing: $f"
1676
- echo "Fix: Add GetSectionTranslationEntries() with 4 languages per section (ref core-seed-data.md §2b)"
1677
- FAIL=true
1678
- fi
1679
-
1680
- # If resources exist, resource translations MUST exist
1681
- HAS_RESOURCE_ENTRIES=$(grep -c 'GetResourceEntries' "$f" 2>/dev/null)
1682
- HAS_RESOURCE_TRANSLATIONS=$(grep -Pc 'ResourceTranslation|GetResourceTranslation|NavigationEntityType\.Resource.*LanguageCode' "$f" 2>/dev/null)
1683
- if [ "$HAS_RESOURCE_ENTRIES" -gt 0 ] && [ "$HAS_RESOURCE_TRANSLATIONS" -eq 0 ]; then
1684
- echo "WARNING: Resources defined but resource translations missing: $f"
1685
- echo "Fix: Add resource translation entries with 4 languages per resource (ref core-seed-data.md §2b)"
1686
- FAIL=true
1687
- fi
1688
- done
1689
- if [ "$FAIL" = true ]; then
1690
- exit 0
1691
- fi
1692
- fi
1693
- ```
1694
-
1695
- ### POST-CHECK C47: Person Extension entities must not duplicate User fields (WARNING)
1696
-
1697
- ```bash
1698
- # Mandatory person extension entities (UserId non-nullable + ITenantEntity) must NOT have
1699
- # person fields (FirstName, LastName, Email, PhoneNumber). These come from the linked User.
1700
- # Optional variants (UserId nullable) are expected to have their own person fields.
1701
- ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null)
1702
- if [ -n "$ENTITY_FILES" ]; then
1703
- FAIL=false
1704
- for f in $ENTITY_FILES; do
1705
- # Check if entity has UserId (person extension pattern)
1706
- HAS_USERID=$(grep -P 'public\s+Guid\s+UserId\s*\{' "$f" 2>/dev/null)
1707
- if [ -z "$HAS_USERID" ]; then continue; fi
1708
-
1709
- # Check if UserId is non-nullable (mandatory variant)
1710
- IS_MANDATORY=$(grep -P 'public\s+Guid\s+UserId\s*\{' "$f" 2>/dev/null | grep -v 'Guid?')
1711
- if [ -z "$IS_MANDATORY" ]; then continue; fi
1712
-
1713
- # Check for ITenantEntity (confirms it's a person extension, not a random FK)
1714
- if ! grep -q "ITenantEntity" "$f"; then continue; fi
1715
-
1716
- # Mandatory person extension: MUST NOT have person fields
1717
- PERSON_FIELDS=$(grep -Pn 'public\s+string\S*\s+(FirstName|LastName|Email|PhoneNumber)\s*\{' "$f" 2>/dev/null)
1718
- if [ -n "$PERSON_FIELDS" ]; then
1719
- echo "WARNING: Mandatory person extension entity duplicates User fields: $f"
1720
- echo " Entity has non-nullable UserId (mandatory variant) — person fields come from User"
1721
- echo "$PERSON_FIELDS"
1722
- echo " Fix: Remove FirstName/LastName/Email/PhoneNumber — use Display* from User join in ResponseDto"
1723
- echo " See references/person-extension-pattern.md section 2"
1724
- FAIL=true
1725
- fi
1726
- done
1727
- if [ "$FAIL" = true ]; then
1728
- exit 0
1729
- fi
1730
- fi
1731
- ```
1732
-
1733
- ### POST-CHECK C48: Person Extension service must Include(User) (CRITICAL)
1734
-
1735
- ```bash
1736
- # Services operating on entities with UserId FK (person extension pattern) MUST include
1737
- # .Include(x => x.User) on all queries. Without this, Display* fields are always null.
1738
- ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null)
1739
- if [ -n "$ENTITY_FILES" ]; then
1740
- FAIL=false
1741
- for f in $ENTITY_FILES; do
1742
- # Check if entity has UserId + ITenantEntity (person extension pattern)
1743
- HAS_USERID=$(grep -P 'public\s+Guid\??\s+UserId\s*\{' "$f" 2>/dev/null)
1744
- if [ -z "$HAS_USERID" ]; then continue; fi
1745
- if ! grep -q "ITenantEntity" "$f"; then continue; fi
1746
-
1747
- # Check if navigation property User? exists (confirms person extension)
1748
- HAS_USER_NAV=$(grep -P 'public\s+User\?\s+User\s*\{' "$f" 2>/dev/null)
1749
- if [ -z "$HAS_USER_NAV" ]; then continue; fi
1750
-
1751
- # Find the corresponding service file
1752
- ENTITY_NAME=$(basename "$f" .cs)
1753
- SERVICE_FILE=$(find src/ -path "*/Services/*" -name "${ENTITY_NAME}Service.cs" ! -name "I${ENTITY_NAME}Service.cs" 2>/dev/null | head -1)
1754
- if [ -z "$SERVICE_FILE" ]; then continue; fi
1755
-
1756
- # Check for Include(x => x.User) or Include("User") pattern
1757
- HAS_INCLUDE=$(grep -P 'Include.*User' "$SERVICE_FILE" 2>/dev/null)
1758
- if [ -z "$HAS_INCLUDE" ]; then
1759
- echo "CRITICAL: Service for Person Extension entity must Include(x => x.User): $SERVICE_FILE"
1760
- echo " Entity: $f has UserId FK + User navigation property"
1761
- echo " Fix: Add .Include(x => x.User) to all queries in $SERVICE_FILE"
1762
- echo " Without Include, Display* fields will always be null"
1763
- echo " See references/person-extension-pattern.md section 5"
1764
- FAIL=true
1765
- fi
1766
- done
1767
- if [ "$FAIL" = true ]; then
1768
- exit 1
1769
- fi
1770
- fi
1771
- ```
1772
-
1773
- ### POST-CHECK C49: Route Ordering in App.tsx — static before dynamic (BLOCKING)
1774
-
1775
- > **Source:** Dashboard 404 bug — `:id` route declared before `dashboard` route in applicationRoutes,
1776
- > React Router matched `dashboard` as an `id` parameter → page not found.
1777
-
1778
- ```bash
1779
- APP_TSX=$(find web/ src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
1780
- if [ -n "$APP_TSX" ]; then
1781
- # Extract all route paths from applicationRoutes in order
1782
- ROUTE_PATHS=$(grep -oP "path:\s*'([^']+)'" "$APP_TSX" | sed "s/path: '//;s/'//")
1783
- FAIL=false
1784
- PREV_PREFIX=""
1785
- PREV_IS_DYNAMIC=false
1786
-
1787
- while IFS= read -r ROUTE; do
1788
- [ -z "$ROUTE" ] && continue
1789
- # Extract prefix (everything before last segment)
1790
- PREFIX=$(echo "$ROUTE" | sed 's|/[^/]*$||')
1791
- LAST_SEGMENT=$(echo "$ROUTE" | grep -oP '[^/]+$')
1792
- IS_DYNAMIC=$(echo "$LAST_SEGMENT" | grep -c '^:')
1793
-
1794
- # Check: if previous route was dynamic and current route is static with same prefix
1795
- if [ "$PREV_IS_DYNAMIC" = true ] && [ "$IS_DYNAMIC" -eq 0 ] && [ "$PREFIX" = "$PREV_PREFIX" ]; then
1796
- echo "BLOCKING: Static route '$ROUTE' is AFTER a dynamic route with prefix '$PREFIX' in App.tsx"
1797
- echo " React Router will match the dynamic route first → '$ROUTE' is unreachable (404)"
1798
- echo " Fix: Move static routes BEFORE dynamic routes within the same prefix group"
1799
- FAIL=true
1800
- fi
1801
-
1802
- PREV_PREFIX="$PREFIX"
1803
- if [ "$IS_DYNAMIC" -gt 0 ]; then
1804
- PREV_IS_DYNAMIC=true
1805
- else
1806
- PREV_IS_DYNAMIC=false
1807
- fi
1808
- done <<< "$ROUTE_PATHS"
1809
-
1810
- # Also check: Navigate (redirect) routes must be at the end
1811
- REDIRECT_LINE=$(grep -n "Navigate" "$APP_TSX" | head -1 | cut -d: -f1)
1812
- if [ -n "$REDIRECT_LINE" ]; then
1813
- ROUTES_AFTER=$(tail -n +"$((REDIRECT_LINE+1))" "$APP_TSX" | grep -cP "path:\s*'" 2>/dev/null)
1814
- if [ "$ROUTES_AFTER" -gt 0 ]; then
1815
- echo "WARNING: Route definitions found AFTER Navigate redirect — redirects should be LAST"
1816
- fi
1817
- fi
1818
-
1819
- if [ "$FAIL" = true ]; then
1820
- exit 1
1821
- fi
1822
- fi
1823
- ```
1824
-
1825
- ### POST-CHECK C50: NavRoute Uniqueness — no duplicate NavRoute values (BLOCKING)
1826
-
1827
- > **Source:** Two controllers sharing the same NavRoute causes routing conflicts — one endpoint
1828
- > silently overrides the other → 404 on the shadowed controller.
1829
-
1830
- ```bash
1831
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1832
- if [ -n "$CTRL_FILES" ]; then
1833
- # Extract all NavRoute values with their file paths
1834
- NAVROUTES=$(grep -Pn '\[NavRoute\("([^"]+)"\)' $CTRL_FILES 2>/dev/null | sed 's/.*\[NavRoute("\([^"]*\)").*/\1/' | sort)
1835
- DUPLICATES=$(echo "$NAVROUTES" | uniq -d)
1836
- if [ -n "$DUPLICATES" ]; then
1837
- echo "BLOCKING: Duplicate NavRoute values detected:"
1838
- for DUP in $DUPLICATES; do
1839
- echo " NavRoute: $DUP"
1840
- grep -l "\[NavRoute(\"$DUP\")\]" $CTRL_FILES 2>/dev/null | while read -r f; do
1841
- echo " - $f"
1842
- done
1843
- done
1844
- echo " Fix: Each controller MUST have a unique NavRoute value"
1845
- exit 1
1846
- fi
1847
- fi
1848
- ```
1849
-
1850
- ### POST-CHECK C51: NavRoute Segments vs Controller Hierarchy (WARNING)
1851
-
1852
- > **Source:** ContractsController in `Controllers/{App}/Employees/` subfolder had NavRoute
1853
- > `human-resources.contracts` (2 segments) instead of `human-resources.employees.contracts`
1854
- > (3 segments) → API resolved to wrong path → 404 on contracts endpoints.
1855
-
1856
- ```bash
1857
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
1858
- if [ -n "$CTRL_FILES" ]; then
1859
- for f in $CTRL_FILES; do
1860
- NAVROUTE=$(grep -oP '\[NavRoute\("\K[^"]+' "$f")
1861
- if [ -z "$NAVROUTE" ]; then continue; fi
1862
-
1863
- DOTS=$(echo "$NAVROUTE" | tr -cd '.' | wc -c)
1864
-
1865
- # Count directory depth after Controllers/
1866
- # e.g., Controllers/HumanResources/Employees/ContractsController.cs = depth 2 (section)
1867
- REL_PATH=$(echo "$f" | sed 's|.*/Controllers/||')
1868
- DIR_DEPTH=$(echo "$REL_PATH" | tr '/' '\n' | wc -l)
1869
- # DIR_DEPTH: 2 = module level (App/Controller.cs), 3 = section level (App/Module/Controller.cs)
1870
-
1871
- if [ "$DIR_DEPTH" -ge 3 ] && [ "$DOTS" -le 1 ]; then
1872
- echo "WARNING: Controller in section subfolder but NavRoute has only $((DOTS+1)) segments"
1873
- echo " File: $f"
1874
- echo " NavRoute: $NAVROUTE"
1875
- echo " Expected: 3+ segments (app.module.section) for controllers in section subfolders"
1876
- echo " Fix: Update NavRoute to include the module segment (e.g., 'app.module.section')"
1877
- fi
1878
-
1879
- if [ "$DOTS" -eq 0 ]; then
1880
- echo "BLOCKING: NavRoute '$NAVROUTE' has only 1 segment (minimum 2 required): $f"
1881
- exit 1
1882
- fi
1883
- done
1884
- fi
1885
- ```
1886
-
1887
- ### POST-CHECK C52: Frontend route paths must include module segment (BLOCKING)
1888
-
1889
- > **Source:** APEX regression — frontend routes used `employees` instead of `employee-management/employees`,
1890
- > causing mismatch with backend navigation seed data routes (`/human-resources/employee-management/employees`).
1891
- > Nav links produced 404 because React Router had no matching route for the full path.
1892
-
1893
- ```bash
1894
- # Compare frontend route paths in App.tsx against backend navigation seed data routes
1895
- APP_TSX=$(find . -path "*/src/App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
1896
- if [ -n "$APP_TSX" ]; then
1897
- # Extract all route paths from applicationRoutes
1898
- ROUTE_PATHS=$(grep -oP "path:\s*'[^']+'" "$APP_TSX" | grep -v "^path: ''" | grep -v ":id" | grep -v "create" | grep -v "edit" | sed "s/path: '//;s/'//")
1899
-
1900
- # Find navigation seed data files to get backend routes
1901
- NAV_SEED_FILES=$(find src/ -name "*NavigationSeedData.cs" -o -name "*NavigationModuleSeedData.cs" 2>/dev/null)
1902
- if [ -n "$NAV_SEED_FILES" ]; then
1903
- # Extract module codes from seed data
1904
- MODULE_CODES=$(grep -oP 'Code\s*=\s*"\K[^"]+' $NAV_SEED_FILES 2>/dev/null | sort -u)
1905
-
1906
- for route in $ROUTE_PATHS; do
1907
- # Skip redirect paths (empty or just module)
1908
- if [ -z "$route" ]; then continue; fi
1909
-
1910
- # Check if route contains a slash (module/section pattern)
1911
- if ! echo "$route" | grep -q "/"; then
1912
- # Single segment route — check if it matches a section code (not a module code)
1913
- IS_MODULE=false
1914
- for mod in $MODULE_CODES; do
1915
- if [ "$route" = "$mod" ]; then IS_MODULE=true; break; fi
1916
- done
1917
- if [ "$IS_MODULE" = false ]; then
1918
- echo "BLOCKING: Frontend route '$route' is missing module segment"
1919
- echo " Expected: '{module}/{route}' (e.g., 'employee-management/$route')"
1920
- echo " Backend navigation seed data defines routes with full hierarchy: /app/module/section"
1921
- echo " Frontend routes must match: module/section (relative to app root)"
1922
- fi
1923
- fi
1924
- done
1925
- fi
1926
- fi
1927
- ```
1928
-
1929
- ### POST-CHECK C53: Enum serialization — JsonStringEnumConverter required (BLOCKING)
1930
-
1931
- > **Source:** Frontend sends enum values as strings ("PaidLeave", "Morning") but C# enums serialize
1932
- > as integers by default. Without `JsonStringEnumConverter`, the JSON binding fails → 400 Bad Request
1933
- > on create/update endpoints. The enum check in step-03 Layer 0 can be skipped by the LLM — this
1934
- > POST-CHECK catches it as defense-in-depth.
1935
-
1936
- ```bash
1937
- # Check if global JsonStringEnumConverter exists in Program.cs
1938
- PROGRAM_CS=$(find src/ -name "Program.cs" -path "*/Api/*" 2>/dev/null | head -1)
1939
- HAS_GLOBAL_CONVERTER=false
1940
- if [ -n "$PROGRAM_CS" ] && grep -q "JsonStringEnumConverter" "$PROGRAM_CS" 2>/dev/null; then
1941
- HAS_GLOBAL_CONVERTER=true
1942
- fi
1943
-
1944
- if [ "$HAS_GLOBAL_CONVERTER" = false ]; then
1945
- # No global converter — every enum in Domain/Enums must have the attribute
1946
- ENUM_FILES=$(find src/ -path "*/Domain/Enums/*" -name "*.cs" 2>/dev/null)
1947
- if [ -n "$ENUM_FILES" ]; then
1948
- for f in $ENUM_FILES; do
1949
- if grep -q "public enum" "$f" && ! grep -q "JsonStringEnumConverter" "$f"; then
1950
- echo "BLOCKING: Enum missing [JsonConverter(typeof(JsonStringEnumConverter))]: $f"
1951
- echo " Frontend sends enum values as strings but C# deserializes as int by default"
1952
- echo " Fix: Add [JsonConverter(typeof(JsonStringEnumConverter))] on the enum"
1953
- echo " Or: Add JsonStringEnumConverter globally in Program.cs"
1954
- fi
1955
- done
1956
- fi
1957
- fi
1958
- ```
1959
-
1960
- ### POST-CHECK C54: No helper method calls inside .Select() on IQueryable (BLOCKING)
1961
-
1962
- > **Source:** Service `GetAllAsync()` used `.Select(a => MapToDto(a))` — EF Core cannot translate
1963
- > helper method calls to SQL → runtime `InvalidOperationException` or `NullReferenceException`
1964
- > (because `.Include()` is ignored when `.Select()` is present, and the helper expects loaded navigations).
1965
-
1966
- ```bash
1967
- SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
1968
- if [ -n "$SERVICE_FILES" ]; then
1969
- # Look for .Select(x => MethodName(x)) or .Select(MethodName) patterns on IQueryable
1970
- BAD_SELECT=$(grep -Pn '\.Select\(\s*\w+\s*=>\s*(?!new\s)[A-Z]\w+\(|\.Select\(\s*(?!x\s*=>)[A-Z]\w+\s*\)' $SERVICE_FILES 2>/dev/null)
1971
- if [ -n "$BAD_SELECT" ]; then
1972
- echo "BLOCKING: Helper method call inside .Select() on IQueryable — EF Core cannot translate to SQL"
1973
- echo "$BAD_SELECT"
1974
- echo "Fix: Use inline DTO construction: .Select(x => new ResponseDto(x.Id, x.Name, x.FK.Code))"
1975
- echo " Helper methods (MapToDto, ToDto) are only safe AFTER materialization (ToListAsync, FirstAsync)"
1976
- exit 1
1977
- fi
1978
- fi
1979
- ```
8
+ 1. `mcp__smartstack__validate_conventions()` naming, routes, NavRoute kebab-case
9
+ 2. `mcp__smartstack__validate_security()` — tenant isolation, TenantId, authorization
10
+ 3. `mcp__smartstack__validate_frontend_routes()` — lazy loading, route alignment
1980
11
 
1981
12
  ---
1982
13
 
1983
- ## Architecture — Clean Architecture Layer Isolation
1984
-
1985
- ### POST-CHECK A1: Domain must not import other layers (BLOCKING)
1986
-
1987
- ```bash
1988
- DOMAIN_FILES=$(find src/ -path "*/Domain/*" -name "*.cs" 2>/dev/null)
1989
- if [ -n "$DOMAIN_FILES" ]; then
1990
- BAD_IMPORTS=$(grep -Pn 'using\s+[\w.]*\.(Application|Infrastructure|Api)[\w.]*;' $DOMAIN_FILES 2>/dev/null)
1991
- if [ -n "$BAD_IMPORTS" ]; then
1992
- echo "BLOCKING: Domain layer imports Application/Infrastructure/Api — violates Clean Architecture"
1993
- echo "Domain is the core, it must not depend on any other layer"
1994
- echo "$BAD_IMPORTS"
1995
- echo "Fix: Move shared types to Domain or remove the dependency"
1996
- exit 1
1997
- fi
1998
- fi
1999
- ```
2000
-
2001
- ### POST-CHECK A2: Application must not import Infrastructure or Api (BLOCKING)
2002
-
2003
- ```bash
2004
- APP_FILES=$(find src/ -path "*/Application/*" -name "*.cs" 2>/dev/null)
2005
- if [ -n "$APP_FILES" ]; then
2006
- BAD_IMPORTS=$(grep -Pn 'using\s+[\w.]*\.(Infrastructure|Api)[\w.]*;' $APP_FILES 2>/dev/null)
2007
- if [ -n "$BAD_IMPORTS" ]; then
2008
- echo "BLOCKING: Application layer imports Infrastructure/Api violates Clean Architecture"
2009
- echo "Application defines interfaces, Infrastructure implements them"
2010
- echo "$BAD_IMPORTS"
2011
- echo "Fix: Define an interface in Application and implement it in Infrastructure"
2012
- exit 1
2013
- fi
2014
- fi
2015
- ```
2016
-
2017
- ### POST-CHECK A3: Controllers must not inject DbContext (BLOCKING)
2018
-
2019
- ```bash
2020
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
2021
- if [ -n "$CTRL_FILES" ]; then
2022
- BAD_DBCONTEXT=$(grep -Pn 'private\s+readonly\s+\w*DbContext|DbContext\s+\w+[,)]' $CTRL_FILES 2>/dev/null)
2023
- if [ -n "$BAD_DBCONTEXT" ]; then
2024
- echo "BLOCKING: Controller injects DbContext directly violates Clean Architecture"
2025
- echo "Controllers must use Application services, not access the database directly"
2026
- echo "$BAD_DBCONTEXT"
2027
- echo "Fix: Create an Application service with the required business logic and inject it instead"
2028
- exit 1
2029
- fi
2030
- fi
2031
- ```
2032
-
2033
- ### POST-CHECK A4: API must return DTOs, not Domain entities (WARNING)
2034
-
2035
- ```bash
2036
- # Scan controller return types for Domain entity names without Dto/Response/ViewModel suffix
2037
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
2038
- ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null)
2039
- if [ -n "$CTRL_FILES" ] && [ -n "$ENTITY_FILES" ]; then
2040
- ENTITY_NAMES=$(grep -ohP 'public\s+class\s+(\w+)\s*:' $ENTITY_FILES 2>/dev/null | grep -oP '\w+(?=\s*:)' | grep -v '^public$' | sort -u)
2041
- for ENTITY in $ENTITY_NAMES; do
2042
- BAD_RETURN=$(grep -Pn "ActionResult<$ENTITY>|ActionResult<IEnumerable<$ENTITY>>|ActionResult<List<$ENTITY>>" $CTRL_FILES 2>/dev/null)
2043
- if [ -n "$BAD_RETURN" ]; then
2044
- echo "WARNING: Controller returns Domain entity '$ENTITY' instead of a DTO"
2045
- echo "$BAD_RETURN"
2046
- echo "Fix: Return ${ENTITY}ResponseDto instead of $ENTITY"
2047
- fi
2048
- done
2049
- fi
2050
- ```
2051
-
2052
- ### POST-CHECK A5: Service interfaces in Application, implementations in Infrastructure (WARNING)
2053
-
2054
- ```bash
2055
- # Check for service implementations (non-interface classes) in Application or Api layers
2056
- APP_SERVICES=$(find src/ -path "*/Application/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
2057
- if [ -n "$APP_SERVICES" ]; then
2058
- for f in $APP_SERVICES; do
2059
- if grep -q 'public class.*Service' "$f" 2>/dev/null; then
2060
- echo "WARNING: Service implementation found in Application layer: $f"
2061
- echo "Fix: Move implementation to Infrastructure/Services/. Application should only contain interfaces."
2062
- fi
2063
- done
2064
- fi
2065
-
2066
- # Check for service interfaces (I*Service) in Domain or Api layers
2067
- DOMAIN_INTERFACES=$(find src/ -path "*/Domain/*" -name "I*Service.cs" 2>/dev/null)
2068
- API_INTERFACES=$(find src/ -path "*/Api/*" -name "I*Service.cs" 2>/dev/null)
2069
- for f in $DOMAIN_INTERFACES $API_INTERFACES; do
2070
- if [ -n "$f" ] && grep -q 'public interface.*Service' "$f" 2>/dev/null; then
2071
- echo "WARNING: Service interface found outside Application layer: $f"
2072
- echo "Fix: Move to Application/Interfaces/"
2073
- fi
2074
- done
2075
- ```
2076
-
2077
- ### POST-CHECK A6: No EF Core attributes in Domain entities (BLOCKING)
2078
-
2079
- ```bash
2080
- DOMAIN_FILES=$(find src/ -path "*/Domain/*" -name "*.cs" 2>/dev/null)
2081
- if [ -n "$DOMAIN_FILES" ]; then
2082
- BAD_EF=$(grep -Pn '\[Table\(|\[Column\(|\[Index\(|using\s+Microsoft\.EntityFrameworkCore' $DOMAIN_FILES 2>/dev/null)
2083
- if [ -n "$BAD_EF" ]; then
2084
- echo "BLOCKING: EF Core attributes or using directives found in Domain layer"
2085
- echo "Domain entities must be persistence-ignorant EF configuration belongs in Infrastructure"
2086
- echo "$BAD_EF"
2087
- echo "Fix: Move [Table], [Column], [Index] to IEntityTypeConfiguration<T> in Infrastructure/Persistence/Configurations/"
2088
- exit 1
2089
- fi
2090
- fi
2091
- ```
2092
-
2093
- ### POST-CHECK A7: No direct repository usage in controllers (WARNING)
2094
-
2095
- ```bash
2096
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
2097
- if [ -n "$CTRL_FILES" ]; then
2098
- BAD_REPO=$(grep -Pn 'IRepository<|IGenericRepository<|private\s+readonly\s+IRepository|private\s+readonly\s+IGenericRepository' $CTRL_FILES 2>/dev/null)
2099
- if [ -n "$BAD_REPO" ]; then
2100
- echo "WARNING: Controller injects repository directlyshould use Application services"
2101
- echo "$BAD_REPO"
2102
- echo "Fix: Controllers should depend on Application services (I*Service), not repositories"
2103
- fi
2104
- fi
2105
- ```
2106
-
2107
- ### POST-CHECK A8: API endpoints must match handoff apiEndpointSummary (BLOCKING)
2108
-
2109
- > **LESSON LEARNED (audit ba-002):** 4/17 API endpoints (export, calculate, balance upsert, year-end)
2110
- > were missing but all API tasks were marked COMPLETE. This check reconciles actual controller
2111
- > endpoints against the handoff contract.
2112
-
2113
- ```bash
2114
- # Read apiEndpointSummary from PRD and verify each operation exists in controllers
2115
- PRD_FILE=".ralph/prd.json"
2116
- if [ ! -f "$PRD_FILE" ]; then
2117
- # Try module-specific PRD
2118
- if [ -f ".ralph/modules-queue.json" ]; then
2119
- PRD_FILE=$(cat .ralph/modules-queue.json | grep -o '"prdFile":"[^"]*"' | tail -1 | cut -d'"' -f4)
2120
- fi
2121
- fi
2122
-
2123
- if [ -f "$PRD_FILE" ]; then
2124
- # Extract operation names from apiEndpointSummary
2125
- OPERATIONS=$(cat "$PRD_FILE" | grep -o '"operation"\s*:\s*"[^"]*"' | cut -d'"' -f4 2>/dev/null)
2126
-
2127
- if [ -n "$OPERATIONS" ]; then
2128
- CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
2129
- MISSING_OPS=""
2130
- TOTAL_OPS=0
2131
- FOUND_OPS=0
2132
-
2133
- for op in $OPERATIONS; do
2134
- TOTAL_OPS=$((TOTAL_OPS + 1))
2135
- FOUND=false
2136
- if [ -n "$CTRL_FILES" ]; then
2137
- for f in $CTRL_FILES; do
2138
- if grep -q "$op" "$f" 2>/dev/null; then
2139
- FOUND=true
2140
- break
2141
- fi
2142
- done
2143
- fi
2144
- if [ "$FOUND" = true ]; then
2145
- FOUND_OPS=$((FOUND_OPS + 1))
2146
- else
2147
- MISSING_OPS="$MISSING_OPS $op"
2148
- fi
2149
- done
2150
-
2151
- if [ -n "$MISSING_OPS" ]; then
2152
- echo "BLOCKING: API endpoints missing from controllers (handoff contract violation)"
2153
- echo "Found: $FOUND_OPS/$TOTAL_OPS operations"
2154
- echo "Missing operations:$MISSING_OPS"
2155
- echo "Fix: Implement missing endpoints in the appropriate Controller"
2156
- exit 1
2157
- else
2158
- echo "POST-CHECK A8: OK — $FOUND_OPS/$TOTAL_OPS API operations found"
2159
- fi
2160
- fi
2161
- fi
2162
- ```
14
+ ## Execution Commands
15
+
16
+ ```bash
17
+ # Run all checks by category
18
+ bash references/checks/security-checks.sh
19
+ bash references/checks/backend-checks.sh
20
+ bash references/checks/frontend-checks.sh
21
+ bash references/checks/seed-checks.sh
22
+ bash references/checks/architecture-checks.sh
23
+ bash references/checks/infrastructure-checks.sh
24
+ ```
25
+
26
+ ## Check Index
27
+
28
+ ### Security — Tenant Isolation & Authorization (S1-S9)
29
+
30
+ | ID | Severity | Description | Script |
31
+ |----|----------|-------------|--------|
32
+ | S1 | BLOCKING | Services must filter by TenantId (OWASP A01) — MCP overlap | security-checks.sh |
33
+ | S2 | BLOCKING | Controllers must use [RequirePermission], not [Authorize] — MCP overlap | security-checks.sh |
34
+ | S3 | BLOCKING | Services must inject ICurrentTenantService (tenant isolation) | security-checks.sh |
35
+ | S4 | BLOCKING | HasQueryFilter must not use Guid.Empty (OWASP A01) MCP overlap | security-checks.sh |
36
+ | S5 | BLOCKING | Services must NOT use TenantId!.Value (null-forgiving crash pattern) — MCP overlap | security-checks.sh |
37
+ | S6 | BLOCKING | Pages must use lazy loading (no static page imports in routes) — MCP overlap | security-checks.sh |
38
+ | S7 | BLOCKING | Controllers must NOT use Guid.Empty for tenantId/userId (OWASP A01) — MCP overlap | security-checks.sh |
39
+ | S8 | BLOCKING | Write endpoints must NOT use Read permissions | security-checks.sh |
40
+ | S9 | BLOCKING | FK relationships must enforce tenant isolation | security-checks.sh |
41
+
42
+ ### Backend Entity, Service & Controller Checks (V1-V2, C8, C12, C14, C28-C31, C54)
43
+
44
+ | ID | Severity | Description | Script |
45
+ |----|----------|-------------|--------|
46
+ | V1 | BLOCKING | Controllers with POST/PUT must have corresponding Validators | backend-checks.sh |
47
+ | V2 | WARNING | Validators must be registered in DI | backend-checks.sh |
48
+ | C8 | BLOCKING | Backend APIs must support search parameter for EntityLookup | backend-checks.sh |
49
+ | C12 | WARNING | GetAll methods must return PaginatedResult<T> | backend-checks.sh |
50
+ | C14 | WARNING | Entities must implement IAuditableEntity + Create/Update validator pairs | backend-checks.sh |
51
+ | C28 | WARNING | Pagination type must be PaginatedResult<T> — no aliases | backend-checks.sh |
52
+ | C29 | BLOCKING | Code generation — ICodeGenerator must be registered for auto-generated entities | backend-checks.sh |
53
+ | C30 | BLOCKING | Code regex must support hyphens | backend-checks.sh |
54
+ | C31 | WARNING | CreateDto must NOT have Code field when service uses ICodeGenerator | backend-checks.sh |
55
+ | C54 | BLOCKING | No helper method calls inside .Select() on IQueryable | backend-checks.sh |
56
+
57
+ ### Frontend — CSS, Forms, Components, I18n (C3a, C3-C7, C9, C11, C24-C27, C36-C37, C49, C52)
58
+
59
+ | ID | Severity | Description | Script |
60
+ |----|----------|-------------|--------|
61
+ | C3a | BLOCKING | Frontend must not be empty if Layer 3 was planned | frontend-checks.sh |
62
+ | C3 | WARNING | Translation files must exist for all 4 languages (if frontend) | frontend-checks.sh |
63
+ | C4 | WARNING | Forms must be full pages with routes — ZERO modals/popups/drawers/slide-overs | frontend-checks.sh |
64
+ | C5 | CRITICAL | Create/Edit pages must exist as separate route pages | frontend-checks.sh |
65
+ | C6 | WARNING | Form pages must have companion test files | frontend-checks.sh |
66
+ | C7 | WARNING | FK fields must use EntityLookup — NO <input>, NO <select> | frontend-checks.sh |
67
+ | C9 | WARNING | No hardcoded Tailwind colors in generated pages | frontend-checks.sh |
68
+ | C11 | BLOCKING | Frontend routes must use kebab-case | frontend-checks.sh |
69
+ | C24 | WARNING | i18n must use separate JSON files per language (not embedded in index.ts) | frontend-checks.sh |
70
+ | C25 | WARNING | Pages must use useTranslation hook (no hardcoded user-facing strings) | frontend-checks.sh |
71
+ | C26 | WARNING | List/Detail pages must include DocToggleButton (documentation panel) | frontend-checks.sh |
72
+ | C27 | WARNING | Module documentation must be generated (doc-data.ts) | frontend-checks.sh |
73
+ | C36 | CRITICAL | Frontend navigate() calls must have matching route definitions | frontend-checks.sh |
74
+ | C37 | CRITICAL | Detail page tabs must NOT navigate() — content switches locally | frontend-checks.sh |
75
+ | C49 | BLOCKING | Route Ordering in App.tsx static before dynamic | frontend-checks.sh |
76
+ | C52 | BLOCKING | Frontend route paths must include module segment | frontend-checks.sh |
77
+
78
+ ### Seed Data — Navigation, Roles, Permissions (C1-C2, C10, C15-C23, C32-C35, C44-C48, C53)
79
+
80
+ | ID | Severity | Description | Script |
81
+ |----|----------|-------------|--------|
82
+ | C1 | WARNING | Navigation routes must be full paths starting with / | seed-checks.sh |
83
+ | C2 | WARNING | Seed data must not use deterministic/sequential/fixed GUIDs | seed-checks.sh |
84
+ | C10 | CRITICAL | Routes seed data must match frontend application routes | seed-checks.sh |
85
+ | C15 | WARNING | RolePermission seed data must NOT use deterministic role GUIDs | seed-checks.sh |
86
+ | C16 | CRITICAL | Cross-tenant entities must use Guid? TenantId | seed-checks.sh |
87
+ | C17 | CRITICAL | Scoped entities must have EntityScope property | seed-checks.sh |
88
+ | C18 | BLOCKING | Permissions.cs static constants must exist | seed-checks.sh |
89
+ | C19 | BLOCKING | ApplicationRolesSeedData.cs must exist | seed-checks.sh |
90
+ | C20 | WARNING | Section route completeness (NavigationSection → frontend route + permissions) | seed-checks.sh |
91
+ | C21 | WARNING | FORBIDDEN route patterns /list and /detail/:id | seed-checks.sh |
92
+ | C22 | WARNING | Permission path segment count (2-4 dots expected) | seed-checks.sh |
93
+ | C23 | BLOCKING | IClientSeedDataProvider must have 4 methods + DI registration | seed-checks.sh |
94
+ | C32 | CRITICAL | Translation seed data must have idempotency guard | seed-checks.sh |
95
+ | C33 | CRITICAL | Resource seed data must use actual section IDs from DB | seed-checks.sh |
96
+ | C34 | BLOCKING | NavRoute segments must use kebab-case for multi-word codes — MCP overlap | seed-checks.sh |
97
+ | C35 | BLOCKING | Permission codes must use kebab-case matching NavRoute codes — MCP overlap | seed-checks.sh |
98
+ | C44 | CRITICAL | RolesSeedData must map standard role-permission matrix | seed-checks.sh |
99
+ | C45 | BLOCKING | PermissionAction enum must use valid typed values only | seed-checks.sh |
100
+ | C46 | WARNING | Navigation translation completeness — 4 languages per level | seed-checks.sh |
101
+ | C47 | WARNING | Person Extension entities must not duplicate User fields | seed-checks.sh |
102
+ | C48 | CRITICAL | Person Extension service must Include(User) | seed-checks.sh |
103
+ | C53 | BLOCKING | Enum serialization — JsonStringEnumConverter required | seed-checks.sh |
104
+
105
+ ### Architecture — Clean Architecture Layer Isolation (A1-A8)
106
+
107
+ | ID | Severity | Description | Script |
108
+ |----|----------|-------------|--------|
109
+ | A1 | BLOCKING | Domain must not import other layers | architecture-checks.sh |
110
+ | A2 | BLOCKING | Application must not import Infrastructure or Api | architecture-checks.sh |
111
+ | A3 | BLOCKING | Controllers must not inject DbContext | architecture-checks.sh |
112
+ | A4 | WARNING | API must return DTOs, not Domain entities | architecture-checks.sh |
113
+ | A5 | WARNING | Service interfaces in Application, implementations in Infrastructure | architecture-checks.sh |
114
+ | A6 | BLOCKING | No EF Core attributes in Domain entities | architecture-checks.sh |
115
+ | A7 | WARNING | No direct repository usage in controllers | architecture-checks.sh |
116
+ | A8 | BLOCKING | API endpoints must match handoff apiEndpointSummary | architecture-checks.sh |
117
+
118
+ ### Infrastructure Migration & Build (C13, C38-C43, C50-C51, C56)
119
+
120
+ | ID | Severity | Description | Script |
121
+ |----|----------|-------------|--------|
122
+ | C13 | WARNING | i18n files must contain required structural keys | infrastructure-checks.sh |
123
+ | C38 | BLOCKING | Migration ModelSnapshot must contain ALL entities registered in DbContext | infrastructure-checks.sh |
124
+ | C39 | CRITICAL | I18n namespace files must be registered in i18n config | infrastructure-checks.sh |
125
+ | C40 | BLOCKING | FluentValidation validators must be registered via DI | infrastructure-checks.sh |
126
+ | C41 | WARNING | Date/date properties in DTOs must use DateOnly, not string | infrastructure-checks.sh |
127
+ | C42 | BLOCKING | Every module with entities must have a migration covering them | infrastructure-checks.sh |
128
+ | C43 | BLOCKING | Controllers must NOT have both [Route] and [NavRoute] attributes | infrastructure-checks.sh |
129
+ | C50 | BLOCKING | NavRoute Uniqueness — no duplicate NavRoute values | infrastructure-checks.sh |
130
+ | C51 | WARNING | NavRoute Segments vs Controller Hierarchy (depth matching) | infrastructure-checks.sh |
131
+ | C56 | BLOCKING | Hierarchy Artifact Completeness current run only (entities + sections) | infrastructure-checks.sh |
2163
132
 
2164
133
  ---
2165
134
 
@@ -2168,5 +137,4 @@ fi
2168
137
  After running all checks above, report the total count:
2169
138
  - **N BLOCKING** issues must be fixed before committing
2170
139
  - **M WARNING** issues should be reviewed but are non-blocking
2171
-
2172
- **If ANY POST-CHECK fails → fix in step-06, re-validate.**
140
+ - **K CRITICAL** issues must be fixed immediately