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