@atlashub/smartstack-cli 4.31.0 → 4.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.documentation/commands.html +952 -116
  2. package/.documentation/index.html +2 -2
  3. package/.documentation/init.html +358 -174
  4. package/dist/mcp-entry.mjs +271 -44
  5. package/dist/mcp-entry.mjs.map +1 -1
  6. package/package.json +1 -1
  7. package/templates/mcp-scaffolding/controller.cs.hbs +54 -128
  8. package/templates/project/README.md +19 -0
  9. package/templates/skills/apex/SKILL.md +16 -10
  10. package/templates/skills/apex/_shared.md +1 -1
  11. package/templates/skills/apex/references/checks/architecture-checks.sh +154 -0
  12. package/templates/skills/apex/references/checks/backend-checks.sh +194 -0
  13. package/templates/skills/apex/references/checks/frontend-checks.sh +448 -0
  14. package/templates/skills/apex/references/checks/infrastructure-checks.sh +255 -0
  15. package/templates/skills/apex/references/checks/security-checks.sh +153 -0
  16. package/templates/skills/apex/references/checks/seed-checks.sh +536 -0
  17. package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +49 -192
  18. package/templates/skills/apex/references/parallel-execution.md +18 -5
  19. package/templates/skills/apex/references/post-checks.md +124 -2156
  20. package/templates/skills/apex/references/smartstack-api.md +160 -957
  21. package/templates/skills/apex/references/smartstack-frontend-compliance.md +23 -1
  22. package/templates/skills/apex/references/smartstack-frontend.md +134 -1022
  23. package/templates/skills/apex/references/smartstack-layers.md +12 -6
  24. package/templates/skills/apex/steps/step-00-init.md +81 -238
  25. package/templates/skills/apex/steps/step-03-execute.md +25 -751
  26. package/templates/skills/apex/steps/step-03a-layer0-domain.md +118 -0
  27. package/templates/skills/apex/steps/step-03b-layer1-seed.md +91 -0
  28. package/templates/skills/apex/steps/step-03c-layer2-backend.md +240 -0
  29. package/templates/skills/apex/steps/step-03d-layer3-frontend.md +300 -0
  30. package/templates/skills/apex/steps/step-03e-layer4-devdata.md +44 -0
  31. package/templates/skills/apex/steps/step-04-examine.md +70 -150
  32. package/templates/skills/application/references/frontend-i18n-and-output.md +2 -2
  33. package/templates/skills/application/references/frontend-route-naming.md +5 -1
  34. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +49 -198
  35. package/templates/skills/application/references/frontend-verification.md +11 -11
  36. package/templates/skills/application/steps/step-05-frontend.md +26 -15
  37. package/templates/skills/application/templates-frontend.md +4 -0
  38. package/templates/skills/cli-app-sync/SKILL.md +2 -2
  39. package/templates/skills/cli-app-sync/references/comparison-map.md +1 -1
  40. package/templates/skills/controller/references/controller-code-templates.md +70 -67
  41. package/templates/skills/controller/references/mcp-scaffold-workflow.md +5 -1
