@atlashub/smartstack-cli 3.24.0 → 3.26.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 (33) hide show
  1. package/dist/index.js +5 -0
  2. package/dist/index.js.map +1 -1
  3. package/dist/mcp-entry.mjs +51 -14
  4. package/dist/mcp-entry.mjs.map +1 -1
  5. package/package.json +1 -1
  6. package/templates/skills/apex/SKILL.md +26 -5
  7. package/templates/skills/apex/_shared.md +3 -3
  8. package/templates/skills/apex/references/agent-teams-protocol.md +8 -8
  9. package/templates/skills/apex/references/challenge-questions.md +165 -0
  10. package/templates/skills/apex/references/post-checks.md +457 -0
  11. package/templates/skills/apex/references/smartstack-api.md +234 -14
  12. package/templates/skills/apex/references/smartstack-frontend.md +20 -0
  13. package/templates/skills/apex/references/smartstack-layers.md +16 -4
  14. package/templates/skills/apex/steps/step-00-init.md +84 -56
  15. package/templates/skills/apex/steps/step-01-analyze.md +73 -87
  16. package/templates/skills/apex/steps/step-03-execute.md +6 -4
  17. package/templates/skills/apex/steps/step-04-examine.md +198 -0
  18. package/templates/skills/apex/steps/{step-05-examine.md → step-05-deep-review.md} +6 -6
  19. package/templates/skills/apex/steps/step-06-resolve.md +2 -2
  20. package/templates/skills/business-analyse/SKILL.md +28 -0
  21. package/templates/skills/business-analyse/references/agent-module-prompt.md +255 -0
  22. package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +26 -10
  23. package/templates/skills/business-analyse/references/team-orchestration.md +437 -0
  24. package/templates/skills/business-analyse/steps/step-02-decomposition.md +31 -4
  25. package/templates/skills/business-analyse/steps/step-03a1-setup.md +21 -0
  26. package/templates/skills/business-analyse/steps/step-03d-validate.md +84 -0
  27. package/templates/skills/efcore/steps/migration/step-02-create.md +14 -1
  28. package/templates/skills/ralph-loop/references/category-rules.md +26 -2
  29. package/templates/skills/ralph-loop/references/compact-loop.md +1 -1
  30. package/templates/skills/ralph-loop/references/core-seed-data.md +45 -10
  31. package/templates/skills/ralph-loop/steps/step-02-execute.md +128 -1
  32. package/templates/skills/validate-feature/steps/step-01-compile.md +4 -1
  33. package/templates/skills/apex/steps/step-04-validate.md +0 -448
