@atlashub/smartstack-cli 4.26.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/dist/mcp-entry.mjs +33 -11
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/agents/ba-writer.md +46 -46
- package/templates/project/appsettings.json.template +4 -6
- package/templates/skills/apex/SKILL.md +1 -0
- package/templates/skills/apex/references/challenge-questions.md +17 -0
- package/templates/skills/apex/references/core-seed-data.md +27 -4
- package/templates/skills/apex/references/post-checks.md +330 -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 +102 -4
- 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-00-init.md +19 -9
- 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/business-analyse/steps/step-04-consolidate.md +2 -14
- 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 +90 -3
- 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
|
@@ -18,17 +18,18 @@ Write and update granular JSON files for project-level (multi-app), application-
|
|
|
18
18
|
- Module-level: `docs/{app}/{module}/business-analyse/v{X.Y}/index.json` + thematic files
|
|
19
19
|
|
|
20
20
|
**Thematic files (v2 granular architecture):**
|
|
21
|
-
- `index.json` — metadata, version, hash manifest, module registry
|
|
22
|
-
- `cadrage.json` — stakeholders, problem/vision, risks, acceptance criteria
|
|
23
|
-
- `
|
|
24
|
-
- `
|
|
25
|
-
- `
|
|
26
|
-
- `
|
|
27
|
-
- `
|
|
28
|
-
- `
|
|
29
|
-
- `
|
|
30
|
-
- `
|
|
31
|
-
- `
|
|
21
|
+
- `index.json` — metadata, version, hash manifest, module registry (ALL scopes)
|
|
22
|
+
- `cadrage.json` — stakeholders, problem/vision, risks, acceptance criteria (ALL scopes)
|
|
23
|
+
- `validation.json` — validation rules and consistency checks (ALL scopes)
|
|
24
|
+
- `consolidation.json` — cross-module interactions and E2E flows (application/project ONLY)
|
|
25
|
+
- `navigation.json` — navigation tree (application/project ONLY, created by ba-design-ui)
|
|
26
|
+
- `entities.json` — entity definitions with attributes and relationships (**MODULE ONLY**)
|
|
27
|
+
- `rules.json` — business rules with categories and conditions (**MODULE ONLY**)
|
|
28
|
+
- `usecases.json` — use cases and functional requirements (**MODULE ONLY**)
|
|
29
|
+
- `permissions.json` — permission matrix and role assignments (**MODULE ONLY**)
|
|
30
|
+
- `screens.json` — UI wireframes and navigation (**MODULE ONLY**)
|
|
31
|
+
- `handoff.json` — complexity, file catalog, BR-to-code mapping (**MODULE ONLY**)
|
|
32
|
+
- `review.json` — preserved review comments and change summary (ALL scopes)
|
|
32
33
|
|
|
33
34
|
> **Backward compatibility:** If only 1 application, the project level is NOT created. The application-level index.json remains the master.
|
|
34
35
|
|
|
@@ -62,12 +63,12 @@ Create initial index.json and empty thematic files with metadata and draft statu
|
|
|
62
63
|
- For project scope: modules: []
|
|
63
64
|
- For application scope: modules: []
|
|
64
65
|
- For module scope: (no modules array)
|
|
65
|
-
5. Create empty thematic files
|
|
66
|
-
- Project
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
5. Create empty thematic files — ONLY the files listed for the scope. Creating ANY unlisted file is a **BLOCKING ERROR**.
|
|
67
|
+
- Project: cadrage.json, validation.json, consolidation.json — **EXACTLY 3 files, NO OTHERS**
|
|
68
|
+
- Application: cadrage.json, validation.json, consolidation.json — **EXACTLY 3 files, NO OTHERS**
|
|
69
|
+
- Module: entities.json, rules.json, usecases.json, permissions.json, screens.json, validation.json, handoff.json — **EXACTLY 7 files**
|
|
70
|
+
|
|
71
|
+
**FORBIDDEN at project/application level:** entities.json, rules.json, usecases.json, permissions.json, screens.json, handoff.json — these exist ONLY at module level.
|
|
71
72
|
6. Update `.business-analyse/config.json` with new lastFeatureId
|
|
72
73
|
7. IF scope = "module" AND applicationRef provided AND moduleCode provided:
|
|
73
74
|
a. Read master index.json (via applicationRef FEAT-NNN)
|
|
@@ -110,8 +111,8 @@ Create a project-level index.json for multi-application analysis. Only used when
|
|
|
110
111
|
- fileHashes: {}
|
|
111
112
|
- applications: []
|
|
112
113
|
- applicationDependencyGraph: {}
|
|
113
|
-
5. Create thematic files
|
|
114
|
-
|
|
114
|
+
5. Create thematic files — **EXACTLY 3 files, NO OTHERS:** cadrage.json, validation.json, consolidation.json
|
|
115
|
+
**FORBIDDEN at project level:** entities.json, rules.json, usecases.json, permissions.json, screens.json, handoff.json — these exist ONLY at module level.
|
|
115
116
|
6. Update `.business-analyse/config.json` with new lastProjectId
|
|
116
117
|
7. Deploy schemas to `docs/business-analyse/schemas/` (including project-schema.json)
|
|
117
118
|
8. Return project ID (PROJ-NNN) and path
|
|
@@ -178,6 +179,10 @@ Write a complete thematic file and update its hash in index.json.
|
|
|
178
179
|
|
|
179
180
|
**Process:**
|
|
180
181
|
1. Find and read index.json (use findFeature if given ID)
|
|
182
|
+
1b. **SCOPE GUARD (BLOCKING):** If index.json scope is "project" or "application", verify themeName is ALLOWED:
|
|
183
|
+
- Allowed: [cadrage, validation, consolidation, navigation, review]
|
|
184
|
+
- FORBIDDEN: [entities, rules, usecases, permissions, screens, handoff]
|
|
185
|
+
If themeName is FORBIDDEN → **REJECT with BLOCKING ERROR.** Do NOT create the file.
|
|
181
186
|
2. Determine thematic filename: `{themeName}.json`
|
|
182
187
|
3. Create full path: `{version_dir}/{themeName}.json`
|
|
183
188
|
4. Write thematic file with pretty-print (2-space indent)
|
|
@@ -335,6 +340,20 @@ Increment the module loop counter in the master index.json.
|
|
|
335
340
|
9. Write back index.json
|
|
336
341
|
10. Return new index and whether loop is complete
|
|
337
342
|
|
|
343
|
+
### cleanupAppLevelFiles
|
|
344
|
+
Remove forbidden thematic files at project/application level and clean their entries from fileHashes.
|
|
345
|
+
|
|
346
|
+
**Input:** featureId: FEAT-NNN
|
|
347
|
+
|
|
348
|
+
**Process:**
|
|
349
|
+
1. Read index.json (verify scope is "project" or "application")
|
|
350
|
+
2. FORBIDDEN = [entities.json, rules.json, usecases.json, permissions.json, screens.json, handoff.json]
|
|
351
|
+
3. For each FORBIDDEN file: if file exists in version directory → DELETE it
|
|
352
|
+
4. For each FORBIDDEN file: if referenced in fileHashes → REMOVE entry
|
|
353
|
+
5. If `files` property exists in index.json: remove entries for forbidden files
|
|
354
|
+
6. Update metadata.updatedAt, write index.json
|
|
355
|
+
7. Return list of cleaned files
|
|
356
|
+
|
|
338
357
|
### createVersion
|
|
339
358
|
Create a new version for refactoring or major changes.
|
|
340
359
|
|
|
@@ -470,43 +489,29 @@ docs/business-analyse/
|
|
|
470
489
|
v1.0/
|
|
471
490
|
index.json ← PROJECT metadata
|
|
472
491
|
cadrage.json
|
|
473
|
-
entities.json
|
|
474
|
-
rules.json
|
|
475
|
-
usecases.json
|
|
476
|
-
permissions.json
|
|
477
|
-
screens.json
|
|
478
492
|
validation.json
|
|
479
493
|
consolidation.json
|
|
494
|
+
# NO entities/rules/usecases/permissions/screens/handoff — MODULE ONLY
|
|
480
495
|
|
|
481
496
|
docs/{app}/business-analyse/
|
|
482
497
|
v1.0/
|
|
483
498
|
index.json ← APPLICATION metadata
|
|
484
499
|
cadrage.json
|
|
485
|
-
entities.json
|
|
486
|
-
rules.json
|
|
487
|
-
usecases.json
|
|
488
|
-
permissions.json
|
|
489
|
-
screens.json
|
|
490
500
|
validation.json
|
|
491
501
|
consolidation.json
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
cadrage.json
|
|
495
|
-
entities.json
|
|
496
|
-
...
|
|
502
|
+
navigation.json ← (created by ba-design-ui)
|
|
503
|
+
# NO entities/rules/usecases/permissions/screens/handoff — MODULE ONLY
|
|
497
504
|
|
|
498
505
|
docs/{app}/{module}/business-analyse/
|
|
499
506
|
v1.0/
|
|
500
507
|
index.json ← MODULE metadata
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
508
|
+
entities.json
|
|
509
|
+
rules.json
|
|
510
|
+
usecases.json
|
|
511
|
+
permissions.json
|
|
512
|
+
screens.json
|
|
504
513
|
validation.json
|
|
505
514
|
handoff.json
|
|
506
|
-
v1.1/
|
|
507
|
-
index.json
|
|
508
|
-
discovery.json
|
|
509
|
-
...
|
|
510
515
|
```
|
|
511
516
|
|
|
512
517
|
Versions are stored as separate directories. Each directory contains index.json + thematic files.
|
|
@@ -726,11 +731,6 @@ if (estimatedNewSize > 500 * 1024) { // 500KB
|
|
|
726
731
|
"fileHashes": {
|
|
727
732
|
"index.json": "pqr678...",
|
|
728
733
|
"cadrage.json": "stu901...",
|
|
729
|
-
"entities.json": "vwx234...",
|
|
730
|
-
"rules.json": "yza567...",
|
|
731
|
-
"usecases.json": "bcd890...",
|
|
732
|
-
"permissions.json": "efg123...",
|
|
733
|
-
"screens.json": "hij456...",
|
|
734
734
|
"validation.json": "klm789...",
|
|
735
735
|
"consolidation.json": "nop012..."
|
|
736
736
|
},
|
|
@@ -7,10 +7,8 @@
|
|
|
7
7
|
"FailOnMigrationError": true,
|
|
8
8
|
"EnableDevSeeding": false,
|
|
9
9
|
"CorsOrigins": [
|
|
10
|
-
"http://localhost:
|
|
11
|
-
"http://localhost:
|
|
12
|
-
"http://localhost:5175",
|
|
13
|
-
"http://localhost:6173"
|
|
10
|
+
"http://localhost:3000",
|
|
11
|
+
"http://localhost:5173"
|
|
14
12
|
]
|
|
15
13
|
},
|
|
16
14
|
"Jwt": {
|
|
@@ -34,7 +32,7 @@
|
|
|
34
32
|
"SecretExpiresAt": "",
|
|
35
33
|
"CallbackPath": "/api/auth/google/callback"
|
|
36
34
|
},
|
|
37
|
-
"FrontendUrl": "http://localhost:
|
|
35
|
+
"FrontendUrl": "http://localhost:3000"
|
|
38
36
|
},
|
|
39
37
|
"Session": {
|
|
40
38
|
"IdleTimeoutMinutes": 20,
|
|
@@ -125,7 +123,7 @@
|
|
|
125
123
|
"Provider": "Development",
|
|
126
124
|
"FromEmail": "noreply@{{ProjectDomain}}",
|
|
127
125
|
"FromName": "{{ProjectName}}",
|
|
128
|
-
"BaseUrl": "http://localhost:
|
|
126
|
+
"BaseUrl": "http://localhost:3000",
|
|
129
127
|
"TokenExpiration": {
|
|
130
128
|
"EmailConfirmation": "24:00:00",
|
|
131
129
|
"PasswordReset": "01:00:00"
|
|
@@ -149,6 +149,7 @@ Execute incremental SmartStack development using the APEX methodology. This skil
|
|
|
149
149
|
- **Parallel Agent tool** - Parallel execution for scan (step-01) and within Layer 2/3 (step-03) for multi-entity, unless economy_mode
|
|
150
150
|
- **Tests inline** - Backend tests run after Layer 2, frontend tests run after Layer 3 (max 3 fix iterations each). Step-07 = final sweep (security + coverage).
|
|
151
151
|
- **Exception: seed data** — The templates in core-seed-data.md and person-extension-pattern.md are generated directly because no MCP tool covers seed data creation. This is a documented exception to the "orchestrate, never generate" rule.
|
|
152
|
+
- **Frontend pages: ALWAYS via Skill("ui-components")** — economy_mode affects parallelization only, NOT whether /ui-components is called. NEVER generate .tsx pages directly, even in delegate or economy mode.
|
|
152
153
|
- **Save outputs** if `{save_mode}` = true
|
|
153
154
|
- **Commits per layer** - Atomic commits after each execution layer
|
|
154
155
|
- **Delegate mode** (`-d`): Read PRD context, skip challenge questions, auto+economy mode implied. Used when `/ralph-loop` delegates code generation to `/apex`.
|
|
@@ -82,8 +82,25 @@ questions:
|
|
|
82
82
|
multiSelect: true
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
**Reserved codes (NOT valid sections):**
|
|
86
|
+
- `detail` — auto-generated as `/:id` route when "list" section exists
|
|
87
|
+
- `create` — auto-generated as `/create` route when "list" section exists
|
|
88
|
+
- `edit` — auto-generated as `/:id/edit` route when "list" section exists
|
|
89
|
+
|
|
90
|
+
These are **view modes**, not sections. A "list" section automatically generates 4 pages: ListPage (index), DetailPage (/:id), CreatePage (/create), EditPage (/:id/edit).
|
|
91
|
+
|
|
85
92
|
**Validation:**
|
|
86
93
|
```
|
|
94
|
+
RESERVED_SECTION_CODES = ["detail", "create", "edit"]
|
|
95
|
+
|
|
96
|
+
IF any section.code IN RESERVED_SECTION_CODES:
|
|
97
|
+
DISPLAY: "'{section.code}' is a view mode, not a section.
|
|
98
|
+
The 'list' section automatically includes detail (/:id), create (/create),
|
|
99
|
+
and edit (/:id/edit) pages. Remove '{section.code}' from your sections."
|
|
100
|
+
→ Remove the offending section(s) from the list
|
|
101
|
+
→ Re-ask the sections question if {sections}.length == 0
|
|
102
|
+
→ DO NOT proceed
|
|
103
|
+
|
|
87
104
|
IF {sections}.length == 0:
|
|
88
105
|
DISPLAY: "Every module must have at least one section. Please select or define at least one."
|
|
89
106
|
→ Re-ask the sections question
|
|
@@ -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
|
|
@@ -117,10 +117,136 @@ if [ -n "$APP_TSX" ]; then
|
|
|
117
117
|
fi
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
### POST-CHECK S7: Controllers must NOT use Guid.Empty for tenantId/userId (OWASP A01)
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
124
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
125
|
+
BAD_GUID=$(grep -Pn 'Guid\.Empty' $CTRL_FILES 2>/dev/null)
|
|
126
|
+
if [ -n "$BAD_GUID" ]; then
|
|
127
|
+
echo "BLOCKING (OWASP A01): Controller uses Guid.Empty — tenant isolation bypassed"
|
|
128
|
+
echo "$BAD_GUID"
|
|
129
|
+
echo "Fix: Use _currentTenant.TenantId from ICurrentTenantService"
|
|
130
|
+
exit 1
|
|
131
|
+
fi
|
|
132
|
+
fi
|
|
133
|
+
```
|
|
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
|
+
|
|
120
204
|
---
|
|
121
205
|
|
|
122
206
|
## Backend — Entity, Service & Controller Checks
|
|
123
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
|
+
|
|
124
250
|
### POST-CHECK C1: Navigation routes must be full paths starting with /
|
|
125
251
|
|
|
126
252
|
```bash
|
|
@@ -154,6 +280,39 @@ fi
|
|
|
154
280
|
|
|
155
281
|
## Frontend — CSS, Forms, Components, I18n
|
|
156
282
|
|
|
283
|
+
### POST-CHECK C3a: Frontend must not be empty if Layer 3 was planned (BLOCKING)
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
# If foundation_mode is false AND App.tsx exists, verify frontend was generated
|
|
287
|
+
APP_TSX=$(find web/ src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
288
|
+
if [ -n "$APP_TSX" ]; then
|
|
289
|
+
# Check if applicationRoutes is an empty object
|
|
290
|
+
EMPTY_ROUTES=$(grep -P "applicationRoutes.*=\s*\{[\s/]*\}" "$APP_TSX" 2>/dev/null)
|
|
291
|
+
if [ -n "$EMPTY_ROUTES" ]; then
|
|
292
|
+
echo "BLOCKING: applicationRoutes in App.tsx is empty — Layer 3 frontend was NOT executed"
|
|
293
|
+
echo "Expected: at least one application key with route definitions"
|
|
294
|
+
echo "Fix: Run Layer 3 (scaffold_routes + scaffold_extension + route wiring)"
|
|
295
|
+
exit 1
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
# Check pages/ directory is not empty
|
|
299
|
+
PAGE_COUNT=$(find web/ src/ -path "*/pages/*" -name "*.tsx" -not -path "*/node_modules/*" 2>/dev/null | wc -l)
|
|
300
|
+
if [ "$PAGE_COUNT" -eq 0 ]; then
|
|
301
|
+
echo "BLOCKING: No page components found in pages/ directory"
|
|
302
|
+
echo "Fix: Generate pages via scaffold_extension or /ui-components"
|
|
303
|
+
exit 1
|
|
304
|
+
fi
|
|
305
|
+
|
|
306
|
+
# Check navRoutes.generated.ts exists
|
|
307
|
+
NAV_ROUTES=$(find web/ src/ -name "navRoutes.generated.ts" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
308
|
+
if [ -z "$NAV_ROUTES" ]; then
|
|
309
|
+
echo "BLOCKING: navRoutes.generated.ts not found — scaffold_routes was never called"
|
|
310
|
+
echo "Fix: Run scaffold_routes(source: 'controllers', outputFormat: 'applicationRoutes')"
|
|
311
|
+
exit 1
|
|
312
|
+
fi
|
|
313
|
+
fi
|
|
314
|
+
```
|
|
315
|
+
|
|
157
316
|
### POST-CHECK C3: Translation files must exist for all 4 languages (if frontend)
|
|
158
317
|
|
|
159
318
|
```bash
|
|
@@ -1611,6 +1770,120 @@ if [ -n "$ENTITY_FILES" ]; then
|
|
|
1611
1770
|
fi
|
|
1612
1771
|
```
|
|
1613
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
|
+
|
|
1614
1887
|
---
|
|
1615
1888
|
|
|
1616
1889
|
## Architecture — Clean Architecture Layer Isolation
|
|
@@ -1737,6 +2010,63 @@ if [ -n "$CTRL_FILES" ]; then
|
|
|
1737
2010
|
fi
|
|
1738
2011
|
```
|
|
1739
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
|
+
|
|
1740
2070
|
---
|
|
1741
2071
|
|
|
1742
2072
|
## Summary
|