@@ -0,0 +1,194 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # POST-CHECK: Backend — Entity, Service & Controller Checks
5
+ # V1-V2, C8, C12, C14, C28-C31, C54: Entity validation, API contracts, pagination, code generation
6
+
7
+ FAIL=false
8
+
9
+ # POST-CHECK V1: Controllers with POST/PUT must have corresponding Validators (BLOCKING)
10
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
11
+ if [ -n "$CTRL_FILES" ]; then
12
+ for f in $CTRL_FILES; do
13
+ HAS_WRITE=$(grep -cE "\[Http(Post|Put)\]" "$f")
14
+ if [ "$HAS_WRITE" -gt 0 ]; then
15
+ DTOS=$(grep -oP '(?:Create|Update)\w+Dto' "$f" | sort -u)
16
+ for DTO in $DTOS; do
17
+ VALIDATOR_NAME=$(echo "$DTO" | sed 's/Dto$/Validator/')
18
+ VALIDATOR_FILE=$(find src/ -path "*/Validators/*" -name "${VALIDATOR_NAME}.cs" 2>/dev/null)
19
+ if [ -z "$VALIDATOR_FILE" ]; then
20
+ echo "BLOCKING: Controller $f uses $DTO but no ${VALIDATOR_NAME}.cs found"
21
+ echo "Fix: Create Validator with FluentValidation rules from business rules"
22
+ FAIL=true
23
+ fi
24
+ done
25
+ fi
26
+ done
27
+ fi
28
+
29
+ # POST-CHECK V2: Validators must be registered in DI (WARNING)
30
+ VALIDATOR_FILES=$(find src/ -path "*/Validators/*" -name "*Validator.cs" 2>/dev/null)
31
+ if [ -n "$VALIDATOR_FILES" ]; then
32
+ DI_FILE=$(find src/ -name "DependencyInjection.cs" -o -name "ServiceCollectionExtensions.cs" 2>/dev/null | head -1)
33
+ if [ -n "$DI_FILE" ]; then
34
+ for f in $VALIDATOR_FILES; do
35
+ VALIDATOR_NAME=$(basename "$f" .cs)
36
+ if ! grep -q "$VALIDATOR_NAME" "$DI_FILE"; then
37
+ echo "WARNING: Validator $VALIDATOR_NAME not registered in DI: $DI_FILE"
38
+ fi
39
+ done
40
+ fi
41
+ fi
42
+
43
+ # POST-CHECK C8: Backend APIs must support search parameter for EntityLookup (BLOCKING)
44
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
45
+ if [ -n "$CTRL_FILES" ]; then
46
+ for f in $CTRL_FILES; do
47
+ if grep -q "\[HttpGet\]" "$f" && grep -q "GetAll" "$f"; then
48
+ if ! grep -q "search" "$f"; then
49
+ echo "BLOCKING: Controller missing search parameter on GetAll: $f"
50
+ echo "GetAll endpoints must accept ?search= to enable EntityLookup on frontend"
51
+ echo "Fix: Add [FromQuery] string? search parameter to GetAll action"
52
+ FAIL=true
53
+ fi
54
+ fi
55
+ done
56
+ fi
57
+
58
+ WEB_DIR=$(find . -name "vitest.config.ts" -o -name "vite.config.ts" 2>/dev/null | head -1 | xargs dirname 2>/dev/null)
59
+ if [ -n "$WEB_DIR" ]; then
60
+ LOOKUP_FILES=$(grep -rl "EntityLookup" "$WEB_DIR/src/pages/" "$WEB_DIR/src/components/" 2>/dev/null | grep -v node_modules | grep -v "\.test\." || true)
61
+ if [ -n "$LOOKUP_FILES" ]; then
62
+ for f in $LOOKUP_FILES; do
63
+ ENDPOINTS=$(grep -oP "apiEndpoint=['\"]([^'\"]+)['\"]" "$f" 2>/dev/null | grep -oP "['\"]([^'\"]+)['\"]" | tr -d "'" | tr -d '"' || true)
64
+ for ep in $ENDPOINTS; do
65
+ ENTITY=$(echo "$ep" | sed 's|.*/||' | sed 's/.*/\u&/')
66
+ CTRL=$(find src/ -path "*/Controllers/*${ENTITY}*Controller.cs" 2>/dev/null | head -1)
67
+ if [ -n "$CTRL" ] && ! grep -q "search" "$CTRL"; then
68
+ echo "BLOCKING: EntityLookup in $f calls $ep, but controller $CTRL does not support ?search="
69
+ echo "Fix: Add [FromQuery] string? search parameter to GetAll in $CTRL"
70
+ FAIL=true
71
+ fi
72
+ done
73
+ done
74
+ fi
75
+ fi
76
+
77
+ # POST-CHECK C12: GetAll methods must return PaginatedResult<T>
78
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
79
+ if [ -n "$SERVICE_FILES" ]; then
80
+ BAD_RETURNS=$(grep -Pn '(Task<\s*(?:List|IEnumerable|IList|ICollection|IReadOnlyList|IReadOnlyCollection)<).*GetAll' $SERVICE_FILES 2>/dev/null)
81
+ if [ -n "$BAD_RETURNS" ]; then
82
+ echo "WARNING: GetAll methods must return PaginatedResult<T>, not List/IEnumerable"
83
+ echo "$BAD_RETURNS"
84
+ echo "Fix: Change return type to Task<PaginatedResult<{Entity}ResponseDto>>"
85
+ fi
86
+ fi
87
+
88
+ # POST-CHECK C14: Entities must implement IAuditableEntity + Validators must have Create/Update pairs
89
+ ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null)
90
+ if [ -n "$ENTITY_FILES" ]; then
91
+ for f in $ENTITY_FILES; do
92
+ if grep -q "ITenantEntity" "$f" && ! grep -q "IAuditableEntity" "$f"; then
93
+ echo "WARNING: Entity implements ITenantEntity but NOT IAuditableEntity: $f"
94
+ echo "Pattern: public class Entity : BaseEntity, ITenantEntity, IAuditableEntity"
95
+ fi
96
+ done
97
+ fi
98
+
99
+ CREATE_VALIDATORS=$(find src/ -path "*/Validators/*" -name "Create*Validator.cs" 2>/dev/null)
100
+ if [ -n "$CREATE_VALIDATORS" ]; then
101
+ for f in $CREATE_VALIDATORS; do
102
+ VALIDATOR_DIR=$(dirname "$f")
103
+ ENTITY_NAME=$(basename "$f" | sed 's/^Create\(.*\)Validator\.cs$/\1/')
104
+ if [ ! -f "$VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs" ]; then
105
+ echo "WARNING: Create${ENTITY_NAME}Validator exists but Update${ENTITY_NAME}Validator is missing"
106
+ echo " Found: $f"
107
+ echo " Expected: $VALIDATOR_DIR/Update${ENTITY_NAME}Validator.cs"
108
+ fi
109
+ done
110
+ fi
111
+
112
+ # POST-CHECK C28: Pagination type must be PaginatedResult<T> — no aliases (WARNING)
113
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
114
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
115
+ ALL_FILES="$SERVICE_FILES $CTRL_FILES"
116
+ if [ -n "$(echo $ALL_FILES | tr -d ' ')" ]; then
117
+ BAD_NAMES=$(grep -Pn 'PagedResult<|PaginatedResultDto<|PaginatedResponse<|PageResultDto<' $ALL_FILES 2>/dev/null || true)
118
+ if [ -n "$BAD_NAMES" ]; then
119
+ echo "WARNING: Pagination type must be PaginatedResult<T> — found non-canonical names"
120
+ echo "$BAD_NAMES"
121
+ echo "FORBIDDEN type names: PagedResult, PaginatedResultDto, PaginatedResponse, PageResultDto"
122
+ echo "Fix: Use PaginatedResult<T> from SmartStack.Application.Common.Models everywhere"
123
+ fi
124
+ fi
125
+
126
+ # POST-CHECK C29: Code generation — ICodeGenerator must be registered for auto-generated entities (BLOCKING)
127
+ FEATURE_FILES=$(find docs/ -name "feature.json" 2>/dev/null)
128
+ DI_FILE=$(find src/ -name "DependencyInjection.cs" -path "*/Infrastructure/*" 2>/dev/null | head -1)
129
+ if [ -n "$FEATURE_FILES" ] && [ -n "$DI_FILE" ]; then
130
+ for FEATURE in $FEATURE_FILES; do
131
+ ENTITIES_WITH_CODE=$(python3 -c "
132
+ import json, sys
133
+ try:
134
+ with open('$FEATURE') as f:
135
+ data = json.load(f)
136
+ for e in data.get('analysis', {}).get('entities', []):
137
+ cp = e.get('codePattern', {})
138
+ if cp.get('strategy', 'manual') != 'manual':
139
+ print(e['name'])
140
+ except: pass
141
+ " 2>/dev/null || true)
142
+ for ENTITY in $ENTITIES_WITH_CODE; do
143
+ if ! grep -q "ICodeGenerator<$ENTITY>" "$DI_FILE" 2>/dev/null; then
144
+ echo "BLOCKING: Entity $ENTITY has auto-generated code pattern but ICodeGenerator<$ENTITY> is not registered in DI"
145
+ echo "Fix: Add CodeGenerator<$ENTITY> registration in DependencyInjection.cs — see references/code-generation.md"
146
+ FAIL=true
147
+ fi
148
+ done
149
+ done
150
+ fi
151
+
152
+ # POST-CHECK C30: Code regex must support hyphens (BLOCKING)
153
+ VALIDATOR_FILES=$(find src/ -path "*/Validators/*" -name "*Validator.cs" 2>/dev/null)
154
+ if [ -n "$VALIDATOR_FILES" ]; then
155
+ OLD_REGEX=$(grep -rn '\^\\[a-z0-9_\\]+\$' $VALIDATOR_FILES 2>/dev/null | grep -v '\-' || true)
156
+ if [ -n "$OLD_REGEX" ]; then
157
+ echo "BLOCKING: Code validator uses old regex without hyphen support"
158
+ echo "$OLD_REGEX"
159
+ echo "Fix: Update regex to ^[a-z0-9_-]+$ to support auto-generated codes with hyphens"
160
+ FAIL=true
161
+ fi
162
+ fi
163
+
164
+ # POST-CHECK C31: CreateDto must NOT have Code field when service uses ICodeGenerator (WARNING)
165
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
166
+ if [ -n "$SERVICE_FILES" ]; then
167
+ for f in $SERVICE_FILES; do
168
+ if grep -q "ICodeGenerator" "$f"; then
169
+ ENTITY=$(basename "$f" | sed 's/Service\.cs$//')
170
+ DTO_FILE=$(find src/ -path "*/DTOs/*" -name "Create${ENTITY}Dto.cs" 2>/dev/null | head -1)
171
+ if [ -n "$DTO_FILE" ] && grep -q "public string Code" "$DTO_FILE"; then
172
+ echo "WARNING: Create${ENTITY}Dto has Code field but service uses ICodeGenerator (code is auto-generated)"
173
+ echo "Fix: Remove Code from Create${ENTITY}Dto — it is auto-generated by ICodeGenerator<${ENTITY}>"
174
+ fi
175
+ fi
176
+ done
177
+ fi
178
+
179
+ # POST-CHECK C54: No helper method calls inside .Select() on IQueryable (BLOCKING)
180
+ SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
181
+ if [ -n "$SERVICE_FILES" ]; then
182
+ BAD_SELECT=$(grep -Pn '\.Select\(\s*\w+\s*=>\s*(?!new\s)[A-Z]\w+\(|\.Select\(\s*(?!x\s*=>)[A-Z]\w+\s*\)' $SERVICE_FILES 2>/dev/null || true)
183
+ if [ -n "$BAD_SELECT" ]; then
184
+ echo "BLOCKING: Helper method call inside .Select() on IQueryable — EF Core cannot translate to SQL"
185
+ echo "$BAD_SELECT"
186
+ echo "Fix: Use inline DTO construction: .Select(x => new ResponseDto(x.Id, x.Name, x.FK.Code))"
187
+ echo " Helper methods (MapToDto, ToDto) are only safe AFTER materialization (ToListAsync, FirstAsync)"
188
+ FAIL=true
189
+ fi
190
+ fi
191
+
192
+ if [ "$FAIL" = true ]; then
193
+ exit 1
194
+ fi
@@ -0,0 +1,448 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # POST-CHECK: Frontend — CSS, Forms, Components, I18n
5
+ # C3a, C3-C7, C9, C11, C24-C27, C36-C37, C49, C52: Page structure, forms, i18n, routing
6
+
7
+ FAIL=false
8
+
9
+ # POST-CHECK C3a: Frontend must not be empty if Layer 3 was planned (BLOCKING)
10
+ APP_TSX=$(find web/ src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
11
+ if [ -n "$APP_TSX" ]; then
12
+ # Check for DynamicRouter pattern (v3.7+)
13
+ COMPONENT_REGISTRY=$(find web/ src/ -name "componentRegistry.generated.ts" -not -path "*/node_modules/*" 2>/dev/null | head -1)
14
+ if [ -n "$COMPONENT_REGISTRY" ]; then
15
+ echo "OK: componentRegistry.generated.ts found (DynamicRouter pattern)"
16
+ else
17
+ EMPTY_ROUTES=$(grep -P "applicationRoutes.*=\s*\{[\s/]*\}" "$APP_TSX" 2>/dev/null || true)
18
+ if [ -n "$EMPTY_ROUTES" ]; then
19
+ echo "BLOCKING: applicationRoutes in App.tsx is empty — Layer 3 frontend was NOT executed"
20
+ echo "Expected: at least one application key with route definitions"
21
+ echo "Fix: Run Layer 3 (scaffold_routes + scaffold_extension)"
22
+ FAIL=true
23
+ fi
24
+ fi
25
+
26
+ PAGE_COUNT=$(find web/ src/ -path "*/pages/*" -name "*.tsx" -not -path "*/node_modules/*" 2>/dev/null | wc -l)
27
+ if [ "$PAGE_COUNT" -eq 0 ]; then
28
+ echo "BLOCKING: No page components found in pages/ directory"
29
+ echo "Fix: Generate pages via scaffold_extension or /ui-components"
30
+ FAIL=true
31
+ fi
32
+
33
+ NAV_ROUTES=$(find web/ src/ -name "navRoutes.generated.ts" -not -path "*/node_modules/*" 2>/dev/null | head -1)
34
+ if [ -z "$NAV_ROUTES" ]; then
35
+ echo "BLOCKING: navRoutes.generated.ts not found — scaffold_routes was never called"
36
+ echo "Fix: Run scaffold_routes(source: 'controllers', outputFormat: 'componentRegistry')"
37
+ FAIL=true
38
+ fi
39
+ fi
40
+
41
+ # POST-CHECK C3: Translation files must exist for all 4 languages (if frontend)
42
+ TSX_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null || true)
43
+ if [ -n "$TSX_FILES" ]; then
44
+ NAMESPACES=$(grep -ohP "useTranslation\(\[?'([^']+)" $TSX_FILES 2>/dev/null | sed "s/.*'//" | sort -u || true)
45
+ for NS in $NAMESPACES; do
46
+ for LANG in fr en it de; do
47
+ if [ ! -f "src/i18n/locales/$LANG/$NS.json" ]; then
48
+ echo "WARNING: Missing translation file: src/i18n/locales/$LANG/$NS.json"
49
+ fi
50
+ done
51
+ done
52
+ fi
53
+
54
+ # POST-CHECK C4: Forms must be full pages with routes — ZERO modals/popups/drawers/slide-overs
55
+ PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null || true)
56
+ if [ -n "$PAGE_FILES" ]; then
57
+ MODAL_IMPORTS=$(grep -Pn "import.*(?:Modal|Dialog|Drawer|Popup|Sheet|SlideOver|Overlay)" $PAGE_FILES 2>/dev/null || true)
58
+ if [ -n "$MODAL_IMPORTS" ]; then
59
+ echo "WARNING: Form pages must NOT use Modal/Dialog/Drawer/Popup/SlideOver components"
60
+ echo "$MODAL_IMPORTS"
61
+ FAIL=true
62
+ fi
63
+
64
+ FORM_STATE=$(grep -Pn "useState.*(?:isOpen|showModal|showDialog|showCreate|showEdit|showForm|isCreating|isEditing|showDrawer|showPanel|showSlideOver|selectedEntity|editingEntity)" $PAGE_FILES 2>/dev/null || true)
65
+ if [ -n "$FORM_STATE" ]; then
66
+ echo "WARNING: Inline form state detected — forms embedded in ListPage as drawers/panels"
67
+ echo "Create/Edit forms MUST be separate page components with their own URL routes"
68
+ echo "$FORM_STATE"
69
+ FAIL=true
70
+ fi
71
+
72
+ if [ "$FAIL" = true ]; then
73
+ echo ""
74
+ echo "Fix: Create EntityCreatePage.tsx with route /create and EntityEditPage.tsx with route /:id/edit"
75
+ echo "NEVER embed create/edit forms as inline drawers, panels, or slide-overs in ListPage"
76
+ fi
77
+ fi
78
+
79
+ # POST-CHECK C5: Create/Edit pages must exist as separate route pages (CRITICAL)
80
+ LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test || true)
81
+ if [ -n "$LIST_PAGES" ]; then
82
+ FAIL_C5=false
83
+ for LIST_PAGE in $LIST_PAGES; do
84
+ PAGE_DIR=$(dirname "$LIST_PAGE")
85
+ MODULE_NAME=$(basename "$PAGE_DIR")
86
+
87
+ HAS_CREATE_NAV=$(grep -P "navigate\(.*['/]create" "$LIST_PAGE" 2>/dev/null || true)
88
+ HAS_EDIT_NAV=$(grep -P "navigate\(.*['/]edit|navigate\(.*/:id/edit" "$LIST_PAGE" 2>/dev/null || true)
89
+
90
+ CREATE_PAGE=$(find "$PAGE_DIR" -name "*CreatePage.tsx" 2>/dev/null || true)
91
+ if [ -z "$CREATE_PAGE" ]; then
92
+ if [ -n "$HAS_CREATE_NAV" ]; then
93
+ echo "CRITICAL: Module $MODULE_NAME ListPage navigates to /create but CreatePage does NOT exist"
94
+ echo " Dead link: $HAS_CREATE_NAV"
95
+ echo " Fix: Create ${MODULE_NAME}CreatePage.tsx in $PAGE_DIR"
96
+ FAIL_C5=true
97
+ fi
98
+ fi
99
+
100
+ EDIT_PAGE=$(find "$PAGE_DIR" -name "*EditPage.tsx" 2>/dev/null || true)
101
+ if [ -z "$EDIT_PAGE" ]; then
102
+ if [ -n "$HAS_EDIT_NAV" ]; then
103
+ echo "CRITICAL: Module $MODULE_NAME ListPage navigates to /:id/edit but EditPage does NOT exist"
104
+ echo " Dead link: $HAS_EDIT_NAV"
105
+ echo " Fix: Create ${MODULE_NAME}EditPage.tsx in $PAGE_DIR"
106
+ FAIL_C5=true
107
+ fi
108
+ fi
109
+ done
110
+
111
+ if [ "$FAIL_C5" = true ]; then
112
+ echo ""
113
+ echo "CRITICAL: Create/Edit pages are MISSING but ListPage buttons link to them."
114
+ echo "Users will see white screen / 404 when clicking Create or Edit buttons."
115
+ echo "Fix: Generate form pages using /ui-components skill patterns (smartstack-frontend.md section 3b)"
116
+ FAIL=true
117
+ fi
118
+ fi
119
+
120
+ # POST-CHECK C6: Form pages must have companion test files
121
+ PAGE_FILES=$(find src/pages/ -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null || true)
122
+ if [ -n "$PAGE_FILES" ]; then
123
+ ALL_TESTS=$(find src/pages/ -name "*.test.tsx" 2>/dev/null || true)
124
+ if [ -z "$ALL_TESTS" ]; then
125
+ echo "WARNING: No frontend test files found in src/pages/"
126
+ echo "Every form page MUST have a companion .test.tsx file"
127
+ fi
128
+ fi
129
+
130
+ FORM_PAGES=$(find src/pages/ -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test || true)
131
+ if [ -n "$FORM_PAGES" ]; then
132
+ for FORM_PAGE in $FORM_PAGES; do
133
+ TEST_FILE="${FORM_PAGE%.tsx}.test.tsx"
134
+ if [ ! -f "$TEST_FILE" ]; then
135
+ echo "WARNING: Form page missing test file: $FORM_PAGE"
136
+ echo "Expected: $TEST_FILE"
137
+ echo "All form pages MUST have companion test files (rendering, validation, submit, navigation)"
138
+ fi
139
+ done
140
+ fi
141
+
142
+ # POST-CHECK C7: FK fields must use EntityLookup — NO <input>, NO <select> (WARNING)
143
+ ALL_PAGES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\." || true)
144
+ if [ -n "$ALL_PAGES" ]; then
145
+ FAIL_C7=false
146
+
147
+ FK_INPUTS=$(grep -Pn '<input[^>]*(?:name|value)=["\x27{][^>]*[a-zA-Z]Id["\x27}]' $ALL_PAGES 2>/dev/null | grep -Pv 'type=["\x27]hidden["\x27]' || true)
148
+ if [ -n "$FK_INPUTS" ]; then
149
+ echo "WARNING: FK fields rendered as <input> — MUST use EntityLookup component"
150
+ echo "$FK_INPUTS"
151
+ FAIL_C7=true
152
+ fi
153
+
154
+ FK_SELECTS=$(grep -Pn '<select[^>]*value=\{[^}]*[a-zA-Z]Id\b' $ALL_PAGES 2>/dev/null || true)
155
+ if [ -n "$FK_SELECTS" ]; then
156
+ echo "WARNING: FK fields rendered as <select> dropdown — MUST use EntityLookup component"
157
+ echo "A <select> loaded from API state is NOT a valid substitute for EntityLookup."
158
+ echo "EntityLookup provides: debounced search, paginated results, display name resolution."
159
+ echo "$FK_SELECTS"
160
+ FAIL_C7=true
161
+ fi
162
+
163
+ FK_SELECT_ONCHANGE=$(grep -Pn 'onChange=.*[a-zA-Z]Id[^a-zA-Z].*e\.target\.value' $ALL_PAGES 2>/dev/null || true)
164
+ if [ -n "$FK_SELECT_ONCHANGE" ]; then
165
+ echo "WARNING: FK field set via e.target.value (select/input pattern) — use EntityLookup onChange(id)"
166
+ echo "$FK_SELECT_ONCHANGE"
167
+ FAIL_C7=true
168
+ fi
169
+
170
+ 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 || true)
171
+ if [ -n "$FK_PLACEHOLDER" ]; then
172
+ echo "WARNING: Form has placeholder for FK field selection — use EntityLookup search instead"
173
+ echo "$FK_PLACEHOLDER"
174
+ FAIL_C7=true
175
+ fi
176
+
177
+ FK_OPTIONS=$(grep -Pn '<option[^>]*value=\{[^}]*\.id\}' $ALL_PAGES 2>/dev/null || true)
178
+ if [ -n "$FK_OPTIONS" ]; then
179
+ echo "WARNING: <option> with entity .id value detected — this is a FK <select> anti-pattern"
180
+ echo "Replace the entire <select>/<option> block with <EntityLookup />"
181
+ echo "$FK_OPTIONS"
182
+ FAIL_C7=true
183
+ fi
184
+
185
+ if [ "$FAIL_C7" = true ]; then
186
+ echo ""
187
+ echo "Fix: Replace ALL FK fields with <EntityLookup /> from @/components/ui/EntityLookup"
188
+ echo "See smartstack-frontend.md section 6 for the EntityLookup pattern"
189
+ echo "FORBIDDEN for FK Guid fields: <input>, <select>, <option>, e.target.value"
190
+ echo "REQUIRED: <EntityLookup apiEndpoint={...} mapOption={...} value={...} onChange={...} />"
191
+ fi
192
+ fi
193
+
194
+ # POST-CHECK C9: No hardcoded Tailwind colors in generated pages (WARNING)
195
+ ALL_PAGES=$(find src/pages/ src/components/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\." || true)
196
+ if [ -n "$ALL_PAGES" ]; then
197
+ HARDCODED=$(grep -Pn '(bg|text|border)-(?!\[)(red|blue|green|gray|white|black|slate|zinc|neutral|stone)-' $ALL_PAGES 2>/dev/null || true)
198
+ if [ -n "$HARDCODED" ]; then
199
+ echo "WARNING: Pages MUST use CSS variables instead of hardcoded Tailwind colors"
200
+ echo "SmartStack uses a theme system — hardcoded colors break dark mode and custom themes"
201
+ echo ""
202
+ echo "Fix mapping:"
203
+ echo " bg-white → bg-[var(--bg-card)]"
204
+ echo " bg-gray-50 → bg-[var(--bg-primary)]"
205
+ echo " text-gray-900 → text-[var(--text-primary)]"
206
+ echo " text-gray-500 → text-[var(--text-secondary)]"
207
+ echo " border-gray-200 → border-[var(--border-color)]"
208
+ echo " bg-blue-600 → bg-[var(--color-accent-500)]"
209
+ echo " text-blue-600 → text-[var(--color-accent-500)]"
210
+ echo " text-red-500 → text-[var(--error-text)]"
211
+ echo " bg-green-500 → bg-[var(--success-bg)]"
212
+ echo ""
213
+ echo "See references/smartstack-frontend.md section 4 for full variable reference"
214
+ echo ""
215
+ echo "$HARDCODED"
216
+ fi
217
+ fi
218
+
219
+ # POST-CHECK C11: Frontend routes must use kebab-case (BLOCKING)
220
+ APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
221
+ if [ -n "$APP_TSX" ]; then
222
+ FE_PATHS=$(grep -oP "path:\s*['\"]([^'\"]+)['\"]" "$APP_TSX" | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"')
223
+ for FE_PATH in $FE_PATHS; do
224
+ for SEG in $(echo "$FE_PATH" | tr '/' '\n'); do
225
+ echo "$SEG" | grep -qP '^:' && continue
226
+ if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
227
+ SEED_MATCH=$(echo "$SEED_ROUTES" | tr '/' '\n' | grep -P "^[a-z]+-[a-z]+" | tr -d '-' | grep -x "$SEG" || true)
228
+ if [ -n "$SEED_MATCH" ]; then
229
+ echo "BLOCKING: Frontend route segment '$SEG' appears to be missing hyphens"
230
+ echo "Seed data uses kebab-case (e.g., 'human-resources') but frontend has '$SEG'"
231
+ echo "Fix: Use kebab-case in App.tsx route paths to match seed data exactly"
232
+ FAIL=true
233
+ fi
234
+ fi
235
+ done
236
+ done
237
+ fi
238
+
239
+ # POST-CHECK C24: i18n must use separate JSON files per language (not embedded in index.ts)
240
+ TSX_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\." || true)
241
+ if [ -n "$TSX_FILES" ]; then
242
+ if [ ! -d "src/i18n/locales" ]; then
243
+ echo "WARNING: Missing src/i18n/locales/ directory"
244
+ echo "Translations must be in separate JSON files: src/i18n/locales/{fr,en,it,de}/{module}.json"
245
+ echo "NEVER embed translations in src/i18n/index.ts or a single TypeScript file"
246
+ fi
247
+
248
+ I18N_INDEX=$(find src/i18n/ -maxdepth 1 -name "index.ts" -o -name "index.tsx" -o -name "config.ts" 2>/dev/null || true)
249
+ if [ -n "$I18N_INDEX" ]; then
250
+ EMBEDDED=$(grep -Pn '^\s*(resources|translations)\s*[:=]\s*\{' $I18N_INDEX 2>/dev/null || true)
251
+ if [ -n "$EMBEDDED" ]; then
252
+ echo "WARNING: Translations embedded in i18n config file — must be in separate JSON files"
253
+ echo "Found embedded translations in:"
254
+ echo "$EMBEDDED"
255
+ echo ""
256
+ echo "Fix: Move translations to src/i18n/locales/{fr,en,it,de}/{module}.json"
257
+ echo "The i18n config should import from locales/ directory, not contain inline translations"
258
+ fi
259
+ fi
260
+
261
+ for LANG in fr en it de; do
262
+ if [ ! -d "src/i18n/locales/$LANG" ]; then
263
+ echo "WARNING: Missing language directory: src/i18n/locales/$LANG/"
264
+ echo "SmartStack requires 4 languages: fr, en, it, de"
265
+ fi
266
+ done
267
+
268
+ for LANG in fr en it de; do
269
+ JSON_COUNT=$(find "src/i18n/locales/$LANG" -name "*.json" 2>/dev/null | wc -l)
270
+ if [ "$JSON_COUNT" -eq 0 ]; then
271
+ echo "WARNING: No translation JSON files in src/i18n/locales/$LANG/"
272
+ echo "Each module must have a {module}.json file per language"
273
+ fi
274
+ done
275
+ fi
276
+
277
+ # POST-CHECK C25: Pages must use useTranslation hook (no hardcoded user-facing strings)
278
+ PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\." || true)
279
+ if [ -n "$PAGE_FILES" ]; then
280
+ TOTAL_PAGES=$(echo "$PAGE_FILES" | wc -l)
281
+ I18N_PAGES=$(grep -l "useTranslation" $PAGE_FILES 2>/dev/null | wc -l)
282
+ if [ "$TOTAL_PAGES" -gt 0 ] && [ "$I18N_PAGES" -eq 0 ]; then
283
+ echo "WARNING: No page files use useTranslation — all user-facing text must be translated"
284
+ echo "Found $TOTAL_PAGES page files but 0 use useTranslation"
285
+ echo ""
286
+ echo "Fix: Import and use useTranslation in every page component:"
287
+ echo " const { t } = useTranslation(['{module}']);"
288
+ echo " t('{module}:title', 'Fallback text')"
289
+ fi
290
+
291
+ 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 || true)
292
+ if [ -n "$HARDCODED_TEXT" ]; then
293
+ echo "WARNING: Possible hardcoded user-facing strings detected in JSX"
294
+ echo "All user-facing text MUST use t('namespace:key', 'Fallback')"
295
+ echo "$HARDCODED_TEXT"
296
+ fi
297
+ fi
298
+
299
+ # POST-CHECK C26: List/Detail pages must include DocToggleButton (documentation panel)
300
+ LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\." || true)
301
+ if [ -n "$LIST_PAGES" ]; then
302
+ MISSING_DOC=0
303
+ for PAGE in $LIST_PAGES; do
304
+ if ! grep -q "DocToggleButton" "$PAGE" 2>/dev/null; then
305
+ echo "WARNING: Page missing DocToggleButton: $PAGE"
306
+ echo " Import: import { DocToggleButton } from '@/components/docs/DocToggleButton';"
307
+ echo " Place in header actions: <DocToggleButton />"
308
+ MISSING_DOC=$((MISSING_DOC + 1))
309
+ fi
310
+ done
311
+ if [ "$MISSING_DOC" -gt 0 ]; then
312
+ echo ""
313
+ echo "WARNING: $MISSING_DOC pages missing DocToggleButton — users cannot access inline documentation"
314
+ echo "See smartstack-frontend.md section 7 for placement pattern"
315
+ fi
316
+ fi
317
+
318
+ # POST-CHECK C27: Module documentation must be generated (doc-data.ts)
319
+ TSX_PAGES=$(find src/pages/ -name "*.tsx" -not -name "*.test.*" 2>/dev/null | grep -v node_modules | grep -v "docs/" || true)
320
+ DOC_DATA=$(find src/pages/docs/ -name "doc-data.ts" 2>/dev/null || true)
321
+ if [ -n "$TSX_PAGES" ] && [ -z "$DOC_DATA" ]; then
322
+ echo "WARNING: Frontend pages exist but no documentation generated"
323
+ echo "Fix: Invoke /documentation {module} --type user to generate doc-data.ts"
324
+ echo "The DocToggleButton in page headers will link to this documentation"
325
+ fi
326
+
327
+ # POST-CHECK C36: Frontend navigate() calls must have matching route definitions (CRITICAL)
328
+ PAGE_FILES=$(find web/ -name "*.tsx" -path "*/pages/*" ! -name "*.test.tsx" 2>/dev/null || true)
329
+ if [ -n "$PAGE_FILES" ]; then
330
+ NAV_TARGETS=$(grep -oP "navigate\(['\"]([^'\"]+)['\"]" $PAGE_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u || true)
331
+ APP_FILES=$(find web/ -name "App.tsx" -o -name "routes.tsx" -o -name "applicationRoutes*.tsx" -o -name "clientRoutes*.tsx" -o -name "componentRegistry.generated.ts" 2>/dev/null || true)
332
+ if [ -n "$APP_FILES" ] && [ -n "$NAV_TARGETS" ]; then
333
+ ROUTE_PATHS=$(grep -oP "path:\s*['\"]([^'\"]+)['\"]" $APP_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u || true)
334
+ for TARGET in $NAV_TARGETS; do
335
+ if echo "$TARGET" | grep -qP '^(:|/api|http|-[0-9])'; then continue; fi
336
+ LAST_SEG=$(echo "$TARGET" | grep -oP '[a-z][-a-z0-9]*$' || true)
337
+ if [ -z "$LAST_SEG" ]; then continue; fi
338
+ FOUND=$(echo "$ROUTE_PATHS" | grep -F "$LAST_SEG" 2>/dev/null || true)
339
+ if [ -z "$FOUND" ]; then
340
+ SEG_PASCAL=$(echo "$LAST_SEG" | sed -r 's/(^|-)([a-z])/\U\2/g')
341
+ PAGE_EXISTS=$(find web/ -name "${SEG_PASCAL}Page.tsx" -o -name "${SEG_PASCAL}ListPage.tsx" -o -name "${SEG_PASCAL}sPage.tsx" 2>/dev/null || true)
342
+ if [ -z "$PAGE_EXISTS" ]; then
343
+ SOURCE_FILE=$(grep -rl "navigate(['\"].*${LAST_SEG}" $PAGE_FILES 2>/dev/null | head -1 || true)
344
+ echo "CRITICAL: Dead link detected — navigate('$TARGET') in $SOURCE_FILE"
345
+ echo " Route segment '$LAST_SEG' has no matching route in App.tsx and no page component"
346
+ echo " Fix: Either create the page component + route, or remove the navigate() button"
347
+ FAIL=true
348
+ fi
349
+ fi
350
+ done
351
+ fi
352
+ fi
353
+
354
+ # POST-CHECK C37: Detail page tabs must NOT navigate() — content switches locally (CRITICAL)
355
+ DETAIL_PAGES=$(find src/ web/ -name "*DetailPage.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\." || true)
356
+ if [ -n "$DETAIL_PAGES" ]; then
357
+ FAIL_C37=false
358
+ for DP in $DETAIL_PAGES; do
359
+ HAS_TABS=$(grep -P "useState.*activeTab|setActiveTab" "$DP" 2>/dev/null || true)
360
+ if [ -z "$HAS_TABS" ]; then continue; fi
361
+
362
+ TAB_NAVIGATE=$(grep -Pn "navigate\(" "$DP" 2>/dev/null | grep -v "navigate\(\s*['\"]edit['\"]" | grep -v "navigate\(\s*-1\s*\)" | grep -v "navigate\(\s*['\`].*/:id/edit" | grep -v "//" || true)
363
+ if [ -n "$TAB_NAVIGATE" ]; then
364
+ RELATIVE_NAV=$(echo "$TAB_NAVIGATE" | grep -P "navigate\(['\"\`]\.\./" 2>/dev/null || true)
365
+ if [ -n "$RELATIVE_NAV" ]; then
366
+ echo "CRITICAL: Detail page tabs use navigate() instead of local content switching: $DP"
367
+ echo " Tab click handlers MUST only call setActiveTab() — render content inline"
368
+ echo " Found navigate() calls (likely in tab handlers):"
369
+ echo "$RELATIVE_NAV"
370
+ echo ""
371
+ echo " Fix: Remove navigate() from tab handlers. Render sub-resource content inline:"
372
+ echo " {activeTab === 'leaves' && <LeaveRequestsTable employeeId={entity.id} />}"
373
+ echo " See smartstack-frontend.md section 3 'Tab Behavior Rules' for the correct pattern."
374
+ FAIL_C37=true
375
+ fi
376
+ fi
377
+ done
378
+ if [ "$FAIL_C37" = true ]; then
379
+ FAIL=true
380
+ fi
381
+ fi
382
+
383
+ # POST-CHECK C49: Route Ordering in App.tsx — static before dynamic (BLOCKING)
384
+ APP_TSX=$(find web/ src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
385
+ if [ -n "$APP_TSX" ]; then
386
+ ROUTE_PATHS=$(grep -oP "path:\s*'([^']+)'" "$APP_TSX" | sed "s/path: '//;s/'//")
387
+ FAIL_C49=false
388
+ PREV_PREFIX=""
389
+ PREV_IS_DYNAMIC=false
390
+
391
+ while IFS= read -r ROUTE; do
392
+ [ -z "$ROUTE" ] && continue
393
+ PREFIX=$(echo "$ROUTE" | sed 's|/[^/]*$||')
394
+ LAST_SEGMENT=$(echo "$ROUTE" | grep -oP '[^/]+$')
395
+ IS_DYNAMIC=$(echo "$LAST_SEGMENT" | grep -c '^:')
396
+
397
+ if [ "$PREV_IS_DYNAMIC" = true ] && [ "$IS_DYNAMIC" -eq 0 ] && [ "$PREFIX" = "$PREV_PREFIX" ]; then
398
+ echo "BLOCKING: Static route '$ROUTE' is AFTER a dynamic route with prefix '$PREFIX' in App.tsx"
399
+ echo " React Router will match the dynamic route first → '$ROUTE' is unreachable (404)"
400
+ echo " Fix: Move static routes BEFORE dynamic routes within the same prefix group"
401
+ FAIL_C49=true
402
+ fi
403
+
404
+ PREV_PREFIX="$PREFIX"
405
+ if [ "$IS_DYNAMIC" -gt 0 ]; then
406
+ PREV_IS_DYNAMIC=true
407
+ else
408
+ PREV_IS_DYNAMIC=false
409
+ fi
410
+ done <<< "$ROUTE_PATHS"
411
+
412
+ if [ "$FAIL_C49" = true ]; then
413
+ FAIL=true
414
+ fi
415
+ fi
416
+
417
+ # POST-CHECK C52: Frontend route paths must include module segment (BLOCKING)
418
+ APP_TSX=$(find . -path "*/src/App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
419
+ if [ -n "$APP_TSX" ]; then
420
+ ROUTE_PATHS=$(grep -oP "path:\s*'[^']+'" "$APP_TSX" | grep -v "^path: ''" | grep -v ":id" | grep -v "create" | grep -v "edit" | sed "s/path: '//;s/'//")
421
+
422
+ NAV_SEED_FILES=$(find src/ -name "*NavigationSeedData.cs" -o -name "*NavigationModuleSeedData.cs" 2>/dev/null)
423
+ if [ -n "$NAV_SEED_FILES" ]; then
424
+ MODULE_CODES=$(grep -oP 'Code\s*=\s*"\K[^"]+' $NAV_SEED_FILES 2>/dev/null | sort -u || true)
425
+
426
+ for route in $ROUTE_PATHS; do
427
+ if [ -z "$route" ]; then continue; fi
428
+
429
+ if ! echo "$route" | grep -q "/"; then
430
+ IS_MODULE=false
431
+ for mod in $MODULE_CODES; do
432
+ if [ "$route" = "$mod" ]; then IS_MODULE=true; break; fi
433
+ done
434
+ if [ "$IS_MODULE" = false ]; then
435
+ echo "BLOCKING: Frontend route '$route' is missing module segment"
436
+ echo " Expected: '{module}/{route}' (e.g., 'employee-management/$route')"
437
+ echo " Backend navigation seed data defines routes with full hierarchy: /app/module/section"
438
+ echo " Frontend routes must match: module/section (relative to app root)"
439
+ FAIL=true
440
+ fi
441
+ fi
442
+ done
443
+ fi
444
+ fi
445
+
446
+ if [ "$FAIL" = true ]; then
447
+ exit 1
448
+ fi