@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.
- package/.documentation/commands.html +952 -116
- package/.documentation/index.html +2 -2
- package/.documentation/init.html +358 -174
- package/dist/mcp-entry.mjs +271 -44
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/mcp-scaffolding/controller.cs.hbs +54 -128
- package/templates/project/README.md +19 -0
- package/templates/skills/apex/SKILL.md +16 -10
- package/templates/skills/apex/_shared.md +1 -1
- package/templates/skills/apex/references/checks/architecture-checks.sh +154 -0
- package/templates/skills/apex/references/checks/backend-checks.sh +194 -0
- package/templates/skills/apex/references/checks/frontend-checks.sh +448 -0
- package/templates/skills/apex/references/checks/infrastructure-checks.sh +255 -0
- package/templates/skills/apex/references/checks/security-checks.sh +153 -0
- package/templates/skills/apex/references/checks/seed-checks.sh +536 -0
- package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +49 -192
- package/templates/skills/apex/references/parallel-execution.md +18 -5
- package/templates/skills/apex/references/post-checks.md +124 -2156
- package/templates/skills/apex/references/smartstack-api.md +160 -957
- package/templates/skills/apex/references/smartstack-frontend-compliance.md +23 -1
- package/templates/skills/apex/references/smartstack-frontend.md +134 -1022
- package/templates/skills/apex/references/smartstack-layers.md +12 -6
- package/templates/skills/apex/steps/step-00-init.md +81 -238
- package/templates/skills/apex/steps/step-03-execute.md +25 -751
- package/templates/skills/apex/steps/step-03a-layer0-domain.md +118 -0
- package/templates/skills/apex/steps/step-03b-layer1-seed.md +91 -0
- package/templates/skills/apex/steps/step-03c-layer2-backend.md +240 -0
- package/templates/skills/apex/steps/step-03d-layer3-frontend.md +300 -0
- package/templates/skills/apex/steps/step-03e-layer4-devdata.md +44 -0
- package/templates/skills/apex/steps/step-04-examine.md +70 -150
- package/templates/skills/application/references/frontend-i18n-and-output.md +2 -2
- package/templates/skills/application/references/frontend-route-naming.md +5 -1
- package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +49 -198
- package/templates/skills/application/references/frontend-verification.md +11 -11
- package/templates/skills/application/steps/step-05-frontend.md +26 -15
- package/templates/skills/application/templates-frontend.md +4 -0
- package/templates/skills/cli-app-sync/SKILL.md +2 -2
- package/templates/skills/cli-app-sync/references/comparison-map.md +1 -1
- package/templates/skills/controller/references/controller-code-templates.md +70 -67
- package/templates/skills/controller/references/mcp-scaffold-workflow.md +5 -1
|
@@ -1,2165 +1,134 @@
|
|
|
1
|
-
# POST-CHECKs
|
|
1
|
+
# POST-CHECKs — Compact Reference
|
|
2
2
|
|
|
3
3
|
> **Referenced by:** step-04-examine.md (section 6b)
|
|
4
|
-
>
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
##
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
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
|