@atlashub/smartstack-cli 4.31.0 → 4.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.documentation/commands.html +952 -116
  2. package/.documentation/index.html +2 -2
  3. package/.documentation/init.html +358 -174
  4. package/dist/mcp-entry.mjs +271 -44
  5. package/dist/mcp-entry.mjs.map +1 -1
  6. package/package.json +1 -1
  7. package/templates/mcp-scaffolding/controller.cs.hbs +54 -128
  8. package/templates/project/README.md +19 -0
  9. package/templates/skills/apex/SKILL.md +16 -10
  10. package/templates/skills/apex/_shared.md +1 -1
  11. package/templates/skills/apex/references/checks/architecture-checks.sh +154 -0
  12. package/templates/skills/apex/references/checks/backend-checks.sh +194 -0
  13. package/templates/skills/apex/references/checks/frontend-checks.sh +448 -0
  14. package/templates/skills/apex/references/checks/infrastructure-checks.sh +255 -0
  15. package/templates/skills/apex/references/checks/security-checks.sh +153 -0
  16. package/templates/skills/apex/references/checks/seed-checks.sh +536 -0
  17. package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +49 -192
  18. package/templates/skills/apex/references/parallel-execution.md +18 -5
  19. package/templates/skills/apex/references/post-checks.md +124 -2156
  20. package/templates/skills/apex/references/smartstack-api.md +160 -957
  21. package/templates/skills/apex/references/smartstack-frontend-compliance.md +23 -1
  22. package/templates/skills/apex/references/smartstack-frontend.md +134 -1022
  23. package/templates/skills/apex/references/smartstack-layers.md +12 -6
  24. package/templates/skills/apex/steps/step-00-init.md +81 -238
  25. package/templates/skills/apex/steps/step-03-execute.md +25 -751
  26. package/templates/skills/apex/steps/step-03a-layer0-domain.md +118 -0
  27. package/templates/skills/apex/steps/step-03b-layer1-seed.md +91 -0
  28. package/templates/skills/apex/steps/step-03c-layer2-backend.md +240 -0
  29. package/templates/skills/apex/steps/step-03d-layer3-frontend.md +300 -0
  30. package/templates/skills/apex/steps/step-03e-layer4-devdata.md +44 -0
  31. package/templates/skills/apex/steps/step-04-examine.md +70 -150
  32. package/templates/skills/application/references/frontend-i18n-and-output.md +2 -2
  33. package/templates/skills/application/references/frontend-route-naming.md +5 -1
  34. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +49 -198
  35. package/templates/skills/application/references/frontend-verification.md +11 -11
  36. package/templates/skills/application/steps/step-05-frontend.md +26 -15
  37. package/templates/skills/application/templates-frontend.md +4 -0
  38. package/templates/skills/cli-app-sync/SKILL.md +2 -2
  39. package/templates/skills/cli-app-sync/references/comparison-map.md +1 -1
  40. package/templates/skills/controller/references/controller-code-templates.md +70 -67
  41. package/templates/skills/controller/references/mcp-scaffold-workflow.md +5 -1
