@atlashub/smartstack-cli 4.27.0 → 4.28.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/post-checks.md +282 -0
- package/templates/skills/apex/references/smartstack-layers.md +31 -0
- package/templates/skills/apex/steps/step-02-plan.md +9 -0
- package/templates/skills/apex/steps/step-03-execute.md +39 -2
- package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +33 -0
- 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-02-structure.md +20 -6
- package/templates/skills/business-analyse/steps/step-03-specify.md +7 -0
- 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
|
|
@@ -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,120 @@ 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
|
+
|
|
1662
1887
|
---
|
|
1663
1888
|
|
|
1664
1889
|
## Architecture — Clean Architecture Layer Isolation
|
|
@@ -1785,6 +2010,63 @@ if [ -n "$CTRL_FILES" ]; then
|
|
|
1785
2010
|
fi
|
|
1786
2011
|
```
|
|
1787
2012
|
|
|
2013
|
+
### POST-CHECK A8: API endpoints must match handoff apiEndpointSummary (BLOCKING)
|
|
2014
|
+
|
|
2015
|
+
> **LESSON LEARNED (audit ba-002):** 4/17 API endpoints (export, calculate, balance upsert, year-end)
|
|
2016
|
+
> were missing but all API tasks were marked COMPLETE. This check reconciles actual controller
|
|
2017
|
+
> endpoints against the handoff contract.
|
|
2018
|
+
|
|
2019
|
+
```bash
|
|
2020
|
+
# Read apiEndpointSummary from PRD and verify each operation exists in controllers
|
|
2021
|
+
PRD_FILE=".ralph/prd.json"
|
|
2022
|
+
if [ ! -f "$PRD_FILE" ]; then
|
|
2023
|
+
# Try module-specific PRD
|
|
2024
|
+
if [ -f ".ralph/modules-queue.json" ]; then
|
|
2025
|
+
PRD_FILE=$(cat .ralph/modules-queue.json | grep -o '"prdFile":"[^"]*"' | tail -1 | cut -d'"' -f4)
|
|
2026
|
+
fi
|
|
2027
|
+
fi
|
|
2028
|
+
|
|
2029
|
+
if [ -f "$PRD_FILE" ]; then
|
|
2030
|
+
# Extract operation names from apiEndpointSummary
|
|
2031
|
+
OPERATIONS=$(cat "$PRD_FILE" | grep -o '"operation"\s*:\s*"[^"]*"' | cut -d'"' -f4 2>/dev/null)
|
|
2032
|
+
|
|
2033
|
+
if [ -n "$OPERATIONS" ]; then
|
|
2034
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
2035
|
+
MISSING_OPS=""
|
|
2036
|
+
TOTAL_OPS=0
|
|
2037
|
+
FOUND_OPS=0
|
|
2038
|
+
|
|
2039
|
+
for op in $OPERATIONS; do
|
|
2040
|
+
TOTAL_OPS=$((TOTAL_OPS + 1))
|
|
2041
|
+
FOUND=false
|
|
2042
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
2043
|
+
for f in $CTRL_FILES; do
|
|
2044
|
+
if grep -q "$op" "$f" 2>/dev/null; then
|
|
2045
|
+
FOUND=true
|
|
2046
|
+
break
|
|
2047
|
+
fi
|
|
2048
|
+
done
|
|
2049
|
+
fi
|
|
2050
|
+
if [ "$FOUND" = true ]; then
|
|
2051
|
+
FOUND_OPS=$((FOUND_OPS + 1))
|
|
2052
|
+
else
|
|
2053
|
+
MISSING_OPS="$MISSING_OPS $op"
|
|
2054
|
+
fi
|
|
2055
|
+
done
|
|
2056
|
+
|
|
2057
|
+
if [ -n "$MISSING_OPS" ]; then
|
|
2058
|
+
echo "BLOCKING: API endpoints missing from controllers (handoff contract violation)"
|
|
2059
|
+
echo "Found: $FOUND_OPS/$TOTAL_OPS operations"
|
|
2060
|
+
echo "Missing operations:$MISSING_OPS"
|
|
2061
|
+
echo "Fix: Implement missing endpoints in the appropriate Controller"
|
|
2062
|
+
exit 1
|
|
2063
|
+
else
|
|
2064
|
+
echo "POST-CHECK A8: OK — $FOUND_OPS/$TOTAL_OPS API operations found"
|
|
2065
|
+
fi
|
|
2066
|
+
fi
|
|
2067
|
+
fi
|
|
2068
|
+
```
|
|
2069
|
+
|
|
1788
2070
|
---
|
|
1789
2071
|
|
|
1790
2072
|
## Summary
|
|
@@ -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,12 +309,41 @@ 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: `''` or `'{section}'`)
|
|
319
|
+
2. Create route (`'{section}/create'`)
|
|
320
|
+
3. Static sections (`'{section}/dashboard'`, `'{section}/departments'`, etc.)
|
|
321
|
+
4. Dynamic routes (`'{section}/:id'`, `'{section}/:id/edit'`)
|
|
322
|
+
5. Redirect routes (`Navigate`) — ALWAYS LAST
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
// ✅ CORRECT — static before dynamic
|
|
326
|
+
{ path: 'employees', element: <EmployeesPage /> },
|
|
327
|
+
{ path: 'employees/create', element: <CreateEmployeePage /> },
|
|
328
|
+
{ path: 'employees/dashboard', element: <EmployeeDashboardPage /> },
|
|
329
|
+
{ path: 'employees/:id', element: <EmployeeDetailPage /> },
|
|
330
|
+
{ path: 'employees/:id/edit', element: <EditEmployeePage /> },
|
|
331
|
+
{ path: '', element: <Navigate to="employees" replace /> },
|
|
332
|
+
|
|
333
|
+
// ❌ WRONG — :id before dashboard → dashboard is unreachable (matched as id="dashboard")
|
|
334
|
+
{ path: 'employees/:id', element: <EmployeeDetailPage /> },
|
|
335
|
+
{ path: 'employees/dashboard', element: <EmployeeDashboardPage /> }, // NEVER REACHED
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
POST-CHECK C49 detects this anti-pattern and BLOCKS.
|
|
339
|
+
|
|
310
340
|
### Section Routes (when module has sections)
|
|
311
341
|
|
|
312
342
|
If the module defines `{sections}`, generate frontend routes for EACH section as children of the module route:
|
|
313
343
|
|
|
314
344
|
```tsx
|
|
315
345
|
// Section routes are nested inside the module's children array
|
|
346
|
+
// IMPORTANT: static routes BEFORE dynamic routes (see Route Ordering rule above)
|
|
316
347
|
{ path: '{section-kebab}', element: <Suspense fallback={<PageLoader />}><{Section}Page /></Suspense> },
|
|
317
348
|
{ path: '{section-kebab}/create', element: <Suspense fallback={<PageLoader />}><Create{Section}Page /></Suspense> },
|
|
318
349
|
{ path: '{section-kebab}/:id', element: <Suspense fallback={<PageLoader />}><{Section}DetailPage /></Suspense> },
|
|
@@ -108,6 +108,15 @@ Verify the plan respects dependencies:
|
|
|
108
108
|
- Build gate between EVERY layer (must pass): Layer 0 → 1 → 2 → 3 → 4
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
### 4b. Module Code Collision Guard (BLOCKING)
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
IF appCode == moduleCode:
|
|
115
|
+
BLOCK — module_code identique à app_code cause des segments doublés (e.g. /hr/hr).
|
|
116
|
+
Suggestion : "{appCode}-core", "{appCode}-management"
|
|
117
|
+
ASK user pour un module_code différent.
|
|
118
|
+
```
|
|
119
|
+
|
|
111
120
|
---
|
|
112
121
|
|
|
113
122
|
## 5. Estimated Commits
|
|
@@ -298,6 +298,39 @@ After controller generation, verify `[NavRoute]` attribute is present on every c
|
|
|
298
298
|
- When calling `scaffold_extension(type: "controller")`, always pass `navRoute` in options
|
|
299
299
|
- This is REQUIRED for `scaffold_routes` to auto-detect routes in Layer 3
|
|
300
300
|
|
|
301
|
+
### Guard: NavRoute Uniqueness and Segment Count (MANDATORY)
|
|
302
|
+
|
|
303
|
+
**BEFORE proceeding past Layer 2**, verify for EACH controller:
|
|
304
|
+
|
|
305
|
+
1. **Unique NavRoute:** No two controllers may share the same `[NavRoute("...")]` value. Duplicate NavRoutes cause routing conflicts → 404s on one of the controllers.
|
|
306
|
+
|
|
307
|
+
2. **Segment count matches hierarchy:** Count the dots in the NavRoute value:
|
|
308
|
+
- 1 dot = 2 segments (module-level, e.g., `human-resources.employees`) — controller is at `Controllers/{App}/`
|
|
309
|
+
- 2 dots = 3 segments (section-level, e.g., `human-resources.employees.contracts`) — controller is at `Controllers/{App}/{Module}/` or in a section subfolder
|
|
310
|
+
- **If a controller is in a section subfolder** (e.g., `Controllers/{App}/Employees/ContractsController.cs`) **but has only 2 segments** → the API route will be wrong → 404. It MUST have 3 segments.
|
|
311
|
+
- 0 dots = INVALID → BLOCK
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
# Quick validation
|
|
315
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
316
|
+
for f in $CTRL_FILES; do
|
|
317
|
+
NAVROUTE=$(grep -oP '\[NavRoute\("\K[^"]+' "$f")
|
|
318
|
+
if [ -n "$NAVROUTE" ]; then
|
|
319
|
+
DOTS=$(echo "$NAVROUTE" | tr -cd '.' | wc -c)
|
|
320
|
+
if [ "$DOTS" -eq 0 ]; then
|
|
321
|
+
echo "BLOCKING: NavRoute '$NAVROUTE' has only 1 segment (need minimum 2): $f"
|
|
322
|
+
exit 1
|
|
323
|
+
fi
|
|
324
|
+
# Check if controller is in a section subfolder but NavRoute has only 2 segments
|
|
325
|
+
DEPTH=$(echo "$f" | grep -oP 'Controllers/[^/]+/[^/]+/' | wc -l)
|
|
326
|
+
if [ "$DEPTH" -gt 0 ] && [ "$DOTS" -eq 1 ]; then
|
|
327
|
+
echo "WARNING: Controller in section subfolder but NavRoute has only 2 segments: $f"
|
|
328
|
+
echo " NavRoute: $NAVROUTE — expected 3 segments (app.module.section)"
|
|
329
|
+
fi
|
|
330
|
+
fi
|
|
331
|
+
done
|
|
332
|
+
```
|
|
333
|
+
|
|
301
334
|
```bash
|
|
302
335
|
# Quick check: all controllers must have [NavRoute] (not just [Route])
|
|
303
336
|
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
@@ -386,11 +419,15 @@ TaskUpdate(taskId: progress_tracker_id,
|
|
|
386
419
|
|
|
387
420
|
For each module:
|
|
388
421
|
- API client: MCP scaffold_api_client
|
|
389
|
-
- Routes
|
|
422
|
+
- Routes — TWO mandatory steps:
|
|
423
|
+
1. Generate registry: MCP scaffold_routes (source: 'controllers', outputFormat: 'applicationRoutes', dryRun: false)
|
|
424
|
+
→ Creates navRoutes.generated.ts (required by POST-CHECK C2)
|
|
425
|
+
2. Wire to App.tsx (see below)
|
|
390
426
|
- Wire Routes to App.tsx: After scaffold_routes, routes must be wired into App.tsx:
|
|
391
427
|
→ Read App.tsx and detect the routing pattern
|
|
392
428
|
→ Pattern A (`applicationRoutes: ApplicationRouteExtensions`): add routes to `applicationRoutes['{application_kebab}'][]` with RELATIVE paths
|
|
393
429
|
→ Pattern B (JSX `<Route>`): nest routes inside `<Route path="/{application}" element={<AppLayout />}>` + duplicate in tenant block
|
|
430
|
+
→ **BEFORE wiring:** Verify route ordering — static routes (`create`, `dashboard`, `departments`) MUST come BEFORE dynamic routes (`:id`, `:id/edit`). Redirect routes (`Navigate`) MUST be LAST. See `references/smartstack-layers.md` "RULE — Frontend Route Ordering".
|
|
394
431
|
→ Do not add business routes to `clientRoutes[]` — it is only for non-app routes (`/about`, `/pricing`)
|
|
395
432
|
→ All business applications use `<AppLayout />` as layout wrapper
|
|
396
433
|
→ See `references/frontend-route-wiring-app-tsx.md` for full Pattern A/B detection and examples
|
|
@@ -480,7 +517,7 @@ IF NOT economy_mode AND entities.length > 1:
|
|
|
480
517
|
prompt='Execute Layer 3 frontend for {EntityName}:
|
|
481
518
|
**MANDATORY: Read references/smartstack-frontend.md FIRST**
|
|
482
519
|
- API client: MCP scaffold_api_client
|
|
483
|
-
- Routes: MCP scaffold_routes (outputFormat: "applicationRoutes")
|
|
520
|
+
- Routes: MCP scaffold_routes (outputFormat: "applicationRoutes", dryRun: false) → MUST generate navRoutes.generated.ts
|
|
484
521
|
- Wire Routes to App.tsx (BLOCKING): detect Pattern A/B, wire accordingly
|
|
485
522
|
→ See references/frontend-route-wiring-app-tsx.md for full patterns
|
|
486
523
|
→ Verify: mcp__smartstack__validate_frontend_routes (scope: "routes")
|
|
@@ -69,6 +69,39 @@ const applicationRoutes: ApplicationRouteExtensions = {
|
|
|
69
69
|
|
|
70
70
|
Routes are automatically injected into BOTH standard (`/{application}/...`) and tenant-prefixed (`/t/:slug/{application}/...`) route trees by `mergeRoutes()`. No manual duplication needed.
|
|
71
71
|
|
|
72
|
+
#### RULE — Route Ordering in applicationRoutes
|
|
73
|
+
|
|
74
|
+
> **CRITICAL:** Routes within each application key MUST follow static-before-dynamic order.
|
|
75
|
+
> React Router matches top-to-bottom — a `:id` route placed before a `dashboard` route
|
|
76
|
+
> will match `dashboard` as an `id` parameter → 404.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
const applicationRoutes: ApplicationRouteExtensions = {
|
|
80
|
+
'human-resources': [
|
|
81
|
+
// ✅ CORRECT ORDER — static before dynamic
|
|
82
|
+
{ path: 'employees', element: <EmployeesPage /> },
|
|
83
|
+
{ path: 'employees/create', element: <CreateEmployeePage /> },
|
|
84
|
+
{ path: 'employees/dashboard', element: <EmployeeDashboardPage /> },
|
|
85
|
+
{ path: 'employees/:id', element: <EmployeeDetailPage /> },
|
|
86
|
+
{ path: 'employees/:id/edit', element: <EditEmployeePage /> },
|
|
87
|
+
|
|
88
|
+
// Redirect routes ALWAYS LAST
|
|
89
|
+
{ path: '', element: <Navigate to="employees" replace /> },
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// ❌ FORBIDDEN — :id before static routes
|
|
96
|
+
'human-resources': [
|
|
97
|
+
{ path: 'employees/:id', element: <EmployeeDetailPage /> }, // ← WRONG: catches 'dashboard'
|
|
98
|
+
{ path: 'employees/dashboard', element: <DashboardPage /> }, // ← NEVER REACHED
|
|
99
|
+
]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
See `smartstack-layers.md` "RULE — Frontend Route Ordering" for the full ordering specification.
|
|
103
|
+
POST-CHECK C49 detects and BLOCKS this anti-pattern.
|
|
104
|
+
|
|
72
105
|
#### Custom Applications (Pattern A)
|
|
73
106
|
|
|
74
107
|
Custom application keys (any key **not** in the built-in list: `administration`, `support`, `user`, `api`) are fully supported in `applicationRoutes`. `mergeRoutes()` automatically:
|
|
@@ -89,6 +89,21 @@ FOR each entity in analysis.entities[]:
|
|
|
89
89
|
IF endpoints.length === 0 -> ERROR: "Entity {entity.name} has NO endpoints — missing controller"
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
## H. Permission Granularity Check
|
|
93
|
+
|
|
94
|
+
> Validates that section permission modes are respected in the generated permission data.
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
FOR each module:
|
|
98
|
+
FOR each section in anticipatedSections[]:
|
|
99
|
+
IF section.sectionType == "view" OR section.permissionMode == "inherit":
|
|
100
|
+
IF section has own permissions in seedDataCore.permissions[] -> WARNING: "Section '{code}' is type 'view' but has own permissions — should inherit from module"
|
|
101
|
+
IF section.permissionMode == "read-only":
|
|
102
|
+
IF section has create/update/delete permissions -> ERROR: "Section '{code}' is read-only but has write permissions"
|
|
103
|
+
IF section.sectionType == "primary" AND NOT section.permissionMode:
|
|
104
|
+
-> WARNING: "Primary section '{code}' missing explicit permissionMode — defaulting to 'crud'"
|
|
105
|
+
```
|
|
106
|
+
|
|
92
107
|
## Result Aggregation
|
|
93
108
|
|
|
94
109
|
```json
|
|
@@ -100,6 +115,8 @@ FOR each entity in analysis.entities[]:
|
|
|
100
115
|
{ "check": "id-uniqueness", "module": "...", "status": "PASS|ERROR", "details": "..." },
|
|
101
116
|
{ "check": "wireframe-layout", "module": "...", "status": "PASS|ERROR", "details": "..." },
|
|
102
117
|
{ "check": "seeddata-translations", "module": "...", "status": "PASS|ERROR", "details": "..." }
|
|
118
|
+
,
|
|
119
|
+
{ "check": "permission-granularity", "module": "...", "status": "PASS|WARNING|ERROR", "details": "..." }
|
|
103
120
|
]
|
|
104
121
|
}
|
|
105
122
|
```
|
|
@@ -34,13 +34,18 @@
|
|
|
34
34
|
|
|
35
35
|
> **RULE:** Sections = functional zones only. `create`/`edit` are separate pages with own URL routes (`/create` and `/:id/edit`). `detail` is a tabbed page reached from `list`.
|
|
36
36
|
|
|
37
|
-
| featureType | Sections (functional zones) | List page includes | Form pages | Detail page tabs |
|
|
38
|
-
|
|
39
|
-
| data-centric | list | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations} |
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
37
|
+
| featureType | Sections (functional zones) | permissionMode | List page includes | Form pages | Detail page tabs |
|
|
38
|
+
|---|---|---|---|---|---|
|
|
39
|
+
| data-centric | list | `crud` | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations} |
|
|
40
|
+
| data-centric | (detail — implicit) | `inherit` | — | — | — |
|
|
41
|
+
| workflow | list | `crud` | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations}, Historique |
|
|
42
|
+
| workflow | approve | `custom:approve,reject` | — | — | — |
|
|
43
|
+
| integration | list | `crud` | grid, filters, config button | `/create` page, `/:id/edit` page | Infos, Config, Logs |
|
|
44
|
+
| reporting | dashboard | `read-only` | — | — | — |
|
|
45
|
+
| full-module | list | `crud` | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations}, Historique |
|
|
46
|
+
| full-module | dashboard | `read-only` | — | — | — |
|
|
47
|
+
|
|
48
|
+
> **RULE:** `detail` is NEVER a section with its own permission set. It is always `sectionType: view`, `permissionMode: inherit`. Detail pages inherit permissions from their parent module.
|
|
44
49
|
|
|
45
50
|
## Component Generation Rules
|
|
46
51
|
|