@atlashub/smartstack-cli 4.31.0 → 4.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.documentation/commands.html +952 -116
- package/.documentation/index.html +2 -2
- package/.documentation/init.html +358 -174
- package/dist/mcp-entry.mjs +271 -44
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/mcp-scaffolding/controller.cs.hbs +54 -128
- package/templates/project/README.md +19 -0
- package/templates/skills/apex/SKILL.md +16 -10
- package/templates/skills/apex/_shared.md +1 -1
- package/templates/skills/apex/references/checks/architecture-checks.sh +154 -0
- package/templates/skills/apex/references/checks/backend-checks.sh +194 -0
- package/templates/skills/apex/references/checks/frontend-checks.sh +448 -0
- package/templates/skills/apex/references/checks/infrastructure-checks.sh +255 -0
- package/templates/skills/apex/references/checks/security-checks.sh +153 -0
- package/templates/skills/apex/references/checks/seed-checks.sh +536 -0
- package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +49 -192
- package/templates/skills/apex/references/parallel-execution.md +18 -5
- package/templates/skills/apex/references/post-checks.md +124 -2156
- package/templates/skills/apex/references/smartstack-api.md +160 -957
- package/templates/skills/apex/references/smartstack-frontend-compliance.md +23 -1
- package/templates/skills/apex/references/smartstack-frontend.md +134 -1022
- package/templates/skills/apex/references/smartstack-layers.md +12 -6
- package/templates/skills/apex/steps/step-00-init.md +81 -238
- package/templates/skills/apex/steps/step-03-execute.md +25 -751
- package/templates/skills/apex/steps/step-03a-layer0-domain.md +118 -0
- package/templates/skills/apex/steps/step-03b-layer1-seed.md +91 -0
- package/templates/skills/apex/steps/step-03c-layer2-backend.md +240 -0
- package/templates/skills/apex/steps/step-03d-layer3-frontend.md +300 -0
- package/templates/skills/apex/steps/step-03e-layer4-devdata.md +44 -0
- package/templates/skills/apex/steps/step-04-examine.md +70 -150
- package/templates/skills/application/references/frontend-i18n-and-output.md +2 -2
- package/templates/skills/application/references/frontend-route-naming.md +5 -1
- package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +49 -198
- package/templates/skills/application/references/frontend-verification.md +11 -11
- package/templates/skills/application/steps/step-05-frontend.md +26 -15
- package/templates/skills/application/templates-frontend.md +4 -0
- package/templates/skills/cli-app-sync/SKILL.md +2 -2
- package/templates/skills/cli-app-sync/references/comparison-map.md +1 -1
- package/templates/skills/controller/references/controller-code-templates.md +70 -67
- package/templates/skills/controller/references/mcp-scaffold-workflow.md +5 -1
|
@@ -0,0 +1,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
|