@@ -0,0 +1,255 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # POST-CHECK: Infrastructure — Migration & Build
5
+ # C13, C38-C43, C50-C51, C56: Database migrations, DI registration, routing uniqueness
6
+
7
+ FAIL=false
8
+
9
+ # POST-CHECK C13: i18n files must contain required structural keys
10
+ I18N_DIR="src/i18n/locales/fr"
11
+ if [ -d "$I18N_DIR" ]; then
12
+ REQUIRED_KEYS="actions columns empty errors form labels messages validation"
13
+ for JSON_FILE in "$I18N_DIR"/*.json; do
14
+ [ ! -f "$JSON_FILE" ] && continue
15
+ BASENAME=$(basename "$JSON_FILE")
16
+ case "$BASENAME" in common.json|navigation.json) continue;; esac
17
+ for KEY in $REQUIRED_KEYS; do
18
+ if ! jq -e "has(\"$KEY\")" "$JSON_FILE" > /dev/null 2>&1; then
19
+ echo "WARNING: i18n file missing required key '$KEY': $JSON_FILE"
20
+ echo "Module i18n files MUST contain: $REQUIRED_KEYS"
21
+ fi
22
+ done
23
+ done
24
+ fi
25
+
26
+ # POST-CHECK C38: Migration ModelSnapshot must contain ALL entities registered in DbContext (BLOCKING)
27
+ SNAPSHOT=$(find src/ -name "*ModelSnapshot.cs" -path "*/Migrations/*" 2>/dev/null | head -1)
28
+ DBCONTEXT=$(find src/ -name "*DbContext.cs" -path "*/Persistence/*" ! -name "*DesignTime*" 2>/dev/null | head -1)
29
+ if [ -n "$SNAPSHOT" ] && [ -n "$DBCONTEXT" ]; then
30
+ DBSET_ENTITIES=$(grep -oP 'DbSet<(\w+)>' "$DBCONTEXT" 2>/dev/null | grep -oP '<\K\w+(?=>)' | sort -u || true)
31
+ FAIL_C38=false
32
+ for ENTITY in $DBSET_ENTITIES; do
33
+ if echo "$ENTITY" | grep -qP '^(Navigation|Tenant|User|Role|Permission|AuditLog|ApplicationTracking)'; then
34
+ continue
35
+ fi
36
+ if ! grep -q "Entity<$ENTITY>" "$SNAPSHOT" 2>/dev/null; then
37
+ echo "BLOCKING: Entity '$ENTITY' is registered as DbSet in $DBCONTEXT but MISSING from ModelSnapshot"
38
+ echo " This means no migration was created for this entity — it will not exist in the database."
39
+ echo " Fix: Run 'dotnet ef migrations add' to include all new entities"
40
+ FAIL_C38=true
41
+ fi
42
+ done
43
+ if [ "$FAIL_C38" = true ]; then
44
+ echo ""
45
+ echo " Root cause: Migration was likely created once for the first batch of entities,"
46
+ echo " but additional entities were added later without regenerating the migration."
47
+ echo " Fix: Create a new migration that covers ALL missing entities."
48
+ FAIL=true
49
+ fi
50
+ fi
51
+
52
+ # POST-CHECK C39: I18n namespace files must be registered in i18n config (CRITICAL)
53
+ 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)
54
+ if [ -n "$I18N_CONFIG" ]; then
55
+ 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 || true)
56
+ if [ -n "$FR_FILES" ]; then
57
+ FAIL_C39=false
58
+ for JSON_FILE in $FR_FILES; do
59
+ NS=$(basename "$JSON_FILE" .json)
60
+ if ! grep -q "$NS" "$I18N_CONFIG" 2>/dev/null; then
61
+ echo "CRITICAL: i18n namespace '$NS' (from $JSON_FILE) is not registered in $I18N_CONFIG"
62
+ echo " Pages using useTranslation(['$NS']) will get empty translations at runtime"
63
+ echo " Fix: Add '$NS' to the resources/ns configuration in $I18N_CONFIG"
64
+ FAIL_C39=true
65
+ fi
66
+ done
67
+ if [ "$FAIL_C39" = true ]; then
68
+ FAIL=true
69
+ fi
70
+ fi
71
+ fi
72
+
73
+ # POST-CHECK C40: FluentValidation validators must be registered via DI (BLOCKING)
74
+ VALIDATOR_FILES=$(find src/ -name "*Validator.cs" -path "*/Validators/*" 2>/dev/null | grep -v test | grep -v Test)
75
+ if [ -n "$VALIDATOR_FILES" ]; then
76
+ DI_FILE=$(find src/ -name "DependencyInjection.cs" -o -name "ServiceCollectionExtensions.cs" 2>/dev/null | grep -v test | head -1)
77
+ if [ -z "$DI_FILE" ]; then
78
+ echo "BLOCKING: Validators exist but no DependencyInjection.cs found for DI registration"
79
+ FAIL=true
80
+ else
81
+ HAS_ASSEMBLY_REG=$(grep -c "AddValidatorsFromAssembly\|AddValidatorsFromAssemblyContaining" "$DI_FILE" 2>/dev/null || true)
82
+ if [ "$HAS_ASSEMBLY_REG" -eq 0 ]; then
83
+ VALIDATOR_COUNT=$(echo "$VALIDATOR_FILES" | wc -l)
84
+ REGISTERED_COUNT=0
85
+ for VF in $VALIDATOR_FILES; do
86
+ VN=$(basename "$VF" .cs)
87
+ if grep -q "$VN" "$DI_FILE" 2>/dev/null; then
88
+ REGISTERED_COUNT=$((REGISTERED_COUNT + 1))
89
+ fi
90
+ done
91
+ if [ "$REGISTERED_COUNT" -eq 0 ]; then
92
+ echo "BLOCKING: $VALIDATOR_COUNT validators exist but NONE are registered in DI ($DI_FILE)"
93
+ echo " Fix: Add 'services.AddValidatorsFromAssemblyContaining<Create{Entity}DtoValidator>();' to $DI_FILE"
94
+ echo " Or use 'services.AddValidatorsFromAssembly(typeof(Create{Entity}DtoValidator).Assembly);'"
95
+ FAIL=true
96
+ fi
97
+ fi
98
+ fi
99
+ fi
100
+
101
+ # POST-CHECK C41: Date/date properties in DTOs must use DateOnly, not string (WARNING)
102
+ DTO_FILES=$(find src/ -name "*Dto.cs" -path "*/DTOs/*" 2>/dev/null)
103
+ if [ -n "$DTO_FILES" ]; then
104
+ FAIL_C41=false
105
+ for f in $DTO_FILES; do
106
+ BAD_DATES=$(grep -Pn 'string\??\s+\w*[Dd]ate\w*\s*[{;,]' "$f" 2>/dev/null | grep -vi "Updated\|Created\|format\|pattern\|string\|parse" || true)
107
+ if [ -n "$BAD_DATES" ]; then
108
+ echo "WARNING: DTO has string type for date field — must use DateOnly: $f"
109
+ echo "$BAD_DATES"
110
+ echo " Fix: Change 'string Date' to 'DateOnly Date' (or 'DateOnly? Date' if nullable)"
111
+ echo " DateOnly is the correct .NET type for date-only values (no time component)"
112
+ fi
113
+ done
114
+ fi
115
+
116
+ # POST-CHECK C42: Every module with entities must have a migration covering them (BLOCKING)
117
+ ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null | grep -v test)
118
+ MIGRATION_DIR=$(find src/ -path "*/Migrations" -type d 2>/dev/null | head -1)
119
+ if [ -n "$ENTITY_FILES" ] && [ -n "$MIGRATION_DIR" ]; then
120
+ MIGRATION_FILES=$(find "$MIGRATION_DIR" -name "*.cs" ! -name "*ModelSnapshot*" ! -name "*DesignTime*" 2>/dev/null)
121
+ if [ -z "$MIGRATION_FILES" ]; then
122
+ echo "BLOCKING: Entity files exist in Domain/Entities but NO migration files found in $MIGRATION_DIR"
123
+ FAIL=true
124
+ else
125
+ FAIL_C42=false
126
+ for EF in $ENTITY_FILES; do
127
+ ENTITY_NAME=$(basename "$EF" .cs)
128
+ if grep -qP '^\s*(public\s+)?(abstract|interface)\s' "$EF" 2>/dev/null; then continue; fi
129
+ FOUND=$(grep -l "$ENTITY_NAME" $MIGRATION_FILES 2>/dev/null || true)
130
+ if [ -z "$FOUND" ]; then
131
+ echo "BLOCKING: Entity '$ENTITY_NAME' ($EF) not found in any migration file"
132
+ echo " This entity will NOT have a database table."
133
+ echo " Fix: Run 'dotnet ef migrations add' to create a migration covering this entity"
134
+ FAIL_C42=true
135
+ fi
136
+ done
137
+ if [ "$FAIL_C42" = true ]; then
138
+ FAIL=true
139
+ fi
140
+ fi
141
+ fi
142
+
143
+ # POST-CHECK C43: Controllers must NOT have both [Route] and [NavRoute] attributes (BLOCKING)
144
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
145
+ if [ -n "$CTRL_FILES" ]; then
146
+ FAIL_C43=false
147
+ for f in $CTRL_FILES; do
148
+ HAS_NAVROUTE=$(grep -c '\[NavRoute(' "$f" 2>/dev/null || true)
149
+ HAS_ROUTE=$(grep -c '\[Route(' "$f" 2>/dev/null || true)
150
+ if [ "$HAS_NAVROUTE" -gt 0 ] && [ "$HAS_ROUTE" -gt 0 ]; then
151
+ NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"' "$f" 2>/dev/null | head -1)
152
+ ROUTE_VAL=$(grep -oP 'Route\("([^"]+)"' "$f" 2>/dev/null | head -1)
153
+ echo "BLOCKING: Controller has BOTH [Route] and [NavRoute] — remove [Route]: $f"
154
+ echo " Found: [$ROUTE_VAL] + [$NAVROUTE_VAL]"
155
+ echo " In SmartStack, [NavRoute] resolves routes dynamically from the database."
156
+ echo " Having [Route] alongside it causes route conflicts and 404s."
157
+ echo " Fix: Remove the [Route(...)] attribute, keep only [NavRoute(...)]"
158
+ FAIL_C43=true
159
+ fi
160
+ done
161
+ if [ "$FAIL_C43" = true ]; then
162
+ FAIL=true
163
+ fi
164
+ fi
165
+
166
+ # POST-CHECK C50: NavRoute Uniqueness — no duplicate NavRoute values (BLOCKING)
167
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
168
+ if [ -n "$CTRL_FILES" ]; then
169
+ NAVROUTES=$(grep -Pn '\[NavRoute\("([^"]+)"\)' $CTRL_FILES 2>/dev/null | sed 's/.*\[NavRoute("\([^"]*\)").*/\1/' | sort || true)
170
+ DUPLICATES=$(echo "$NAVROUTES" | uniq -d || true)
171
+ if [ -n "$DUPLICATES" ]; then
172
+ echo "BLOCKING: Duplicate NavRoute values detected:"
173
+ for DUP in $DUPLICATES; do
174
+ echo " NavRoute: $DUP"
175
+ grep -l "\[NavRoute(\"$DUP\")\]" $CTRL_FILES 2>/dev/null | while read -r f; do
176
+ echo " - $f"
177
+ done
178
+ done
179
+ echo " Fix: Each controller MUST have a unique NavRoute value"
180
+ FAIL=true
181
+ fi
182
+ fi
183
+
184
+ # POST-CHECK C51: NavRoute Segments vs Controller Hierarchy (WARNING)
185
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
186
+ if [ -n "$CTRL_FILES" ]; then
187
+ for f in $CTRL_FILES; do
188
+ NAVROUTE=$(grep -oP '\[NavRoute\("\K[^"]+' "$f")
189
+ if [ -z "$NAVROUTE" ]; then continue; fi
190
+
191
+ DOTS=$(echo "$NAVROUTE" | tr -cd '.' | wc -c)
192
+
193
+ REL_PATH=$(echo "$f" | sed 's|.*/Controllers/||')
194
+ DIR_DEPTH=$(echo "$REL_PATH" | tr '/' '\n' | wc -l)
195
+
196
+ if [ "$DIR_DEPTH" -ge 3 ] && [ "$DOTS" -le 1 ]; then
197
+ echo "WARNING: Controller in section subfolder but NavRoute has only $((DOTS+1)) segments"
198
+ echo " File: $f"
199
+ echo " NavRoute: $NAVROUTE"
200
+ echo " Expected: 3+ segments (app.module.section) for controllers in section subfolders"
201
+ echo " Fix: Update NavRoute to include the module segment (e.g., 'app.module.section')"
202
+ fi
203
+
204
+ if [ "$DOTS" -eq 0 ]; then
205
+ echo "BLOCKING: NavRoute '$NAVROUTE' has only 1 segment (minimum 2 required): $f"
206
+ FAIL=true
207
+ fi
208
+ done
209
+ fi
210
+
211
+ # POST-CHECK C56: Hierarchy Artifact Completeness — current run only (BLOCKING)
212
+ # REQUIRES: {entities} and {sections} from current apex run context
213
+ # If not available, skip this check (cannot determine scope)
214
+
215
+ if [ -n "${ENTITIES:-}" ] && [ -n "${SECTIONS:-}" ]; then
216
+ FAIL_C56=false
217
+
218
+ for ENTITY in $ENTITIES; do
219
+ CTRL=$(find src/ -path "*/Controllers/*" -name "${ENTITY}*Controller.cs" 2>/dev/null | head -1)
220
+ if [ -z "$CTRL" ]; then
221
+ echo "BLOCKING: Entity '$ENTITY' has no controller file"
222
+ FAIL_C56=true
223
+ fi
224
+ if ! grep -q "\[NavRoute" "$CTRL" 2>/dev/null; then
225
+ echo "BLOCKING: Controller for '$ENTITY' ($CTRL) missing [NavRoute] attribute"
226
+ echo "SmartStack convention: [NavRoute] is the ONLY route attribute needed"
227
+ FAIL_C56=true
228
+ fi
229
+ if grep -q "\[Route(" "$CTRL" 2>/dev/null && grep -q "\[NavRoute" "$CTRL" 2>/dev/null; then
230
+ echo "BLOCKING: Controller $CTRL has BOTH [Route] and [NavRoute] (see C43)"
231
+ FAIL_C56=true
232
+ fi
233
+ done
234
+
235
+ PERM_SEED=$(find src/ -path "*/Seeding/Data/*" -name "*PermissionsSeedData.cs" 2>/dev/null)
236
+ NAV_SEED=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" ! -name "*Application*" 2>/dev/null)
237
+ for SECTION in $SECTIONS; do
238
+ if [ -n "$PERM_SEED" ] && ! grep -q "\"$SECTION\"" $PERM_SEED 2>/dev/null; then
239
+ echo "BLOCKING: Section '$SECTION' missing from PermissionsSeedData"
240
+ FAIL_C56=true
241
+ fi
242
+ if [ -n "$NAV_SEED" ] && ! grep -q "\"$SECTION\"" $NAV_SEED 2>/dev/null; then
243
+ echo "BLOCKING: Section '$SECTION' missing from NavigationSeedData"
244
+ FAIL_C56=true
245
+ fi
246
+ done
247
+
248
+ if [ "$FAIL_C56" = true ]; then
249
+ FAIL=true
250
+ fi
251
+ fi
252
+
253
+ if [ "$FAIL" = true ]; then
254
+ exit 1
255
+ fi
@@ -0,0 +1,153 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # POST-CHECK: Security — Tenant Isolation & Authorization (defense-in-depth with MCP)
5
+ # S1-S9: Blocks multi-tenant data leakage, RBAC bypass, and authorization errors
6
+
7
+ FAIL=false
8
+
9
+ # POST-CHECK S1: All services must filter by TenantId (OWASP A01)
10
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
11
+ if [ -n "$SERVICE_FILES" ]; then
12
+ for f in $SERVICE_FILES; do
13
+ HAS_TENANT_FILTER=$(grep -c "TenantId" "$f")
14
+ HAS_OPTIONAL_ENTITY=false
15
+ if grep -q "IOptionalTenantEntity\|IScopedTenantEntity" "$f"; then
16
+ HAS_OPTIONAL_ENTITY=true
17
+ fi
18
+ if [ "$HAS_TENANT_FILTER" -eq 0 ] && [ "$HAS_OPTIONAL_ENTITY" = false ]; then
19
+ echo "BLOCKING (OWASP A01): Service missing TenantId filter or optional tenant entity: $f"
20
+ echo "Every service query MUST filter by _currentTenant.TenantId"
21
+ FAIL=true
22
+ fi
23
+ if grep -q "Guid.Empty" "$f"; then
24
+ echo "BLOCKING (OWASP A01): Service uses Guid.Empty instead of _currentTenant.TenantId: $f"
25
+ FAIL=true
26
+ fi
27
+ done
28
+ fi
29
+
30
+ # POST-CHECK S2: Controllers must use [RequirePermission], not just [Authorize] (BLOCKING)
31
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
32
+ if [ -n "$CTRL_FILES" ]; then
33
+ for f in $CTRL_FILES; do
34
+ if grep -q "\[Authorize\]" "$f" && ! grep -q "\[RequirePermission" "$f"; then
35
+ echo "BLOCKING: Controller uses [Authorize] without [RequirePermission]: $f"
36
+ echo "[Authorize] alone provides NO RBAC enforcement — any authenticated user has access"
37
+ echo "Fix: Add [RequirePermission(Permissions.{Module}.{Action})] on each endpoint"
38
+ FAIL=true
39
+ fi
40
+ done
41
+ fi
42
+
43
+ # POST-CHECK S3: Services must inject ICurrentTenantService (tenant isolation)
44
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
45
+ if [ -n "$SERVICE_FILES" ]; then
46
+ for f in $SERVICE_FILES; do
47
+ if ! grep -qE "ICurrentTenantService|ICurrentUser" "$f"; then
48
+ echo "BLOCKING: Service missing tenant context injection: $f"
49
+ echo "All services MUST inject ICurrentTenantService for tenant isolation"
50
+ FAIL=true
51
+ fi
52
+ done
53
+ fi
54
+
55
+ # POST-CHECK S4: HasQueryFilter must not use Guid.Empty (OWASP A01)
56
+ CONFIG_FILES=$(find src/ -path "*/Configurations/*" -name "*Configuration.cs" 2>/dev/null)
57
+ if [ -n "$CONFIG_FILES" ]; then
58
+ BAD_FILTER=$(grep -Pn 'HasQueryFilter.*Guid\.Empty' $CONFIG_FILES 2>/dev/null)
59
+ if [ -n "$BAD_FILTER" ]; then
60
+ echo "BLOCKING (OWASP A01): HasQueryFilter uses Guid.Empty — bypasses tenant isolation: $BAD_FILTER"
61
+ echo "The EF global query filter MUST use the injected TenantId, not Guid.Empty"
62
+ FAIL=true
63
+ fi
64
+ fi
65
+
66
+ # POST-CHECK S5: Services must NOT use TenantId!.Value (null-forgiving crash pattern)
67
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
68
+ if [ -n "$SERVICE_FILES" ]; then
69
+ BAD_PATTERN=$(grep -Pn 'TenantId!\.' $SERVICE_FILES 2>/dev/null)
70
+ if [ -n "$BAD_PATTERN" ]; then
71
+ echo "BLOCKING: TenantId!.Value (null-forgiving) detected — NullReferenceException risk: $BAD_PATTERN"
72
+ echo "Fix: Use guard clause: var tenantId = _currentTenant.TenantId ?? throw new TenantContextRequiredException();"
73
+ FAIL=true
74
+ fi
75
+ fi
76
+
77
+ # POST-CHECK S6: Pages must use lazy loading (no static page imports in routes)
78
+ APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
79
+ ROUTE_FILES=$(find src/routes/ -name "*.tsx" -o -name "*.ts" 2>/dev/null)
80
+ if [ -n "$APP_TSX" ]; then
81
+ STATIC_IMPORTS=$(grep -Pn "^import .+ from '@/pages/" "$APP_TSX" $ROUTE_FILES 2>/dev/null)
82
+ if [ -n "$STATIC_IMPORTS" ]; then
83
+ echo "BLOCKING: Static page imports in route files — MUST use React.lazy()"
84
+ echo "Static imports load ALL pages upfront, killing initial load performance"
85
+ echo "$STATIC_IMPORTS"
86
+ echo "Fix: const Page = lazy(() => import('@/pages/...').then(m => ({ default: m.Page })))"
87
+ FAIL=true
88
+ fi
89
+ fi
90
+
91
+ # POST-CHECK S7: Controllers must NOT use Guid.Empty for tenantId/userId (OWASP A01)
92
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
93
+ if [ -n "$CTRL_FILES" ]; then
94
+ BAD_GUID=$(grep -Pn 'Guid\.Empty' $CTRL_FILES 2>/dev/null)
95
+ if [ -n "$BAD_GUID" ]; then
96
+ echo "BLOCKING (OWASP A01): Controller uses Guid.Empty — tenant isolation bypassed"
97
+ echo "$BAD_GUID"
98
+ echo "Fix: Use _currentTenant.TenantId from ICurrentTenantService"
99
+ FAIL=true
100
+ fi
101
+ fi
102
+
103
+ # POST-CHECK S8: Write endpoints must NOT use Read permissions (BLOCKING)
104
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
105
+ if [ -n "$CTRL_FILES" ]; then
106
+ for f in $CTRL_FILES; do
107
+ WRITE_WITH_READ=$(grep -Pn '\[(HttpPost|HttpPut|HttpDelete|HttpPatch)' "$f" | while read -r line; do
108
+ LINE_NUM=$(echo "$line" | cut -d: -f1)
109
+ sed -n "$((LINE_NUM-2)),$((LINE_NUM+5))p" "$f" | grep -P 'RequirePermission.*\.Read\b' | head -1
110
+ done)
111
+ if [ -n "$WRITE_WITH_READ" ]; then
112
+ echo "BLOCKING: Write endpoint uses Read permission in $f"
113
+ echo "$WRITE_WITH_READ"
114
+ echo "Fix: POST/PUT/DELETE/PATCH endpoints must use Create/Update/Delete permissions, never Read"
115
+ FAIL=true
116
+ fi
117
+ done
118
+ fi
119
+
120
+ # POST-CHECK S9: FK relationships must enforce tenant isolation (BLOCKING)
121
+ CONFIG_FILES=$(find src/ -path "*/Configurations/*" -name "*Configuration.cs" 2>/dev/null)
122
+ if [ -n "$CONFIG_FILES" ]; then
123
+ for f in $CONFIG_FILES; do
124
+ ENTITY_NAME=$(grep -oP 'IEntityTypeConfiguration<(\w+)>' "$f" | grep -oP '<\K[^>]+')
125
+ if [ -z "$ENTITY_NAME" ]; then continue; fi
126
+
127
+ ENTITY_FILE=$(find src/ -path "*/Domain/*" -name "${ENTITY_NAME}.cs" 2>/dev/null | head -1)
128
+ if [ -z "$ENTITY_FILE" ]; then continue; fi
129
+
130
+ IS_TENANT=$(grep -cE 'ITenantEntity|BaseEntity|TenantId' "$ENTITY_FILE")
131
+ if [ "$IS_TENANT" -eq 0 ]; then continue; fi
132
+
133
+ FK_TARGETS=$(grep -oP 'HasOne<(\w+)>|HasOne\(e\s*=>\s*e\.(\w+)\)' "$f" | grep -oP '\w+(?=>|\))' | sort -u)
134
+ for TARGET in $FK_TARGETS; do
135
+ TARGET_FILE=$(find src/ -path "*/Domain/*" -name "${TARGET}.cs" 2>/dev/null | head -1)
136
+ if [ -n "$TARGET_FILE" ]; then
137
+ TARGET_IS_TENANT=$(grep -cE 'ITenantEntity|BaseEntity|TenantId' "$TARGET_FILE")
138
+ if [ "$TARGET_IS_TENANT" -gt 0 ]; then
139
+ HAS_QUERY_FILTER=$(grep -c 'HasQueryFilter' "$f")
140
+ if [ "$HAS_QUERY_FILTER" -eq 0 ]; then
141
+ echo "WARNING: $f — FK from tenant entity $ENTITY_NAME to tenant entity $TARGET without HasQueryFilter"
142
+ echo "Cross-tenant data leakage possible if FK is assigned without tenant validation"
143
+ echo "Fix: Add tenant validation in service layer before assigning FK, or use composite FK (TenantId + EntityId)"
144
+ fi
145
+ fi
146
+ fi
147
+ done
148
+ done
149
+ fi
150
+
151
+ if [ "$FAIL" = true ]; then
152
+ exit 1
153
+ fi