@atlashub/smartstack-cli 3.30.0 → 3.32.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/installation.html +7 -2
- package/README.md +7 -1
- package/dist/index.js +33 -37
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +547 -97
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/scripts/health-check.sh +2 -1
- package/templates/mcp-scaffolding/controller.cs.hbs +10 -7
- package/templates/mcp-scaffolding/entity-extension.cs.hbs +132 -124
- package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +4 -4
- package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +38 -15
- package/templates/mcp-scaffolding/tests/service.test.cs.hbs +20 -8
- package/templates/skills/apex/SKILL.md +7 -9
- package/templates/skills/apex/_shared.md +9 -2
- package/templates/skills/apex/references/code-generation.md +412 -0
- package/templates/skills/apex/references/post-checks.md +377 -37
- package/templates/skills/apex/references/smartstack-api.md +229 -5
- package/templates/skills/apex/references/smartstack-frontend.md +368 -11
- package/templates/skills/apex/references/smartstack-layers.md +54 -7
- package/templates/skills/apex/steps/step-00-init.md +1 -2
- package/templates/skills/apex/steps/step-01-analyze.md +45 -2
- package/templates/skills/apex/steps/step-02-plan.md +23 -2
- package/templates/skills/apex/steps/step-03-execute.md +195 -5
- package/templates/skills/apex/steps/step-04-examine.md +18 -5
- package/templates/skills/apex/steps/step-05-deep-review.md +9 -11
- package/templates/skills/apex/steps/step-06-resolve.md +5 -9
- package/templates/skills/apex/steps/step-07-tests.md +66 -1
- package/templates/skills/apex/steps/step-08-run-tests.md +12 -3
- package/templates/skills/application/references/provider-template.md +62 -39
- package/templates/skills/application/templates-backend.md +3 -3
- package/templates/skills/application/templates-frontend.md +12 -12
- package/templates/skills/application/templates-seed.md +14 -4
- package/templates/skills/business-analyse/SKILL.md +10 -7
- package/templates/skills/business-analyse/questionnaire/04-data.md +8 -0
- package/templates/skills/business-analyse/references/agent-module-prompt.md +84 -5
- package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +83 -19
- package/templates/skills/business-analyse/references/consolidation-structural-checks.md +6 -2
- package/templates/skills/business-analyse/references/team-orchestration.md +470 -113
- package/templates/skills/business-analyse/references/validation-checklist.md +5 -4
- package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +44 -0
- package/templates/skills/business-analyse/steps/step-03a2-analysis.md +72 -1
- package/templates/skills/business-analyse/steps/step-03c-compile.md +93 -7
- package/templates/skills/business-analyse/steps/step-03d-validate.md +34 -2
- package/templates/skills/business-analyse/steps/step-04b-analyze.md +40 -0
- package/templates/skills/controller/references/controller-code-templates.md +2 -2
- package/templates/skills/controller/templates.md +12 -12
- package/templates/skills/feature-full/steps/step-01-implementation.md +4 -4
- package/templates/skills/ralph-loop/references/category-rules.md +44 -2
- package/templates/skills/ralph-loop/references/compact-loop.md +37 -0
- package/templates/skills/ralph-loop/references/core-seed-data.md +51 -20
- package/templates/skills/review-code/references/owasp-api-top10.md +1 -1
|
@@ -28,13 +28,22 @@ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Se
|
|
|
28
28
|
if [ -n "$SERVICE_FILES" ]; then
|
|
29
29
|
# Check each service file has TenantId reference (either _currentUser.TenantId or TenantId filter)
|
|
30
30
|
for f in $SERVICE_FILES; do
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
# Accept either _currentTenant.TenantId (strict guard clause or nullable usage)
|
|
32
|
+
# or entities with IOptionalTenantEntity/IScopedTenantEntity (optional tenant pattern)
|
|
33
|
+
HAS_TENANT_FILTER=$(grep -c "TenantId" "$f")
|
|
34
|
+
HAS_OPTIONAL_ENTITY=false
|
|
35
|
+
if grep -q "IOptionalTenantEntity\|IScopedTenantEntity" "$f"; then
|
|
36
|
+
HAS_OPTIONAL_ENTITY=true
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if [ "$HAS_TENANT_FILTER" -eq 0 ] && [ "$HAS_OPTIONAL_ENTITY" = false ]; then
|
|
40
|
+
echo "BLOCKING (OWASP A01): Service missing TenantId filter or optional tenant entity: $f"
|
|
41
|
+
echo "Every service query MUST filter by _currentTenant.TenantId"
|
|
42
|
+
echo "OR work with entities that implement IOptionalTenantEntity/IScopedTenantEntity"
|
|
34
43
|
exit 1
|
|
35
44
|
fi
|
|
36
45
|
if grep -q "Guid.Empty" "$f"; then
|
|
37
|
-
echo "BLOCKING (OWASP A01): Service uses Guid.Empty instead of
|
|
46
|
+
echo "BLOCKING (OWASP A01): Service uses Guid.Empty instead of _currentTenant.TenantId: $f"
|
|
38
47
|
exit 1
|
|
39
48
|
fi
|
|
40
49
|
done
|
|
@@ -123,19 +132,35 @@ if [ -n "$ROUTE_FILES" ]; then
|
|
|
123
132
|
fi
|
|
124
133
|
```
|
|
125
134
|
|
|
126
|
-
### POST-CHECK 8: Forms must be full pages with routes — ZERO modals/popups/drawers
|
|
135
|
+
### POST-CHECK 8: Forms must be full pages with routes — ZERO modals/popups/drawers/slide-overs
|
|
127
136
|
|
|
128
137
|
```bash
|
|
129
|
-
# Check for modal/dialog/drawer imports in page files
|
|
138
|
+
# Check for modal/dialog/drawer/slide-over imports AND inline form state in page files
|
|
130
139
|
PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
|
|
131
140
|
if [ -n "$PAGE_FILES" ]; then
|
|
132
|
-
|
|
141
|
+
FAIL=false
|
|
142
|
+
|
|
143
|
+
# 8a. Component imports (Modal, Dialog, Drawer, etc.)
|
|
144
|
+
MODAL_IMPORTS=$(grep -Pn "import.*(?:Modal|Dialog|Drawer|Popup|Sheet|SlideOver|Overlay)" $PAGE_FILES 2>/dev/null)
|
|
133
145
|
if [ -n "$MODAL_IMPORTS" ]; then
|
|
134
|
-
echo "BLOCKING: Form pages must NOT use Modal/Dialog/Drawer/Popup components"
|
|
135
|
-
echo "Create/Edit forms MUST be full pages with their own URL routes"
|
|
136
|
-
echo "Found modal imports in page files:"
|
|
146
|
+
echo "BLOCKING: Form pages must NOT use Modal/Dialog/Drawer/Popup/SlideOver components"
|
|
137
147
|
echo "$MODAL_IMPORTS"
|
|
148
|
+
FAIL=true
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# 8b. Inline form state (catches drawers/slide-overs built without external components)
|
|
152
|
+
FORM_STATE=$(grep -Pn "useState.*(?:isOpen|showModal|showDialog|showCreate|showEdit|showForm|isCreating|isEditing|showDrawer|showPanel|showSlideOver|selectedEntity|editingEntity)" $PAGE_FILES 2>/dev/null)
|
|
153
|
+
if [ -n "$FORM_STATE" ]; then
|
|
154
|
+
echo "BLOCKING: Inline form state detected — forms embedded in ListPage as drawers/panels"
|
|
155
|
+
echo "Create/Edit forms MUST be separate page components with their own URL routes"
|
|
156
|
+
echo "$FORM_STATE"
|
|
157
|
+
FAIL=true
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
if [ "$FAIL" = true ]; then
|
|
161
|
+
echo ""
|
|
138
162
|
echo "Fix: Create EntityCreatePage.tsx with route /create and EntityEditPage.tsx with route /:id/edit"
|
|
163
|
+
echo "NEVER embed create/edit forms as inline drawers, panels, or slide-overs in ListPage"
|
|
139
164
|
exit 1
|
|
140
165
|
fi
|
|
141
166
|
fi
|
|
@@ -193,31 +218,64 @@ if [ -n "$FORM_PAGES" ]; then
|
|
|
193
218
|
fi
|
|
194
219
|
```
|
|
195
220
|
|
|
196
|
-
### POST-CHECK 11: FK fields must
|
|
221
|
+
### POST-CHECK 11: FK fields must use EntityLookup — NO `<input>`, NO `<select>` (BLOCKING)
|
|
197
222
|
|
|
198
223
|
```bash
|
|
199
|
-
# Check for FK fields rendered as
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
224
|
+
# Check ALL page files for FK fields rendered as <input> or <select> instead of EntityLookup
|
|
225
|
+
# Scans ALL .tsx files (not just CreatePage/EditPage — forms may be embedded in ListPage drawers)
|
|
226
|
+
ALL_PAGES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
227
|
+
if [ -n "$ALL_PAGES" ]; then
|
|
228
|
+
FAIL=false
|
|
229
|
+
|
|
230
|
+
# 1. Detect <input> with name/value binding to FK fields (fields ending in "Id")
|
|
231
|
+
FK_INPUTS=$(grep -Pn '<input[^>]*(?:name|value)=["\x27{][^>]*[a-zA-Z]Id["\x27}]' $ALL_PAGES 2>/dev/null | grep -Pv 'type=["\x27]hidden["\x27]')
|
|
204
232
|
if [ -n "$FK_INPUTS" ]; then
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
echo "BLOCKING: FK fields rendered as plain text inputs — MUST use EntityLookup component"
|
|
209
|
-
echo "Users cannot type GUIDs manually. Use <EntityLookup /> from @/components/ui/EntityLookup"
|
|
210
|
-
echo "See smartstack-frontend.md section 6 for the EntityLookup pattern"
|
|
211
|
-
echo "$FK_TEXT_INPUTS"
|
|
212
|
-
exit 1
|
|
213
|
-
fi
|
|
233
|
+
echo "BLOCKING: FK fields rendered as <input> — MUST use EntityLookup component"
|
|
234
|
+
echo "$FK_INPUTS"
|
|
235
|
+
FAIL=true
|
|
214
236
|
fi
|
|
215
237
|
|
|
216
|
-
#
|
|
217
|
-
|
|
238
|
+
# 2. Detect <select> with value binding to FK fields (e.g., value={formData.departmentId})
|
|
239
|
+
FK_SELECTS=$(grep -Pn '<select[^>]*value=\{[^}]*[a-zA-Z]Id\b' $ALL_PAGES 2>/dev/null)
|
|
240
|
+
if [ -n "$FK_SELECTS" ]; then
|
|
241
|
+
echo "BLOCKING: FK fields rendered as <select> dropdown — MUST use EntityLookup component"
|
|
242
|
+
echo "A <select> loaded from API state is NOT a valid substitute for EntityLookup."
|
|
243
|
+
echo "EntityLookup provides: debounced search, paginated results, display name resolution."
|
|
244
|
+
echo "$FK_SELECTS"
|
|
245
|
+
FAIL=true
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
# 3. Detect onChange handlers setting FK fields from <select> (e.g., setFormData({...formData, departmentId: e.target.value}))
|
|
249
|
+
FK_SELECT_ONCHANGE=$(grep -Pn 'onChange=.*[a-zA-Z]Id[^a-zA-Z].*e\.target\.value' $ALL_PAGES 2>/dev/null)
|
|
250
|
+
if [ -n "$FK_SELECT_ONCHANGE" ]; then
|
|
251
|
+
echo "BLOCKING: FK field set via e.target.value (select/input pattern) — use EntityLookup onChange(id)"
|
|
252
|
+
echo "$FK_SELECT_ONCHANGE"
|
|
253
|
+
FAIL=true
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
# 4. Check for placeholders mentioning "ID", "GUID", or "Select..." for FK fields
|
|
257
|
+
FK_PLACEHOLDER=$(grep -Pn 'placeholder=["\x27].*(?:[Ee]nter.*[Ii][Dd]|[Gg][Uu][Ii][Dd]|[Ss]elect.*[Ee]mployee|[Ss]elect.*[Dd]epartment|[Ss]elect.*[Pp]osition|[Ss]elect.*[Pp]roject|[Ss]elect.*[Cc]ategory)' $ALL_PAGES 2>/dev/null)
|
|
218
258
|
if [ -n "$FK_PLACEHOLDER" ]; then
|
|
219
|
-
echo "BLOCKING: Form has placeholder
|
|
259
|
+
echo "BLOCKING: Form has placeholder for FK field selection — use EntityLookup search instead"
|
|
220
260
|
echo "$FK_PLACEHOLDER"
|
|
261
|
+
FAIL=true
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
# 5. Detect <option> elements with GUID-like values (sign of FK <select>)
|
|
265
|
+
FK_OPTIONS=$(grep -Pn '<option[^>]*value=\{[^}]*\.id\}' $ALL_PAGES 2>/dev/null)
|
|
266
|
+
if [ -n "$FK_OPTIONS" ]; then
|
|
267
|
+
echo "BLOCKING: <option> with entity .id value detected — this is a FK <select> anti-pattern"
|
|
268
|
+
echo "Replace the entire <select>/<option> block with <EntityLookup />"
|
|
269
|
+
echo "$FK_OPTIONS"
|
|
270
|
+
FAIL=true
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
if [ "$FAIL" = true ]; then
|
|
274
|
+
echo ""
|
|
275
|
+
echo "Fix: Replace ALL FK fields with <EntityLookup /> from @/components/ui/EntityLookup"
|
|
276
|
+
echo "See smartstack-frontend.md section 6 for the EntityLookup pattern"
|
|
277
|
+
echo "FORBIDDEN for FK Guid fields: <input>, <select>, <option>, e.target.value"
|
|
278
|
+
echo "REQUIRED: <EntityLookup apiEndpoint={...} mapOption={...} value={...} onChange={...} />"
|
|
221
279
|
exit 1
|
|
222
280
|
fi
|
|
223
281
|
fi
|
|
@@ -242,7 +300,7 @@ if [ -n "$CTRL_FILES" ]; then
|
|
|
242
300
|
fi
|
|
243
301
|
```
|
|
244
302
|
|
|
245
|
-
### POST-CHECK 13: No hardcoded Tailwind colors in generated pages
|
|
303
|
+
### POST-CHECK 13: No hardcoded Tailwind colors in generated pages (BLOCKING)
|
|
246
304
|
|
|
247
305
|
```bash
|
|
248
306
|
# Scan all page and component files directly (works for uncommitted/untracked files, Windows/WSL compatible)
|
|
@@ -250,9 +308,24 @@ ALL_PAGES=$(find src/pages/ src/components/ -name "*.tsx" 2>/dev/null | grep -v
|
|
|
250
308
|
if [ -n "$ALL_PAGES" ]; then
|
|
251
309
|
HARDCODED=$(grep -Pn '(bg|text|border)-(?!\[)(red|blue|green|gray|white|black|slate|zinc|neutral|stone)-' $ALL_PAGES 2>/dev/null)
|
|
252
310
|
if [ -n "$HARDCODED" ]; then
|
|
253
|
-
echo "
|
|
254
|
-
echo "
|
|
311
|
+
echo "BLOCKING: Pages MUST use CSS variables instead of hardcoded Tailwind colors"
|
|
312
|
+
echo "SmartStack uses a theme system — hardcoded colors break dark mode and custom themes"
|
|
313
|
+
echo ""
|
|
314
|
+
echo "Fix mapping:"
|
|
315
|
+
echo " bg-white → bg-[var(--bg-card)]"
|
|
316
|
+
echo " bg-gray-50 → bg-[var(--bg-primary)]"
|
|
317
|
+
echo " text-gray-900 → text-[var(--text-primary)]"
|
|
318
|
+
echo " text-gray-500 → text-[var(--text-secondary)]"
|
|
319
|
+
echo " border-gray-200 → border-[var(--border-color)]"
|
|
320
|
+
echo " bg-blue-600 → bg-[var(--color-accent-500)]"
|
|
321
|
+
echo " text-blue-600 → text-[var(--color-accent-500)]"
|
|
322
|
+
echo " text-red-500 → text-[var(--error-text)]"
|
|
323
|
+
echo " bg-green-500 → bg-[var(--success-bg)]"
|
|
324
|
+
echo ""
|
|
325
|
+
echo "See references/smartstack-frontend.md section 4 for full variable reference"
|
|
326
|
+
echo ""
|
|
255
327
|
echo "$HARDCODED"
|
|
328
|
+
exit 1
|
|
256
329
|
fi
|
|
257
330
|
fi
|
|
258
331
|
```
|
|
@@ -509,7 +582,35 @@ if [ -n "$SERVICE_FILES" ]; then
|
|
|
509
582
|
fi
|
|
510
583
|
```
|
|
511
584
|
|
|
512
|
-
### POST-CHECK 22:
|
|
585
|
+
### POST-CHECK 22: Cross-tenant entities must use Guid? TenantId
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
for entity in $(find src/ -path "*/Domain/*" -name "*.cs" ! -name "I*.cs" 2>/dev/null); do
|
|
589
|
+
if grep -q "IOptionalTenantEntity\|IScopedTenantEntity" "$entity"; then
|
|
590
|
+
if grep -q "public Guid TenantId" "$entity" && ! grep -q "public Guid? TenantId" "$entity"; then
|
|
591
|
+
echo "BLOCKING: Entity with IOptionalTenantEntity/IScopedTenantEntity must use Guid? TenantId (nullable)"
|
|
592
|
+
exit 1
|
|
593
|
+
fi
|
|
594
|
+
fi
|
|
595
|
+
done
|
|
596
|
+
echo "POST-CHECK 22: OK"
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### POST-CHECK 23: Scoped entities must have EntityScope property
|
|
600
|
+
|
|
601
|
+
```bash
|
|
602
|
+
for entity in $(find src/ -path "*/Domain/*" -name "*.cs" ! -name "I*.cs" 2>/dev/null); do
|
|
603
|
+
if grep -q "IScopedTenantEntity" "$entity"; then
|
|
604
|
+
if ! grep -q "EntityScope\|Scope" "$entity"; then
|
|
605
|
+
echo "BLOCKING: Entity with IScopedTenantEntity must have EntityScope Scope property"
|
|
606
|
+
exit 1
|
|
607
|
+
fi
|
|
608
|
+
fi
|
|
609
|
+
done
|
|
610
|
+
echo "POST-CHECK 23: OK"
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### POST-CHECK 24: Permissions.cs static constants must exist (BLOCKING)
|
|
513
614
|
|
|
514
615
|
```bash
|
|
515
616
|
# Every module with controllers MUST have a Permissions.cs with static constants
|
|
@@ -527,7 +628,7 @@ if [ -n "$CTRL_FILES" ]; then
|
|
|
527
628
|
fi
|
|
528
629
|
```
|
|
529
630
|
|
|
530
|
-
### POST-CHECK
|
|
631
|
+
### POST-CHECK 25: ApplicationRolesSeedData.cs must exist (BLOCKING)
|
|
531
632
|
|
|
532
633
|
```bash
|
|
533
634
|
# If any RolesSeedData exists, ApplicationRolesSeedData MUST also exist
|
|
@@ -544,7 +645,7 @@ if [ -n "$ROLE_SEED" ]; then
|
|
|
544
645
|
fi
|
|
545
646
|
```
|
|
546
647
|
|
|
547
|
-
### POST-CHECK
|
|
648
|
+
### POST-CHECK 25b: Section route completeness (NavigationSection → frontend route + permissions)
|
|
548
649
|
|
|
549
650
|
```bash
|
|
550
651
|
# Every NavigationSection seed data route MUST have a corresponding frontend route in App.tsx
|
|
@@ -593,7 +694,7 @@ if [ -n "$SECTION_SEED_FILES" ] && [ -n "$PERM_FILE" ]; then
|
|
|
593
694
|
fi
|
|
594
695
|
```
|
|
595
696
|
|
|
596
|
-
### POST-CHECK
|
|
697
|
+
### POST-CHECK 26: FORBIDDEN route patterns — /list and /detail/:id (BLOCKING)
|
|
597
698
|
|
|
598
699
|
```bash
|
|
599
700
|
# 1. Check seed data for FORBIDDEN suffixes
|
|
@@ -624,7 +725,7 @@ fi
|
|
|
624
725
|
echo "OK: No forbidden /list or /detail route patterns found"
|
|
625
726
|
```
|
|
626
727
|
|
|
627
|
-
### POST-CHECK
|
|
728
|
+
### POST-CHECK 27: Permission path segment count (WARNING)
|
|
628
729
|
|
|
629
730
|
```bash
|
|
630
731
|
PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "PermissionsSeedData.cs" 2>/dev/null)
|
|
@@ -646,7 +747,7 @@ if [ -n "$PERM_FILES" ]; then
|
|
|
646
747
|
fi
|
|
647
748
|
```
|
|
648
749
|
|
|
649
|
-
### POST-CHECK
|
|
750
|
+
### POST-CHECK 28: IClientSeedDataProvider must have 4 methods + DI registration (BLOCKING)
|
|
650
751
|
|
|
651
752
|
```bash
|
|
652
753
|
PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
|
|
@@ -676,4 +777,243 @@ if [ -n "$PROVIDER" ]; then
|
|
|
676
777
|
fi
|
|
677
778
|
```
|
|
678
779
|
|
|
780
|
+
### POST-CHECK 29: i18n must use separate JSON files per language (not embedded in index.ts)
|
|
781
|
+
|
|
782
|
+
```bash
|
|
783
|
+
# Translations MUST be in src/i18n/locales/{lang}/{module}.json, NOT embedded in a single .ts file
|
|
784
|
+
TSX_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
785
|
+
if [ -n "$TSX_FILES" ]; then
|
|
786
|
+
# Check if i18n locales directory exists
|
|
787
|
+
if [ ! -d "src/i18n/locales" ]; then
|
|
788
|
+
echo "BLOCKING: Missing src/i18n/locales/ directory"
|
|
789
|
+
echo "Translations must be in separate JSON files: src/i18n/locales/{fr,en,it,de}/{module}.json"
|
|
790
|
+
echo "NEVER embed translations in src/i18n/index.ts or a single TypeScript file"
|
|
791
|
+
exit 1
|
|
792
|
+
fi
|
|
793
|
+
|
|
794
|
+
# Check for embedded translations in index.ts (common anti-pattern)
|
|
795
|
+
I18N_INDEX=$(find src/i18n/ -maxdepth 1 -name "index.ts" -o -name "index.tsx" -o -name "config.ts" 2>/dev/null)
|
|
796
|
+
if [ -n "$I18N_INDEX" ]; then
|
|
797
|
+
EMBEDDED=$(grep -Pn '^\s*(resources|translations)\s*[:=]\s*\{' $I18N_INDEX 2>/dev/null)
|
|
798
|
+
if [ -n "$EMBEDDED" ]; then
|
|
799
|
+
echo "BLOCKING: Translations embedded in i18n config file — must be in separate JSON files"
|
|
800
|
+
echo "Found embedded translations in:"
|
|
801
|
+
echo "$EMBEDDED"
|
|
802
|
+
echo ""
|
|
803
|
+
echo "Fix: Move translations to src/i18n/locales/{fr,en,it,de}/{module}.json"
|
|
804
|
+
echo "The i18n config should import from locales/ directory, not contain inline translations"
|
|
805
|
+
exit 1
|
|
806
|
+
fi
|
|
807
|
+
fi
|
|
808
|
+
|
|
809
|
+
# Verify all 4 language directories exist
|
|
810
|
+
for LANG in fr en it de; do
|
|
811
|
+
if [ ! -d "src/i18n/locales/$LANG" ]; then
|
|
812
|
+
echo "BLOCKING: Missing language directory: src/i18n/locales/$LANG/"
|
|
813
|
+
echo "SmartStack requires 4 languages: fr, en, it, de"
|
|
814
|
+
exit 1
|
|
815
|
+
fi
|
|
816
|
+
done
|
|
817
|
+
|
|
818
|
+
# Verify at least one JSON file exists per language
|
|
819
|
+
for LANG in fr en it de; do
|
|
820
|
+
JSON_COUNT=$(find "src/i18n/locales/$LANG" -name "*.json" 2>/dev/null | wc -l)
|
|
821
|
+
if [ "$JSON_COUNT" -eq 0 ]; then
|
|
822
|
+
echo "BLOCKING: No translation JSON files in src/i18n/locales/$LANG/"
|
|
823
|
+
echo "Each module must have a {module}.json file per language"
|
|
824
|
+
exit 1
|
|
825
|
+
fi
|
|
826
|
+
done
|
|
827
|
+
fi
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
### POST-CHECK 30: Pages must use useTranslation hook (no hardcoded user-facing strings)
|
|
831
|
+
|
|
832
|
+
```bash
|
|
833
|
+
# Verify that page components use i18n — detect hardcoded strings in JSX
|
|
834
|
+
PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
835
|
+
if [ -n "$PAGE_FILES" ]; then
|
|
836
|
+
# Check that at least 80% of pages import useTranslation
|
|
837
|
+
TOTAL_PAGES=$(echo "$PAGE_FILES" | wc -l)
|
|
838
|
+
I18N_PAGES=$(grep -l "useTranslation" $PAGE_FILES 2>/dev/null | wc -l)
|
|
839
|
+
if [ "$TOTAL_PAGES" -gt 0 ] && [ "$I18N_PAGES" -eq 0 ]; then
|
|
840
|
+
echo "BLOCKING: No page files use useTranslation — all user-facing text must be translated"
|
|
841
|
+
echo "Found $TOTAL_PAGES page files but 0 use useTranslation"
|
|
842
|
+
echo ""
|
|
843
|
+
echo "Fix: Import and use useTranslation in every page component:"
|
|
844
|
+
echo " const { t } = useTranslation(['{module}']);"
|
|
845
|
+
echo " t('{module}:title', 'Fallback text')"
|
|
846
|
+
exit 1
|
|
847
|
+
fi
|
|
848
|
+
|
|
849
|
+
# Check for common hardcoded English strings in JSX (heuristic)
|
|
850
|
+
HARDCODED_TEXT=$(grep -Pn '>\s*(Create|Edit|Delete|Save|Cancel|Search|Loading|Error|No data|Not found|Submit|Back|Actions|Name|Status|Description)\s*<' $PAGE_FILES 2>/dev/null | grep -v '{t(' | head -10)
|
|
851
|
+
if [ -n "$HARDCODED_TEXT" ]; then
|
|
852
|
+
echo "WARNING: Possible hardcoded user-facing strings detected in JSX"
|
|
853
|
+
echo "All user-facing text MUST use t('namespace:key', 'Fallback')"
|
|
854
|
+
echo "$HARDCODED_TEXT"
|
|
855
|
+
fi
|
|
856
|
+
fi
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
### POST-CHECK 31: List/Detail pages must include DocToggleButton (documentation panel)
|
|
860
|
+
|
|
861
|
+
```bash
|
|
862
|
+
# Every list and detail page MUST have DocToggleButton for inline documentation access
|
|
863
|
+
LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
864
|
+
if [ -n "$LIST_PAGES" ]; then
|
|
865
|
+
MISSING_DOC=0
|
|
866
|
+
for PAGE in $LIST_PAGES; do
|
|
867
|
+
if ! grep -q "DocToggleButton" "$PAGE" 2>/dev/null; then
|
|
868
|
+
echo "WARNING: Page missing DocToggleButton: $PAGE"
|
|
869
|
+
echo " Import: import { DocToggleButton } from '@/components/docs/DocToggleButton';"
|
|
870
|
+
echo " Place in header actions: <DocToggleButton />"
|
|
871
|
+
MISSING_DOC=$((MISSING_DOC + 1))
|
|
872
|
+
fi
|
|
873
|
+
done
|
|
874
|
+
if [ "$MISSING_DOC" -gt 0 ]; then
|
|
875
|
+
echo ""
|
|
876
|
+
echo "WARNING: $MISSING_DOC pages missing DocToggleButton — users cannot access inline documentation"
|
|
877
|
+
echo "See smartstack-frontend.md section 7 for placement pattern"
|
|
878
|
+
fi
|
|
879
|
+
fi
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
### POST-CHECK 32: Module documentation must be generated (doc-data.ts)
|
|
883
|
+
|
|
884
|
+
```bash
|
|
885
|
+
# After frontend pages exist, /documentation should have been called
|
|
886
|
+
TSX_PAGES=$(find src/pages/ -name "*.tsx" -not -name "*.test.*" 2>/dev/null | grep -v node_modules | grep -v "docs/")
|
|
887
|
+
DOC_DATA=$(find src/pages/docs/ -name "doc-data.ts" 2>/dev/null)
|
|
888
|
+
if [ -n "$TSX_PAGES" ] && [ -z "$DOC_DATA" ]; then
|
|
889
|
+
echo "WARNING: Frontend pages exist but no documentation generated"
|
|
890
|
+
echo "Fix: Invoke /documentation {module} --type user to generate doc-data.ts"
|
|
891
|
+
echo "The DocToggleButton in page headers will link to this documentation"
|
|
892
|
+
fi
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
### POST-CHECK 33: Pagination type must be PaginatedResult<T> — no aliases (BLOCKING)
|
|
896
|
+
|
|
897
|
+
```bash
|
|
898
|
+
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
899
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
900
|
+
ALL_FILES="$SERVICE_FILES $CTRL_FILES"
|
|
901
|
+
if [ -n "$(echo $ALL_FILES | tr -d ' ')" ]; then
|
|
902
|
+
BAD_NAMES=$(grep -Pn 'PagedResult<|PaginatedResultDto<|PaginatedResponse<|PageResultDto<' $ALL_FILES 2>/dev/null)
|
|
903
|
+
if [ -n "$BAD_NAMES" ]; then
|
|
904
|
+
echo "BLOCKING: Pagination type must be PaginatedResult<T> — found non-canonical names"
|
|
905
|
+
echo "$BAD_NAMES"
|
|
906
|
+
echo "FORBIDDEN type names: PagedResult, PaginatedResultDto, PaginatedResponse, PageResultDto"
|
|
907
|
+
echo "Fix: Use PaginatedResult<T> from SmartStack.Application.Common.Models everywhere"
|
|
908
|
+
exit 1
|
|
909
|
+
fi
|
|
910
|
+
fi
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
### POST-CHECK 34: Code generation — ICodeGenerator must be registered for auto-generated entities (BLOCKING)
|
|
914
|
+
|
|
915
|
+
```bash
|
|
916
|
+
# If feature.json has entities with codePattern.strategy != "manual",
|
|
917
|
+
# verify that ICodeGenerator<Entity> is registered in DI
|
|
918
|
+
FEATURE_FILES=$(find docs/ -name "feature.json" 2>/dev/null)
|
|
919
|
+
DI_FILE=$(find src/ -name "DependencyInjection.cs" -path "*/Infrastructure/*" 2>/dev/null | head -1)
|
|
920
|
+
if [ -n "$FEATURE_FILES" ] && [ -n "$DI_FILE" ]; then
|
|
921
|
+
for FEATURE in $FEATURE_FILES; do
|
|
922
|
+
ENTITIES_WITH_CODE=$(python3 -c "
|
|
923
|
+
import json, sys
|
|
924
|
+
try:
|
|
925
|
+
with open('$FEATURE') as f:
|
|
926
|
+
data = json.load(f)
|
|
927
|
+
for e in data.get('analysis', {}).get('entities', []):
|
|
928
|
+
cp = e.get('codePattern', {})
|
|
929
|
+
if cp.get('strategy', 'manual') != 'manual':
|
|
930
|
+
print(e['name'])
|
|
931
|
+
except: pass
|
|
932
|
+
" 2>/dev/null)
|
|
933
|
+
for ENTITY in $ENTITIES_WITH_CODE; do
|
|
934
|
+
if ! grep -q "ICodeGenerator<$ENTITY>" "$DI_FILE" 2>/dev/null; then
|
|
935
|
+
echo "BLOCKING: Entity $ENTITY has auto-generated code pattern but ICodeGenerator<$ENTITY> is not registered in DI"
|
|
936
|
+
echo "Fix: Add CodeGenerator<$ENTITY> registration in DependencyInjection.cs — see references/code-generation.md"
|
|
937
|
+
exit 1
|
|
938
|
+
fi
|
|
939
|
+
done
|
|
940
|
+
done
|
|
941
|
+
fi
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
### POST-CHECK 35: Code regex must support hyphens (BLOCKING)
|
|
945
|
+
|
|
946
|
+
```bash
|
|
947
|
+
VALIDATOR_FILES=$(find src/ -path "*/Validators/*" -name "*Validator.cs" 2>/dev/null)
|
|
948
|
+
if [ -n "$VALIDATOR_FILES" ]; then
|
|
949
|
+
OLD_REGEX=$(grep -rn '\^\\[a-z0-9_\\]+\$' $VALIDATOR_FILES 2>/dev/null | grep -v '\-')
|
|
950
|
+
if [ -n "$OLD_REGEX" ]; then
|
|
951
|
+
echo "BLOCKING: Code validator uses old regex without hyphen support"
|
|
952
|
+
echo "$OLD_REGEX"
|
|
953
|
+
echo "Fix: Update regex to ^[a-z0-9_-]+$ to support auto-generated codes with hyphens"
|
|
954
|
+
exit 1
|
|
955
|
+
fi
|
|
956
|
+
fi
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
### POST-CHECK 36: CreateDto must NOT have Code field when service uses ICodeGenerator (WARNING)
|
|
960
|
+
|
|
961
|
+
```bash
|
|
962
|
+
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
963
|
+
if [ -n "$SERVICE_FILES" ]; then
|
|
964
|
+
for f in $SERVICE_FILES; do
|
|
965
|
+
if grep -q "ICodeGenerator" "$f"; then
|
|
966
|
+
ENTITY=$(basename "$f" | sed 's/Service\.cs$//')
|
|
967
|
+
DTO_FILE=$(find src/ -path "*/DTOs/*" -name "Create${ENTITY}Dto.cs" 2>/dev/null | head -1)
|
|
968
|
+
if [ -n "$DTO_FILE" ] && grep -q "public string Code" "$DTO_FILE"; then
|
|
969
|
+
echo "WARNING: Create${ENTITY}Dto has Code field but service uses ICodeGenerator (code is auto-generated)"
|
|
970
|
+
echo "Fix: Remove Code from Create${ENTITY}Dto — it is auto-generated by ICodeGenerator<${ENTITY}>"
|
|
971
|
+
fi
|
|
972
|
+
fi
|
|
973
|
+
done
|
|
974
|
+
fi
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
### POST-CHECK 37: Translation seed data must have idempotency guard (BLOCKING)
|
|
978
|
+
|
|
979
|
+
```bash
|
|
980
|
+
PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
|
|
981
|
+
if [ -n "$PROVIDER" ]; then
|
|
982
|
+
# Check if NavigationTranslations.Add is used WITHOUT a preceding AnyAsync guard
|
|
983
|
+
# Pattern: any .Add(NavigationTranslation.Create(...)) that is NOT inside an AnyAsync check
|
|
984
|
+
TRANSLATION_ADDS=$(grep -c "NavigationTranslations.Add" "$PROVIDER" 2>/dev/null)
|
|
985
|
+
TRANSLATION_GUARDS=$(grep -c "NavigationTranslations.AnyAsync" "$PROVIDER" 2>/dev/null)
|
|
986
|
+
|
|
987
|
+
if [ "$TRANSLATION_ADDS" -gt 0 ] && [ "$TRANSLATION_GUARDS" -eq 0 ]; then
|
|
988
|
+
echo "BLOCKING: Translation seed data inserts without idempotency guard in $PROVIDER"
|
|
989
|
+
echo "Fix: Before each NavigationTranslations.Add block, check existence:"
|
|
990
|
+
echo " if (!await context.NavigationTranslations.AnyAsync("
|
|
991
|
+
echo " t => t.EntityId == {Module}NavigationSeedData.{Module}ModuleId"
|
|
992
|
+
echo " && t.EntityType == NavigationEntityType.Module, ct))"
|
|
993
|
+
echo " { foreach (var t in ...) { context.NavigationTranslations.Add(...); } }"
|
|
994
|
+
echo "The unique index IX_nav_Translations_EntityType_EntityId_LanguageCode will crash on duplicates."
|
|
995
|
+
exit 1
|
|
996
|
+
fi
|
|
997
|
+
fi
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
### POST-CHECK 38: Resource seed data must use actual section IDs from DB (BLOCKING)
|
|
1001
|
+
|
|
1002
|
+
```bash
|
|
1003
|
+
PROVIDER=$(find src/ -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
|
|
1004
|
+
if [ -n "$PROVIDER" ]; then
|
|
1005
|
+
# Check if NavigationResource.Create uses secEntry.Id or resEntry.SectionId (deterministic GUIDs)
|
|
1006
|
+
# instead of actualSection.Id (real DB ID). This causes FK_nav_Resources_nav_Sections_SectionId violation.
|
|
1007
|
+
if grep -Pn 'NavigationResource\.Create\(' "$PROVIDER" | grep -q 'resEntry\.SectionId\|secEntry\.Id'; then
|
|
1008
|
+
echo "BLOCKING: Resource seed data uses deterministic GUID as SectionId in $PROVIDER"
|
|
1009
|
+
echo "NavigationSection.Create() generates its own ID — deterministic seed GUIDs do NOT exist in nav_Sections."
|
|
1010
|
+
echo "Fix: Query actual section from DB before creating resources:"
|
|
1011
|
+
echo " var actualSection = await context.NavigationSections"
|
|
1012
|
+
echo " .FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == modEntity.Id, ct);"
|
|
1013
|
+
echo " NavigationResource.Create(actualSection.Id, ...) // NOT secEntry.Id or resEntry.SectionId"
|
|
1014
|
+
exit 1
|
|
1015
|
+
fi
|
|
1016
|
+
fi
|
|
1017
|
+
```
|
|
1018
|
+
|
|
679
1019
|
**If ANY POST-CHECK fails → fix in step-03, re-validate.**
|