@atlashub/smartstack-cli 4.27.0 → 4.29.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/package.json +1 -1
- package/templates/skills/apex/references/core-seed-data.md +27 -4
- package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +29 -7
- package/templates/skills/apex/references/post-checks.md +324 -0
- package/templates/skills/apex/references/smartstack-frontend.md +23 -8
- package/templates/skills/apex/references/smartstack-layers.md +53 -6
- package/templates/skills/apex/steps/step-02-plan.md +9 -0
- package/templates/skills/apex/steps/step-03-execute.md +49 -3
- package/templates/skills/apex/steps/step-04-examine.md +4 -0
- package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +33 -0
- package/templates/skills/business-analyse/questionnaire/01-context.md +12 -12
- package/templates/skills/business-analyse/questionnaire/02-stakeholders-scope.md +45 -45
- package/templates/skills/business-analyse/questionnaire/03-data-ui.md +39 -39
- package/templates/skills/business-analyse/questionnaire/05-cross-module.md +32 -32
- package/templates/skills/business-analyse/questionnaire.md +11 -11
- package/templates/skills/business-analyse/references/consolidation-structural-checks.md +17 -0
- package/templates/skills/business-analyse/references/spec-auto-inference.md +12 -7
- package/templates/skills/business-analyse/steps/step-00-init.md +2 -2
- package/templates/skills/business-analyse/steps/step-01-cadrage.md +3 -3
- package/templates/skills/business-analyse/steps/step-02-structure.md +22 -8
- package/templates/skills/business-analyse/steps/step-03-specify.md +22 -15
- package/templates/skills/controller/references/mcp-scaffold-workflow.md +20 -0
- package/templates/skills/derive-prd/references/handoff-file-templates.md +25 -1
- package/templates/skills/derive-prd/references/handoff-seeddata-generation.md +3 -1
- package/templates/skills/ralph-loop/references/category-completeness.md +125 -0
- package/templates/skills/ralph-loop/references/compact-loop.md +66 -10
- package/templates/skills/ralph-loop/references/module-transition.md +60 -0
- package/templates/skills/ralph-loop/steps/step-04-check.md +207 -12
- package/templates/skills/ralph-loop/steps/step-05-report.md +205 -14
package/package.json
CHANGED
|
@@ -274,7 +274,7 @@ public static class {ModulePascal}NavigationSeedData
|
|
|
274
274
|
Description = "{desc_en}",
|
|
275
275
|
Icon = "{icon}", // Lucide React icon name
|
|
276
276
|
IconType = IconType.Lucide,
|
|
277
|
-
Route = ToKebabCase($"/{appCode}/{moduleCode}"),
|
|
277
|
+
Route = ToKebabCase($"/{appCode}/{moduleCode}"), // Absolute path for sidebar navigation (App.tsx routes are RELATIVE to module root)
|
|
278
278
|
DisplayOrder = {displayOrder},
|
|
279
279
|
IsActive = true
|
|
280
280
|
};
|
|
@@ -731,13 +731,33 @@ public class PermissionSeedEntry
|
|
|
731
731
|
}
|
|
732
732
|
```
|
|
733
733
|
|
|
734
|
-
### Step C2: Section-Level Permissions (CONDITIONAL
|
|
734
|
+
### Step C2: Section-Level Permissions (CONDITIONAL — filtered by `permissionMode`)
|
|
735
735
|
|
|
736
736
|
> When `seedDataCore.navigationSections` exists and is non-empty in feature.json,
|
|
737
737
|
> add section-level permission GUIDs and entries to `PermissionsSeedData.cs`.
|
|
738
|
+
>
|
|
739
|
+
> **Permission generation is conditional on each section's `permissionMode`:**
|
|
740
|
+
>
|
|
741
|
+
> | permissionMode | Permissions generated |
|
|
742
|
+
> |---|---|
|
|
743
|
+
> | `crud` | wildcard + read + create + update + delete |
|
|
744
|
+
> | `custom:action1,action2` | wildcard + read + custom actions |
|
|
745
|
+
> | `read-only` | read only (NO CRUD wildcard) |
|
|
746
|
+
> | `inherit` | NO section-level permissions (inherits from module) |
|
|
747
|
+
>
|
|
748
|
+
> **Default inference (when `permissionMode` is absent):**
|
|
749
|
+
> - `code === "detail"` → `inherit`
|
|
750
|
+
> - `code === "dashboard"` → `read-only`
|
|
751
|
+
> - otherwise → `crud`
|
|
752
|
+
>
|
|
753
|
+
> **IMPORTANT:** Sections with `permissionMode: inherit` MUST be skipped entirely.
|
|
754
|
+
> Do NOT generate any permission GUIDs, entries, or constants for them.
|
|
738
755
|
|
|
739
756
|
```csharp
|
|
740
757
|
// --- Add to {ModulePascal}PermissionsSeedData class AFTER module-level permissions ---
|
|
758
|
+
// FILTER: Only generate for sections where permissionMode is NOT "inherit"
|
|
759
|
+
// For "read-only" sections: generate ONLY ReadPermId (no Wildcard, Create, Update, Delete)
|
|
760
|
+
// For "custom:x,y" sections: generate Wildcard + Read + custom action GUIDs
|
|
741
761
|
|
|
742
762
|
// Section-level permissions (for each section in navSections[])
|
|
743
763
|
public static readonly Guid {SectionPascal}WildcardPermId = Guid.NewGuid();
|
|
@@ -1155,8 +1175,10 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
1155
1175
|
public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct)
|
|
1156
1176
|
{
|
|
1157
1177
|
// Check idempotence — verify by Code (roles may already exist from SmartStack core)
|
|
1178
|
+
// Scope by ApplicationId — without this, roles from OTHER apps cause false positives
|
|
1158
1179
|
var existingRoleCodes = await context.Roles
|
|
1159
|
-
.Where(r => r.
|
|
1180
|
+
.Where(r => r.ApplicationId == ApplicationRolesSeedData.ApplicationId
|
|
1181
|
+
&& (r.Code == "admin" || r.Code == "manager" || r.Code == "contributor" || r.Code == "viewer"))
|
|
1160
1182
|
.Select(r => r.Code)
|
|
1161
1183
|
.ToListAsync(ct);
|
|
1162
1184
|
|
|
@@ -1213,8 +1235,9 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
1213
1235
|
// Application-scoped roles (admin, manager, contributor, viewer) are created by
|
|
1214
1236
|
// SeedRolesAsync() above. System roles use their own IDs that do not match
|
|
1215
1237
|
// DeterministicGuid("role:admin").
|
|
1238
|
+
// Load THIS application's roles + system roles only
|
|
1216
1239
|
var roles = await context.Roles
|
|
1217
|
-
.Where(r => r.ApplicationId
|
|
1240
|
+
.Where(r => r.ApplicationId == ApplicationRolesSeedData.ApplicationId || r.IsSystem)
|
|
1218
1241
|
.ToListAsync(ct);
|
|
1219
1242
|
|
|
1220
1243
|
// Resolve permissions
|
|
@@ -56,20 +56,42 @@ Read App.tsx and detect which pattern is used:
|
|
|
56
56
|
|
|
57
57
|
→ Add routes to `applicationRoutes.{application}[]` with **RELATIVE** paths (no leading `/`)
|
|
58
58
|
|
|
59
|
+
> **CRITICAL — Include Module Segment:** Paths are relative to `/{application}/`.
|
|
60
|
+
> For a 3-level hierarchy (app → module → sections), paths MUST be `{module_kebab}/{section_kebab}`.
|
|
61
|
+
> Using just `{section_kebab}` omits the module segment → route resolves to `/{app}/{section}` instead
|
|
62
|
+
> of `/{app}/{module}/{section}` → mismatch with backend navigation seed data → 404.
|
|
63
|
+
|
|
59
64
|
```tsx
|
|
60
65
|
const applicationRoutes: ApplicationRouteExtensions = {
|
|
61
66
|
'human-resources': [
|
|
62
67
|
// existing routes...
|
|
68
|
+
// Paths include module segment: employee-management/employees (NOT just employees)
|
|
63
69
|
{ path: '{module_kebab}/{section_kebab}', element: <{EntityName}ListPage /> },
|
|
64
70
|
{ path: '{module_kebab}/{section_kebab}/create', element: <Create{EntityName}Page /> },
|
|
65
71
|
{ path: '{module_kebab}/{section_kebab}/:id', element: <{EntityName}DetailPage /> },
|
|
66
|
-
{ path: '{module_kebab}/{section_kebab}/:id/edit', element: <
|
|
72
|
+
{ path: '{module_kebab}/{section_kebab}/:id/edit', element: <Edit{EntityName}Page /> },
|
|
67
73
|
|
|
68
74
|
// Parent redirect routes (MANDATORY — prevents /login redirect on parent navigation)
|
|
69
75
|
{ path: '{module_kebab}', element: <Navigate to="{module_kebab}/{first_section_kebab}" replace /> },
|
|
70
76
|
{ path: '', element: <Navigate to="{first_module_kebab}/{first_section_kebab}" replace /> },
|
|
71
77
|
],
|
|
72
78
|
};
|
|
79
|
+
|
|
80
|
+
// Concrete example: app=human-resources, module=employee-management, sections=employees,absences
|
|
81
|
+
const applicationRoutes: ApplicationRouteExtensions = {
|
|
82
|
+
'human-resources': [
|
|
83
|
+
{ path: 'employee-management/employees', element: <EmployeesPage /> }, // ✅
|
|
84
|
+
{ path: 'employee-management/employees/create', element: <CreateEmployeePage /> }, // ✅
|
|
85
|
+
{ path: 'employee-management/employees/:id', element: <EmployeeDetailPage /> }, // ✅
|
|
86
|
+
// ...absences...
|
|
87
|
+
{ path: 'employee-management', element: <Navigate to="employee-management/employees" replace /> },
|
|
88
|
+
{ path: '', element: <Navigate to="employee-management/employees" replace /> },
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ❌ WRONG — missing module segment:
|
|
93
|
+
// { path: 'employees', ... } → resolves to /human-resources/employees (backend expects /human-resources/employee-management/employees)
|
|
94
|
+
```
|
|
73
95
|
```
|
|
74
96
|
|
|
75
97
|
Routes are automatically injected into BOTH standard (`/{application}/...`) and tenant-prefixed (`/t/:slug/{application}/...`) route trees by `mergeRoutes()`. No manual duplication needed.
|
|
@@ -162,10 +184,10 @@ For each application, add:
|
|
|
162
184
|
{ path: '{module}', element: <Navigate to="{module}/{first_section}" replace /> }
|
|
163
185
|
```
|
|
164
186
|
|
|
165
|
-
**Example:** For NavRoutes `human-resources.employees
|
|
187
|
+
**Example:** For NavRoutes `human-resources.employee-management.employees` and `human-resources.employee-management.absences`:
|
|
166
188
|
```tsx
|
|
167
|
-
{ path: '
|
|
168
|
-
{ path: '', element: <Navigate to="employees
|
|
189
|
+
{ path: 'employee-management', element: <Navigate to="employee-management/employees" replace /> },
|
|
190
|
+
{ path: '', element: <Navigate to="employee-management/employees" replace /> },
|
|
169
191
|
```
|
|
170
192
|
|
|
171
193
|
The `to` prop is resolved relative to the **parent route** (`/{application}`), so always use the full path from the application root.
|
|
@@ -186,12 +208,12 @@ The `to` prop is resolved relative to the **parent route** (`/{application}`), s
|
|
|
186
208
|
{ path: '/human-resources/employees/management', element: <EmployeePage /> },
|
|
187
209
|
];
|
|
188
210
|
```
|
|
189
|
-
Custom application routes go in `applicationRoutes` with RELATIVE paths:
|
|
211
|
+
Custom application routes go in `applicationRoutes` with RELATIVE paths (including module segment):
|
|
190
212
|
```tsx
|
|
191
|
-
// ✅ CORRECT
|
|
213
|
+
// ✅ CORRECT — module segment included
|
|
192
214
|
const applicationRoutes: ApplicationRouteExtensions = {
|
|
193
215
|
'human-resources': [
|
|
194
|
-
{ path: 'employees
|
|
216
|
+
{ path: 'employee-management/employees', element: <EmployeesPage /> },
|
|
195
217
|
],
|
|
196
218
|
};
|
|
197
219
|
```
|
|
@@ -132,10 +132,121 @@ if [ -n "$CTRL_FILES" ]; then
|
|
|
132
132
|
fi
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
+
### POST-CHECK S8: Write endpoints must NOT use Read permissions (BLOCKING)
|
|
136
|
+
|
|
137
|
+
> **Source:** audit ba-002 finding C1 — Cancel endpoint (POST) used `Permissions.Absences.Read`,
|
|
138
|
+
> allowing any reader to cancel absences. Write operations MUST use write permissions.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
142
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
143
|
+
for f in $CTRL_FILES; do
|
|
144
|
+
# Extract blocks: find each method with Http verb + RequirePermission
|
|
145
|
+
# Check POST/PUT/DELETE/PATCH endpoints that use *.Read permission
|
|
146
|
+
WRITE_WITH_READ=$(grep -Pn '\[(HttpPost|HttpPut|HttpDelete|HttpPatch)' "$f" | while read -r line; do
|
|
147
|
+
LINE_NUM=$(echo "$line" | cut -d: -f1)
|
|
148
|
+
# Look within 5 lines for RequirePermission with .Read
|
|
149
|
+
sed -n "$((LINE_NUM-2)),$((LINE_NUM+5))p" "$f" | grep -P 'RequirePermission.*\.Read\b' | head -1
|
|
150
|
+
done)
|
|
151
|
+
if [ -n "$WRITE_WITH_READ" ]; then
|
|
152
|
+
echo "BLOCKING: Write endpoint uses Read permission in $f"
|
|
153
|
+
echo "$WRITE_WITH_READ"
|
|
154
|
+
echo "Fix: POST/PUT/DELETE/PATCH endpoints must use Create/Update/Delete permissions, never Read"
|
|
155
|
+
exit 1
|
|
156
|
+
fi
|
|
157
|
+
done
|
|
158
|
+
fi
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### POST-CHECK S9: FK relationships must enforce tenant isolation (BLOCKING)
|
|
162
|
+
|
|
163
|
+
> **Source:** audit ba-002 finding C5 — Department.ManagerId FK referenced Employee without
|
|
164
|
+
> tenant constraint, allowing cross-tenant data leakage through navigation properties.
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
CONFIG_FILES=$(find src/ -path "*/Configurations/*" -name "*Configuration.cs" 2>/dev/null)
|
|
168
|
+
if [ -n "$CONFIG_FILES" ]; then
|
|
169
|
+
for f in $CONFIG_FILES; do
|
|
170
|
+
# Check if entity implements ITenantEntity
|
|
171
|
+
ENTITY_NAME=$(grep -oP 'IEntityTypeConfiguration<(\w+)>' "$f" | grep -oP '<\K[^>]+')
|
|
172
|
+
if [ -z "$ENTITY_NAME" ]; then continue; fi
|
|
173
|
+
|
|
174
|
+
# Check if the entity is tenant-scoped (has TenantId in its properties or base class)
|
|
175
|
+
ENTITY_FILE=$(find src/ -path "*/Domain/*" -name "${ENTITY_NAME}.cs" 2>/dev/null | head -1)
|
|
176
|
+
if [ -z "$ENTITY_FILE" ]; then continue; fi
|
|
177
|
+
|
|
178
|
+
IS_TENANT=$(grep -cE 'ITenantEntity|BaseEntity|TenantId' "$ENTITY_FILE")
|
|
179
|
+
if [ "$IS_TENANT" -eq 0 ]; then continue; fi
|
|
180
|
+
|
|
181
|
+
# For tenant entities, check FK relationships point to other tenant entities
|
|
182
|
+
# Warning: FK to non-tenant entity from tenant entity = potential cross-tenant leak
|
|
183
|
+
FK_TARGETS=$(grep -oP 'HasOne<(\w+)>|HasOne\(e\s*=>\s*e\.(\w+)\)' "$f" | grep -oP '\w+(?=>|\))' | sort -u)
|
|
184
|
+
for TARGET in $FK_TARGETS; do
|
|
185
|
+
TARGET_FILE=$(find src/ -path "*/Domain/*" -name "${TARGET}.cs" 2>/dev/null | head -1)
|
|
186
|
+
if [ -n "$TARGET_FILE" ]; then
|
|
187
|
+
TARGET_IS_TENANT=$(grep -cE 'ITenantEntity|BaseEntity|TenantId' "$TARGET_FILE")
|
|
188
|
+
if [ "$TARGET_IS_TENANT" -gt 0 ]; then
|
|
189
|
+
# Both are tenant entities — verify the FK has a composite or filtered relationship
|
|
190
|
+
# At minimum, warn that cross-tenant reference is possible without query filter
|
|
191
|
+
HAS_QUERY_FILTER=$(grep -c 'HasQueryFilter' "$f")
|
|
192
|
+
if [ "$HAS_QUERY_FILTER" -eq 0 ]; then
|
|
193
|
+
echo "WARNING: $f — FK from tenant entity $ENTITY_NAME to tenant entity $TARGET without HasQueryFilter"
|
|
194
|
+
echo "Cross-tenant data leakage possible if FK is assigned without tenant validation"
|
|
195
|
+
echo "Fix: Add tenant validation in service layer before assigning FK, or use composite FK (TenantId + EntityId)"
|
|
196
|
+
fi
|
|
197
|
+
fi
|
|
198
|
+
fi
|
|
199
|
+
done
|
|
200
|
+
done
|
|
201
|
+
fi
|
|
202
|
+
```
|
|
203
|
+
|
|
135
204
|
---
|
|
136
205
|
|
|
137
206
|
## Backend — Entity, Service & Controller Checks
|
|
138
207
|
|
|
208
|
+
### POST-CHECK V1: Controllers with POST/PUT must have corresponding Validators (BLOCKING)
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
212
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
213
|
+
for f in $CTRL_FILES; do
|
|
214
|
+
# Check if controller has POST or PUT endpoints
|
|
215
|
+
HAS_WRITE=$(grep -cE "\[Http(Post|Put)\]" "$f")
|
|
216
|
+
if [ "$HAS_WRITE" -gt 0 ]; then
|
|
217
|
+
# Extract DTO names from POST/PUT method parameters
|
|
218
|
+
DTOS=$(grep -oP '(?:Create|Update)\w+Dto' "$f" | sort -u)
|
|
219
|
+
for DTO in $DTOS; do
|
|
220
|
+
VALIDATOR_NAME=$(echo "$DTO" | sed 's/Dto$/Validator/')
|
|
221
|
+
VALIDATOR_FILE=$(find src/ -path "*/Validators/*" -name "${VALIDATOR_NAME}.cs" 2>/dev/null)
|
|
222
|
+
if [ -z "$VALIDATOR_FILE" ]; then
|
|
223
|
+
echo "BLOCKING: Controller $f uses $DTO but no ${VALIDATOR_NAME}.cs found"
|
|
224
|
+
echo "Fix: Create Validator with FluentValidation rules from business rules"
|
|
225
|
+
exit 1
|
|
226
|
+
fi
|
|
227
|
+
done
|
|
228
|
+
fi
|
|
229
|
+
done
|
|
230
|
+
fi
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### POST-CHECK V2: Validators must be registered in DI (WARNING)
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
VALIDATOR_FILES=$(find src/ -path "*/Validators/*" -name "*Validator.cs" 2>/dev/null)
|
|
237
|
+
if [ -n "$VALIDATOR_FILES" ]; then
|
|
238
|
+
DI_FILE=$(find src/ -name "DependencyInjection.cs" -o -name "ServiceCollectionExtensions.cs" 2>/dev/null | head -1)
|
|
239
|
+
if [ -n "$DI_FILE" ]; then
|
|
240
|
+
for f in $VALIDATOR_FILES; do
|
|
241
|
+
VALIDATOR_NAME=$(basename "$f" .cs)
|
|
242
|
+
if ! grep -q "$VALIDATOR_NAME" "$DI_FILE"; then
|
|
243
|
+
echo "WARNING: Validator $VALIDATOR_NAME not registered in DI: $DI_FILE"
|
|
244
|
+
fi
|
|
245
|
+
done
|
|
246
|
+
fi
|
|
247
|
+
fi
|
|
248
|
+
```
|
|
249
|
+
|
|
139
250
|
### POST-CHECK C1: Navigation routes must be full paths starting with /
|
|
140
251
|
|
|
141
252
|
```bash
|
|
@@ -1659,6 +1770,162 @@ if [ -n "$ENTITY_FILES" ]; then
|
|
|
1659
1770
|
fi
|
|
1660
1771
|
```
|
|
1661
1772
|
|
|
1773
|
+
### POST-CHECK C49: Route Ordering in App.tsx — static before dynamic (BLOCKING)
|
|
1774
|
+
|
|
1775
|
+
> **Source:** Dashboard 404 bug — `:id` route declared before `dashboard` route in applicationRoutes,
|
|
1776
|
+
> React Router matched `dashboard` as an `id` parameter → page not found.
|
|
1777
|
+
|
|
1778
|
+
```bash
|
|
1779
|
+
APP_TSX=$(find web/ src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
1780
|
+
if [ -n "$APP_TSX" ]; then
|
|
1781
|
+
# Extract all route paths from applicationRoutes in order
|
|
1782
|
+
ROUTE_PATHS=$(grep -oP "path:\s*'([^']+)'" "$APP_TSX" | sed "s/path: '//;s/'//")
|
|
1783
|
+
FAIL=false
|
|
1784
|
+
PREV_PREFIX=""
|
|
1785
|
+
PREV_IS_DYNAMIC=false
|
|
1786
|
+
|
|
1787
|
+
while IFS= read -r ROUTE; do
|
|
1788
|
+
[ -z "$ROUTE" ] && continue
|
|
1789
|
+
# Extract prefix (everything before last segment)
|
|
1790
|
+
PREFIX=$(echo "$ROUTE" | sed 's|/[^/]*$||')
|
|
1791
|
+
LAST_SEGMENT=$(echo "$ROUTE" | grep -oP '[^/]+$')
|
|
1792
|
+
IS_DYNAMIC=$(echo "$LAST_SEGMENT" | grep -c '^:')
|
|
1793
|
+
|
|
1794
|
+
# Check: if previous route was dynamic and current route is static with same prefix
|
|
1795
|
+
if [ "$PREV_IS_DYNAMIC" = true ] && [ "$IS_DYNAMIC" -eq 0 ] && [ "$PREFIX" = "$PREV_PREFIX" ]; then
|
|
1796
|
+
echo "BLOCKING: Static route '$ROUTE' is AFTER a dynamic route with prefix '$PREFIX' in App.tsx"
|
|
1797
|
+
echo " React Router will match the dynamic route first → '$ROUTE' is unreachable (404)"
|
|
1798
|
+
echo " Fix: Move static routes BEFORE dynamic routes within the same prefix group"
|
|
1799
|
+
FAIL=true
|
|
1800
|
+
fi
|
|
1801
|
+
|
|
1802
|
+
PREV_PREFIX="$PREFIX"
|
|
1803
|
+
if [ "$IS_DYNAMIC" -gt 0 ]; then
|
|
1804
|
+
PREV_IS_DYNAMIC=true
|
|
1805
|
+
else
|
|
1806
|
+
PREV_IS_DYNAMIC=false
|
|
1807
|
+
fi
|
|
1808
|
+
done <<< "$ROUTE_PATHS"
|
|
1809
|
+
|
|
1810
|
+
# Also check: Navigate (redirect) routes must be at the end
|
|
1811
|
+
REDIRECT_LINE=$(grep -n "Navigate" "$APP_TSX" | head -1 | cut -d: -f1)
|
|
1812
|
+
if [ -n "$REDIRECT_LINE" ]; then
|
|
1813
|
+
ROUTES_AFTER=$(tail -n +"$((REDIRECT_LINE+1))" "$APP_TSX" | grep -cP "path:\s*'" 2>/dev/null)
|
|
1814
|
+
if [ "$ROUTES_AFTER" -gt 0 ]; then
|
|
1815
|
+
echo "WARNING: Route definitions found AFTER Navigate redirect — redirects should be LAST"
|
|
1816
|
+
fi
|
|
1817
|
+
fi
|
|
1818
|
+
|
|
1819
|
+
if [ "$FAIL" = true ]; then
|
|
1820
|
+
exit 1
|
|
1821
|
+
fi
|
|
1822
|
+
fi
|
|
1823
|
+
```
|
|
1824
|
+
|
|
1825
|
+
### POST-CHECK C50: NavRoute Uniqueness — no duplicate NavRoute values (BLOCKING)
|
|
1826
|
+
|
|
1827
|
+
> **Source:** Two controllers sharing the same NavRoute causes routing conflicts — one endpoint
|
|
1828
|
+
> silently overrides the other → 404 on the shadowed controller.
|
|
1829
|
+
|
|
1830
|
+
```bash
|
|
1831
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
1832
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
1833
|
+
# Extract all NavRoute values with their file paths
|
|
1834
|
+
NAVROUTES=$(grep -Pn '\[NavRoute\("([^"]+)"\)' $CTRL_FILES 2>/dev/null | sed 's/.*\[NavRoute("\([^"]*\)").*/\1/' | sort)
|
|
1835
|
+
DUPLICATES=$(echo "$NAVROUTES" | uniq -d)
|
|
1836
|
+
if [ -n "$DUPLICATES" ]; then
|
|
1837
|
+
echo "BLOCKING: Duplicate NavRoute values detected:"
|
|
1838
|
+
for DUP in $DUPLICATES; do
|
|
1839
|
+
echo " NavRoute: $DUP"
|
|
1840
|
+
grep -l "\[NavRoute(\"$DUP\")\]" $CTRL_FILES 2>/dev/null | while read -r f; do
|
|
1841
|
+
echo " - $f"
|
|
1842
|
+
done
|
|
1843
|
+
done
|
|
1844
|
+
echo " Fix: Each controller MUST have a unique NavRoute value"
|
|
1845
|
+
exit 1
|
|
1846
|
+
fi
|
|
1847
|
+
fi
|
|
1848
|
+
```
|
|
1849
|
+
|
|
1850
|
+
### POST-CHECK C51: NavRoute Segments vs Controller Hierarchy (WARNING)
|
|
1851
|
+
|
|
1852
|
+
> **Source:** ContractsController in `Controllers/{App}/Employees/` subfolder had NavRoute
|
|
1853
|
+
> `human-resources.contracts` (2 segments) instead of `human-resources.employees.contracts`
|
|
1854
|
+
> (3 segments) → API resolved to wrong path → 404 on contracts endpoints.
|
|
1855
|
+
|
|
1856
|
+
```bash
|
|
1857
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
1858
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
1859
|
+
for f in $CTRL_FILES; do
|
|
1860
|
+
NAVROUTE=$(grep -oP '\[NavRoute\("\K[^"]+' "$f")
|
|
1861
|
+
if [ -z "$NAVROUTE" ]; then continue; fi
|
|
1862
|
+
|
|
1863
|
+
DOTS=$(echo "$NAVROUTE" | tr -cd '.' | wc -c)
|
|
1864
|
+
|
|
1865
|
+
# Count directory depth after Controllers/
|
|
1866
|
+
# e.g., Controllers/HumanResources/Employees/ContractsController.cs = depth 2 (section)
|
|
1867
|
+
REL_PATH=$(echo "$f" | sed 's|.*/Controllers/||')
|
|
1868
|
+
DIR_DEPTH=$(echo "$REL_PATH" | tr '/' '\n' | wc -l)
|
|
1869
|
+
# DIR_DEPTH: 2 = module level (App/Controller.cs), 3 = section level (App/Module/Controller.cs)
|
|
1870
|
+
|
|
1871
|
+
if [ "$DIR_DEPTH" -ge 3 ] && [ "$DOTS" -le 1 ]; then
|
|
1872
|
+
echo "WARNING: Controller in section subfolder but NavRoute has only $((DOTS+1)) segments"
|
|
1873
|
+
echo " File: $f"
|
|
1874
|
+
echo " NavRoute: $NAVROUTE"
|
|
1875
|
+
echo " Expected: 3+ segments (app.module.section) for controllers in section subfolders"
|
|
1876
|
+
echo " Fix: Update NavRoute to include the module segment (e.g., 'app.module.section')"
|
|
1877
|
+
fi
|
|
1878
|
+
|
|
1879
|
+
if [ "$DOTS" -eq 0 ]; then
|
|
1880
|
+
echo "BLOCKING: NavRoute '$NAVROUTE' has only 1 segment (minimum 2 required): $f"
|
|
1881
|
+
exit 1
|
|
1882
|
+
fi
|
|
1883
|
+
done
|
|
1884
|
+
fi
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
### POST-CHECK C52: Frontend route paths must include module segment (BLOCKING)
|
|
1888
|
+
|
|
1889
|
+
> **Source:** APEX regression — frontend routes used `employees` instead of `employee-management/employees`,
|
|
1890
|
+
> causing mismatch with backend navigation seed data routes (`/human-resources/employee-management/employees`).
|
|
1891
|
+
> Nav links produced 404 because React Router had no matching route for the full path.
|
|
1892
|
+
|
|
1893
|
+
```bash
|
|
1894
|
+
# Compare frontend route paths in App.tsx against backend navigation seed data routes
|
|
1895
|
+
APP_TSX=$(find . -path "*/src/App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
1896
|
+
if [ -n "$APP_TSX" ]; then
|
|
1897
|
+
# Extract all route paths from applicationRoutes
|
|
1898
|
+
ROUTE_PATHS=$(grep -oP "path:\s*'[^']+'" "$APP_TSX" | grep -v "^path: ''" | grep -v ":id" | grep -v "create" | grep -v "edit" | sed "s/path: '//;s/'//")
|
|
1899
|
+
|
|
1900
|
+
# Find navigation seed data files to get backend routes
|
|
1901
|
+
NAV_SEED_FILES=$(find src/ -name "*NavigationSeedData.cs" -o -name "*NavigationModuleSeedData.cs" 2>/dev/null)
|
|
1902
|
+
if [ -n "$NAV_SEED_FILES" ]; then
|
|
1903
|
+
# Extract module codes from seed data
|
|
1904
|
+
MODULE_CODES=$(grep -oP 'Code\s*=\s*"\K[^"]+' $NAV_SEED_FILES 2>/dev/null | sort -u)
|
|
1905
|
+
|
|
1906
|
+
for route in $ROUTE_PATHS; do
|
|
1907
|
+
# Skip redirect paths (empty or just module)
|
|
1908
|
+
if [ -z "$route" ]; then continue; fi
|
|
1909
|
+
|
|
1910
|
+
# Check if route contains a slash (module/section pattern)
|
|
1911
|
+
if ! echo "$route" | grep -q "/"; then
|
|
1912
|
+
# Single segment route — check if it matches a section code (not a module code)
|
|
1913
|
+
IS_MODULE=false
|
|
1914
|
+
for mod in $MODULE_CODES; do
|
|
1915
|
+
if [ "$route" = "$mod" ]; then IS_MODULE=true; break; fi
|
|
1916
|
+
done
|
|
1917
|
+
if [ "$IS_MODULE" = false ]; then
|
|
1918
|
+
echo "BLOCKING: Frontend route '$route' is missing module segment"
|
|
1919
|
+
echo " Expected: '{module}/{route}' (e.g., 'employee-management/$route')"
|
|
1920
|
+
echo " Backend navigation seed data defines routes with full hierarchy: /app/module/section"
|
|
1921
|
+
echo " Frontend routes must match: module/section (relative to app root)"
|
|
1922
|
+
fi
|
|
1923
|
+
fi
|
|
1924
|
+
done
|
|
1925
|
+
fi
|
|
1926
|
+
fi
|
|
1927
|
+
```
|
|
1928
|
+
|
|
1662
1929
|
---
|
|
1663
1930
|
|
|
1664
1931
|
## Architecture — Clean Architecture Layer Isolation
|
|
@@ -1785,6 +2052,63 @@ if [ -n "$CTRL_FILES" ]; then
|
|
|
1785
2052
|
fi
|
|
1786
2053
|
```
|
|
1787
2054
|
|
|
2055
|
+
### POST-CHECK A8: API endpoints must match handoff apiEndpointSummary (BLOCKING)
|
|
2056
|
+
|
|
2057
|
+
> **LESSON LEARNED (audit ba-002):** 4/17 API endpoints (export, calculate, balance upsert, year-end)
|
|
2058
|
+
> were missing but all API tasks were marked COMPLETE. This check reconciles actual controller
|
|
2059
|
+
> endpoints against the handoff contract.
|
|
2060
|
+
|
|
2061
|
+
```bash
|
|
2062
|
+
# Read apiEndpointSummary from PRD and verify each operation exists in controllers
|
|
2063
|
+
PRD_FILE=".ralph/prd.json"
|
|
2064
|
+
if [ ! -f "$PRD_FILE" ]; then
|
|
2065
|
+
# Try module-specific PRD
|
|
2066
|
+
if [ -f ".ralph/modules-queue.json" ]; then
|
|
2067
|
+
PRD_FILE=$(cat .ralph/modules-queue.json | grep -o '"prdFile":"[^"]*"' | tail -1 | cut -d'"' -f4)
|
|
2068
|
+
fi
|
|
2069
|
+
fi
|
|
2070
|
+
|
|
2071
|
+
if [ -f "$PRD_FILE" ]; then
|
|
2072
|
+
# Extract operation names from apiEndpointSummary
|
|
2073
|
+
OPERATIONS=$(cat "$PRD_FILE" | grep -o '"operation"\s*:\s*"[^"]*"' | cut -d'"' -f4 2>/dev/null)
|
|
2074
|
+
|
|
2075
|
+
if [ -n "$OPERATIONS" ]; then
|
|
2076
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
2077
|
+
MISSING_OPS=""
|
|
2078
|
+
TOTAL_OPS=0
|
|
2079
|
+
FOUND_OPS=0
|
|
2080
|
+
|
|
2081
|
+
for op in $OPERATIONS; do
|
|
2082
|
+
TOTAL_OPS=$((TOTAL_OPS + 1))
|
|
2083
|
+
FOUND=false
|
|
2084
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
2085
|
+
for f in $CTRL_FILES; do
|
|
2086
|
+
if grep -q "$op" "$f" 2>/dev/null; then
|
|
2087
|
+
FOUND=true
|
|
2088
|
+
break
|
|
2089
|
+
fi
|
|
2090
|
+
done
|
|
2091
|
+
fi
|
|
2092
|
+
if [ "$FOUND" = true ]; then
|
|
2093
|
+
FOUND_OPS=$((FOUND_OPS + 1))
|
|
2094
|
+
else
|
|
2095
|
+
MISSING_OPS="$MISSING_OPS $op"
|
|
2096
|
+
fi
|
|
2097
|
+
done
|
|
2098
|
+
|
|
2099
|
+
if [ -n "$MISSING_OPS" ]; then
|
|
2100
|
+
echo "BLOCKING: API endpoints missing from controllers (handoff contract violation)"
|
|
2101
|
+
echo "Found: $FOUND_OPS/$TOTAL_OPS operations"
|
|
2102
|
+
echo "Missing operations:$MISSING_OPS"
|
|
2103
|
+
echo "Fix: Implement missing endpoints in the appropriate Controller"
|
|
2104
|
+
exit 1
|
|
2105
|
+
else
|
|
2106
|
+
echo "POST-CHECK A8: OK — $FOUND_OPS/$TOTAL_OPS API operations found"
|
|
2107
|
+
fi
|
|
2108
|
+
fi
|
|
2109
|
+
fi
|
|
2110
|
+
```
|
|
2111
|
+
|
|
1788
2112
|
---
|
|
1789
2113
|
|
|
1790
2114
|
## Summary
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
```tsx
|
|
16
16
|
// Named exports — use .then() to wrap
|
|
17
|
+
// Path: @/pages/{App}/{Module}/{Section}/{Page}
|
|
17
18
|
const EmployeesPage = lazy(() =>
|
|
18
|
-
import('@/pages/HumanResources/Employees/EmployeesPage')
|
|
19
|
+
import('@/pages/HumanResources/EmployeeManagement/Employees/EmployeesPage')
|
|
19
20
|
.then(m => ({ default: m.EmployeesPage }))
|
|
20
21
|
);
|
|
21
22
|
|
|
@@ -49,7 +50,7 @@ element: (
|
|
|
49
50
|
**Incorrect patterns:**
|
|
50
51
|
```tsx
|
|
51
52
|
// WRONG: static import in route file
|
|
52
|
-
import { EmployeesPage } from '@/pages/HumanResources/Employees/EmployeesPage';
|
|
53
|
+
import { EmployeesPage } from '@/pages/HumanResources/EmployeeManagement/Employees/EmployeesPage';
|
|
53
54
|
|
|
54
55
|
// WRONG: no Suspense wrapper
|
|
55
56
|
element: <EmployeesPage />
|
|
@@ -64,8 +65,9 @@ In the client `App.tsx` (where application routes are defined), all page imports
|
|
|
64
65
|
|
|
65
66
|
**Correct — Lazy imports in client App.tsx:**
|
|
66
67
|
```tsx
|
|
68
|
+
// Path includes module level: {App}/{Module}/{Section}/{Page}
|
|
67
69
|
const ClientsListPage = lazy(() =>
|
|
68
|
-
import('@/pages/HumanResources/Clients/ClientsListPage')
|
|
70
|
+
import('@/pages/HumanResources/ClientManagement/Clients/ClientsListPage')
|
|
69
71
|
.then(m => ({ default: m.ClientsListPage }))
|
|
70
72
|
);
|
|
71
73
|
```
|
|
@@ -73,7 +75,7 @@ const ClientsListPage = lazy(() =>
|
|
|
73
75
|
**Do not use — Static imports in client App.tsx:**
|
|
74
76
|
```tsx
|
|
75
77
|
// WRONG: Static import kills code splitting
|
|
76
|
-
import { ClientsListPage } from '@/pages/HumanResources/Clients/ClientsListPage';
|
|
78
|
+
import { ClientsListPage } from '@/pages/HumanResources/ClientManagement/Clients/ClientsListPage';
|
|
77
79
|
```
|
|
78
80
|
|
|
79
81
|
> **Note:** The `smartstackRoutes.tsx` from the npm package may use static imports internally — this is acceptable for the package. But client `App.tsx` code MUST always use lazy imports for business pages.
|
|
@@ -698,18 +700,19 @@ export function EntityEditPage() {
|
|
|
698
700
|
|
|
699
701
|
```tsx
|
|
700
702
|
// In route files — form pages are also lazy-loaded
|
|
703
|
+
// Path: @/pages/{App}/{Module}/{Section}/{Page}
|
|
701
704
|
const EntityCreatePage = lazy(() =>
|
|
702
|
-
import('@/pages/HumanResources/Employees/EntityCreatePage')
|
|
705
|
+
import('@/pages/HumanResources/EmployeeManagement/Employees/EntityCreatePage')
|
|
703
706
|
.then(m => ({ default: m.EntityCreatePage }))
|
|
704
707
|
);
|
|
705
708
|
const EntityEditPage = lazy(() =>
|
|
706
|
-
import('@/pages/HumanResources/Employees/EntityEditPage')
|
|
709
|
+
import('@/pages/HumanResources/EmployeeManagement/Employees/EntityEditPage')
|
|
707
710
|
.then(m => ({ default: m.EntityEditPage }))
|
|
708
711
|
);
|
|
709
712
|
|
|
710
|
-
// Route registration —
|
|
713
|
+
// Route registration — Pattern B (JSX nested children):
|
|
711
714
|
{
|
|
712
|
-
path: 'employees',
|
|
715
|
+
path: 'employee-management/employees',
|
|
713
716
|
children: [
|
|
714
717
|
{ index: true, element: <Suspense fallback={<PageLoader />}><EmployeesPage /></Suspense> },
|
|
715
718
|
{ path: 'create', element: <Suspense fallback={<PageLoader />}><EntityCreatePage /></Suspense> },
|
|
@@ -718,6 +721,18 @@ const EntityEditPage = lazy(() =>
|
|
|
718
721
|
]
|
|
719
722
|
}
|
|
720
723
|
|
|
724
|
+
// Route registration — Pattern A (applicationRoutes flat paths):
|
|
725
|
+
// CRITICAL: paths are RELATIVE to /{app}/ and MUST include the module segment
|
|
726
|
+
const applicationRoutes: ApplicationRouteExtensions = {
|
|
727
|
+
'human-resources': [
|
|
728
|
+
{ path: 'employee-management/employees', element: <EmployeesPage /> }, // ✅ includes module
|
|
729
|
+
{ path: 'employee-management/employees/create', element: <CreateEmployeePage /> }, // ✅
|
|
730
|
+
{ path: 'employee-management/employees/:id', element: <EmployeeDetailPage /> }, // ✅
|
|
731
|
+
{ path: 'employee-management/employees/:id/edit', element: <EditEmployeePage /> }, // ✅
|
|
732
|
+
// ❌ WRONG: { path: 'employees', ... } — missing module segment → 404 on nav click
|
|
733
|
+
],
|
|
734
|
+
};
|
|
735
|
+
|
|
721
736
|
// Section-level routes — children of the module route (when module has sections)
|
|
722
737
|
//
|
|
723
738
|
// > **IMPORTANT:** The `list` and `detail` sections do NOT generate additional route entries.
|
|
@@ -257,6 +257,8 @@ var sectionRoute = $"{moduleRoute}/{ToKebabCase(sectionCode)}";
|
|
|
257
257
|
- POST-CHECK C34 detects non-kebab-case NavRoute values. POST-CHECK C35 detects non-kebab-case permissions
|
|
258
258
|
- Do not combine `[Route("api/...")]` with `[NavRoute]` — causes 404s (POST-CHECK C43)
|
|
259
259
|
- `[NavRoute]` is the only route attribute needed — resolves routes from DB at startup
|
|
260
|
+
- **NavRoute uniqueness:** Each controller MUST have a unique NavRoute value. Two controllers sharing the same NavRoute causes routing conflicts and 404s (POST-CHECK C50)
|
|
261
|
+
- **NavRoute segment count vs hierarchy:** If a controller lives in a section subfolder (e.g., `Controllers/{App}/Employees/ContractsController.cs`), its NavRoute MUST have 3 segments (`app.module.section`), not 2. A 2-segment NavRoute on a section controller causes API 404s because the resolved route doesn't match the expected path (POST-CHECK C51)
|
|
260
262
|
- Do not use `[Authorize]` without specific permission
|
|
261
263
|
- Swagger XML documentation
|
|
262
264
|
- Return DTOs, never domain entities
|
|
@@ -307,16 +309,61 @@ const [loading, setLoading] = useState(true);
|
|
|
307
309
|
|
|
308
310
|
**Loader:** `Loader2` from `lucide-react` for spinners, `PageLoader` for Suspense fallback
|
|
309
311
|
|
|
312
|
+
### RULE — Frontend Route Ordering
|
|
313
|
+
|
|
314
|
+
> **CRITICAL:** React Router matches routes top-to-bottom. Dynamic routes (`:id`, `:id/edit`) placed BEFORE static routes (`dashboard`, `create`) will swallow the static paths — e.g., `/employees/dashboard` matches `:id` with `id="dashboard"` → 404.
|
|
315
|
+
|
|
316
|
+
**Order (MANDATORY):**
|
|
317
|
+
|
|
318
|
+
1. Index/list route (path: `'{module}/{section}'`)
|
|
319
|
+
2. Create route (`'{module}/{section}/create'`)
|
|
320
|
+
3. Static sections (`'{module}/{section}/dashboard'`, etc.)
|
|
321
|
+
4. Dynamic routes (`'{module}/{section}/:id'`, `'{module}/{section}/:id/edit'`)
|
|
322
|
+
5. Module redirect (`'{module}'` → `'{module}/{first_section}'`)
|
|
323
|
+
6. Application redirect (`''` → `'{first_module}/{first_section}'`) — ALWAYS LAST
|
|
324
|
+
|
|
325
|
+
```tsx
|
|
326
|
+
// ✅ CORRECT — module segment included, static before dynamic
|
|
327
|
+
// (inside applicationRoutes['human-resources'], paths are relative to /human-resources/)
|
|
328
|
+
{ path: 'employee-management/employees', element: <EmployeesPage /> },
|
|
329
|
+
{ path: 'employee-management/employees/create', element: <CreateEmployeePage /> },
|
|
330
|
+
{ path: 'employee-management/employees/dashboard', element: <EmployeeDashboardPage /> },
|
|
331
|
+
{ path: 'employee-management/employees/:id', element: <EmployeeDetailPage /> },
|
|
332
|
+
{ path: 'employee-management/employees/:id/edit', element: <EditEmployeePage /> },
|
|
333
|
+
{ path: 'employee-management', element: <Navigate to="employee-management/employees" replace /> },
|
|
334
|
+
{ path: '', element: <Navigate to="employee-management/employees" replace /> },
|
|
335
|
+
|
|
336
|
+
// ❌ WRONG — missing module segment (resolves to /human-resources/employees instead of /human-resources/employee-management/employees)
|
|
337
|
+
{ path: 'employees', element: <EmployeesPage /> },
|
|
338
|
+
|
|
339
|
+
// ❌ WRONG — :id before dashboard → dashboard is unreachable (matched as id="dashboard")
|
|
340
|
+
{ path: 'employee-management/employees/:id', element: <EmployeeDetailPage /> },
|
|
341
|
+
{ path: 'employee-management/employees/dashboard', element: <EmployeeDashboardPage /> }, // NEVER REACHED
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
POST-CHECK C49 detects this anti-pattern and BLOCKS.
|
|
345
|
+
|
|
310
346
|
### Section Routes (when module has sections)
|
|
311
347
|
|
|
312
|
-
If the module defines `{sections}`, generate frontend routes for EACH section as children of the
|
|
348
|
+
If the module defines `{sections}`, generate frontend routes for EACH section as children of the application route.
|
|
349
|
+
|
|
350
|
+
> **CRITICAL — Module Segment Required:** Route paths in `applicationRoutes['{app}']` are RELATIVE to the application root.
|
|
351
|
+
> They MUST include the module segment: `{module-kebab}/{section-kebab}`, NOT just `{section-kebab}`.
|
|
352
|
+
> Without the module segment, the route resolves to `/{app}/{section}` instead of `/{app}/{module}/{section}`,
|
|
353
|
+
> causing a mismatch with backend navigation seed data routes → nav links produce 404s.
|
|
313
354
|
|
|
314
355
|
```tsx
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
{ path: '{section-kebab}
|
|
319
|
-
{ path: '{section-kebab}
|
|
356
|
+
// Routes in applicationRoutes['human-resources'] — RELATIVE paths include module segment
|
|
357
|
+
// IMPORTANT: static routes BEFORE dynamic routes (see Route Ordering rule above)
|
|
358
|
+
// Example: app=human-resources, module=employee-management, section=employees
|
|
359
|
+
{ path: '{module-kebab}/{section-kebab}', element: <Suspense fallback={<PageLoader />}><{Section}Page /></Suspense> },
|
|
360
|
+
{ path: '{module-kebab}/{section-kebab}/create', element: <Suspense fallback={<PageLoader />}><Create{Section}Page /></Suspense> },
|
|
361
|
+
{ path: '{module-kebab}/{section-kebab}/:id', element: <Suspense fallback={<PageLoader />}><{Section}DetailPage /></Suspense> },
|
|
362
|
+
{ path: '{module-kebab}/{section-kebab}/:id/edit', element: <Suspense fallback={<PageLoader />}><Edit{Section}Page /></Suspense> },
|
|
363
|
+
|
|
364
|
+
// Concrete example:
|
|
365
|
+
// { path: 'employee-management/employees', element: ... }, → /human-resources/employee-management/employees ✅
|
|
366
|
+
// { path: 'employees', element: ... }, → /human-resources/employees ❌ WRONG (missing module)
|
|
320
367
|
```
|
|
321
368
|
|
|
322
369
|
Section pages live in `src/pages/{AppPascal}/{Module}/{Section}/`.
|