@atlashub/smartstack-cli 4.31.0 → 4.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.documentation/commands.html +952 -116
- package/.documentation/index.html +2 -2
- package/.documentation/init.html +358 -174
- package/dist/mcp-entry.mjs +271 -44
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/mcp-scaffolding/controller.cs.hbs +54 -128
- package/templates/project/README.md +19 -0
- package/templates/skills/apex/SKILL.md +16 -10
- package/templates/skills/apex/_shared.md +1 -1
- package/templates/skills/apex/references/checks/architecture-checks.sh +154 -0
- package/templates/skills/apex/references/checks/backend-checks.sh +194 -0
- package/templates/skills/apex/references/checks/frontend-checks.sh +448 -0
- package/templates/skills/apex/references/checks/infrastructure-checks.sh +255 -0
- package/templates/skills/apex/references/checks/security-checks.sh +153 -0
- package/templates/skills/apex/references/checks/seed-checks.sh +536 -0
- package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +49 -192
- package/templates/skills/apex/references/parallel-execution.md +18 -5
- package/templates/skills/apex/references/post-checks.md +124 -2156
- package/templates/skills/apex/references/smartstack-api.md +160 -957
- package/templates/skills/apex/references/smartstack-frontend-compliance.md +23 -1
- package/templates/skills/apex/references/smartstack-frontend.md +134 -1022
- package/templates/skills/apex/references/smartstack-layers.md +12 -6
- package/templates/skills/apex/steps/step-00-init.md +81 -238
- package/templates/skills/apex/steps/step-03-execute.md +25 -751
- package/templates/skills/apex/steps/step-03a-layer0-domain.md +118 -0
- package/templates/skills/apex/steps/step-03b-layer1-seed.md +91 -0
- package/templates/skills/apex/steps/step-03c-layer2-backend.md +240 -0
- package/templates/skills/apex/steps/step-03d-layer3-frontend.md +300 -0
- package/templates/skills/apex/steps/step-03e-layer4-devdata.md +44 -0
- package/templates/skills/apex/steps/step-04-examine.md +70 -150
- package/templates/skills/application/references/frontend-i18n-and-output.md +2 -2
- package/templates/skills/application/references/frontend-route-naming.md +5 -1
- package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +49 -198
- package/templates/skills/application/references/frontend-verification.md +11 -11
- package/templates/skills/application/steps/step-05-frontend.md +26 -15
- package/templates/skills/application/templates-frontend.md +4 -0
- package/templates/skills/cli-app-sync/SKILL.md +2 -2
- package/templates/skills/cli-app-sync/references/comparison-map.md +1 -1
- package/templates/skills/controller/references/controller-code-templates.md +70 -67
- package/templates/skills/controller/references/mcp-scaffold-workflow.md +5 -1
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# POST-CHECK: Seed Data — Navigation, Roles, Permissions
|
|
5
|
+
# C1-C2, C10, C15-C23, C32-C35, C44-C48, C53: Navigation completeness, role matrix, idempotency, translations
|
|
6
|
+
|
|
7
|
+
FAIL=false
|
|
8
|
+
|
|
9
|
+
# POST-CHECK C1: Navigation routes must be full paths starting with /
|
|
10
|
+
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
|
|
11
|
+
if [ -n "$SEED_FILES" ]; then
|
|
12
|
+
BAD_ROUTES=$(grep -Pn 'NavigationApplication\.Create\(|NavigationModule\.Create\(|NavigationSection\.Create\(|NavigationResource\.Create\(' $SEED_FILES | grep -v '"/[a-z]' || true)
|
|
13
|
+
if [ -n "$BAD_ROUTES" ]; then
|
|
14
|
+
echo "WARNING: Navigation routes must be full paths starting with /"
|
|
15
|
+
echo "$BAD_ROUTES"
|
|
16
|
+
echo "Expected: \"/human-resources\" NOT \"humanresources\""
|
|
17
|
+
fi
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# POST-CHECK C2: Seed data must not use deterministic/sequential/fixed GUIDs
|
|
21
|
+
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
|
|
22
|
+
if [ -n "$SEED_FILES" ]; then
|
|
23
|
+
BAD_GUIDS=$(grep -Pn 'GenerateDeterministicGuid|GenerateGuid\(int|11111111-1111-1111-1111-' $SEED_FILES 2>/dev/null || true)
|
|
24
|
+
if [ -n "$BAD_GUIDS" ]; then
|
|
25
|
+
echo "WARNING: Seed data must use Guid.NewGuid(), not deterministic/sequential/fixed GUIDs"
|
|
26
|
+
echo "$BAD_GUIDS"
|
|
27
|
+
fi
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# POST-CHECK C10: Routes seed data must match frontend application routes
|
|
31
|
+
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 || true)
|
|
32
|
+
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
33
|
+
if [ -n "$APP_TSX" ] && [ -n "$SEED_ROUTES" ]; then
|
|
34
|
+
FRONTEND_PATHS=$(grep -oP "path:\s*'([^']+)'" "$APP_TSX" | grep -oP "'[^']+'" | tr -d "'" | sort -u || true)
|
|
35
|
+
if [ -n "$FRONTEND_PATHS" ]; then
|
|
36
|
+
MISMATCH_FOUND=false
|
|
37
|
+
for SEED_ROUTE in $SEED_ROUTES; do
|
|
38
|
+
DEPTH=$(echo "$SEED_ROUTE" | tr '/' '\n' | grep -c '.')
|
|
39
|
+
if [ "$DEPTH" -lt 3 ]; then continue; fi
|
|
40
|
+
SEED_SUFFIX=$(echo "$SEED_ROUTE" | sed 's|^/[^/]*/||')
|
|
41
|
+
SEED_NORM=$(echo "$SEED_SUFFIX" | tr '[:upper:]' '[:lower:]' | tr -d '-')
|
|
42
|
+
MATCH_FOUND=false
|
|
43
|
+
for FE_PATH in $FRONTEND_PATHS; do
|
|
44
|
+
if echo "$FE_PATH" | grep -qP '/list$'; then
|
|
45
|
+
echo "WARNING: Frontend route ends with /list — should use index route instead: $FE_PATH"
|
|
46
|
+
fi
|
|
47
|
+
FE_BASE=$(echo "$FE_PATH" | sed 's|/list$||;s|/new$||;s|/:id.*||;s|/create$||')
|
|
48
|
+
FE_NORM=$(echo "$FE_BASE" | tr '[:upper:]' '[:lower:]' | tr -d '-')
|
|
49
|
+
if [ "$SEED_NORM" = "$FE_NORM" ]; then
|
|
50
|
+
MATCH_FOUND=true
|
|
51
|
+
break
|
|
52
|
+
fi
|
|
53
|
+
done
|
|
54
|
+
if [ "$MATCH_FOUND" = false ]; then
|
|
55
|
+
echo "CRITICAL: Seed data route has no matching frontend route: $SEED_ROUTE"
|
|
56
|
+
MISMATCH_FOUND=true
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
if [ "$MISMATCH_FOUND" = true ]; then
|
|
60
|
+
echo "Fix: Ensure every NavigationSeedData route has a corresponding PageRegistry.register() entry in componentRegistry.generated.ts (or route entry in App.tsx for legacy projects)"
|
|
61
|
+
FAIL=true
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# POST-CHECK C15: RolePermission seed data must NOT use deterministic role GUIDs
|
|
67
|
+
SEED_ALL_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
|
|
68
|
+
SEED_CONST_FILES=$(find src/ -path "*/Seeding/*" -name "SeedConstants.cs" 2>/dev/null)
|
|
69
|
+
if [ -n "$SEED_ALL_FILES" ]; then
|
|
70
|
+
BAD_ROLE_GUID=$(grep -Pn 'DeterministicGuid\("role:' $SEED_ALL_FILES $SEED_CONST_FILES 2>/dev/null || true)
|
|
71
|
+
if [ -n "$BAD_ROLE_GUID" ]; then
|
|
72
|
+
echo "WARNING: Deterministic GUID for role detected (e.g., DeterministicGuid(\"role:admin\"))"
|
|
73
|
+
echo "System roles are pre-seeded by SmartStack core with their own IDs"
|
|
74
|
+
echo "Fix: In SeedRolePermissionsAsync(), look up roles by Code:"
|
|
75
|
+
echo " var roles = await context.Roles.Where(r => r.IsSystem || r.ApplicationId != null).ToListAsync(ct);"
|
|
76
|
+
echo " var role = roles.FirstOrDefault(r => r.Code == mapping.RoleCode);"
|
|
77
|
+
echo "$BAD_ROLE_GUID"
|
|
78
|
+
fi
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
ROLE_PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" 2>/dev/null)
|
|
82
|
+
if [ -n "$ROLE_PERM_FILES" ]; then
|
|
83
|
+
BAD_ROLE_REF=$(grep -Pn 'GenerateRoleGuid|GetAdminRoleId|GetManagerRoleId|GetViewerRoleId|GetContributorRoleId' $ROLE_PERM_FILES 2>/dev/null || true)
|
|
84
|
+
if [ -n "$BAD_ROLE_REF" ]; then
|
|
85
|
+
echo "WARNING: RolesSeedData uses hardcoded role GUID helpers instead of Code-based lookup"
|
|
86
|
+
echo "Fix: Use RoleCode string (e.g., 'admin') and resolve in SeedRolePermissionsAsync()"
|
|
87
|
+
echo "$BAD_ROLE_REF"
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# POST-CHECK C16: Cross-tenant entities must use Guid? TenantId
|
|
92
|
+
for entity in $(find src/ -path "*/Domain/*" -name "*.cs" ! -name "I*.cs" 2>/dev/null); do
|
|
93
|
+
if grep -q "IOptionalTenantEntity\|IScopedTenantEntity" "$entity"; then
|
|
94
|
+
if grep -q "public Guid TenantId" "$entity" && ! grep -q "public Guid? TenantId" "$entity"; then
|
|
95
|
+
echo "CRITICAL: Entity with IOptionalTenantEntity/IScopedTenantEntity must use Guid? TenantId (nullable)"
|
|
96
|
+
FAIL=true
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
done
|
|
100
|
+
|
|
101
|
+
# POST-CHECK C17: Scoped entities must have EntityScope property
|
|
102
|
+
for entity in $(find src/ -path "*/Domain/*" -name "*.cs" ! -name "I*.cs" 2>/dev/null); do
|
|
103
|
+
if grep -q "IScopedTenantEntity" "$entity"; then
|
|
104
|
+
if ! grep -q "EntityScope\|Scope" "$entity"; then
|
|
105
|
+
echo "CRITICAL: Entity with IScopedTenantEntity must have EntityScope Scope property"
|
|
106
|
+
FAIL=true
|
|
107
|
+
fi
|
|
108
|
+
fi
|
|
109
|
+
done
|
|
110
|
+
|
|
111
|
+
# POST-CHECK C18: Permissions.cs static constants must exist (BLOCKING)
|
|
112
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
113
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
114
|
+
PERM_REFS=$(grep -ohP 'Permissions\.\w+\.\w+' $CTRL_FILES 2>/dev/null | sed 's/Permissions\.\([^.]*\)\..*/\1/' | sort -u || true)
|
|
115
|
+
for MODULE in $PERM_REFS; do
|
|
116
|
+
PERM_FILE=$(find src/ -name "Permissions.cs" -exec grep -l "static class $MODULE" {} \; 2>/dev/null || true)
|
|
117
|
+
if [ -z "$PERM_FILE" ]; then
|
|
118
|
+
echo "BLOCKING: Controller references Permissions.${MODULE}.* but no Permissions.cs defines static class ${MODULE}"
|
|
119
|
+
echo "Fix: Create Application/Authorization/Permissions.cs with: public static class ${MODULE} { public const string Read = \"...\"; ... }"
|
|
120
|
+
FAIL=true
|
|
121
|
+
fi
|
|
122
|
+
done
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# POST-CHECK C19: ApplicationRolesSeedData.cs must exist (BLOCKING)
|
|
126
|
+
ROLE_SEED=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" 2>/dev/null | head -1)
|
|
127
|
+
if [ -n "$ROLE_SEED" ]; then
|
|
128
|
+
APP_ROLE_SEED=$(find src/ -path "*/Seeding/Data/ApplicationRolesSeedData.cs" 2>/dev/null | head -1)
|
|
129
|
+
if [ -z "$APP_ROLE_SEED" ]; then
|
|
130
|
+
echo "BLOCKING: RolesSeedData exists but ApplicationRolesSeedData.cs NOT FOUND"
|
|
131
|
+
echo "ApplicationRolesSeedData defines the 4 application-scoped roles (admin, manager, contributor, viewer)"
|
|
132
|
+
echo "Without it, SeedRolesAsync() has no role entries to create → RBAC broken"
|
|
133
|
+
echo "Fix: Create src/Infrastructure/Persistence/Seeding/Data/ApplicationRolesSeedData.cs"
|
|
134
|
+
FAIL=true
|
|
135
|
+
fi
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
# POST-CHECK C20: Section route completeness (NavigationSection → frontend route + permissions)
|
|
139
|
+
SECTION_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSectionSeedData.cs" 2>/dev/null)
|
|
140
|
+
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
141
|
+
if [ -n "$SECTION_SEED_FILES" ] && [ -n "$APP_TSX" ]; then
|
|
142
|
+
SECTION_ROUTES=$(grep -Poh '"/[a-z][a-z0-9/-]+"' $SECTION_SEED_FILES 2>/dev/null | tr -d '"' | sort -u || true)
|
|
143
|
+
for SECTION_ROUTE in $SECTION_ROUTES; do
|
|
144
|
+
SECTION_SEG=$(echo "$SECTION_ROUTE" | rev | cut -d'/' -f1 | rev)
|
|
145
|
+
if ! grep -q "'$SECTION_SEG'" "$APP_TSX" && ! grep -q "\"$SECTION_SEG\"" "$APP_TSX"; then
|
|
146
|
+
echo "WARNING: NavigationSection seed data route has no matching frontend route: $SECTION_ROUTE"
|
|
147
|
+
echo "Expected path segment '$SECTION_SEG' in App.tsx application route block"
|
|
148
|
+
echo "Fix: Add section child routes to the module's children array in App.tsx"
|
|
149
|
+
fi
|
|
150
|
+
done
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
154
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
155
|
+
for f in $CTRL_FILES; do
|
|
156
|
+
SECTION_NAVROUTE=$(grep -oP 'NavRoute\("[a-z-]+\.[a-z-]+' "$f" 2>/dev/null)
|
|
157
|
+
if [ -n "$SECTION_NAVROUTE" ] && ! grep -q "\[RequirePermission" "$f"; then
|
|
158
|
+
echo "CRITICAL: Controller has [NavRoute] but no [RequirePermission]: $f"
|
|
159
|
+
echo "Fix: Add [RequirePermission(Permissions.{Section}.{Action})] on each endpoint"
|
|
160
|
+
FAIL=true
|
|
161
|
+
fi
|
|
162
|
+
done
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
PERM_FILE=$(find src/ -name "Permissions.cs" -path "*/Authorization/*" 2>/dev/null | head -1)
|
|
166
|
+
if [ -n "$SECTION_SEED_FILES" ] && [ -n "$PERM_FILE" ]; then
|
|
167
|
+
SECTION_CODES=$(grep -oP 'Code\s*=\s*"([a-z]+)"' $SECTION_SEED_FILES 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u || true)
|
|
168
|
+
for CODE in $SECTION_CODES; do
|
|
169
|
+
PASCAL=$(echo "$CODE" | sed 's/^./\U&/')
|
|
170
|
+
if ! grep -q "static class $PASCAL" "$PERM_FILE" 2>/dev/null; then
|
|
171
|
+
echo "WARNING: Section '$CODE' in seed data has no matching Permissions.$PASCAL static class"
|
|
172
|
+
echo "Fix: Add section-level permissions via MCP generate_permissions with 3-segment navRoute (app.module.section)"
|
|
173
|
+
fi
|
|
174
|
+
done
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
# POST-CHECK C21: FORBIDDEN route patterns — /list and /detail/:id (WARNING)
|
|
178
|
+
SEED_NAV_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -o -name "*NavigationSectionSeedData.cs" 2>/dev/null)
|
|
179
|
+
if [ -n "$SEED_NAV_FILES" ]; then
|
|
180
|
+
BAD_ROUTES=$(grep -Pn 'Route\s*=\s*.*"[^"]*/(list|detail)["/]' $SEED_NAV_FILES 2>/dev/null | grep -v '//.*Route' || true)
|
|
181
|
+
if [ -n "$BAD_ROUTES" ]; then
|
|
182
|
+
echo "WARNING: FORBIDDEN route pattern in seed data"
|
|
183
|
+
echo " - 'list' section route = module route (NO /list suffix)"
|
|
184
|
+
echo " - 'detail' section route = module route + /:id (NOT /detail/:id)"
|
|
185
|
+
echo "$BAD_ROUTES"
|
|
186
|
+
fi
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
190
|
+
if [ -n "$APP_TSX" ]; then
|
|
191
|
+
BAD_FE=$(grep -Pn "path:\s*['\"](?:list|detail)" "$APP_TSX" 2>/dev/null || true)
|
|
192
|
+
if [ -n "$BAD_FE" ]; then
|
|
193
|
+
echo "WARNING: FORBIDDEN frontend route path"
|
|
194
|
+
echo " - list = index: true (no 'list' path segment)"
|
|
195
|
+
echo " - detail = ':id' (no 'detail' path segment)"
|
|
196
|
+
echo "$BAD_FE"
|
|
197
|
+
fi
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
# POST-CHECK C22: Permission path segment count (WARNING)
|
|
201
|
+
PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "PermissionsSeedData.cs" 2>/dev/null)
|
|
202
|
+
if [ -n "$PERM_FILES" ]; then
|
|
203
|
+
while IFS= read -r line; do
|
|
204
|
+
PATH_VAL=$(echo "$line" | grep -oP '"[^"]*\.[^"]*"' | tr -d '"' || true)
|
|
205
|
+
if [ -n "$PATH_VAL" ]; then
|
|
206
|
+
DOTS=$(echo "$PATH_VAL" | tr -cd '.' | wc -c)
|
|
207
|
+
if echo "$PATH_VAL" | grep -qP '\.\*$'; then
|
|
208
|
+
continue
|
|
209
|
+
elif [ "$DOTS" -lt 2 ] || [ "$DOTS" -gt 4 ]; then
|
|
210
|
+
echo "WARNING: Permission path has unexpected segment count ($((DOTS+1)) segments): $PATH_VAL"
|
|
211
|
+
fi
|
|
212
|
+
fi
|
|
213
|
+
done < <(grep -n 'Path\s*=' $PERM_FILES 2>/dev/null)
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
# POST-CHECK C23: IClientSeedDataProvider must have 4 methods + DI registration (BLOCKING)
|
|
217
|
+
PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
|
|
218
|
+
if [ -n "$PROVIDER" ]; then
|
|
219
|
+
METHODS_FOUND=0
|
|
220
|
+
for METHOD in SeedNavigationAsync SeedRolesAsync SeedPermissionsAsync SeedRolePermissionsAsync; do
|
|
221
|
+
if grep -q "$METHOD" "$PROVIDER"; then
|
|
222
|
+
METHODS_FOUND=$((METHODS_FOUND + 1))
|
|
223
|
+
else
|
|
224
|
+
echo "BLOCKING: IClientSeedDataProvider missing method: $METHOD in $PROVIDER"
|
|
225
|
+
FAIL=true
|
|
226
|
+
fi
|
|
227
|
+
done
|
|
228
|
+
if [ "$METHODS_FOUND" -lt 4 ]; then
|
|
229
|
+
echo "Fix: IClientSeedDataProvider must implement all 4 methods: SeedNavigationAsync, SeedRolesAsync, SeedPermissionsAsync, SeedRolePermissionsAsync"
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
DI_FILE=$(find src/ -name "DependencyInjection.cs" -path "*/Infrastructure/*" 2>/dev/null | head -1)
|
|
233
|
+
if [ -n "$DI_FILE" ]; then
|
|
234
|
+
if ! grep -q "IClientSeedDataProvider" "$DI_FILE"; then
|
|
235
|
+
echo "BLOCKING: IClientSeedDataProvider not registered in DependencyInjection.cs"
|
|
236
|
+
echo "Fix: Add services.AddScoped<IClientSeedDataProvider, {App}SeedDataProvider>()"
|
|
237
|
+
FAIL=true
|
|
238
|
+
fi
|
|
239
|
+
fi
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
# POST-CHECK C32: Translation seed data must have idempotency guard (CRITICAL)
|
|
243
|
+
PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
|
|
244
|
+
if [ -n "$PROVIDER" ]; then
|
|
245
|
+
TRANSLATION_ADDS=$(grep -c "NavigationTranslations.Add" "$PROVIDER" 2>/dev/null || true)
|
|
246
|
+
TRANSLATION_GUARDS=$(grep -c "NavigationTranslations.AnyAsync" "$PROVIDER" 2>/dev/null || true)
|
|
247
|
+
|
|
248
|
+
if [ "$TRANSLATION_ADDS" -gt 0 ] && [ "$TRANSLATION_GUARDS" -eq 0 ]; then
|
|
249
|
+
echo "CRITICAL: Translation seed data inserts without idempotency guard in $PROVIDER"
|
|
250
|
+
echo "Fix: Before each NavigationTranslations.Add block, check existence:"
|
|
251
|
+
echo " if (!await context.NavigationTranslations.AnyAsync("
|
|
252
|
+
echo " t => t.EntityId == {Module}NavigationSeedData.{Module}ModuleId"
|
|
253
|
+
echo " && t.EntityType == NavigationEntityType.Module, ct))"
|
|
254
|
+
echo " { foreach (var t in ...) { context.NavigationTranslations.Add(...); } }"
|
|
255
|
+
echo "The unique index IX_nav_Translations_EntityType_EntityId_LanguageCode will crash on duplicates."
|
|
256
|
+
FAIL=true
|
|
257
|
+
fi
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
# POST-CHECK C33: Resource seed data must use actual section IDs from DB (CRITICAL)
|
|
261
|
+
PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
|
|
262
|
+
if [ -n "$PROVIDER" ]; then
|
|
263
|
+
if grep -Pn 'NavigationResource\.Create\(' "$PROVIDER" | grep -q 'resEntry\.SectionId\|secEntry\.Id'; then
|
|
264
|
+
echo "CRITICAL: Resource seed data uses seed-time GUID as SectionId in $PROVIDER"
|
|
265
|
+
echo "NavigationSection.Create() generates its own ID — seed-time GUIDs do NOT exist in nav_Sections."
|
|
266
|
+
echo "Fix: Query actual section from DB before creating resources:"
|
|
267
|
+
echo " var actualSection = await context.NavigationSections"
|
|
268
|
+
echo " .FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == modEntity.Id, ct);"
|
|
269
|
+
echo " NavigationResource.Create(actualSection.Id, ...) // NOT secEntry.Id or resEntry.SectionId"
|
|
270
|
+
FAIL=true
|
|
271
|
+
fi
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
# POST-CHECK C34: NavRoute segments must use kebab-case for multi-word codes (BLOCKING)
|
|
275
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
276
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
277
|
+
for f in $CTRL_FILES; do
|
|
278
|
+
NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' || true)
|
|
279
|
+
if [ -n "$NAVROUTE_VAL" ]; then
|
|
280
|
+
for SEG in $(echo "$NAVROUTE_VAL" | tr '.' '\n'); do
|
|
281
|
+
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
282
|
+
echo "BLOCKING: NavRoute segment '$SEG' in $f appears to be concatenated multi-word without hyphens"
|
|
283
|
+
echo " Full NavRoute: $NAVROUTE_VAL"
|
|
284
|
+
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
|
|
285
|
+
echo " SmartStack convention (from SmartStack.app): 'support-client.my-tickets'"
|
|
286
|
+
FAIL=true
|
|
287
|
+
fi
|
|
288
|
+
done
|
|
289
|
+
fi
|
|
290
|
+
done
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -o -name "NavigationApplicationSeedData.cs" 2>/dev/null)
|
|
294
|
+
if [ -n "$SEED_FILES" ]; then
|
|
295
|
+
CODES=$(grep -oP 'Code\s*=\s*"([^"]+)"' $SEED_FILES 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u || true)
|
|
296
|
+
for CODE in $CODES; do
|
|
297
|
+
if echo "$CODE" | grep -qP '^[a-z]{10,}$'; then
|
|
298
|
+
echo "BLOCKING: Navigation seed data Code '$CODE' appears to be concatenated multi-word without hyphens"
|
|
299
|
+
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
|
|
300
|
+
FAIL=true
|
|
301
|
+
fi
|
|
302
|
+
done
|
|
303
|
+
fi
|
|
304
|
+
|
|
305
|
+
# POST-CHECK C35: Permission codes must use kebab-case matching NavRoute codes (BLOCKING)
|
|
306
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
307
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
308
|
+
for f in $CTRL_FILES; do
|
|
309
|
+
PERM_VALS=$(grep -oP 'RequirePermission\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' || true)
|
|
310
|
+
for PERM in $PERM_VALS; do
|
|
311
|
+
SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
|
|
312
|
+
for SEG in $SEGMENTS; do
|
|
313
|
+
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
314
|
+
echo "BLOCKING: Permission code segment '$SEG' in $f appears concatenated without hyphens"
|
|
315
|
+
echo " Full permission: $PERM"
|
|
316
|
+
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
|
|
317
|
+
echo " SmartStack convention: 'support-client.my-tickets.read'"
|
|
318
|
+
FAIL=true
|
|
319
|
+
fi
|
|
320
|
+
done
|
|
321
|
+
done
|
|
322
|
+
done
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
PERM_FILES=$(find src/ -path "*/Authorization/Permissions.cs" 2>/dev/null)
|
|
326
|
+
if [ -n "$PERM_FILES" ]; then
|
|
327
|
+
for f in $PERM_FILES; do
|
|
328
|
+
CONST_VALS=$(grep -oP '=\s*"([^"]+)"' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' || true)
|
|
329
|
+
for PERM in $CONST_VALS; do
|
|
330
|
+
SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
|
|
331
|
+
for SEG in $SEGMENTS; do
|
|
332
|
+
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
333
|
+
echo "BLOCKING: Permissions.cs constant segment '$SEG' in $f appears concatenated without hyphens"
|
|
334
|
+
echo " Full permission: $PERM"
|
|
335
|
+
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
|
|
336
|
+
FAIL=true
|
|
337
|
+
fi
|
|
338
|
+
done
|
|
339
|
+
done
|
|
340
|
+
done
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
SEED_PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*PermissionsSeedData.cs" 2>/dev/null)
|
|
344
|
+
if [ -n "$SEED_PERM_FILES" ]; then
|
|
345
|
+
PATHS=$(grep -oP '"[a-z][a-z0-9.-]+\.(read|create|update|delete|\*)"' $SEED_PERM_FILES 2>/dev/null | tr -d '"' || true)
|
|
346
|
+
for PERM in $PATHS; do
|
|
347
|
+
SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
|
|
348
|
+
for SEG in $SEGMENTS; do
|
|
349
|
+
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
350
|
+
echo "BLOCKING: PermissionsSeedData path segment '$SEG' appears concatenated without hyphens"
|
|
351
|
+
echo " Full permission path: $PERM"
|
|
352
|
+
echo " Fix: Use kebab-case matching NavRoute: 'humanresources' → 'human-resources'"
|
|
353
|
+
FAIL=true
|
|
354
|
+
fi
|
|
355
|
+
done
|
|
356
|
+
done
|
|
357
|
+
fi
|
|
358
|
+
|
|
359
|
+
# POST-CHECK C44: RolesSeedData must map standard role-permission matrix (CRITICAL)
|
|
360
|
+
ROLE_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" ! -name "ApplicationRolesSeedData.cs" 2>/dev/null)
|
|
361
|
+
if [ -n "$ROLE_SEED_FILES" ]; then
|
|
362
|
+
FAIL_C44=false
|
|
363
|
+
for f in $ROLE_SEED_FILES; do
|
|
364
|
+
BASENAME=$(basename "$f")
|
|
365
|
+
if [ "$BASENAME" = "ApplicationRolesSeedData.cs" ]; then continue; fi
|
|
366
|
+
|
|
367
|
+
HAS_ADMIN_WILDCARD=$(grep -Pc '(admin|Admin).*\*' "$f" 2>/dev/null || true)
|
|
368
|
+
if [ "$HAS_ADMIN_WILDCARD" -eq 0 ]; then
|
|
369
|
+
HAS_ADMIN_ACCESS=$(grep -Pc '(admin|Admin).*(Access|Wildcard|IsWildcard)' "$f" 2>/dev/null || true)
|
|
370
|
+
if [ "$HAS_ADMIN_ACCESS" -eq 0 ]; then
|
|
371
|
+
echo "CRITICAL: Admin role missing wildcard (*) permission in $f"
|
|
372
|
+
echo "Fix: Admin must map to wildcard permission (navRoute.*) or use IsWildcard=true"
|
|
373
|
+
FAIL_C44=true
|
|
374
|
+
fi
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
VIEWER_WRITE=$(grep -Pc '(viewer|Viewer).*(\.delete|\.create|\.update|Delete|Create|Update)' "$f" 2>/dev/null || true)
|
|
378
|
+
if [ "$VIEWER_WRITE" -gt 0 ]; then
|
|
379
|
+
echo "CRITICAL: Viewer role has write permissions (create/update/delete) in $f"
|
|
380
|
+
echo "Fix: Viewer must only have read permission. Remove create/update/delete mappings."
|
|
381
|
+
FAIL_C44=true
|
|
382
|
+
fi
|
|
383
|
+
|
|
384
|
+
MANAGER_DELETE=$(grep -Pc '(manager|Manager).*(\.delete|Delete)' "$f" 2>/dev/null || true)
|
|
385
|
+
if [ "$MANAGER_DELETE" -gt 0 ]; then
|
|
386
|
+
echo "WARNING: Manager role has delete permission in $f"
|
|
387
|
+
echo "SmartStack standard: Manager = CRU (no delete). Verify this is intentional."
|
|
388
|
+
fi
|
|
389
|
+
done
|
|
390
|
+
if [ "$FAIL_C44" = true ]; then
|
|
391
|
+
FAIL=true
|
|
392
|
+
fi
|
|
393
|
+
fi
|
|
394
|
+
|
|
395
|
+
# POST-CHECK C45: PermissionAction enum must use valid typed values only (BLOCKING)
|
|
396
|
+
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
|
|
397
|
+
if [ -n "$SEED_FILES" ]; then
|
|
398
|
+
FAIL_C45=false
|
|
399
|
+
for f in $SEED_FILES; do
|
|
400
|
+
ENUM_PARSE=$(grep -Pn 'Enum\.Parse<PermissionAction>' "$f" 2>/dev/null || true)
|
|
401
|
+
if [ -n "$ENUM_PARSE" ]; then
|
|
402
|
+
echo "BLOCKING: Enum.Parse<PermissionAction> detected — runtime crash risk: $f"
|
|
403
|
+
echo "$ENUM_PARSE"
|
|
404
|
+
echo "Fix: Use typed enum directly: PermissionAction.Read (NOT Enum.Parse<PermissionAction>(\"Read\"))"
|
|
405
|
+
FAIL_C45=true
|
|
406
|
+
fi
|
|
407
|
+
|
|
408
|
+
INVALID_CAST=$(grep -Pn '\(PermissionAction\)\s*([1-9]\d{1,}|[2-9]\d)' "$f" 2>/dev/null || true)
|
|
409
|
+
if [ -n "$INVALID_CAST" ]; then
|
|
410
|
+
echo "BLOCKING: Invalid PermissionAction cast detected (value > 10): $f"
|
|
411
|
+
echo "$INVALID_CAST"
|
|
412
|
+
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)"
|
|
413
|
+
FAIL_C45=true
|
|
414
|
+
fi
|
|
415
|
+
done
|
|
416
|
+
if [ "$FAIL_C45" = true ]; then
|
|
417
|
+
FAIL=true
|
|
418
|
+
fi
|
|
419
|
+
fi
|
|
420
|
+
|
|
421
|
+
# POST-CHECK C46: Navigation translation completeness — 4 languages per level (WARNING)
|
|
422
|
+
NAV_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" ! -name "*Application*" 2>/dev/null)
|
|
423
|
+
if [ -n "$NAV_SEED_FILES" ]; then
|
|
424
|
+
FAIL_C46=false
|
|
425
|
+
for f in $NAV_SEED_FILES; do
|
|
426
|
+
HAS_FR=$(grep -c '"fr"' "$f" 2>/dev/null || true)
|
|
427
|
+
HAS_EN=$(grep -c '"en"' "$f" 2>/dev/null || true)
|
|
428
|
+
HAS_IT=$(grep -c '"it"' "$f" 2>/dev/null || true)
|
|
429
|
+
HAS_DE=$(grep -c '"de"' "$f" 2>/dev/null || true)
|
|
430
|
+
|
|
431
|
+
if [ "$HAS_FR" -eq 0 ] || [ "$HAS_EN" -eq 0 ] || [ "$HAS_IT" -eq 0 ] || [ "$HAS_DE" -eq 0 ]; then
|
|
432
|
+
echo "WARNING: Missing language(s) in navigation translations: $f"
|
|
433
|
+
echo " fr=$HAS_FR, en=$HAS_EN, it=$HAS_IT, de=$HAS_DE (all must be > 0)"
|
|
434
|
+
echo "Fix: Add NavigationTranslationSeedEntry for all 4 languages (fr, en, it, de)"
|
|
435
|
+
FAIL_C46=true
|
|
436
|
+
fi
|
|
437
|
+
|
|
438
|
+
HAS_SECTION_ENTRIES=$(grep -c 'GetSectionEntries' "$f" 2>/dev/null || true)
|
|
439
|
+
HAS_SECTION_TRANSLATIONS=$(grep -c 'GetSectionTranslationEntries' "$f" 2>/dev/null || true)
|
|
440
|
+
if [ "$HAS_SECTION_ENTRIES" -gt 0 ] && [ "$HAS_SECTION_TRANSLATIONS" -eq 0 ]; then
|
|
441
|
+
echo "WARNING: Sections defined but GetSectionTranslationEntries() missing: $f"
|
|
442
|
+
echo "Fix: Add GetSectionTranslationEntries() with 4 languages per section (ref core-seed-data.md §2b)"
|
|
443
|
+
FAIL_C46=true
|
|
444
|
+
fi
|
|
445
|
+
|
|
446
|
+
HAS_RESOURCE_ENTRIES=$(grep -c 'GetResourceEntries' "$f" 2>/dev/null || true)
|
|
447
|
+
HAS_RESOURCE_TRANSLATIONS=$(grep -Pc 'ResourceTranslation|GetResourceTranslation|NavigationEntityType\.Resource.*LanguageCode' "$f" 2>/dev/null || true)
|
|
448
|
+
if [ "$HAS_RESOURCE_ENTRIES" -gt 0 ] && [ "$HAS_RESOURCE_TRANSLATIONS" -eq 0 ]; then
|
|
449
|
+
echo "WARNING: Resources defined but resource translations missing: $f"
|
|
450
|
+
echo "Fix: Add resource translation entries with 4 languages per resource (ref core-seed-data.md §2b)"
|
|
451
|
+
FAIL_C46=true
|
|
452
|
+
fi
|
|
453
|
+
done
|
|
454
|
+
fi
|
|
455
|
+
|
|
456
|
+
# POST-CHECK C47: Person Extension entities must not duplicate User fields (WARNING)
|
|
457
|
+
ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null)
|
|
458
|
+
if [ -n "$ENTITY_FILES" ]; then
|
|
459
|
+
FAIL_C47=false
|
|
460
|
+
for f in $ENTITY_FILES; do
|
|
461
|
+
HAS_USERID=$(grep -P 'public\s+Guid\s+UserId\s*\{' "$f" 2>/dev/null || true)
|
|
462
|
+
if [ -z "$HAS_USERID" ]; then continue; fi
|
|
463
|
+
|
|
464
|
+
IS_MANDATORY=$(grep -P 'public\s+Guid\s+UserId\s*\{' "$f" 2>/dev/null | grep -v 'Guid?' || true)
|
|
465
|
+
if [ -z "$IS_MANDATORY" ]; then continue; fi
|
|
466
|
+
|
|
467
|
+
if ! grep -q "ITenantEntity" "$f"; then continue; fi
|
|
468
|
+
|
|
469
|
+
PERSON_FIELDS=$(grep -Pn 'public\s+string\S*\s+(FirstName|LastName|Email|PhoneNumber)\s*\{' "$f" 2>/dev/null || true)
|
|
470
|
+
if [ -n "$PERSON_FIELDS" ]; then
|
|
471
|
+
echo "WARNING: Mandatory person extension entity duplicates User fields: $f"
|
|
472
|
+
echo " Entity has non-nullable UserId (mandatory variant) — person fields come from User"
|
|
473
|
+
echo "$PERSON_FIELDS"
|
|
474
|
+
echo " Fix: Remove FirstName/LastName/Email/PhoneNumber — use Display* from User join in ResponseDto"
|
|
475
|
+
echo " See references/person-extension-pattern.md section 2"
|
|
476
|
+
FAIL_C47=true
|
|
477
|
+
fi
|
|
478
|
+
done
|
|
479
|
+
fi
|
|
480
|
+
|
|
481
|
+
# POST-CHECK C48: Person Extension service must Include(User) (CRITICAL)
|
|
482
|
+
ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null)
|
|
483
|
+
if [ -n "$ENTITY_FILES" ]; then
|
|
484
|
+
FAIL_C48=false
|
|
485
|
+
for f in $ENTITY_FILES; do
|
|
486
|
+
HAS_USERID=$(grep -P 'public\s+Guid\??\s+UserId\s*\{' "$f" 2>/dev/null || true)
|
|
487
|
+
if [ -z "$HAS_USERID" ]; then continue; fi
|
|
488
|
+
if ! grep -q "ITenantEntity" "$f"; then continue; fi
|
|
489
|
+
|
|
490
|
+
HAS_USER_NAV=$(grep -P 'public\s+User\?\s+User\s*\{' "$f" 2>/dev/null || true)
|
|
491
|
+
if [ -z "$HAS_USER_NAV" ]; then continue; fi
|
|
492
|
+
|
|
493
|
+
ENTITY_NAME=$(basename "$f" .cs)
|
|
494
|
+
SERVICE_FILE=$(find src/ -path "*/Services/*" -name "${ENTITY_NAME}Service.cs" ! -name "I${ENTITY_NAME}Service.cs" 2>/dev/null | head -1)
|
|
495
|
+
if [ -z "$SERVICE_FILE" ]; then continue; fi
|
|
496
|
+
|
|
497
|
+
HAS_INCLUDE=$(grep -P 'Include.*User' "$SERVICE_FILE" 2>/dev/null || true)
|
|
498
|
+
if [ -z "$HAS_INCLUDE" ]; then
|
|
499
|
+
echo "CRITICAL: Service for Person Extension entity must Include(x => x.User): $SERVICE_FILE"
|
|
500
|
+
echo " Entity: $f has UserId FK + User navigation property"
|
|
501
|
+
echo " Fix: Add .Include(x => x.User) to all queries in $SERVICE_FILE"
|
|
502
|
+
echo " Without Include, Display* fields will always be null"
|
|
503
|
+
echo " See references/person-extension-pattern.md section 5"
|
|
504
|
+
FAIL_C48=true
|
|
505
|
+
fi
|
|
506
|
+
done
|
|
507
|
+
if [ "$FAIL_C48" = true ]; then
|
|
508
|
+
FAIL=true
|
|
509
|
+
fi
|
|
510
|
+
fi
|
|
511
|
+
|
|
512
|
+
# POST-CHECK C53: Enum serialization — JsonStringEnumConverter required (BLOCKING)
|
|
513
|
+
PROGRAM_CS=$(find src/ -name "Program.cs" -path "*/Api/*" 2>/dev/null | head -1)
|
|
514
|
+
HAS_GLOBAL_CONVERTER=false
|
|
515
|
+
if [ -n "$PROGRAM_CS" ] && grep -q "JsonStringEnumConverter" "$PROGRAM_CS" 2>/dev/null; then
|
|
516
|
+
HAS_GLOBAL_CONVERTER=true
|
|
517
|
+
fi
|
|
518
|
+
|
|
519
|
+
if [ "$HAS_GLOBAL_CONVERTER" = false ]; then
|
|
520
|
+
ENUM_FILES=$(find src/ -path "*/Domain/Enums/*" -name "*.cs" 2>/dev/null)
|
|
521
|
+
if [ -n "$ENUM_FILES" ]; then
|
|
522
|
+
for f in $ENUM_FILES; do
|
|
523
|
+
if grep -q "public enum" "$f" && ! grep -q "JsonStringEnumConverter" "$f"; then
|
|
524
|
+
echo "BLOCKING: Enum missing [JsonConverter(typeof(JsonStringEnumConverter))]: $f"
|
|
525
|
+
echo " Frontend sends enum values as strings but C# deserializes as int by default"
|
|
526
|
+
echo " Fix: Add [JsonConverter(typeof(JsonStringEnumConverter))] on the enum"
|
|
527
|
+
echo " Or: Add JsonStringEnumConverter globally in Program.cs"
|
|
528
|
+
FAIL=true
|
|
529
|
+
fi
|
|
530
|
+
done
|
|
531
|
+
fi
|
|
532
|
+
fi
|
|
533
|
+
|
|
534
|
+
if [ "$FAIL" = true ]; then
|
|
535
|
+
exit 1
|
|
536
|
+
fi
|