@@ -0,0 +1,457 @@
1
+ # BLOCKING POST-CHECKs
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
+ ### POST-CHECK 1: Navigation routes must be full paths starting with /
7
+
8
+ ```bash
9
+ # Find all seed data files and check route values
10
+ SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
11
+ if [ -n "$SEED_FILES" ]; then
12
+ # Check for short routes (no leading /) in Create() calls for navigation entities
13
+ BAD_ROUTES=$(grep -Pn 'NavigationApplication\.Create\(|NavigationModule\.Create\(|NavigationSection\.Create\(|NavigationResource\.Create\(' $SEED_FILES | grep -v '"/[a-z]')
14
+ if [ -n "$BAD_ROUTES" ]; then
15
+ echo "BLOCKING: Navigation routes must be full paths starting with /"
16
+ echo "$BAD_ROUTES"
17
+ echo "Expected: \"/business/human-resources\" NOT \"humanresources\""
18
+ exit 1
19
+ fi
20
+ fi
21
+ ```
22
+
23
+ ### POST-CHECK 2: All services must filter by TenantId (OWASP A01)
24
+
25
+ ```bash
26
+ # Find all service implementation files
27
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
28
+ if [ -n "$SERVICE_FILES" ]; then
29
+ # Check each service file has TenantId reference (either _currentUser.TenantId or TenantId filter)
30
+ for f in $SERVICE_FILES; do
31
+ if ! grep -q "TenantId" "$f"; then
32
+ echo "BLOCKING (OWASP A01): Service missing TenantId filter: $f"
33
+ echo "Every service query MUST filter by _currentUser.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 _currentUser.TenantId: $f"
38
+ exit 1
39
+ fi
40
+ done
41
+ fi
42
+ ```
43
+
44
+ ### POST-CHECK 3: Controllers must use [RequirePermission], not just [Authorize]
45
+
46
+ ```bash
47
+ # Find all controller files
48
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
49
+ if [ -n "$CTRL_FILES" ]; then
50
+ for f in $CTRL_FILES; do
51
+ # Check controller has at least one RequirePermission attribute
52
+ if grep -q "\[Authorize\]" "$f" && ! grep -q "\[RequirePermission" "$f"; then
53
+ echo "WARNING: Controller uses [Authorize] without [RequirePermission]: $f"
54
+ echo "Use [RequirePermission(Permissions.{Module}.{Action})] on each endpoint"
55
+ fi
56
+ done
57
+ fi
58
+ ```
59
+
60
+ ### POST-CHECK 4: Seed data must not use Guid.NewGuid()
61
+
62
+ ```bash
63
+ SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
64
+ if [ -n "$SEED_FILES" ]; then
65
+ BAD_GUIDS=$(grep -n "Guid.NewGuid()" $SEED_FILES 2>/dev/null)
66
+ if [ -n "$BAD_GUIDS" ]; then
67
+ echo "BLOCKING: Seed data must use deterministic GUIDs (SHA256), not Guid.NewGuid()"
68
+ echo "$BAD_GUIDS"
69
+ exit 1
70
+ fi
71
+ fi
72
+ ```
73
+
74
+ ### POST-CHECK 5: Services must inject ICurrentTenantService (tenant isolation)
75
+
76
+ ```bash
77
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
78
+ if [ -n "$SERVICE_FILES" ]; then
79
+ for f in $SERVICE_FILES; do
80
+ # Accept either ICurrentTenantService or ICurrentUser (legacy) for tenant context
81
+ if ! grep -qE "ICurrentTenantService|ICurrentUser" "$f"; then
82
+ echo "BLOCKING: Service missing tenant context injection: $f"
83
+ echo "All services MUST inject ICurrentTenantService for tenant isolation"
84
+ echo "Pattern: private readonly ICurrentTenantService _currentTenant;"
85
+ exit 1
86
+ fi
87
+ done
88
+ fi
89
+ ```
90
+
91
+ ### POST-CHECK 6: Translation files must exist for all 4 languages (if frontend)
92
+
93
+ ```bash
94
+ # Find all i18n namespaces used in tsx files
95
+ TSX_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
96
+ if [ -n "$TSX_FILES" ]; then
97
+ NAMESPACES=$(grep -ohP "useTranslation\(\[?'([^']+)" $TSX_FILES | sed "s/.*'//" | sort -u)
98
+ for NS in $NAMESPACES; do
99
+ for LANG in fr en it de; do
100
+ if [ ! -f "src/i18n/locales/$LANG/$NS.json" ]; then
101
+ echo "BLOCKING: Missing translation file: src/i18n/locales/$LANG/$NS.json"
102
+ exit 1
103
+ fi
104
+ done
105
+ done
106
+ fi
107
+ ```
108
+
109
+ ### POST-CHECK 7: Pages must use lazy loading (no static page imports in routes)
110
+
111
+ ```bash
112
+ ROUTE_FILES=$(find src/routes/ -name "*.tsx" -o -name "*.ts" 2>/dev/null)
113
+ if [ -n "$ROUTE_FILES" ]; then
114
+ STATIC_PAGE_IMPORTS=$(grep -Pn "^import .+ from '@/pages/" $ROUTE_FILES 2>/dev/null)
115
+ if [ -n "$STATIC_PAGE_IMPORTS" ]; then
116
+ echo "BLOCKING: Route files must use React.lazy() for page imports, not static imports"
117
+ echo "$STATIC_PAGE_IMPORTS"
118
+ echo "Fix: const Page = lazy(() => import('@/pages/...').then(m => ({ default: m.Page })));"
119
+ exit 1
120
+ fi
121
+ fi
122
+ ```
123
+
124
+ ### POST-CHECK 8: Forms must be full pages with routes — ZERO modals/popups/drawers
125
+
126
+ ```bash
127
+ # Check for modal/dialog/drawer imports in page files (create/edit forms)
128
+ PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
129
+ if [ -n "$PAGE_FILES" ]; then
130
+ MODAL_IMPORTS=$(grep -Pn "import.*(?:Modal|Dialog|Drawer|Popup|Sheet)" $PAGE_FILES 2>/dev/null)
131
+ if [ -n "$MODAL_IMPORTS" ]; then
132
+ echo "BLOCKING: Form pages must NOT use Modal/Dialog/Drawer/Popup components"
133
+ echo "Create/Edit forms MUST be full pages with their own URL routes"
134
+ echo "Found modal imports in page files:"
135
+ echo "$MODAL_IMPORTS"
136
+ echo "Fix: Create EntityCreatePage.tsx with route /create and EntityEditPage.tsx with route /:id/edit"
137
+ exit 1
138
+ fi
139
+ fi
140
+ ```
141
+
142
+ ### POST-CHECK 9: Create/Edit pages must exist as separate route pages
143
+
144
+ ```bash
145
+ # For each module with a list page, verify create and edit pages exist
146
+ LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test)
147
+ if [ -n "$LIST_PAGES" ]; then
148
+ for LIST_PAGE in $LIST_PAGES; do
149
+ PAGE_DIR=$(dirname "$LIST_PAGE")
150
+ MODULE_NAME=$(basename "$PAGE_DIR")
151
+ # Check for create page
152
+ CREATE_PAGE=$(find "$PAGE_DIR" -name "*CreatePage.tsx" 2>/dev/null)
153
+ if [ -z "$CREATE_PAGE" ]; then
154
+ echo "WARNING: Module $MODULE_NAME has a list page but no CreatePage — expected EntityCreatePage.tsx"
155
+ fi
156
+ # Check for edit page
157
+ EDIT_PAGE=$(find "$PAGE_DIR" -name "*EditPage.tsx" 2>/dev/null)
158
+ if [ -z "$EDIT_PAGE" ]; then
159
+ echo "WARNING: Module $MODULE_NAME has a list page but no EditPage — expected EntityEditPage.tsx"
160
+ fi
161
+ done
162
+ fi
163
+ ```
164
+
165
+ ### POST-CHECK 10: Form pages must have companion test files
166
+
167
+ ```bash
168
+ # Minimum requirement: if frontend pages exist, at least 1 test file must be present
169
+ PAGE_FILES=$(find src/pages/ -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null)
170
+ if [ -n "$PAGE_FILES" ]; then
171
+ ALL_TESTS=$(find src/pages/ -name "*.test.tsx" 2>/dev/null)
172
+ if [ -z "$ALL_TESTS" ]; then
173
+ echo "BLOCKING: No frontend test files found in src/pages/"
174
+ echo "Every form page MUST have a companion .test.tsx file"
175
+ exit 1
176
+ fi
177
+ fi
178
+
179
+ # Every CreatePage and EditPage must have a .test.tsx file
180
+ FORM_PAGES=$(find src/pages/ -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test)
181
+ if [ -n "$FORM_PAGES" ]; then
182
+ for FORM_PAGE in $FORM_PAGES; do
183
+ TEST_FILE="${FORM_PAGE%.tsx}.test.tsx"
184
+ if [ ! -f "$TEST_FILE" ]; then
185
+ echo "BLOCKING: Form page missing test file: $FORM_PAGE"
186
+ echo "Expected: $TEST_FILE"
187
+ echo "All form pages MUST have companion test files (rendering, validation, submit, navigation)"
188
+ exit 1
189
+ fi
190
+ done
191
+ fi
192
+ ```
193
+
194
+ ### POST-CHECK 11: FK fields must NOT be plain text inputs (EntityLookup required)
195
+
196
+ ```bash
197
+ # Check for FK fields rendered as plain text inputs in form pages
198
+ FORM_PAGES=$(find src/pages/ -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test | grep -v node_modules)
199
+ if [ -n "$FORM_PAGES" ]; then
200
+ # Detect FK input fields: <input> with name ending in "Id" (catches inputs with or without explicit type)
201
+ FK_INPUTS=$(grep -Pn '<input[^>]*name=["\x27][a-zA-Z]*Id["\x27]' $FORM_PAGES 2>/dev/null)
202
+ if [ -n "$FK_INPUTS" ]; then
203
+ # Filter out hidden inputs (legitimate for FK submission)
204
+ FK_TEXT_INPUTS=$(echo "$FK_INPUTS" | grep -Pv 'type=["\x27]hidden["\x27]')
205
+ if [ -n "$FK_TEXT_INPUTS" ]; then
206
+ echo "BLOCKING: FK fields rendered as plain text inputs — MUST use EntityLookup component"
207
+ echo "Users cannot type GUIDs manually. Use <EntityLookup /> from @/components/ui/EntityLookup"
208
+ echo "See smartstack-frontend.md section 6 for the EntityLookup pattern"
209
+ echo "$FK_TEXT_INPUTS"
210
+ exit 1
211
+ fi
212
+ fi
213
+
214
+ # Check for input placeholders mentioning "ID" or "id" or "Enter...Id"
215
+ FK_PLACEHOLDER=$(grep -Pn 'placeholder=["\x27].*[Ee]nter.*[Ii][Dd]|placeholder=["\x27].*[Gg][Uu][Ii][Dd]' $FORM_PAGES 2>/dev/null)
216
+ if [ -n "$FK_PLACEHOLDER" ]; then
217
+ echo "BLOCKING: Form has placeholder asking user to enter an ID/GUID — use EntityLookup instead"
218
+ echo "$FK_PLACEHOLDER"
219
+ exit 1
220
+ fi
221
+ fi
222
+ ```
223
+
224
+ ### POST-CHECK 12: Backend APIs must support search parameter for EntityLookup
225
+
226
+ ```bash
227
+ # Check that controller GetAll methods accept search parameter
228
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
229
+ if [ -n "$CTRL_FILES" ]; then
230
+ for f in $CTRL_FILES; do
231
+ # Check if controller has GetAll but no search parameter
232
+ if grep -q "\[HttpGet\]" "$f" && grep -q "GetAll" "$f"; then
233
+ if ! grep -q "search" "$f"; then
234
+ echo "WARNING: Controller missing search parameter on GetAll: $f"
235
+ echo "GetAll endpoints MUST accept ?search= to enable EntityLookup on frontend"
236
+ echo "Fix: Add [FromQuery] string? search parameter to GetAll action"
237
+ fi
238
+ fi
239
+ done
240
+ fi
241
+ ```
242
+
243
+ ### POST-CHECK 13: No hardcoded Tailwind colors in generated pages
244
+
245
+ ```bash
246
+ # Scan all page and component files directly (works for uncommitted/untracked files, Windows/WSL compatible)
247
+ ALL_PAGES=$(find src/pages/ src/components/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
248
+ if [ -n "$ALL_PAGES" ]; then
249
+ HARDCODED=$(grep -Pn '(bg|text|border)-(?!\[)(red|blue|green|gray|white|black|slate|zinc|neutral|stone)-' $ALL_PAGES 2>/dev/null)
250
+ if [ -n "$HARDCODED" ]; then
251
+ echo "WARNING: Pages should use CSS variables instead of hardcoded Tailwind colors"
252
+ echo "Fix: bg-[var(--bg-card)] instead of bg-white, text-[var(--text-primary)] instead of text-gray-900"
253
+ echo "$HARDCODED"
254
+ fi
255
+ fi
256
+ ```
257
+
258
+ ### POST-CHECK 14: Routes seed data must match frontend contextRoutes
259
+
260
+ ```bash
261
+ 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)
262
+ APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
263
+ if [ -n "$APP_TSX" ] && [ -n "$SEED_ROUTES" ]; then
264
+ FRONTEND_PATHS=$(grep -oP "path:\s*'([^']+)'" "$APP_TSX" | grep -oP "'[^']+'" | tr -d "'" | sort -u)
265
+ if [ -n "$FRONTEND_PATHS" ]; then
266
+ MISMATCH_FOUND=false
267
+ for SEED_ROUTE in $SEED_ROUTES; do
268
+ DEPTH=$(echo "$SEED_ROUTE" | tr '/' '\n' | grep -c '.')
269
+ if [ "$DEPTH" -lt 3 ]; then continue; fi
270
+ SEED_SUFFIX=$(echo "$SEED_ROUTE" | sed 's|^/[^/]*/||')
271
+ SEED_NORM=$(echo "$SEED_SUFFIX" | tr '[:upper:]' '[:lower:]' | tr -d '-')
272
+ MATCH_FOUND=false
273
+ for FE_PATH in $FRONTEND_PATHS; do
274
+ FE_BASE=$(echo "$FE_PATH" | sed 's|/list$||;s|/new$||;s|/:id.*||;s|/create$||')
275
+ FE_NORM=$(echo "$FE_BASE" | tr '[:upper:]' '[:lower:]' | tr -d '-')
276
+ if [ "$SEED_NORM" = "$FE_NORM" ]; then
277
+ MATCH_FOUND=true
278
+ break
279
+ fi
280
+ done
281
+ if [ "$MATCH_FOUND" = false ]; then
282
+ echo "BLOCKING: Seed data route has no matching frontend route: $SEED_ROUTE"
283
+ MISMATCH_FOUND=true
284
+ fi
285
+ done
286
+ if [ "$MISMATCH_FOUND" = true ]; then
287
+ echo "Fix: Ensure every NavigationSeedData route has a corresponding contextRoutes entry in App.tsx"
288
+ exit 1
289
+ fi
290
+ fi
291
+ fi
292
+ ```
293
+
294
+ ### POST-CHECK 15: HasQueryFilter must not use Guid.Empty (OWASP A01)
295
+
296
+ ```bash
297
+ CONFIG_FILES=$(find src/ -path "*/Configurations/*" -name "*Configuration.cs" 2>/dev/null)
298
+ if [ -n "$CONFIG_FILES" ]; then
299
+ BAD_FILTERS=$(grep -Pn 'HasQueryFilter.*Guid\.Empty' $CONFIG_FILES 2>/dev/null)
300
+ if [ -n "$BAD_FILTERS" ]; then
301
+ echo "BLOCKING (OWASP A01): HasQueryFilter uses Guid.Empty instead of runtime tenant isolation"
302
+ echo "$BAD_FILTERS"
303
+ echo ""
304
+ echo "Anti-pattern: .HasQueryFilter(e => e.TenantId != Guid.Empty)"
305
+ echo "Fix: Remove HasQueryFilter. Tenant isolation is handled by SmartStack base DbContext"
306
+ exit 1
307
+ fi
308
+ fi
309
+ ```
310
+
311
+ ### POST-CHECK 16: GetAll methods must return PaginatedResult<T>
312
+
313
+ ```bash
314
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
315
+ if [ -n "$SERVICE_FILES" ]; then
316
+ BAD_RETURNS=$(grep -Pn '(Task<\s*(?:List|IEnumerable|IList|ICollection|IReadOnlyList|IReadOnlyCollection)<).*GetAll' $SERVICE_FILES 2>/dev/null)
317
+ if [ -n "$BAD_RETURNS" ]; then
318
+ echo "BLOCKING: GetAll methods must return PaginatedResult<T>, not List/IEnumerable"
319
+ echo "$BAD_RETURNS"
320
+ echo "Fix: Change return type to Task<PaginatedResult<{Entity}ResponseDto>>"
321
+ exit 1
322
+ fi
323
+ fi
324
+ ```
325
+
326
+ ### POST-CHECK 17: i18n files must contain required structural keys
327
+
328
+ ```bash
329
+ I18N_DIR="src/i18n/locales/fr"
330
+ if [ -d "$I18N_DIR" ]; then
331
+ REQUIRED_KEYS="actions columns empty errors form labels messages validation"
332
+ for JSON_FILE in "$I18N_DIR"/*.json; do
333
+ [ ! -f "$JSON_FILE" ] && continue
334
+ BASENAME=$(basename "$JSON_FILE")
335
+ case "$BASENAME" in common.json|navigation.json) continue;; esac
336
+ for KEY in $REQUIRED_KEYS; do
337
+ if ! jq -e "has(\"$KEY\")" "$JSON_FILE" > /dev/null 2>&1; then
338
+ echo "BLOCKING: i18n file missing required key '$KEY': $JSON_FILE"
339
+ echo "Module i18n files MUST contain: $REQUIRED_KEYS"
340
+ exit 1
341
+ fi
342
+ done
343
+ done
344
+ fi
345
+ ```
346
+
347
+ ### POST-CHECK 18: Entities must implement IAuditableEntity + Validators must have Create/Update pairs
348
+
349
+ ```bash
350
+ ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null)
351
+ if [ -n "$ENTITY_FILES" ]; then
352
+ for f in $ENTITY_FILES; do
353
+ if grep -q "ITenantEntity" "$f" && ! grep -q "IAuditableEntity" "$f"; then
354
+ echo "BLOCKING: Entity implements ITenantEntity but NOT IAuditableEntity: $f"
355
+ echo "Pattern: public class Entity : BaseEntity, ITenantEntity, IAuditableEntity"
356
+ exit 1
357
+ fi
358
+ done
359
+ fi
360
+ CREATE_VALIDATORS=$(find src/ -path "*/Validators/*" -name "Create*Validator.cs" 2>/dev/null)
361
+ if [ -n "$CREATE_VALIDATORS" ]; then
362
+ for f in $CREATE_VALIDATORS; do
363
+ VALIDATOR_DIR=$(dirname "$f")
364
+ ENTITY_NAME=$(basename "$f" | sed 's/^Create\(.*\)Validator\.cs$/\1/')
365
+ if [ ! -f "$VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs" ]; then
366
+ echo "BLOCKING: Create${ENTITY_NAME}Validator exists but Update${ENTITY_NAME}Validator is missing"
367
+ echo " Found: $f"
368
+ echo " Expected: $VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs"
369
+ exit 1
370
+ fi
371
+ done
372
+ fi
373
+ ```
374
+
375
+ ### POST-CHECK 19: SeedConstants must NOT contain ContextId (pre-seeded by SmartStack core)
376
+
377
+ ```bash
378
+ # NavigationContext IDs (business, platform, personal) are pre-seeded by SmartStack core
379
+ # with hardcoded GUIDs. Client code MUST look them up by code at runtime, NEVER generate them.
380
+ SEED_CONST_FILES=$(find src/ -path "*/Seeding/*" -name "SeedConstants.cs" 2>/dev/null)
381
+ SEED_ALL_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
382
+ if [ -n "$SEED_CONST_FILES" ]; then
383
+ BAD_CONTEXT_ID=$(grep -Pn 'ContextId\s*=' $SEED_CONST_FILES 2>/dev/null)
384
+ if [ -n "$BAD_CONTEXT_ID" ]; then
385
+ echo "BLOCKING: SeedConstants must NOT contain a ContextId constant"
386
+ echo "NavigationContext IDs are pre-seeded by SmartStack core with hardcoded GUIDs"
387
+ echo "Fix: Remove ContextId from SeedConstants. In SeedDataProvider, query:"
388
+ echo " var ctx = await db.NavigationContexts.FirstOrDefaultAsync(c => c.Code == \"business\", ct);"
389
+ echo "$BAD_CONTEXT_ID"
390
+ exit 1
391
+ fi
392
+ fi
393
+ if [ -n "$SEED_ALL_FILES" ]; then
394
+ BAD_CTX_GUID=$(grep -Pn 'DeterministicGuid\("nav:(business|platform|personal)"\)' $SEED_ALL_FILES 2>/dev/null)
395
+ if [ -n "$BAD_CTX_GUID" ]; then
396
+ echo "BLOCKING: Deterministic GUID for NavigationContext detected"
397
+ echo "Context IDs (business, platform, personal) are pre-seeded by SmartStack core"
398
+ echo "Fix: Look up context by code at runtime in SeedDataProvider.SeedNavigationAsync()"
399
+ echo "$BAD_CTX_GUID"
400
+ exit 1
401
+ fi
402
+ fi
403
+ ```
404
+
405
+ ### POST-CHECK 20: RolePermission seed data must NOT use deterministic role GUIDs
406
+
407
+ ```bash
408
+ # System roles (admin, manager, contributor, viewer) are pre-seeded by SmartStack core.
409
+ # RolePermission mappings MUST look up roles by Code at runtime, NEVER use deterministic GUIDs.
410
+ SEED_ALL_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
411
+ SEED_CONST_FILES=$(find src/ -path "*/Seeding/*" -name "SeedConstants.cs" 2>/dev/null)
412
+ if [ -n "$SEED_ALL_FILES" ]; then
413
+ BAD_ROLE_GUID=$(grep -Pn 'DeterministicGuid\("role:' $SEED_ALL_FILES $SEED_CONST_FILES 2>/dev/null)
414
+ if [ -n "$BAD_ROLE_GUID" ]; then
415
+ echo "BLOCKING: Deterministic GUID for role detected (e.g., DeterministicGuid(\"role:admin\"))"
416
+ echo "System roles are pre-seeded by SmartStack core with their own IDs"
417
+ echo "Fix: In SeedRolePermissionsAsync(), look up roles by Code:"
418
+ echo " var roles = await context.Roles.Where(r => r.IsSystem || r.ApplicationId != null).ToListAsync(ct);"
419
+ echo " var role = roles.FirstOrDefault(r => r.Code == mapping.RoleCode);"
420
+ echo "$BAD_ROLE_GUID"
421
+ exit 1
422
+ fi
423
+ fi
424
+ # Also check for GenerateRoleGuid usage in RolePermission mapping files (not in ApplicationRolesSeedData itself)
425
+ ROLE_PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*RolesSeedData.cs" 2>/dev/null)
426
+ if [ -n "$ROLE_PERM_FILES" ]; then
427
+ BAD_ROLE_REF=$(grep -Pn 'GenerateRoleGuid|GetAdminRoleId|GetManagerRoleId|GetViewerRoleId|GetContributorRoleId' $ROLE_PERM_FILES 2>/dev/null)
428
+ if [ -n "$BAD_ROLE_REF" ]; then
429
+ echo "WARNING: RolesSeedData uses hardcoded role GUID helpers instead of Code-based lookup"
430
+ echo "Fix: Use RoleCode string (e.g., 'admin') and resolve in SeedRolePermissionsAsync()"
431
+ echo "$BAD_ROLE_REF"
432
+ fi
433
+ fi
434
+ ```
435
+
436
+ ### POST-CHECK 21: Services must NOT use TenantId!.Value (null-forgiving crash pattern)
437
+
438
+ ```bash
439
+ # The !.Value pattern on Guid? throws InvalidOperationException (500) instead of clean 401
440
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
441
+ if [ -n "$SERVICE_FILES" ]; then
442
+ BAD_PATTERN=$(grep -Pn 'TenantId!\s*\.Value|TenantId!\s*\.ToString|\.TenantId!' $SERVICE_FILES 2>/dev/null)
443
+ if [ -n "$BAD_PATTERN" ]; then
444
+ echo "BLOCKING: Services use TenantId!.Value — causes 500 instead of 401 when tenant context is missing"
445
+ echo "$BAD_PATTERN"
446
+ echo ""
447
+ echo "Fix: Replace with guard clause at the start of every method:"
448
+ echo " var tenantId = _currentTenant.TenantId"
449
+ echo " ?? throw new UnauthorizedAccessException(\"Tenant context is required\");"
450
+ echo ""
451
+ echo "This produces a clean 401 via GlobalExceptionHandlerMiddleware instead of an opaque 500."
452
+ exit 1
453
+ fi
454
+ fi
455
+ ```
456
+
457
+ **If ANY POST-CHECK fails → fix in step-03, re-validate.**