@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.
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +51 -14
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/skills/apex/SKILL.md +26 -5
- package/templates/skills/apex/_shared.md +3 -3
- package/templates/skills/apex/references/agent-teams-protocol.md +8 -8
- package/templates/skills/apex/references/challenge-questions.md +165 -0
- package/templates/skills/apex/references/post-checks.md +457 -0
- package/templates/skills/apex/references/smartstack-api.md +234 -14
- package/templates/skills/apex/references/smartstack-frontend.md +20 -0
- package/templates/skills/apex/references/smartstack-layers.md +16 -4
- package/templates/skills/apex/steps/step-00-init.md +84 -56
- package/templates/skills/apex/steps/step-01-analyze.md +73 -87
- package/templates/skills/apex/steps/step-03-execute.md +6 -4
- package/templates/skills/apex/steps/step-04-examine.md +198 -0
- package/templates/skills/apex/steps/{step-05-examine.md → step-05-deep-review.md} +6 -6
- package/templates/skills/apex/steps/step-06-resolve.md +2 -2
- package/templates/skills/business-analyse/SKILL.md +28 -0
- package/templates/skills/business-analyse/references/agent-module-prompt.md +255 -0
- package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +26 -10
- package/templates/skills/business-analyse/references/team-orchestration.md +437 -0
- package/templates/skills/business-analyse/steps/step-02-decomposition.md +31 -4
- package/templates/skills/business-analyse/steps/step-03a1-setup.md +21 -0
- package/templates/skills/business-analyse/steps/step-03d-validate.md +84 -0
- package/templates/skills/efcore/steps/migration/step-02-create.md +14 -1
- package/templates/skills/ralph-loop/references/category-rules.md +26 -2
- package/templates/skills/ralph-loop/references/compact-loop.md +1 -1
- package/templates/skills/ralph-loop/references/core-seed-data.md +45 -10
- package/templates/skills/ralph-loop/steps/step-02-execute.md +128 -1
- package/templates/skills/validate-feature/steps/step-01-compile.md +4 -1
- 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.**
|