@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.
Files changed (27) hide show
  1. package/dist/mcp-entry.mjs +33 -11
  2. package/dist/mcp-entry.mjs.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/agents/ba-writer.md +46 -46
  5. package/templates/project/appsettings.json.template +4 -6
  6. package/templates/skills/apex/SKILL.md +1 -0
  7. package/templates/skills/apex/references/challenge-questions.md +17 -0
  8. package/templates/skills/apex/references/core-seed-data.md +27 -4
  9. package/templates/skills/apex/references/post-checks.md +330 -0
  10. package/templates/skills/apex/references/smartstack-layers.md +31 -0
  11. package/templates/skills/apex/steps/step-02-plan.md +9 -0
  12. package/templates/skills/apex/steps/step-03-execute.md +102 -4
  13. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +33 -0
  14. package/templates/skills/business-analyse/references/consolidation-structural-checks.md +17 -0
  15. package/templates/skills/business-analyse/references/spec-auto-inference.md +12 -7
  16. package/templates/skills/business-analyse/steps/step-00-init.md +19 -9
  17. package/templates/skills/business-analyse/steps/step-02-structure.md +20 -6
  18. package/templates/skills/business-analyse/steps/step-03-specify.md +7 -0
  19. package/templates/skills/business-analyse/steps/step-04-consolidate.md +2 -14
  20. package/templates/skills/controller/references/mcp-scaffold-workflow.md +20 -0
  21. package/templates/skills/derive-prd/references/handoff-file-templates.md +25 -1
  22. package/templates/skills/derive-prd/references/handoff-seeddata-generation.md +3 -1
  23. package/templates/skills/ralph-loop/references/category-completeness.md +125 -0
  24. package/templates/skills/ralph-loop/references/compact-loop.md +90 -3
  25. package/templates/skills/ralph-loop/references/module-transition.md +60 -0
  26. package/templates/skills/ralph-loop/steps/step-04-check.md +207 -12
  27. package/templates/skills/ralph-loop/steps/step-05-report.md +205 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlashub/smartstack-cli",
3
- "version": "4.26.0",
3
+ "version": "4.28.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -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
- - `entities.json` — entity definitions with attributes and relationships
24
- - `rules.json` — business rules with categories and conditions
25
- - `usecases.json` — use cases and functional requirements
26
- - `permissions.json` — permission matrix and role assignments
27
- - `screens.json` — UI wireframes and navigation
28
- - `validation.json` — validation rules and consistency checks
29
- - `handoff.json` — complexity, file catalog, BR-to-code mapping
30
- - `consolidation.json` — cross-module interactions and E2E flows (application-level only)
31
- - `review.json` — preserved review comments and change summary
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 appropriate for scope:
66
- - Project/application: cadrage.json, validation.json, handoff.json, consolidation.json
67
- > **NOTE:** Do NOT create entities.json, rules.json, usecases.json, permissions.json, screens.json at project/application level.
68
- > These thematic files contain module-specific data and are created ONLY at module level by step-03 (specify).
69
- > Creating empty versions at app level causes downstream tools (derive-prd) to read empty arrays instead of module data.
70
- - Module: entities.json, rules.json, usecases.json, permissions.json, screens.json, validation.json, handoff.json
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 (cadrage.json, validation.json, consolidation.json)
114
- > Do NOT create entities/rules/usecases/permissions/screens at project level — these exist only at module level.
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
- v1.1/
493
- index.json
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
- discovery.json
502
- analysis.json
503
- specification.json
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:5173",
11
- "http://localhost:5174",
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:6173"
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:5173",
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: only if `navSections[]` defined)
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.Code == "admin" || r.Code == "manager" || r.Code == "contributor" || r.Code == "viewer")
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 != null || r.IsSystem)
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