@atlashub/smartstack-cli 3.33.0 → 3.35.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/agents.html +5 -1
- package/.documentation/apex.html +644 -0
- package/.documentation/business-analyse.html +81 -1
- package/.documentation/cli-commands.html +5 -1
- package/.documentation/commands.html +5 -1
- package/.documentation/efcore.html +5 -1
- package/.documentation/gitflow.html +5 -1
- package/.documentation/hooks.html +5 -1
- package/.documentation/index.html +60 -2
- package/.documentation/init.html +414 -1
- package/.documentation/installation.html +5 -1
- package/.documentation/ralph-loop.html +365 -216
- package/.documentation/test-web.html +5 -1
- package/dist/index.js +32 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +7 -24
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -2
- package/templates/agents/ba-writer.md +142 -15
- package/templates/mcp-scaffolding/controller.cs.hbs +5 -1
- package/templates/skills/apex/SKILL.md +9 -3
- package/templates/skills/apex/_shared.md +49 -4
- package/templates/skills/{ralph-loop → apex}/references/core-seed-data.md +20 -11
- package/templates/skills/{ralph-loop → apex}/references/error-classification.md +2 -1
- package/templates/skills/apex/references/post-checks.md +463 -3
- package/templates/skills/apex/references/smartstack-api.md +76 -8
- package/templates/skills/apex/references/smartstack-frontend.md +74 -1
- package/templates/skills/apex/references/smartstack-layers.md +21 -3
- package/templates/skills/apex/steps/step-00-init.md +121 -1
- package/templates/skills/apex/steps/step-01-analyze.md +58 -0
- package/templates/skills/apex/steps/step-02-plan.md +36 -0
- package/templates/skills/apex/steps/step-03-execute.md +114 -7
- package/templates/skills/apex/steps/step-04-examine.md +116 -2
- package/templates/skills/business-analyse/SKILL.md +31 -20
- package/templates/skills/business-analyse/_module-loop.md +68 -9
- package/templates/skills/business-analyse/_shared.md +80 -21
- package/templates/skills/business-analyse/questionnaire/00-application.md +4 -2
- package/templates/skills/business-analyse/questionnaire/00b-project.md +85 -0
- package/templates/skills/business-analyse/references/deploy-modes.md +69 -0
- package/templates/skills/business-analyse/references/team-orchestration.md +158 -7
- package/templates/skills/business-analyse/schemas/application-schema.json +15 -1
- package/templates/skills/business-analyse/schemas/project-schema.json +490 -0
- package/templates/skills/business-analyse/schemas/sections/metadata-schema.json +2 -1
- package/templates/skills/business-analyse/steps/step-00-init.md +220 -38
- package/templates/skills/business-analyse/steps/step-01-cadrage.md +184 -5
- package/templates/skills/business-analyse/steps/step-01b-applications.md +423 -0
- package/templates/skills/business-analyse/steps/step-02-decomposition.md +23 -6
- package/templates/skills/business-analyse/steps/step-03c-compile.md +14 -2
- package/templates/skills/business-analyse/steps/step-03d-validate.md +32 -7
- package/templates/skills/business-analyse/steps/step-04a-collect.md +111 -0
- package/templates/skills/business-analyse/steps/step-05a-handoff.md +296 -103
- package/templates/skills/business-analyse/steps/step-05b-deploy.md +46 -14
- package/templates/skills/documentation/SKILL.md +92 -2
- package/templates/skills/ralph-loop/SKILL.md +14 -17
- package/templates/skills/ralph-loop/references/category-rules.md +63 -683
- package/templates/skills/ralph-loop/references/compact-loop.md +188 -428
- package/templates/skills/ralph-loop/references/section-splitting.md +439 -0
- package/templates/skills/ralph-loop/references/team-orchestration.md +13 -14
- package/templates/skills/ralph-loop/steps/step-01-task.md +27 -0
- package/templates/skills/ralph-loop/steps/step-02-execute.md +80 -691
- package/templates/skills/ralph-loop/steps/step-03-commit.md +38 -79
- package/templates/skills/ralph-loop/steps/step-04-check.md +39 -58
- package/templates/skills/ralph-loop/steps/step-05-report.md +31 -123
- package/scripts/health-check.sh +0 -168
- package/scripts/postinstall.js +0 -18
|
@@ -166,27 +166,57 @@ if [ -n "$PAGE_FILES" ]; then
|
|
|
166
166
|
fi
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
-
### POST-CHECK 9: Create/Edit pages must exist as separate route pages
|
|
169
|
+
### POST-CHECK 9: Create/Edit pages must exist as separate route pages (BLOCKING)
|
|
170
170
|
|
|
171
171
|
```bash
|
|
172
172
|
# For each module with a list page, verify create and edit pages exist
|
|
173
|
+
# If ListPage has navigate() calls to /create or /:id/edit, the target pages MUST exist
|
|
173
174
|
LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test)
|
|
175
|
+
FAIL=false
|
|
174
176
|
if [ -n "$LIST_PAGES" ]; then
|
|
175
177
|
for LIST_PAGE in $LIST_PAGES; do
|
|
176
178
|
PAGE_DIR=$(dirname "$LIST_PAGE")
|
|
177
179
|
MODULE_NAME=$(basename "$PAGE_DIR")
|
|
180
|
+
|
|
181
|
+
# Detect if ListPage navigates to /create or /edit routes
|
|
182
|
+
HAS_CREATE_NAV=$(grep -P "navigate\(.*['/]create" "$LIST_PAGE" 2>/dev/null)
|
|
183
|
+
HAS_EDIT_NAV=$(grep -P "navigate\(.*['/]edit|navigate\(.*/:id/edit" "$LIST_PAGE" 2>/dev/null)
|
|
184
|
+
|
|
178
185
|
# Check for create page
|
|
179
186
|
CREATE_PAGE=$(find "$PAGE_DIR" -name "*CreatePage.tsx" 2>/dev/null)
|
|
180
187
|
if [ -z "$CREATE_PAGE" ]; then
|
|
181
|
-
|
|
188
|
+
if [ -n "$HAS_CREATE_NAV" ]; then
|
|
189
|
+
echo "BLOCKING: Module $MODULE_NAME ListPage navigates to /create but CreatePage does NOT exist"
|
|
190
|
+
echo " Dead link: $HAS_CREATE_NAV"
|
|
191
|
+
echo " Fix: Create ${MODULE_NAME}CreatePage.tsx in $PAGE_DIR"
|
|
192
|
+
FAIL=true
|
|
193
|
+
else
|
|
194
|
+
echo "WARNING: Module $MODULE_NAME has a list page but no CreatePage — expected EntityCreatePage.tsx"
|
|
195
|
+
fi
|
|
182
196
|
fi
|
|
197
|
+
|
|
183
198
|
# Check for edit page
|
|
184
199
|
EDIT_PAGE=$(find "$PAGE_DIR" -name "*EditPage.tsx" 2>/dev/null)
|
|
185
200
|
if [ -z "$EDIT_PAGE" ]; then
|
|
186
|
-
|
|
201
|
+
if [ -n "$HAS_EDIT_NAV" ]; then
|
|
202
|
+
echo "BLOCKING: Module $MODULE_NAME ListPage navigates to /:id/edit but EditPage does NOT exist"
|
|
203
|
+
echo " Dead link: $HAS_EDIT_NAV"
|
|
204
|
+
echo " Fix: Create ${MODULE_NAME}EditPage.tsx in $PAGE_DIR"
|
|
205
|
+
FAIL=true
|
|
206
|
+
else
|
|
207
|
+
echo "WARNING: Module $MODULE_NAME has a list page but no EditPage — expected EntityEditPage.tsx"
|
|
208
|
+
fi
|
|
187
209
|
fi
|
|
188
210
|
done
|
|
189
211
|
fi
|
|
212
|
+
|
|
213
|
+
if [ "$FAIL" = true ]; then
|
|
214
|
+
echo ""
|
|
215
|
+
echo "BLOCKING: Create/Edit pages are MISSING but ListPage buttons link to them."
|
|
216
|
+
echo "Users will see white screen / 404 when clicking Create or Edit buttons."
|
|
217
|
+
echo "Fix: Generate form pages using /ui-components skill patterns (smartstack-frontend.md section 3b)"
|
|
218
|
+
exit 1
|
|
219
|
+
fi
|
|
190
220
|
```
|
|
191
221
|
|
|
192
222
|
### POST-CHECK 10: Form pages must have companion test files
|
|
@@ -1016,4 +1046,434 @@ if [ -n "$PROVIDER" ]; then
|
|
|
1016
1046
|
fi
|
|
1017
1047
|
```
|
|
1018
1048
|
|
|
1049
|
+
### POST-CHECK 39: Controllers must NOT have [Route] alongside [NavRoute] (BLOCKING)
|
|
1050
|
+
|
|
1051
|
+
```bash
|
|
1052
|
+
# [NavRoute] REPLACES [Route] — it resolves HTTP routes from the navigation DB at startup.
|
|
1053
|
+
# Having both is redundant and may cause route conflicts.
|
|
1054
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
1055
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
1056
|
+
for f in $CTRL_FILES; do
|
|
1057
|
+
HAS_NAVROUTE=$(grep -P '\[NavRoute\(' "$f" 2>/dev/null)
|
|
1058
|
+
HAS_ROUTE=$(grep -P '\[Route\(' "$f" 2>/dev/null)
|
|
1059
|
+
if [ -n "$HAS_NAVROUTE" ] && [ -n "$HAS_ROUTE" ]; then
|
|
1060
|
+
echo "BLOCKING: Controller has both [Route] and [NavRoute] — [NavRoute] replaces [Route]: $f"
|
|
1061
|
+
echo " Found [NavRoute]: $HAS_NAVROUTE"
|
|
1062
|
+
echo " Found [Route]: $HAS_ROUTE"
|
|
1063
|
+
echo "Fix: Remove the [Route(\"api/...\")] attribute. [NavRoute] resolves routes from navigation DB at startup."
|
|
1064
|
+
exit 1
|
|
1065
|
+
fi
|
|
1066
|
+
done
|
|
1067
|
+
fi
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
### POST-CHECK 40: NavRoute segments must use kebab-case for multi-word codes (BLOCKING)
|
|
1071
|
+
|
|
1072
|
+
```bash
|
|
1073
|
+
# NavRoute segments are navigation entity Codes joined by dots.
|
|
1074
|
+
# Multi-word codes MUST use kebab-case (e.g., "human-resources", NOT "humanresources").
|
|
1075
|
+
# Verified from SmartStack.app: "business.support-client.my-tickets", "platform.administration.access-requests"
|
|
1076
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
1077
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
1078
|
+
for f in $CTRL_FILES; do
|
|
1079
|
+
NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
|
|
1080
|
+
if [ -n "$NAVROUTE_VAL" ]; then
|
|
1081
|
+
# Check each segment for concatenated multi-word (10+ lowercase chars without hyphens)
|
|
1082
|
+
for SEG in $(echo "$NAVROUTE_VAL" | tr '.' '\n'); do
|
|
1083
|
+
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
1084
|
+
echo "BLOCKING: NavRoute segment '$SEG' in $f appears to be concatenated multi-word without hyphens"
|
|
1085
|
+
echo " Full NavRoute: $NAVROUTE_VAL"
|
|
1086
|
+
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
|
|
1087
|
+
echo " SmartStack convention (from SmartStack.app): 'business.support-client.my-tickets'"
|
|
1088
|
+
exit 1
|
|
1089
|
+
fi
|
|
1090
|
+
done
|
|
1091
|
+
fi
|
|
1092
|
+
done
|
|
1093
|
+
fi
|
|
1094
|
+
|
|
1095
|
+
# Also check seed data Code values for navigation entities
|
|
1096
|
+
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -o -name "NavigationApplicationSeedData.cs" 2>/dev/null)
|
|
1097
|
+
if [ -n "$SEED_FILES" ]; then
|
|
1098
|
+
CODES=$(grep -oP 'Code\s*=\s*"([^"]+)"' $SEED_FILES 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"' | sort -u)
|
|
1099
|
+
for CODE in $CODES; do
|
|
1100
|
+
if echo "$CODE" | grep -qP '^[a-z]{10,}$'; then
|
|
1101
|
+
echo "BLOCKING: Navigation seed data Code '$CODE' appears to be concatenated multi-word without hyphens"
|
|
1102
|
+
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
|
|
1103
|
+
exit 1
|
|
1104
|
+
fi
|
|
1105
|
+
done
|
|
1106
|
+
fi
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
### POST-CHECK 41: Permission codes must use kebab-case matching NavRoute codes (BLOCKING)
|
|
1110
|
+
|
|
1111
|
+
```bash
|
|
1112
|
+
# Permission codes in [RequirePermission] and Permissions.cs MUST use kebab-case for multi-word segments.
|
|
1113
|
+
# SmartStack.app convention: "business.support-client.my-tickets.read" (kebab-case everywhere)
|
|
1114
|
+
# FORBIDDEN: "business.humanresources.employees.read" — must be "business.human-resources.employees.read"
|
|
1115
|
+
|
|
1116
|
+
# Check [RequirePermission] attributes in controllers
|
|
1117
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
1118
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
1119
|
+
for f in $CTRL_FILES; do
|
|
1120
|
+
PERM_VALS=$(grep -oP 'RequirePermission\("([^"]+)"\)' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
|
|
1121
|
+
for PERM in $PERM_VALS; do
|
|
1122
|
+
# Check each segment (except the action suffix) for concatenated multi-word without hyphens
|
|
1123
|
+
SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1) # remove last segment (action: read/create/update/delete)
|
|
1124
|
+
for SEG in $SEGMENTS; do
|
|
1125
|
+
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
1126
|
+
echo "BLOCKING: Permission code segment '$SEG' in $f appears concatenated without hyphens"
|
|
1127
|
+
echo " Full permission: $PERM"
|
|
1128
|
+
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
|
|
1129
|
+
echo " SmartStack convention: 'business.support-client.my-tickets.read'"
|
|
1130
|
+
exit 1
|
|
1131
|
+
fi
|
|
1132
|
+
done
|
|
1133
|
+
done
|
|
1134
|
+
done
|
|
1135
|
+
fi
|
|
1136
|
+
|
|
1137
|
+
# Check Permissions.cs constants
|
|
1138
|
+
PERM_FILES=$(find src/ -path "*/Authorization/Permissions.cs" 2>/dev/null)
|
|
1139
|
+
if [ -n "$PERM_FILES" ]; then
|
|
1140
|
+
for f in $PERM_FILES; do
|
|
1141
|
+
CONST_VALS=$(grep -oP '=\s*"([^"]+)"' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
|
|
1142
|
+
for PERM in $CONST_VALS; do
|
|
1143
|
+
SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
|
|
1144
|
+
for SEG in $SEGMENTS; do
|
|
1145
|
+
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
1146
|
+
echo "BLOCKING: Permissions.cs constant segment '$SEG' in $f appears concatenated without hyphens"
|
|
1147
|
+
echo " Full permission: $PERM"
|
|
1148
|
+
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources'"
|
|
1149
|
+
exit 1
|
|
1150
|
+
fi
|
|
1151
|
+
done
|
|
1152
|
+
done
|
|
1153
|
+
done
|
|
1154
|
+
fi
|
|
1155
|
+
|
|
1156
|
+
# Check PermissionsSeedData.cs for mismatched paths
|
|
1157
|
+
SEED_PERM_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*PermissionsSeedData.cs" 2>/dev/null)
|
|
1158
|
+
if [ -n "$SEED_PERM_FILES" ]; then
|
|
1159
|
+
PATHS=$(grep -oP '"[a-z][a-z0-9.-]+\.(read|create|update|delete|\*)"' $SEED_PERM_FILES 2>/dev/null | tr -d '"')
|
|
1160
|
+
for PERM in $PATHS; do
|
|
1161
|
+
SEGMENTS=$(echo "$PERM" | tr '.' '\n' | head -n -1)
|
|
1162
|
+
for SEG in $SEGMENTS; do
|
|
1163
|
+
if echo "$SEG" | grep -qP '^[a-z]{10,}$'; then
|
|
1164
|
+
echo "BLOCKING: PermissionsSeedData path segment '$SEG' appears concatenated without hyphens"
|
|
1165
|
+
echo " Full permission path: $PERM"
|
|
1166
|
+
echo " Fix: Use kebab-case matching NavRoute: 'humanresources' → 'human-resources'"
|
|
1167
|
+
exit 1
|
|
1168
|
+
fi
|
|
1169
|
+
done
|
|
1170
|
+
done
|
|
1171
|
+
fi
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
### POST-CHECK 42: Frontend navigate() calls must have matching route definitions (BLOCKING)
|
|
1175
|
+
|
|
1176
|
+
```bash
|
|
1177
|
+
# Detect dead links: navigate() calls to paths that don't have corresponding page components.
|
|
1178
|
+
# Example: LeavesPage has navigate('../leave-types') but no LeaveTypesPage or route exists.
|
|
1179
|
+
PAGE_FILES=$(find web/ -name "*.tsx" -path "*/pages/*" ! -name "*.test.tsx" 2>/dev/null)
|
|
1180
|
+
if [ -n "$PAGE_FILES" ]; then
|
|
1181
|
+
# Extract navigate targets (relative paths like '../leave-types', './create', etc.)
|
|
1182
|
+
NAV_TARGETS=$(grep -oP "navigate\(['\"]([^'\"]+)['\"]" $PAGE_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u)
|
|
1183
|
+
# Extract route paths from App.tsx or route config
|
|
1184
|
+
APP_FILES=$(find web/ -name "App.tsx" -o -name "routes.tsx" -o -name "clientRoutes*.tsx" 2>/dev/null)
|
|
1185
|
+
if [ -n "$APP_FILES" ] && [ -n "$NAV_TARGETS" ]; then
|
|
1186
|
+
ROUTE_PATHS=$(grep -oP "path:\s*['\"]([^'\"]+)['\"]" $APP_FILES 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'" | tr -d '"' | sort -u)
|
|
1187
|
+
for TARGET in $NAV_TARGETS; do
|
|
1188
|
+
# Skip dynamic segments (:id), back navigation (-1), and absolute URLs
|
|
1189
|
+
if echo "$TARGET" | grep -qP '^(:|/api|http|-[0-9])'; then continue; fi
|
|
1190
|
+
# Extract the last path segment for matching (e.g., '../leave-types' → 'leave-types')
|
|
1191
|
+
LAST_SEG=$(echo "$TARGET" | grep -oP '[a-z][-a-z0-9]*$')
|
|
1192
|
+
if [ -z "$LAST_SEG" ]; then continue; fi
|
|
1193
|
+
# Check if any route path contains this segment
|
|
1194
|
+
FOUND=$(echo "$ROUTE_PATHS" | grep -F "$LAST_SEG" 2>/dev/null)
|
|
1195
|
+
if [ -z "$FOUND" ]; then
|
|
1196
|
+
# Verify no page component exists for this path
|
|
1197
|
+
SEG_PASCAL=$(echo "$LAST_SEG" | sed -r 's/(^|-)([a-z])/\U\2/g')
|
|
1198
|
+
PAGE_EXISTS=$(find web/ -name "${SEG_PASCAL}Page.tsx" -o -name "${SEG_PASCAL}ListPage.tsx" -o -name "${SEG_PASCAL}sPage.tsx" 2>/dev/null)
|
|
1199
|
+
if [ -z "$PAGE_EXISTS" ]; then
|
|
1200
|
+
# Find which file has this navigate call
|
|
1201
|
+
SOURCE_FILE=$(grep -rl "navigate(['\"].*${LAST_SEG}" $PAGE_FILES 2>/dev/null | head -1)
|
|
1202
|
+
echo "BLOCKING: Dead link detected — navigate('$TARGET') in $SOURCE_FILE"
|
|
1203
|
+
echo " Route segment '$LAST_SEG' has no matching route in App.tsx and no page component"
|
|
1204
|
+
echo " Fix: Either create the page component + route, or remove the navigate() button"
|
|
1205
|
+
exit 1
|
|
1206
|
+
fi
|
|
1207
|
+
fi
|
|
1208
|
+
done
|
|
1209
|
+
fi
|
|
1210
|
+
fi
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
### POST-CHECK 43: Detail page tabs must NOT navigate() — content switches locally (BLOCKING)
|
|
1214
|
+
|
|
1215
|
+
```bash
|
|
1216
|
+
# Tabs on detail pages MUST use local state (setActiveTab) — NEVER navigate() to other pages.
|
|
1217
|
+
# Root cause (test-apex-006): EmployeeDetailPage tabs navigated to ../leaves and ../time-tracking
|
|
1218
|
+
# instead of rendering sub-resource content inline. Users lost detail page context.
|
|
1219
|
+
DETAIL_PAGES=$(find src/ web/ -name "*DetailPage.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
1220
|
+
if [ -n "$DETAIL_PAGES" ]; then
|
|
1221
|
+
FAIL=false
|
|
1222
|
+
for DP in $DETAIL_PAGES; do
|
|
1223
|
+
# Check if the page has tabs (activeTab state)
|
|
1224
|
+
HAS_TABS=$(grep -P "useState.*activeTab|setActiveTab" "$DP" 2>/dev/null)
|
|
1225
|
+
if [ -z "$HAS_TABS" ]; then continue; fi
|
|
1226
|
+
|
|
1227
|
+
# Check if any tab click handler calls navigate()
|
|
1228
|
+
# Pattern: function that both references setActiveTab AND navigate()
|
|
1229
|
+
# Look for navigate() calls inside handlers that also set tab state
|
|
1230
|
+
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 "//")
|
|
1231
|
+
if [ -n "$TAB_NAVIGATE" ]; then
|
|
1232
|
+
# Verify this navigate is in a tab handler context (near setActiveTab usage)
|
|
1233
|
+
# Simple heuristic: if file has both setActiveTab AND navigate() to relative paths
|
|
1234
|
+
RELATIVE_NAV=$(echo "$TAB_NAVIGATE" | grep -P "navigate\(['\"\`]\.\./" 2>/dev/null)
|
|
1235
|
+
if [ -n "$RELATIVE_NAV" ]; then
|
|
1236
|
+
echo "BLOCKING: Detail page tabs use navigate() instead of local content switching: $DP"
|
|
1237
|
+
echo " Tab click handlers MUST only call setActiveTab() — render content inline"
|
|
1238
|
+
echo " Found navigate() calls (likely in tab handlers):"
|
|
1239
|
+
echo "$RELATIVE_NAV"
|
|
1240
|
+
echo ""
|
|
1241
|
+
echo " Fix: Remove navigate() from tab handlers. Render sub-resource content inline:"
|
|
1242
|
+
echo " {activeTab === 'leaves' && <LeaveRequestsTable employeeId={entity.id} />}"
|
|
1243
|
+
echo " See smartstack-frontend.md section 3 'Tab Behavior Rules' for the correct pattern."
|
|
1244
|
+
FAIL=true
|
|
1245
|
+
fi
|
|
1246
|
+
fi
|
|
1247
|
+
done
|
|
1248
|
+
if [ "$FAIL" = true ]; then
|
|
1249
|
+
exit 1
|
|
1250
|
+
fi
|
|
1251
|
+
fi
|
|
1252
|
+
```
|
|
1253
|
+
|
|
1254
|
+
### POST-CHECK 44: Migration ModelSnapshot must contain ALL entities registered in DbContext (BLOCKING)
|
|
1255
|
+
|
|
1256
|
+
```bash
|
|
1257
|
+
# Root cause (test-apex-007): 7 entities registered in DbContext but migration only covered 3.
|
|
1258
|
+
# Happens when migration is created ONCE in Layer 0 for the first batch, then additional entities
|
|
1259
|
+
# are added in subsequent iterations without re-running migration.
|
|
1260
|
+
SNAPSHOT=$(find src/ -name "*ModelSnapshot.cs" -path "*/Migrations/*" 2>/dev/null | head -1)
|
|
1261
|
+
DBCONTEXT=$(find src/ -name "*DbContext.cs" -path "*/Persistence/*" ! -name "*DesignTime*" 2>/dev/null | head -1)
|
|
1262
|
+
if [ -n "$SNAPSHOT" ] && [ -n "$DBCONTEXT" ]; then
|
|
1263
|
+
# Extract DbSet entity names from DbContext (DbSet<EntityName>)
|
|
1264
|
+
DBSET_ENTITIES=$(grep -oP 'DbSet<(\w+)>' "$DBCONTEXT" 2>/dev/null | grep -oP '<\K\w+(?=>)' | sort -u)
|
|
1265
|
+
FAIL=false
|
|
1266
|
+
for ENTITY in $DBSET_ENTITIES; do
|
|
1267
|
+
# Skip base SmartStack entities (handled by core migrations)
|
|
1268
|
+
if echo "$ENTITY" | grep -qP '^(Navigation|Tenant|User|Role|Permission|AuditLog|ApplicationTracking)'; then
|
|
1269
|
+
continue
|
|
1270
|
+
fi
|
|
1271
|
+
# Check if the entity appears in ModelSnapshot (builder.Entity<EntityName>)
|
|
1272
|
+
if ! grep -q "Entity<$ENTITY>" "$SNAPSHOT" 2>/dev/null; then
|
|
1273
|
+
echo "BLOCKING: Entity '$ENTITY' is registered as DbSet in $DBCONTEXT but MISSING from ModelSnapshot"
|
|
1274
|
+
echo " This means no migration was created for this entity — it will not exist in the database."
|
|
1275
|
+
echo " Fix: Run 'dotnet ef migrations add' to include all new entities"
|
|
1276
|
+
FAIL=true
|
|
1277
|
+
fi
|
|
1278
|
+
done
|
|
1279
|
+
if [ "$FAIL" = true ]; then
|
|
1280
|
+
echo ""
|
|
1281
|
+
echo " Root cause: Migration was likely created once for the first batch of entities,"
|
|
1282
|
+
echo " but additional entities were added later without regenerating the migration."
|
|
1283
|
+
echo " Fix: Create a new migration that covers ALL missing entities."
|
|
1284
|
+
exit 1
|
|
1285
|
+
fi
|
|
1286
|
+
fi
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
### POST-CHECK 45: I18n namespace files must be registered in i18n config (BLOCKING)
|
|
1290
|
+
|
|
1291
|
+
```bash
|
|
1292
|
+
# Root cause (test-apex-007): i18n JSON files existed in src/i18n/locales/ but were never
|
|
1293
|
+
# registered in the i18n config (config.ts or index.ts). Pages calling useTranslation(['module'])
|
|
1294
|
+
# got empty translations at runtime.
|
|
1295
|
+
I18N_CONFIG=$(find src/ web/ -path "*/i18n/config.ts" -o -path "*/i18n/index.ts" -o -path "*/i18n/i18n.ts" 2>/dev/null | grep -v node_modules | head -1)
|
|
1296
|
+
if [ -n "$I18N_CONFIG" ]; then
|
|
1297
|
+
# Find all module JSON files in the primary language (fr)
|
|
1298
|
+
FR_FILES=$(find src/ web/ -path "*/i18n/locales/fr/*.json" 2>/dev/null | grep -v node_modules | grep -v common.json | grep -v navigation.json)
|
|
1299
|
+
if [ -n "$FR_FILES" ]; then
|
|
1300
|
+
FAIL=false
|
|
1301
|
+
for JSON_FILE in $FR_FILES; do
|
|
1302
|
+
NS=$(basename "$JSON_FILE" .json)
|
|
1303
|
+
# Check if namespace is referenced in config (import or resource key)
|
|
1304
|
+
if ! grep -q "$NS" "$I18N_CONFIG" 2>/dev/null; then
|
|
1305
|
+
echo "BLOCKING: i18n namespace '$NS' (from $JSON_FILE) is not registered in $I18N_CONFIG"
|
|
1306
|
+
echo " Pages using useTranslation(['$NS']) will get empty translations at runtime"
|
|
1307
|
+
echo " Fix: Add '$NS' to the resources/ns configuration in $I18N_CONFIG"
|
|
1308
|
+
FAIL=true
|
|
1309
|
+
fi
|
|
1310
|
+
done
|
|
1311
|
+
if [ "$FAIL" = true ]; then
|
|
1312
|
+
exit 1
|
|
1313
|
+
fi
|
|
1314
|
+
fi
|
|
1315
|
+
fi
|
|
1316
|
+
```
|
|
1317
|
+
|
|
1318
|
+
### POST-CHECK 46: FluentValidation validators must be registered via DI (BLOCKING)
|
|
1319
|
+
|
|
1320
|
+
```bash
|
|
1321
|
+
# Root cause (test-apex-007): Validators existed but were never registered in DI.
|
|
1322
|
+
# Without DI registration, [FromBody] DTOs are never validated — any data is accepted.
|
|
1323
|
+
VALIDATOR_FILES=$(find src/ -name "*Validator.cs" -path "*/Validators/*" 2>/dev/null | grep -v test | grep -v Test)
|
|
1324
|
+
if [ -n "$VALIDATOR_FILES" ]; then
|
|
1325
|
+
# Check DI registration file exists
|
|
1326
|
+
DI_FILE=$(find src/ -name "DependencyInjection.cs" -o -name "ServiceCollectionExtensions.cs" 2>/dev/null | grep -v test | head -1)
|
|
1327
|
+
if [ -z "$DI_FILE" ]; then
|
|
1328
|
+
echo "BLOCKING: Validators exist but no DependencyInjection.cs found for DI registration"
|
|
1329
|
+
exit 1
|
|
1330
|
+
fi
|
|
1331
|
+
# Check for AddValidatorsFromAssembly or individual validator registration
|
|
1332
|
+
HAS_ASSEMBLY_REG=$(grep -c "AddValidatorsFromAssembly\|AddValidatorsFromAssemblyContaining" "$DI_FILE" 2>/dev/null)
|
|
1333
|
+
if [ "$HAS_ASSEMBLY_REG" -eq 0 ]; then
|
|
1334
|
+
# Check individual registrations as fallback
|
|
1335
|
+
VALIDATOR_COUNT=$(echo "$VALIDATOR_FILES" | wc -l)
|
|
1336
|
+
REGISTERED_COUNT=0
|
|
1337
|
+
for VF in $VALIDATOR_FILES; do
|
|
1338
|
+
VN=$(basename "$VF" .cs)
|
|
1339
|
+
if grep -q "$VN" "$DI_FILE" 2>/dev/null; then
|
|
1340
|
+
REGISTERED_COUNT=$((REGISTERED_COUNT + 1))
|
|
1341
|
+
fi
|
|
1342
|
+
done
|
|
1343
|
+
if [ "$REGISTERED_COUNT" -eq 0 ]; then
|
|
1344
|
+
echo "BLOCKING: $VALIDATOR_COUNT validators exist but NONE are registered in DI ($DI_FILE)"
|
|
1345
|
+
echo " Fix: Add 'services.AddValidatorsFromAssemblyContaining<Create{Entity}DtoValidator>();' to $DI_FILE"
|
|
1346
|
+
echo " Or use 'services.AddValidatorsFromAssembly(typeof(Create{Entity}DtoValidator).Assembly);'"
|
|
1347
|
+
exit 1
|
|
1348
|
+
fi
|
|
1349
|
+
fi
|
|
1350
|
+
fi
|
|
1351
|
+
```
|
|
1352
|
+
|
|
1353
|
+
### POST-CHECK 47: Date/date properties in DTOs must use DateOnly, not string (BLOCKING)
|
|
1354
|
+
|
|
1355
|
+
```bash
|
|
1356
|
+
# Root cause (test-apex-007): WorkLog DTO had Date property typed as string instead of DateOnly.
|
|
1357
|
+
# This causes: invalid date parsing, no date validation, inconsistent formats across clients.
|
|
1358
|
+
DTO_FILES=$(find src/ -name "*Dto.cs" -path "*/DTOs/*" 2>/dev/null)
|
|
1359
|
+
if [ -n "$DTO_FILES" ]; then
|
|
1360
|
+
FAIL=false
|
|
1361
|
+
for f in $DTO_FILES; do
|
|
1362
|
+
# Find string properties whose name contains "Date" (case-insensitive)
|
|
1363
|
+
BAD_DATES=$(grep -Pn 'string\??\s+\w*[Dd]ate\w*\s*[{;,]' "$f" 2>/dev/null | grep -vi "Updated\|Created\|format\|pattern\|string\|parse")
|
|
1364
|
+
if [ -n "$BAD_DATES" ]; then
|
|
1365
|
+
echo "BLOCKING: DTO has string type for date field — must use DateOnly: $f"
|
|
1366
|
+
echo "$BAD_DATES"
|
|
1367
|
+
echo " Fix: Change 'string Date' to 'DateOnly Date' (or 'DateOnly? Date' if nullable)"
|
|
1368
|
+
echo " DateOnly is the correct .NET type for date-only values (no time component)"
|
|
1369
|
+
FAIL=true
|
|
1370
|
+
fi
|
|
1371
|
+
done
|
|
1372
|
+
if [ "$FAIL" = true ]; then
|
|
1373
|
+
exit 1
|
|
1374
|
+
fi
|
|
1375
|
+
fi
|
|
1376
|
+
```
|
|
1377
|
+
|
|
1378
|
+
### POST-CHECK 48: NavRoute attribute values must use kebab-case (BLOCKING)
|
|
1379
|
+
|
|
1380
|
+
```bash
|
|
1381
|
+
# Root cause (test-apex-007): Controllers had [NavRoute("business.humanresources.employees")]
|
|
1382
|
+
# instead of [NavRoute("business.human-resources.employees")]. This causes route mismatch with
|
|
1383
|
+
# seed data and permission codes, resulting in 404s at runtime.
|
|
1384
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
1385
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
1386
|
+
FAIL=false
|
|
1387
|
+
for f in $CTRL_FILES; do
|
|
1388
|
+
NAVROUTE_VALS=$(grep -oP 'NavRoute\("([^"]+)"' "$f" 2>/dev/null | grep -oP '"[^"]+"' | tr -d '"')
|
|
1389
|
+
for NR in $NAVROUTE_VALS; do
|
|
1390
|
+
# Check each segment for concatenated multi-word without hyphens
|
|
1391
|
+
SEGMENTS=$(echo "$NR" | tr '.' '\n')
|
|
1392
|
+
for SEG in $SEGMENTS; do
|
|
1393
|
+
# Detect segments that look like concatenated words (lowercase, 8+ chars, no hyphens)
|
|
1394
|
+
# Use a simpler heuristic: lowercase-only segment with known multi-word patterns
|
|
1395
|
+
if echo "$SEG" | grep -qP '^[a-z]{8,}$'; then
|
|
1396
|
+
# Additional check: does it contain a known multi-word pattern?
|
|
1397
|
+
if echo "$SEG" | grep -qP '(human|project|leave|client|support|email|time|work|resource)'; then
|
|
1398
|
+
echo "BLOCKING: NavRoute segment '$SEG' in $f appears to be concatenated multi-word without hyphens"
|
|
1399
|
+
echo " Full NavRoute: $NR"
|
|
1400
|
+
echo " Fix: Use kebab-case: e.g., 'humanresources' → 'human-resources', 'projectmanagement' → 'project-management'"
|
|
1401
|
+
FAIL=true
|
|
1402
|
+
fi
|
|
1403
|
+
fi
|
|
1404
|
+
done
|
|
1405
|
+
done
|
|
1406
|
+
done
|
|
1407
|
+
if [ "$FAIL" = true ]; then
|
|
1408
|
+
exit 1
|
|
1409
|
+
fi
|
|
1410
|
+
fi
|
|
1411
|
+
```
|
|
1412
|
+
|
|
1413
|
+
### POST-CHECK 49: Every module with entities must have a migration covering them (BLOCKING)
|
|
1414
|
+
|
|
1415
|
+
```bash
|
|
1416
|
+
# Complementary to POST-CHECK 44 — checks from the entity side.
|
|
1417
|
+
# Finds entity .cs files in Domain/ and verifies they appear in at least one migration file.
|
|
1418
|
+
ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null | grep -v test)
|
|
1419
|
+
MIGRATION_DIR=$(find src/ -path "*/Migrations" -type d 2>/dev/null | head -1)
|
|
1420
|
+
if [ -n "$ENTITY_FILES" ] && [ -n "$MIGRATION_DIR" ]; then
|
|
1421
|
+
MIGRATION_FILES=$(find "$MIGRATION_DIR" -name "*.cs" ! -name "*ModelSnapshot*" ! -name "*DesignTime*" 2>/dev/null)
|
|
1422
|
+
if [ -z "$MIGRATION_FILES" ]; then
|
|
1423
|
+
echo "BLOCKING: Entity files exist in Domain/Entities but NO migration files found in $MIGRATION_DIR"
|
|
1424
|
+
exit 1
|
|
1425
|
+
fi
|
|
1426
|
+
FAIL=false
|
|
1427
|
+
for EF in $ENTITY_FILES; do
|
|
1428
|
+
ENTITY_NAME=$(basename "$EF" .cs)
|
|
1429
|
+
# Skip abstract base classes and interfaces
|
|
1430
|
+
if grep -qP '^\s*(public\s+)?(abstract|interface)\s' "$EF" 2>/dev/null; then continue; fi
|
|
1431
|
+
# Check if entity appears in any migration (CreateTable or AddColumn or entity reference)
|
|
1432
|
+
FOUND=$(grep -l "$ENTITY_NAME" $MIGRATION_FILES 2>/dev/null)
|
|
1433
|
+
if [ -z "$FOUND" ]; then
|
|
1434
|
+
echo "BLOCKING: Entity '$ENTITY_NAME' ($EF) not found in any migration file"
|
|
1435
|
+
echo " This entity will NOT have a database table."
|
|
1436
|
+
echo " Fix: Run 'dotnet ef migrations add' to create a migration covering this entity"
|
|
1437
|
+
FAIL=true
|
|
1438
|
+
fi
|
|
1439
|
+
done
|
|
1440
|
+
if [ "$FAIL" = true ]; then
|
|
1441
|
+
exit 1
|
|
1442
|
+
fi
|
|
1443
|
+
fi
|
|
1444
|
+
```
|
|
1445
|
+
|
|
1446
|
+
### POST-CHECK 50: Controllers must NOT have both [Route] and [NavRoute] attributes (BLOCKING)
|
|
1447
|
+
|
|
1448
|
+
```bash
|
|
1449
|
+
# Root cause (test-apex-007): All 7 controllers had BOTH [Route("api/...")] and [NavRoute("...")].
|
|
1450
|
+
# In SmartStack, [NavRoute] resolves routes dynamically from Navigation entities at startup.
|
|
1451
|
+
# [Route] is standard ASP.NET Core static routing. When both exist:
|
|
1452
|
+
# - NavRoute middleware tries to resolve from DB → fails if seed data not applied → no route
|
|
1453
|
+
# - [Route] may or may not take over depending on middleware order
|
|
1454
|
+
# - Result: 404 on ALL endpoints
|
|
1455
|
+
# The MCP validate_conventions previously ENCOURAGED adding [Route] with [NavRoute] — this was a bug.
|
|
1456
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
1457
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
1458
|
+
FAIL=false
|
|
1459
|
+
for f in $CTRL_FILES; do
|
|
1460
|
+
HAS_NAVROUTE=$(grep -c '\[NavRoute(' "$f" 2>/dev/null)
|
|
1461
|
+
HAS_ROUTE=$(grep -c '\[Route(' "$f" 2>/dev/null)
|
|
1462
|
+
if [ "$HAS_NAVROUTE" -gt 0 ] && [ "$HAS_ROUTE" -gt 0 ]; then
|
|
1463
|
+
NAVROUTE_VAL=$(grep -oP 'NavRoute\("([^"]+)"' "$f" 2>/dev/null | head -1)
|
|
1464
|
+
ROUTE_VAL=$(grep -oP 'Route\("([^"]+)"' "$f" 2>/dev/null | head -1)
|
|
1465
|
+
echo "BLOCKING: Controller has BOTH [Route] and [NavRoute] — remove [Route]: $f"
|
|
1466
|
+
echo " Found: [$ROUTE_VAL] + [$NAVROUTE_VAL]"
|
|
1467
|
+
echo " In SmartStack, [NavRoute] resolves routes dynamically from the database."
|
|
1468
|
+
echo " Having [Route] alongside it causes route conflicts and 404s."
|
|
1469
|
+
echo " Fix: Remove the [Route(...)] attribute, keep only [NavRoute(...)]"
|
|
1470
|
+
FAIL=true
|
|
1471
|
+
fi
|
|
1472
|
+
done
|
|
1473
|
+
if [ "$FAIL" = true ]; then
|
|
1474
|
+
exit 1
|
|
1475
|
+
fi
|
|
1476
|
+
fi
|
|
1477
|
+
```
|
|
1478
|
+
|
|
1019
1479
|
**If ANY POST-CHECK fails → fix in step-03, re-validate.**
|
|
@@ -491,8 +491,20 @@ public class {Name}Controller : ControllerBase
|
|
|
491
491
|
}
|
|
492
492
|
```
|
|
493
493
|
|
|
494
|
+
**CRITICAL — Route attribute rules:**
|
|
495
|
+
- `[NavRoute]` is the ONLY route attribute needed — it resolves routes dynamically from Navigation entities at startup
|
|
496
|
+
- **FORBIDDEN:** `[Route("api/...")]` alongside `[NavRoute]` — causes route conflicts and 404s at runtime
|
|
497
|
+
- **FORBIDDEN:** `[Route("api/[controller]")]` — this is standard ASP.NET Core, NOT SmartStack
|
|
498
|
+
- If a controller has `[NavRoute]`, there must be NO `[Route]` attribute on the class
|
|
499
|
+
|
|
494
500
|
**CRITICAL:** Use `[RequirePermission(Permissions.{Module}.{Action})]` on EVERY endpoint — NEVER `[Authorize]` alone (no RBAC enforcement).
|
|
495
501
|
|
|
502
|
+
**CRITICAL — Permission paths use IDENTICAL segments to NavRoute codes (kebab-case):**
|
|
503
|
+
- NavRoute: `business.human-resources.employees` → Permission: `business.human-resources.employees.read`
|
|
504
|
+
- NavRoute: `business.human-resources.employees.leaves` → Permission: `business.human-resources.employees.leaves.read`
|
|
505
|
+
- FORBIDDEN: `business.humanresources.employees.read` (no kebab-case — mismatches NavRoute)
|
|
506
|
+
- SmartStack.app convention: `business.support-client.my-tickets.read` (always kebab-case)
|
|
507
|
+
|
|
496
508
|
### Section-Level Controller (NavRoute with 4 segments)
|
|
497
509
|
|
|
498
510
|
When a module has sections, each section gets its own controller with a 4-segment navRoute:
|
|
@@ -504,7 +516,7 @@ When a module has sections, each section gets its own controller with a 4-segmen
|
|
|
504
516
|
[Authorize]
|
|
505
517
|
public class {Section}Controller : ControllerBase
|
|
506
518
|
{
|
|
507
|
-
// Example: business.
|
|
519
|
+
// Example: business.human-resources.employees.departments
|
|
508
520
|
[HttpGet]
|
|
509
521
|
[RequirePermission(Permissions.{Section}.Read)]
|
|
510
522
|
public async Task<ActionResult<PaginatedResult<{Section}ResponseDto>>> GetAll(
|
|
@@ -519,13 +531,46 @@ public class {Section}Controller : ControllerBase
|
|
|
519
531
|
**NavRoute segment rules:**
|
|
520
532
|
| Level | NavRoute format | Example |
|
|
521
533
|
|-------|----------------|---------|
|
|
522
|
-
| Module | `{context}.{app}.{module}` (3 segments) | `business.
|
|
523
|
-
| Section | `{context}.{app}.{module}.{section}` (4 segments) | `business.
|
|
534
|
+
| Module | `{context}.{app}.{module}` (3 segments) | `business.human-resources.employees` |
|
|
535
|
+
| Section | `{context}.{app}.{module}.{section}` (4 segments) | `business.human-resources.employees.departments` |
|
|
524
536
|
|
|
525
537
|
**Namespace:** `SmartStack.Api.Routing` (NOT `SmartStack.Api.Core.Routing`)
|
|
526
538
|
|
|
527
539
|
**NavRoute resolves at startup from DB:** `platform.administration.users` → `api/platform/administration/users`
|
|
528
540
|
|
|
541
|
+
### Sub-Resource Pattern (NavRoute Suffix)
|
|
542
|
+
|
|
543
|
+
When an entity is a child of another entity (e.g., LeaveTypes under Leaves), use `[NavRoute(..., Suffix = "types")]`:
|
|
544
|
+
|
|
545
|
+
```csharp
|
|
546
|
+
// Sub-resource controller: types are nested under leaves
|
|
547
|
+
[ApiController]
|
|
548
|
+
[NavRoute("business.human-resources.employees.leaves", Suffix = "types")]
|
|
549
|
+
[Authorize]
|
|
550
|
+
public class LeaveTypesController : ControllerBase
|
|
551
|
+
{
|
|
552
|
+
[HttpGet]
|
|
553
|
+
[RequirePermission(Permissions.Leaves.Read)] // inherits parent section permission
|
|
554
|
+
public async Task<ActionResult<PaginatedResult<LeaveTypeResponseDto>>> GetAll(...)
|
|
555
|
+
=> Ok(await _service.GetAllAsync(search, page, pageSize, ct));
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
**Alternative pattern** (sub-resource endpoints within parent controller):
|
|
560
|
+
```csharp
|
|
561
|
+
// LeaveTypes as endpoints within LeavesController
|
|
562
|
+
[HttpGet("types")]
|
|
563
|
+
[RequirePermission(Permissions.Leaves.Read)]
|
|
564
|
+
public async Task<ActionResult<PaginatedResult<LeaveTypeResponseDto>>> GetAllLeaveTypes(...)
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
> **CRITICAL — Sub-resource frontend completeness:**
|
|
568
|
+
> If a parent page has a button (e.g., "Manage Leave Types") that `navigate()`s to a sub-resource route,
|
|
569
|
+
> the frontend MUST include a page component for that route. Otherwise → dead link → white screen.
|
|
570
|
+
> - Either create a dedicated sub-resource ListPage (e.g., `LeaveTypesPage.tsx`)
|
|
571
|
+
> - Or DON'T include the navigate() button if pages won't be created
|
|
572
|
+
> - **Prefer separate controllers** (with Suffix) over sub-endpoints in parent controller — easier to route
|
|
573
|
+
|
|
529
574
|
---
|
|
530
575
|
|
|
531
576
|
## Navigation Seed Data Pattern (CRITICAL — routes must be full paths)
|
|
@@ -579,9 +624,9 @@ public static class SeedConstants
|
|
|
579
624
|
// Deterministic GUIDs (SHA256-based, reproducible across environments)
|
|
580
625
|
// NOTE: Application/Module/Section/Resource IDs are deterministic.
|
|
581
626
|
// Context IDs are NOT — they are pre-seeded by SmartStack core.
|
|
582
|
-
public static readonly Guid ApplicationId = DeterministicGuid("nav:business.
|
|
583
|
-
public static readonly Guid ModuleId = DeterministicGuid("nav:business.
|
|
584
|
-
public static readonly Guid SectionId = DeterministicGuid("nav:business.
|
|
627
|
+
public static readonly Guid ApplicationId = DeterministicGuid("nav:business.human-resources");
|
|
628
|
+
public static readonly Guid ModuleId = DeterministicGuid("nav:business.human-resources.employees");
|
|
629
|
+
public static readonly Guid SectionId = DeterministicGuid("nav:business.human-resources.employees.departments");
|
|
585
630
|
|
|
586
631
|
// FORBIDDEN — Context IDs are NOT deterministic, they come from SmartStack core:
|
|
587
632
|
// public static readonly Guid BusinessContextId = DeterministicGuid("nav:business"); // WRONG!
|
|
@@ -609,7 +654,7 @@ if (businessCtx == null) return; // Context not yet seeded by SmartStack core
|
|
|
609
654
|
|
|
610
655
|
// Application: /business/human-resources
|
|
611
656
|
var app = NavigationApplication.Create(
|
|
612
|
-
businessCtx.Id, "
|
|
657
|
+
businessCtx.Id, "human-resources", "Human Resources", "HR Management",
|
|
613
658
|
"Users", IconType.Lucide,
|
|
614
659
|
"/business/human-resources", // FULL PATH — starts with /, kebab-case
|
|
615
660
|
10);
|
|
@@ -664,6 +709,26 @@ services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
|
|
|
664
709
|
|
|
665
710
|
---
|
|
666
711
|
|
|
712
|
+
## DTO Type Mapping (CRITICAL)
|
|
713
|
+
|
|
714
|
+
> **Use the correct .NET type for each property.** Incorrect types cause runtime parsing errors.
|
|
715
|
+
|
|
716
|
+
| Property Pattern | .NET Type | JSON Format | Example |
|
|
717
|
+
|-----------------|-----------|-------------|---------|
|
|
718
|
+
| `*Date`, `StartDate`, `EndDate`, `BirthDate` | `DateOnly` | `"2025-03-15"` | `public DateOnly Date { get; set; }` |
|
|
719
|
+
| `CreatedAt`, `UpdatedAt` | `DateTime` | `"2025-03-15T10:30:00Z"` | `public DateTime CreatedAt { get; set; }` |
|
|
720
|
+
| `*Time`, `StartTime` | `TimeOnly` | `"14:30:00"` | `public TimeOnly StartTime { get; set; }` |
|
|
721
|
+
| Duration, hours | `decimal` | `8.5` | `public decimal HoursWorked { get; set; }` |
|
|
722
|
+
| FK reference | `Guid` | `"uuid-string"` | `public Guid EmployeeId { get; set; }` |
|
|
723
|
+
|
|
724
|
+
**FORBIDDEN in DTOs:**
|
|
725
|
+
- `string Date` / `string StartDate` — use `DateOnly`
|
|
726
|
+
- `string Time` — use `TimeOnly`
|
|
727
|
+
- `DateTime BirthDate` — use `DateOnly` (no time component needed)
|
|
728
|
+
- `int` for hours/duration — use `decimal` for fractional values
|
|
729
|
+
|
|
730
|
+
---
|
|
731
|
+
|
|
667
732
|
## Common Mistakes to Avoid
|
|
668
733
|
|
|
669
734
|
| Mistake | Reality |
|
|
@@ -674,7 +739,7 @@ services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
|
|
|
674
739
|
| `e.IsDeleted` filter | Does NOT exist — no soft delete |
|
|
675
740
|
| `SmartStack.Api.Core.Routing` | Wrong — use `SmartStack.Api.Routing` |
|
|
676
741
|
| `SystemEntity` base class | Does NOT exist — use `BaseEntity` for all |
|
|
677
|
-
| `[Route] + [NavRoute]` | Only `[NavRoute]` needed (resolves route from DB) |
|
|
742
|
+
| `[Route("api/...")] + [NavRoute]` | **FORBIDDEN** — causes 404s. Only `[NavRoute]` needed (resolves route from DB at startup). Remove ALL `[Route]` attributes when `[NavRoute]` is present. |
|
|
678
743
|
| `SmartStack.Domain.Common.Interfaces` | Wrong — interfaces are in `SmartStack.Domain.Common` directly |
|
|
679
744
|
| `[Authorize]` without `[RequirePermission]` | No RBAC enforcement — always use `[RequirePermission]` |
|
|
680
745
|
| `tenantId: Guid.Empty` in services | OWASP A01 — always use validated `_currentTenant.TenantId` |
|
|
@@ -684,8 +749,11 @@ services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
|
|
|
684
749
|
| `UnauthorizedAccessException("Tenant context is required")` | Returns 401 → clears frontend token. Use `TenantContextRequiredException()` (400) |
|
|
685
750
|
| Route `"humanresources"` in seed data | Must be full path `"/business/human-resources"` |
|
|
686
751
|
| Route without leading `/` | All routes must start with `/` |
|
|
752
|
+
| `business.humanresources.employees.read` in permissions | Permission segments MUST match NavRoute kebab-case: `business.human-resources.employees.read` |
|
|
687
753
|
| `Permission.Create()` | Does NOT exist — use `CreateForModule()`, `CreateForSection()`, etc. |
|
|
688
754
|
| `GetAllAsync()` without search param | ALL GetAll endpoints MUST support `?search=` for EntityLookup |
|
|
755
|
+
| `string Date` in DTO | Date-only fields MUST use `DateOnly`, NEVER `string` |
|
|
756
|
+
| `DateTime` for date-only | Use `DateOnly` when no time component needed |
|
|
689
757
|
| FK field as plain text input | Frontend MUST use `EntityLookup` component for Guid FK fields |
|
|
690
758
|
| `PagedResult<T>` / `PaginatedResultDto<T>` | FORBIDDEN — use `PaginatedResult<T>` only |
|
|
691
759
|
|