@atlashub/smartstack-cli 3.32.0 → 3.34.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 +5 -1
- package/.documentation/installation.html +5 -1
- package/.documentation/ralph-loop.html +365 -216
- package/.documentation/test-web.html +5 -1
- package/package.json +1 -1
- package/templates/agents/ba-writer.md +142 -15
- package/templates/skills/apex/SKILL.md +7 -1
- 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 +238 -3
- package/templates/skills/apex/references/smartstack-api.md +47 -7
- package/templates/skills/apex/references/smartstack-frontend.md +47 -1
- package/templates/skills/apex/references/smartstack-layers.md +3 -1
- package/templates/skills/apex/steps/step-00-init.md +48 -1
- package/templates/skills/apex/steps/step-01-analyze.md +37 -0
- package/templates/skills/apex/steps/step-02-plan.md +36 -0
- package/templates/skills/apex/steps/step-03-execute.md +42 -2
- package/templates/skills/apex/steps/step-04-examine.md +110 -2
- package/templates/skills/business-analyse/SKILL.md +29 -19
- package/templates/skills/business-analyse/_module-loop.md +68 -9
- package/templates/skills/business-analyse/_shared.md +71 -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 +2 -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 +30 -4
- package/templates/skills/business-analyse/steps/step-01-cadrage.md +62 -2
- package/templates/skills/business-analyse/steps/step-01b-applications.md +252 -0
- package/templates/skills/business-analyse/steps/step-02-decomposition.md +23 -6
- package/templates/skills/business-analyse/steps/step-03d-validate.md +27 -6
- 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 +9 -17
- package/templates/skills/ralph-loop/references/category-rules.md +43 -692
- package/templates/skills/ralph-loop/references/compact-loop.md +104 -427
- package/templates/skills/ralph-loop/references/team-orchestration.md +13 -14
- package/templates/skills/ralph-loop/steps/step-02-execute.md +49 -704
- 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 +12 -123
|
@@ -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,209 @@ 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
|
+
|
|
1019
1254
|
**If ANY POST-CHECK fails → fix in step-03, re-validate.**
|
|
@@ -493,6 +493,12 @@ public class {Name}Controller : ControllerBase
|
|
|
493
493
|
|
|
494
494
|
**CRITICAL:** Use `[RequirePermission(Permissions.{Module}.{Action})]` on EVERY endpoint — NEVER `[Authorize]` alone (no RBAC enforcement).
|
|
495
495
|
|
|
496
|
+
**CRITICAL — Permission paths use IDENTICAL segments to NavRoute codes (kebab-case):**
|
|
497
|
+
- NavRoute: `business.human-resources.employees` → Permission: `business.human-resources.employees.read`
|
|
498
|
+
- NavRoute: `business.human-resources.employees.leaves` → Permission: `business.human-resources.employees.leaves.read`
|
|
499
|
+
- FORBIDDEN: `business.humanresources.employees.read` (no kebab-case — mismatches NavRoute)
|
|
500
|
+
- SmartStack.app convention: `business.support-client.my-tickets.read` (always kebab-case)
|
|
501
|
+
|
|
496
502
|
### Section-Level Controller (NavRoute with 4 segments)
|
|
497
503
|
|
|
498
504
|
When a module has sections, each section gets its own controller with a 4-segment navRoute:
|
|
@@ -504,7 +510,7 @@ When a module has sections, each section gets its own controller with a 4-segmen
|
|
|
504
510
|
[Authorize]
|
|
505
511
|
public class {Section}Controller : ControllerBase
|
|
506
512
|
{
|
|
507
|
-
// Example: business.
|
|
513
|
+
// Example: business.human-resources.employees.departments
|
|
508
514
|
[HttpGet]
|
|
509
515
|
[RequirePermission(Permissions.{Section}.Read)]
|
|
510
516
|
public async Task<ActionResult<PaginatedResult<{Section}ResponseDto>>> GetAll(
|
|
@@ -519,13 +525,46 @@ public class {Section}Controller : ControllerBase
|
|
|
519
525
|
**NavRoute segment rules:**
|
|
520
526
|
| Level | NavRoute format | Example |
|
|
521
527
|
|-------|----------------|---------|
|
|
522
|
-
| Module | `{context}.{app}.{module}` (3 segments) | `business.
|
|
523
|
-
| Section | `{context}.{app}.{module}.{section}` (4 segments) | `business.
|
|
528
|
+
| Module | `{context}.{app}.{module}` (3 segments) | `business.human-resources.employees` |
|
|
529
|
+
| Section | `{context}.{app}.{module}.{section}` (4 segments) | `business.human-resources.employees.departments` |
|
|
524
530
|
|
|
525
531
|
**Namespace:** `SmartStack.Api.Routing` (NOT `SmartStack.Api.Core.Routing`)
|
|
526
532
|
|
|
527
533
|
**NavRoute resolves at startup from DB:** `platform.administration.users` → `api/platform/administration/users`
|
|
528
534
|
|
|
535
|
+
### Sub-Resource Pattern (NavRoute Suffix)
|
|
536
|
+
|
|
537
|
+
When an entity is a child of another entity (e.g., LeaveTypes under Leaves), use `[NavRoute(..., Suffix = "types")]`:
|
|
538
|
+
|
|
539
|
+
```csharp
|
|
540
|
+
// Sub-resource controller: types are nested under leaves
|
|
541
|
+
[ApiController]
|
|
542
|
+
[NavRoute("business.human-resources.employees.leaves", Suffix = "types")]
|
|
543
|
+
[Authorize]
|
|
544
|
+
public class LeaveTypesController : ControllerBase
|
|
545
|
+
{
|
|
546
|
+
[HttpGet]
|
|
547
|
+
[RequirePermission(Permissions.Leaves.Read)] // inherits parent section permission
|
|
548
|
+
public async Task<ActionResult<PaginatedResult<LeaveTypeResponseDto>>> GetAll(...)
|
|
549
|
+
=> Ok(await _service.GetAllAsync(search, page, pageSize, ct));
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**Alternative pattern** (sub-resource endpoints within parent controller):
|
|
554
|
+
```csharp
|
|
555
|
+
// LeaveTypes as endpoints within LeavesController
|
|
556
|
+
[HttpGet("types")]
|
|
557
|
+
[RequirePermission(Permissions.Leaves.Read)]
|
|
558
|
+
public async Task<ActionResult<PaginatedResult<LeaveTypeResponseDto>>> GetAllLeaveTypes(...)
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
> **CRITICAL — Sub-resource frontend completeness:**
|
|
562
|
+
> If a parent page has a button (e.g., "Manage Leave Types") that `navigate()`s to a sub-resource route,
|
|
563
|
+
> the frontend MUST include a page component for that route. Otherwise → dead link → white screen.
|
|
564
|
+
> - Either create a dedicated sub-resource ListPage (e.g., `LeaveTypesPage.tsx`)
|
|
565
|
+
> - Or DON'T include the navigate() button if pages won't be created
|
|
566
|
+
> - **Prefer separate controllers** (with Suffix) over sub-endpoints in parent controller — easier to route
|
|
567
|
+
|
|
529
568
|
---
|
|
530
569
|
|
|
531
570
|
## Navigation Seed Data Pattern (CRITICAL — routes must be full paths)
|
|
@@ -579,9 +618,9 @@ public static class SeedConstants
|
|
|
579
618
|
// Deterministic GUIDs (SHA256-based, reproducible across environments)
|
|
580
619
|
// NOTE: Application/Module/Section/Resource IDs are deterministic.
|
|
581
620
|
// 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.
|
|
621
|
+
public static readonly Guid ApplicationId = DeterministicGuid("nav:business.human-resources");
|
|
622
|
+
public static readonly Guid ModuleId = DeterministicGuid("nav:business.human-resources.employees");
|
|
623
|
+
public static readonly Guid SectionId = DeterministicGuid("nav:business.human-resources.employees.departments");
|
|
585
624
|
|
|
586
625
|
// FORBIDDEN — Context IDs are NOT deterministic, they come from SmartStack core:
|
|
587
626
|
// public static readonly Guid BusinessContextId = DeterministicGuid("nav:business"); // WRONG!
|
|
@@ -609,7 +648,7 @@ if (businessCtx == null) return; // Context not yet seeded by SmartStack core
|
|
|
609
648
|
|
|
610
649
|
// Application: /business/human-resources
|
|
611
650
|
var app = NavigationApplication.Create(
|
|
612
|
-
businessCtx.Id, "
|
|
651
|
+
businessCtx.Id, "human-resources", "Human Resources", "HR Management",
|
|
613
652
|
"Users", IconType.Lucide,
|
|
614
653
|
"/business/human-resources", // FULL PATH — starts with /, kebab-case
|
|
615
654
|
10);
|
|
@@ -684,6 +723,7 @@ services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
|
|
|
684
723
|
| `UnauthorizedAccessException("Tenant context is required")` | Returns 401 → clears frontend token. Use `TenantContextRequiredException()` (400) |
|
|
685
724
|
| Route `"humanresources"` in seed data | Must be full path `"/business/human-resources"` |
|
|
686
725
|
| Route without leading `/` | All routes must start with `/` |
|
|
726
|
+
| `business.humanresources.employees.read` in permissions | Permission segments MUST match NavRoute kebab-case: `business.human-resources.employees.read` |
|
|
687
727
|
| `Permission.Create()` | Does NOT exist — use `CreateForModule()`, `CreateForSection()`, etc. |
|
|
688
728
|
| `GetAllAsync()` without search param | ALL GetAll endpoints MUST support `?search=` for EntityLookup |
|
|
689
729
|
| FK field as plain text input | Frontend MUST use `EntityLookup` component for Guid FK fields |
|
|
@@ -325,7 +325,7 @@ export function EntityDetailPage() {
|
|
|
325
325
|
useEffect(() => {
|
|
326
326
|
if (!visitedTabsRef.current.has(activeTab)) {
|
|
327
327
|
visitedTabsRef.current.add(activeTab);
|
|
328
|
-
// Load tab-specific data here
|
|
328
|
+
// Load tab-specific data here (e.g., fetch leaves for this employee)
|
|
329
329
|
}
|
|
330
330
|
}, [activeTab]);
|
|
331
331
|
|
|
@@ -336,6 +336,52 @@ export function EntityDetailPage() {
|
|
|
336
336
|
}
|
|
337
337
|
```
|
|
338
338
|
|
|
339
|
+
### Tab Behavior Rules (CRITICAL)
|
|
340
|
+
|
|
341
|
+
> **CRITICAL: Tabs on detail pages switch content LOCALLY — they NEVER navigate to other pages.**
|
|
342
|
+
> Each tab renders its content INLINE within the same page component.
|
|
343
|
+
> Sub-resource data (e.g., an employee's leaves) loads via API call filtered by the parent entity ID.
|
|
344
|
+
|
|
345
|
+
**Tab state management:**
|
|
346
|
+
- Tabs use `useState<TabKey>('info')` for the active tab — LOCAL React state only
|
|
347
|
+
- Tab click handler: `onClick={() => setActiveTab(tabKey)}` — NEVER `navigate()`
|
|
348
|
+
- Tab content: conditional rendering `{activeTab === 'tabKey' && <TabContent />}`
|
|
349
|
+
- Lazy loading: `visitedTabsRef` tracks which tabs have been visited to avoid redundant API calls
|
|
350
|
+
|
|
351
|
+
**Tab content for sub-resources:**
|
|
352
|
+
```tsx
|
|
353
|
+
// CORRECT — sub-resource data loaded INLINE within the tab
|
|
354
|
+
{activeTab === 'leaves' && (
|
|
355
|
+
<div>
|
|
356
|
+
<LeaveRequestsTable employeeId={entity.id} />
|
|
357
|
+
{/* Optional "View all" link INSIDE the tab content area */}
|
|
358
|
+
<Link to={`../leaves?employee=${entity.id}`}>
|
|
359
|
+
{t('employees:tabs.viewAllLeaves', 'View all leave requests')}
|
|
360
|
+
</Link>
|
|
361
|
+
</div>
|
|
362
|
+
)}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**FORBIDDEN tab patterns:**
|
|
366
|
+
```tsx
|
|
367
|
+
// FORBIDDEN — tab click handler navigates to another page
|
|
368
|
+
const handleTabClick = (tab: TabKey) => {
|
|
369
|
+
setActiveTab(tab);
|
|
370
|
+
if (tab === 'leaves') navigate(`../leaves?employee=${id}`); // ← BREAKS tab UX
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// FORBIDDEN — tab content is empty because navigation already left the page
|
|
374
|
+
{activeTab === 'info' && <div>...</div>}
|
|
375
|
+
// Leaves tab: nothing renders here, user is already on another page
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Why this matters:**
|
|
379
|
+
- Navigating away loses the detail page context (entity data, scroll position, other tab state)
|
|
380
|
+
- Users expect tabs to switch content in-place, not redirect to a different page
|
|
381
|
+
- The browser back button should go to the list page, not toggle between tabs
|
|
382
|
+
|
|
383
|
+
**POST-CHECK 43 enforces this rule.**
|
|
384
|
+
|
|
339
385
|
---
|
|
340
386
|
|
|
341
387
|
## 3b. Form Pages Pattern (Create / Edit)
|
|
@@ -101,6 +101,8 @@
|
|
|
101
101
|
|
|
102
102
|
**Rules:**
|
|
103
103
|
- `[RequirePermission(Permissions.{Module}.{Action})]` on EVERY endpoint
|
|
104
|
+
- Permission paths MUST use kebab-case matching NavRoute codes (e.g., `business.human-resources.employees.read`)
|
|
105
|
+
- FORBIDDEN: concatenated segments like `humanresources` — must be `human-resources`
|
|
104
106
|
- NEVER use `[Authorize]` without specific permission
|
|
105
107
|
- Swagger XML documentation
|
|
106
108
|
- Return DTOs, never domain entities
|
|
@@ -178,7 +180,7 @@ private static Guid GenerateDeterministicGuid(string input)
|
|
|
178
180
|
|
|
179
181
|
| Level | Code (C#) | Route (DB) |
|
|
180
182
|
|-------|-----------|-----------|
|
|
181
|
-
| Application | `
|
|
183
|
+
| Application | `human-resources` | `/business/human-resources` |
|
|
182
184
|
| Module | `employees` | `/business/human-resources/employees` |
|
|
183
185
|
| Section | `departments` | `/business/human-resources/employees/departments` |
|
|
184
186
|
| Resource | `export` | `/business/human-resources/employees/departments/export` |
|
|
@@ -28,7 +28,7 @@ Extract flags from the raw input:
|
|
|
28
28
|
```
|
|
29
29
|
Input: "{raw_input}"
|
|
30
30
|
|
|
31
|
-
Flags to detect: -a, -x, -s, -e, -r, -pr
|
|
31
|
+
Flags to detect: -a, -x, -s, -e, -r, -pr, -d {path}
|
|
32
32
|
Remaining text after flag removal = {task_description}
|
|
33
33
|
```
|
|
34
34
|
|
|
@@ -41,6 +41,53 @@ save_mode: false
|
|
|
41
41
|
economy_mode: false
|
|
42
42
|
pr_mode: false
|
|
43
43
|
resume_mode: false
|
|
44
|
+
delegate_mode: false
|
|
45
|
+
delegate_prd_path: null
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**If `-d {path}` detected:** set `delegate_mode = true`, `delegate_prd_path = {path}`, force `auto_mode = true`, `economy_mode = true`.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 1b. Delegate Mode Fast Path (if -d)
|
|
53
|
+
|
|
54
|
+
> **When `/ralph-loop` invokes `/apex -d {prd_path}`, all context is extracted from the PRD file.**
|
|
55
|
+
> Skip interactive sections (hierarchy definition, challenge questions).
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
IF delegate_mode:
|
|
59
|
+
Read prd.json from {delegate_prd_path}
|
|
60
|
+
|
|
61
|
+
Extract context:
|
|
62
|
+
{context_code} = prd.project.context (e.g., "business")
|
|
63
|
+
{app_name} = prd.project.application (e.g., "HumanResources")
|
|
64
|
+
{module_code} = prd.project.module (e.g., "EmployeeManagement")
|
|
65
|
+
{prd_path} = {delegate_prd_path}
|
|
66
|
+
{feature_path} = prd.source.feature_json_path (if exists)
|
|
67
|
+
|
|
68
|
+
Extract sections from infrastructure tasks with _seedDataMeta:
|
|
69
|
+
Find task where _seedDataMeta.coreSeedData.navigationSections exists
|
|
70
|
+
{sections} = _seedDataMeta.coreSeedData.navigationSections[]
|
|
71
|
+
→ map to: { code, label, description, route, displayOrder, navigation }
|
|
72
|
+
|
|
73
|
+
Extract entities from domain tasks:
|
|
74
|
+
{entities} = prd.tasks.filter(t => t.category == 'domain').map(t => entity name from t.description)
|
|
75
|
+
|
|
76
|
+
Extract complexity:
|
|
77
|
+
{module_complexity} = heuristic from task count:
|
|
78
|
+
<= 10 tasks → "simple-crud"
|
|
79
|
+
<= 20 tasks → "crud-rules"
|
|
80
|
+
<= 30 tasks → "crud-workflow"
|
|
81
|
+
> 30 tasks → "complex"
|
|
82
|
+
|
|
83
|
+
Set needs:
|
|
84
|
+
{needs_seed_data} = prd.tasks.some(t => t.category == 'seedData' || t.category == 'infrastructure')
|
|
85
|
+
{needs_migration} = prd.tasks.some(t => t._migrationMeta != null)
|
|
86
|
+
{has_dependencies} = prd.tasks.some(t => t.dependencies.length > 0) ? "references" : "none"
|
|
87
|
+
{key_properties} = [] (extracted during step-01 from feature.json if available)
|
|
88
|
+
|
|
89
|
+
SKIP sections 2 (detect context), 4 (hierarchy), 5 (challenge questions)
|
|
90
|
+
Jump to section 3 (MCP verify) → then section 6 (determine needs, already set) → section 9 (summary)
|
|
44
91
|
```
|
|
45
92
|
|
|
46
93
|
---
|
|
@@ -34,6 +34,43 @@ This saves ~2 minutes of Glob searches on an empty project.
|
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
37
|
+
## 0b. Delegate Mode Fast Path (if delegate_mode)
|
|
38
|
+
|
|
39
|
+
> **When called via `/ralph-loop -d`, PRD tasks provide pre-computed scope. Reduce exploration to verification only.**
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
IF delegate_mode:
|
|
43
|
+
Read PRD from {delegate_prd_path} (already loaded in step-00)
|
|
44
|
+
|
|
45
|
+
1. Extract entity list from domain tasks:
|
|
46
|
+
entities[] = prd.tasks.filter(category == 'domain').map(entity name from description)
|
|
47
|
+
|
|
48
|
+
2. Extract FK relationships from frontend tasks with _frontendMeta:
|
|
49
|
+
fkFields[] = _frontendMeta.fkFields[] (e.g., ["DepartmentId→Department", "CompanyId→Company"])
|
|
50
|
+
|
|
51
|
+
3. Determine tenant modes per entity:
|
|
52
|
+
IF feature.json exists at {feature_path}:
|
|
53
|
+
Read entity definitions → extract tenantMode per entity
|
|
54
|
+
ELSE:
|
|
55
|
+
Default all entities to tenantMode: "strict"
|
|
56
|
+
|
|
57
|
+
4. Read feature.json for wireframes/componentMapping if {feature_path} exists:
|
|
58
|
+
→ enriches frontend generation in step-03
|
|
59
|
+
|
|
60
|
+
5. Perform LIGHTWEIGHT code exploration (verification only):
|
|
61
|
+
- Glob("**/Domain/Entities/**/{module_code}/**/*.cs") → what already exists?
|
|
62
|
+
- Glob("**/Infrastructure/Persistence/Seeding/Data/**/*SeedData*.cs") → existing seed data?
|
|
63
|
+
- Glob("src/pages/**/{module_code}/**/*.tsx") → existing pages?
|
|
64
|
+
→ Only to determine create vs modify vs skip (NOT deep analysis)
|
|
65
|
+
|
|
66
|
+
6. Gap Analysis: Mark all PRD tasks as "create" unless existing code found
|
|
67
|
+
|
|
68
|
+
SKIP: Full Agent Teams exploration (section 2), challenge question follow-up
|
|
69
|
+
Jump to section 5 (Gap Analysis summary) → section 7 (Analysis Summary)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
37
74
|
## 1. Context Sources
|
|
38
75
|
|
|
39
76
|
Read available context (in order of priority):
|
|
@@ -16,6 +16,42 @@ Read `references/smartstack-layers.md` for layer execution rules and skill/MCP m
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## 0. Delegate Mode Fast Path (if delegate_mode)
|
|
20
|
+
|
|
21
|
+
> **When called via `/ralph-loop -d`, PRD tasks already define the scope. Map directly to layers.**
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
IF delegate_mode:
|
|
25
|
+
Read PRD from {delegate_prd_path}
|
|
26
|
+
|
|
27
|
+
Map each PRD task to a layer based on task.category:
|
|
28
|
+
domain → Layer 0
|
|
29
|
+
infrastructure → Layer 0
|
|
30
|
+
application → Layer 1
|
|
31
|
+
api → Layer 1
|
|
32
|
+
seedData → Layer 1
|
|
33
|
+
frontend → Layer 2
|
|
34
|
+
test → Layer 3
|
|
35
|
+
|
|
36
|
+
For each task:
|
|
37
|
+
file_path = task.files_changed.created[0] (primary file)
|
|
38
|
+
action = "create" (default for delegate mode)
|
|
39
|
+
tool = infer from category:
|
|
40
|
+
domain/infrastructure → MCP scaffold_extension
|
|
41
|
+
api → /controller skill or MCP scaffold_extension
|
|
42
|
+
seedData → MCP generate_permissions + ref core-seed-data.md
|
|
43
|
+
frontend → /ui-components skill + MCP scaffold_api_client + MCP scaffold_routes
|
|
44
|
+
test → MCP scaffold_tests
|
|
45
|
+
acceptance_criteria = task.acceptance_criteria
|
|
46
|
+
|
|
47
|
+
Use task._seedDataMeta, _frontendMeta, _migrationMeta for enriched context.
|
|
48
|
+
|
|
49
|
+
SKIP: User checkpoint (section 6) — auto_mode implied
|
|
50
|
+
Jump to section 5 (Estimated Commits) → NEXT STEP
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
19
55
|
## 1. Map Changes to Layers
|
|
20
56
|
|
|
21
57
|
For each element identified in step-01 (create or modify), assign to a SmartStack layer:
|