@atlashub/smartstack-cli 4.31.0 → 4.33.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 (41) hide show
  1. package/.documentation/commands.html +952 -116
  2. package/.documentation/index.html +2 -2
  3. package/.documentation/init.html +358 -174
  4. package/dist/mcp-entry.mjs +271 -44
  5. package/dist/mcp-entry.mjs.map +1 -1
  6. package/package.json +1 -1
  7. package/templates/mcp-scaffolding/controller.cs.hbs +54 -128
  8. package/templates/project/README.md +19 -0
  9. package/templates/skills/apex/SKILL.md +16 -10
  10. package/templates/skills/apex/_shared.md +1 -1
  11. package/templates/skills/apex/references/checks/architecture-checks.sh +154 -0
  12. package/templates/skills/apex/references/checks/backend-checks.sh +194 -0
  13. package/templates/skills/apex/references/checks/frontend-checks.sh +448 -0
  14. package/templates/skills/apex/references/checks/infrastructure-checks.sh +255 -0
  15. package/templates/skills/apex/references/checks/security-checks.sh +153 -0
  16. package/templates/skills/apex/references/checks/seed-checks.sh +536 -0
  17. package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +49 -192
  18. package/templates/skills/apex/references/parallel-execution.md +18 -5
  19. package/templates/skills/apex/references/post-checks.md +124 -2156
  20. package/templates/skills/apex/references/smartstack-api.md +160 -957
  21. package/templates/skills/apex/references/smartstack-frontend-compliance.md +23 -1
  22. package/templates/skills/apex/references/smartstack-frontend.md +134 -1022
  23. package/templates/skills/apex/references/smartstack-layers.md +12 -6
  24. package/templates/skills/apex/steps/step-00-init.md +81 -238
  25. package/templates/skills/apex/steps/step-03-execute.md +25 -751
  26. package/templates/skills/apex/steps/step-03a-layer0-domain.md +118 -0
  27. package/templates/skills/apex/steps/step-03b-layer1-seed.md +91 -0
  28. package/templates/skills/apex/steps/step-03c-layer2-backend.md +240 -0
  29. package/templates/skills/apex/steps/step-03d-layer3-frontend.md +300 -0
  30. package/templates/skills/apex/steps/step-03e-layer4-devdata.md +44 -0
  31. package/templates/skills/apex/steps/step-04-examine.md +70 -150
  32. package/templates/skills/application/references/frontend-i18n-and-output.md +2 -2
  33. package/templates/skills/application/references/frontend-route-naming.md +5 -1
  34. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +49 -198
  35. package/templates/skills/application/references/frontend-verification.md +11 -11
  36. package/templates/skills/application/steps/step-05-frontend.md +26 -15
  37. package/templates/skills/application/templates-frontend.md +4 -0
  38. package/templates/skills/cli-app-sync/SKILL.md +2 -2
  39. package/templates/skills/cli-app-sync/references/comparison-map.md +1 -1
  40. package/templates/skills/controller/references/controller-code-templates.md +70 -67
  41. package/templates/skills/controller/references/mcp-scaffold-workflow.md +5 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlashub/smartstack-cli",
3
- "version": "4.31.0",
3
+ "version": "4.33.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -1,192 +1,118 @@
1
- using System;
2
- using System.Collections.Generic;
3
- using System.Threading;
4
- using System.Threading.Tasks;
5
- using Microsoft.AspNetCore.Authorization;
1
+ using MediatR;
6
2
  using Microsoft.AspNetCore.Mvc;
7
- using Microsoft.Extensions.Logging;
8
3
  using SmartStack.Api.Authorization;
9
4
  {{#if navRoute}}
10
5
  using SmartStack.Api.Routing;
11
6
  {{/if}}
12
- using SmartStack.Application.Common.Models;
7
+ using SmartStack.Application.Common.Authorization;
13
8
 
14
- namespace {{namespace}}.Controllers;
9
+ namespace {{namespace}};
15
10
 
16
11
  /// <summary>
17
- /// API controller for {{name}} operations
12
+ /// API controller for {{name}} operations.
18
13
  /// </summary>
19
14
  [ApiController]
20
15
  {{#if navRoute}}
21
- [NavRoute("{{navRoute}}")]
16
+ [NavRoute("{{navRoute}}"{{#if navRouteSuffix}}, Suffix = "{{navRouteSuffix}}"{{/if}}{{#if customSegment}}, CustomSegment = "{{customSegment}}"{{/if}})]
22
17
  {{else}}
23
18
  [Route("api/[controller]")]
24
19
  {{/if}}
25
- [Authorize]
20
+ [Microsoft.AspNetCore.Authorization.Authorize]
26
21
  [Produces("application/json")]
22
+ [Tags("{{namePlural}}")]
27
23
  public class {{name}}Controller : ControllerBase
28
24
  {
29
- private readonly ILogger<{{name}}Controller> _logger;
30
- // private readonly I{{name}}Service _{{nameCamel}}Service;
25
+ private readonly ISender _mediator;
31
26
 
32
- public {{name}}Controller(
33
- ILogger<{{name}}Controller> logger
34
- // I{{name}}Service {{nameCamel}}Service
35
- )
36
- {
37
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
38
- // _{{nameCamel}}Service = {{nameCamel}}Service ?? throw new ArgumentNullException(nameof({{nameCamel}}Service));
39
- }
27
+ public {{name}}Controller(ISender mediator) => _mediator = mediator;
40
28
 
41
29
  /// <summary>
42
30
  /// Get all {{namePlural}} with pagination
43
31
  /// </summary>
44
32
  [HttpGet]
45
33
  {{#if navRoute}}
46
- [RequirePermission("{{navRoute}}.read")]
34
+ [RequirePermission(Permissions.{{module}}.{{namePlural}}.View)]
47
35
  {{/if}}
48
- [ProducesResponseType(typeof(PaginatedResult<{{name}}Dto>), 200)]
49
- public async Task<ActionResult<PaginatedResult<{{name}}Dto>>> GetAll(
50
- [FromQuery] string? search = null,
51
- [FromQuery] int page = 1,
52
- [FromQuery] int pageSize = 20,
53
- CancellationToken cancellationToken = default)
36
+ [ProducesResponseType(typeof(List<{{name}}ListDto>), StatusCodes.Status200OK)]
37
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
38
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
39
+ public async Task<ActionResult<List<{{name}}ListDto>>> GetAll(CancellationToken ct)
54
40
  {
55
- _logger.LogInformation("Getting all {{namePlural}}");
56
-
57
- // TODO: Implement using service
58
- // var result = await _{{nameCamel}}Service.GetAllAsync(search, page, pageSize, cancellationToken);
59
- // return Ok(result);
60
-
61
- return Ok(PaginatedResult<{{name}}Dto>.Empty(page, pageSize));
41
+ var result = await _mediator.Send(new Get{{namePlural}}Query(), ct);
42
+ return Ok(result);
62
43
  }
63
44
 
64
45
  /// <summary>
65
46
  /// Get {{nameCamel}} by ID
66
47
  /// </summary>
67
- /// <param name="id">{{name}} ID</param>
68
- /// <param name="cancellationToken">Cancellation token</param>
69
- /// <returns>{{name}} details</returns>
70
48
  [HttpGet("{id:guid}")]
71
49
  {{#if navRoute}}
72
- [RequirePermission("{{navRoute}}.read")]
50
+ [RequirePermission(Permissions.{{module}}.{{namePlural}}.View)]
73
51
  {{/if}}
74
- [ProducesResponseType(typeof({{name}}Dto), 200)]
75
- [ProducesResponseType(404)]
76
- public async Task<ActionResult<{{name}}Dto>> GetById(
77
- Guid id,
78
- CancellationToken cancellationToken = default)
52
+ [ProducesResponseType(typeof({{name}}DetailDto), StatusCodes.Status200OK)]
53
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
54
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
55
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
56
+ public async Task<ActionResult<{{name}}DetailDto>> GetById(
57
+ Guid id, CancellationToken ct)
79
58
  {
80
- _logger.LogInformation("Getting {{nameCamel}} {Id}", id);
81
-
82
- // TODO: Implement using service
83
- // var result = await _{{nameCamel}}Service.GetByIdAsync(id, cancellationToken);
84
- // if (result == null) return NotFound();
85
- // return Ok(result);
86
-
87
- return NotFound();
59
+ var result = await _mediator.Send(new Get{{name}}ByIdQuery(id), ct);
60
+ if (result == null) return NotFound(new { message = "{{name}} not found" });
61
+ return Ok(result);
88
62
  }
89
63
 
90
64
  /// <summary>
91
65
  /// Create new {{nameCamel}}
92
66
  /// </summary>
93
- /// <param name="request">Create request</param>
94
- /// <param name="cancellationToken">Cancellation token</param>
95
- /// <returns>Created {{nameCamel}}</returns>
96
67
  [HttpPost]
97
68
  {{#if navRoute}}
98
- [RequirePermission("{{navRoute}}.create")]
69
+ [RequirePermission(Permissions.{{module}}.{{namePlural}}.Create)]
99
70
  {{/if}}
100
- [ProducesResponseType(typeof({{name}}Dto), 201)]
101
- [ProducesResponseType(400)]
102
- public async Task<ActionResult<{{name}}Dto>> Create(
103
- [FromBody] Create{{name}}Request request,
104
- CancellationToken cancellationToken = default)
71
+ [ProducesResponseType(typeof({{name}}DetailDto), StatusCodes.Status201Created)]
72
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
73
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
74
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
75
+ public async Task<ActionResult<{{name}}DetailDto>> Create(
76
+ [FromBody] Create{{name}}Request request, CancellationToken ct)
105
77
  {
106
- _logger.LogInformation("Creating new {{nameCamel}}");
107
-
108
- // TODO: Validate and create using service
109
- // var result = await _{{nameCamel}}Service.CreateAsync(request, cancellationToken);
110
- // return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
111
-
112
- var id = Guid.NewGuid();
113
- return CreatedAtAction(nameof(GetById), new { id }, null);
78
+ var result = await _mediator.Send(new Create{{name}}Command(request), ct);
79
+ return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
114
80
  }
115
81
 
116
82
  /// <summary>
117
83
  /// Update {{nameCamel}}
118
84
  /// </summary>
119
- /// <param name="id">{{name}} ID</param>
120
- /// <param name="request">Update request</param>
121
- /// <param name="cancellationToken">Cancellation token</param>
122
85
  [HttpPut("{id:guid}")]
123
86
  {{#if navRoute}}
124
- [RequirePermission("{{navRoute}}.update")]
87
+ [RequirePermission(Permissions.{{module}}.{{namePlural}}.Update)]
125
88
  {{/if}}
126
- [ProducesResponseType(204)]
127
- [ProducesResponseType(404)]
128
- [ProducesResponseType(400)]
129
- public async Task<ActionResult> Update(
130
- Guid id,
131
- [FromBody] Update{{name}}Request request,
132
- CancellationToken cancellationToken = default)
89
+ [ProducesResponseType(typeof({{name}}DetailDto), StatusCodes.Status200OK)]
90
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
91
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
92
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
93
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
94
+ public async Task<ActionResult<{{name}}DetailDto>> Update(
95
+ Guid id, [FromBody] Update{{name}}Request request, CancellationToken ct)
133
96
  {
134
- _logger.LogInformation("Updating {{nameCamel}} {Id}", id);
135
-
136
- // TODO: Implement using service
137
- // await _{{nameCamel}}Service.UpdateAsync(id, request, cancellationToken);
138
-
139
- return NoContent();
97
+ var result = await _mediator.Send(new Update{{name}}Command(id, request), ct);
98
+ if (result.IsNotFound) return NotFound(new { message = "{{name}} not found" });
99
+ return Ok(result.Data);
140
100
  }
141
101
 
142
102
  /// <summary>
143
103
  /// Delete {{nameCamel}}
144
104
  /// </summary>
145
- /// <param name="id">{{name}} ID</param>
146
- /// <param name="cancellationToken">Cancellation token</param>
147
105
  [HttpDelete("{id:guid}")]
148
106
  {{#if navRoute}}
149
- [RequirePermission("{{navRoute}}.delete")]
107
+ [RequirePermission(Permissions.{{module}}.{{namePlural}}.Delete)]
150
108
  {{/if}}
151
- [ProducesResponseType(204)]
152
- [ProducesResponseType(404)]
153
- public async Task<ActionResult> Delete(
154
- Guid id,
155
- CancellationToken cancellationToken = default)
109
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
110
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
111
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
112
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
113
+ public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
156
114
  {
157
- _logger.LogInformation("Deleting {{nameCamel}} {Id}", id);
158
-
159
- // TODO: Implement using service
160
- // await _{{nameCamel}}Service.DeleteAsync(id, cancellationToken);
161
-
162
- return NoContent();
115
+ var success = await _mediator.Send(new Delete{{name}}Command(id), ct);
116
+ return success ? NoContent() : NotFound(new { message = "{{name}} not found" });
163
117
  }
164
118
  }
165
-
166
- // ============================================================================
167
- // DTOs
168
- // ============================================================================
169
-
170
- /// <summary>
171
- /// {{name}} data transfer object
172
- /// </summary>
173
- public record {{name}}Dto(
174
- Guid Id,
175
- DateTime CreatedAt,
176
- DateTime? UpdatedAt
177
- // TODO: Add additional properties
178
- );
179
-
180
- /// <summary>
181
- /// Request to create a new {{nameCamel}}
182
- /// </summary>
183
- public record Create{{name}}Request(
184
- // TODO: Add creation properties
185
- );
186
-
187
- /// <summary>
188
- /// Request to update a {{nameCamel}}
189
- /// </summary>
190
- public record Update{{name}}Request(
191
- // TODO: Add update properties
192
- );
@@ -62,6 +62,25 @@ Migrations must be applied in order:
62
62
 
63
63
  This is handled automatically in `Program.cs.template`.
64
64
 
65
+ ## Frontend Setup (React)
66
+
67
+ ### Route Registration
68
+
69
+ SmartStack uses **PageRegistry + DynamicRouter** for routing. Register your pages:
70
+
71
+ ```tsx
72
+ import { PageRegistry } from '@atlashub/smartstack';
73
+ import { lazy } from 'react';
74
+
75
+ // Register your application pages
76
+ PageRegistry.register('rh.time.dashboard', lazy(() => import('./pages/TimeDashboard')));
77
+ PageRegistry.register('rh.time.list', lazy(() => import('./pages/TimeList')));
78
+ PageRegistry.register('rh.time.detail', lazy(() => import('./pages/TimeDetail')));
79
+ ```
80
+
81
+ Navigation entries (menu, permissions, routes) are managed in the database.
82
+ DynamicRouter resolves everything automatically — no manual route wiring needed.
83
+
65
84
  ## Usage
66
85
 
67
86
  ```bash
@@ -91,14 +91,19 @@ Execute incremental SmartStack development using the APEX methodology. This skil
91
91
  </entry_point>
92
92
 
93
93
  <step_files>
94
- **Progressive loading - only load current step:**
94
+ **Progressive loading only load current step. Step 03 dispatches to layer sub-steps.**
95
95
 
96
96
  | Step | File | Model | Purpose |
97
97
  |------|------|-------|---------|
98
98
  | 00 | `steps/step-00-init.md` | Sonnet | Parse flags, detect application, verify MCP, define hierarchy (4 levels), scope validation |
99
99
  | 01 | `steps/step-01-analyze.md` | Opus | Explore existing code (parallel Agent tool or sequential) |
100
100
  | 02 | `steps/step-02-plan.md` | Opus | Layer-by-layer plan with skill/MCP mapping |
101
- | 03 | `steps/step-03-execute.md` | Opus | Orchestrate execution via skills and MCP |
101
+ | 03 | `steps/step-03-execute.md` | Opus | **Orchestrator** dispatches to layer sub-steps below |
102
+ | 03a | `steps/step-03a-layer0-domain.md` | Opus | Layer 0: Domain entities, EF configs, migration |
103
+ | 03b | `steps/step-03b-layer1-seed.md` | Opus | Layer 1: Seed data (navigation, permissions, roles) |
104
+ | 03c | `steps/step-03c-layer2-backend.md` | Opus | Layer 2: Services, controllers, backend tests |
105
+ | 03d | `steps/step-03d-layer3-frontend.md` | Opus | Layer 3: Pages, i18n, routes, frontend tests |
106
+ | 03e | `steps/step-03e-layer4-devdata.md` | Opus | Layer 4: Development test data (optional) |
102
107
  | 04 | `steps/step-04-examine.md` | Opus | eXamine: MCP validation, build, POST-CHECKs, acceptance criteria |
103
108
  | 05 | `steps/step-05-deep-review.md` | Opus | Deep Review: adversarial code review (if -x) |
104
109
  | 06 | `steps/step-06-resolve.md` | Opus | Fix BLOCKING findings (if any) |
@@ -126,15 +131,16 @@ Execute incremental SmartStack development using the APEX methodology. This skil
126
131
 
127
132
  | File | Purpose | Loaded by | Stays in context for |
128
133
  |------|---------|-----------|---------------------|
129
- | `references/smartstack-api.md` | BaseEntity, interfaces, entity/config/controller patterns | step-01 | step-03 L0-L2 (released after L2) |
130
- | `references/smartstack-layers.md` | Layer rules, skill/MCP mapping, planning templates, delegate fast path | step-02 | step-03 L0-L2 (released after L2) |
131
- | `references/core-seed-data.md` | Comprehensive seed data templates (navigation, permissions, roles) | step-03 Layer 1 | released after Layer 1 |
132
- | `references/smartstack-frontend.md` | Frontend patterns, EntityLookup, i18n (sections 1-6) | step-03 Layer 3 (deferred) | step-04 |
133
- | `references/smartstack-frontend-compliance.md` | Documentation, form testing, compliance gates (sections 7-9) | step-03 Layer 3 (deferred) | step-04 |
134
+ | `references/smartstack-api.md` | BaseEntity, interfaces, entity/config/controller patterns | step-01 | step-03a/03b/03c (released after L2) |
135
+ | `references/smartstack-layers.md` | Layer rules, skill/MCP mapping, planning templates, delegate fast path | step-02 | step-03a/03b/03c (released after L2) |
136
+ | `references/core-seed-data.md` | Comprehensive seed data templates (navigation, permissions, roles) | step-03b (Layer 1) | released after Layer 1 |
137
+ | `references/smartstack-frontend.md` | Frontend patterns, EntityLookup, i18n (sections 1-6) | step-03d (Layer 3, deferred) | step-04 |
138
+ | `references/smartstack-frontend-compliance.md` | Documentation, form testing, compliance gates (sections 7-9) | step-03d (Layer 3, deferred) | step-04 |
134
139
  | `references/challenge-questions.md` | Hierarchy rules, challenge questions, delegate mode skip | step-00 | — |
135
140
  | `references/error-classification.md` | Build error diagnosis categories A-F | step-03 (build failure), step-04 | — |
136
- | `references/parallel-execution.md` | Agent tool launch patterns, task coordination, decision matrix | step-01, step-03 (if NOT economy_mode) | — |
137
- | `references/post-checks.md` | Security + convention + architecture bash checks (MCP tools run first) | step-04 | — |
141
+ | `references/parallel-execution.md` | Agent tool launch patterns, task coordination, decision matrix | step-01, step-03c/03d (if NOT economy_mode) | — |
142
+ | `references/post-checks.md` | Compact checklist indexes checks in `references/checks/*.sh` | step-04 | — |
143
+ | `references/checks/*.sh` | Bash check scripts (security, backend, frontend, seed, architecture, infrastructure) | step-04 (executed via bash) | — |
138
144
 
139
145
  **Context propagation rule:** Files loaded in step N remain in conversation context for step N+1. Steps mark "do NOT re-read" to avoid duplicate reads.
140
146
  </reference_files>
@@ -148,7 +154,7 @@ Execute incremental SmartStack development using the APEX methodology. This skil
148
154
  - **Layer order** - Layer 0 (domain+infra+migration) → Layer 1 (seed data) → Layer 2 (backend+tests) → Layer 3 (frontend+tests) → Layer 4 (devdata)
149
155
  - **Parallel Agent tool** - Parallel execution for scan (step-01) and within Layer 2/3 (step-03) for multi-entity, unless economy_mode
150
156
  - **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
- - **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.
157
+ - **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 yet. This is a documented exception to the "orchestrate, never generate" rule. <!-- TODO: Remove exception when MCP scaffold_seed_data (B1) is ready -->
152
158
  - **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.
153
159
  - **Save outputs** if `{save_mode}` = true
154
160
  - **Commits per layer** - Atomic commits after each execution layer
@@ -156,7 +156,7 @@ Write back to {delegate_prd_path}
156
156
  | `scaffold_extension` | Create files manually following `smartstack-api.md` entity/service/controller patterns |
157
157
  | `suggest_migration` | Name format: `{context}_v{version}_{sequence}_{Description}` (see existing migrations for version) |
158
158
  | `generate_permissions` | Write `HasData()` code manually following `core-seed-data.md` permission section |
159
- | `scaffold_routes` | Create `applicationRoutes` array manually following `smartstack-frontend.md` §1 |
159
+ | `scaffold_routes` | Create `componentRegistry.generated.ts` with `PageRegistry.register()` calls manually following `smartstack-frontend.md` §1 (or legacy `applicationRoutes` array for pre-v3.7 projects) |
160
160
  | `validate_frontend_routes` | Run POST-CHECK bash scripts from `post-checks.md` |
161
161
  | `validate_security` | Run security POST-CHECKs S1-S6 from `post-checks.md` |
162
162
  | `check_migrations` | Run `dotnet ef migrations has-pending-model-changes` manually |
@@ -0,0 +1,154 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # POST-CHECK: Architecture — Clean Architecture Layer Isolation
5
+ # A1-A8: Layer boundary enforcement, DTO usage, handoff compliance
6
+
7
+ FAIL=false
8
+
9
+ # POST-CHECK A1: Domain must not import other layers (BLOCKING)
10
+ DOMAIN_FILES=$(find src/ -path "*/Domain/*" -name "*.cs" 2>/dev/null)
11
+ if [ -n "$DOMAIN_FILES" ]; then
12
+ BAD_IMPORTS=$(grep -Pn 'using\s+[\w.]*\.(Application|Infrastructure|Api)[\w.]*;' $DOMAIN_FILES 2>/dev/null || true)
13
+ if [ -n "$BAD_IMPORTS" ]; then
14
+ echo "BLOCKING: Domain layer imports Application/Infrastructure/Api — violates Clean Architecture"
15
+ echo "Domain is the core, it must not depend on any other layer"
16
+ echo "$BAD_IMPORTS"
17
+ echo "Fix: Move shared types to Domain or remove the dependency"
18
+ FAIL=true
19
+ fi
20
+ fi
21
+
22
+ # POST-CHECK A2: Application must not import Infrastructure or Api (BLOCKING)
23
+ APP_FILES=$(find src/ -path "*/Application/*" -name "*.cs" 2>/dev/null)
24
+ if [ -n "$APP_FILES" ]; then
25
+ BAD_IMPORTS=$(grep -Pn 'using\s+[\w.]*\.(Infrastructure|Api)[\w.]*;' $APP_FILES 2>/dev/null || true)
26
+ if [ -n "$BAD_IMPORTS" ]; then
27
+ echo "BLOCKING: Application layer imports Infrastructure/Api — violates Clean Architecture"
28
+ echo "Application defines interfaces, Infrastructure implements them"
29
+ echo "$BAD_IMPORTS"
30
+ echo "Fix: Define an interface in Application and implement it in Infrastructure"
31
+ FAIL=true
32
+ fi
33
+ fi
34
+
35
+ # POST-CHECK A3: Controllers must not inject DbContext (BLOCKING)
36
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
37
+ if [ -n "$CTRL_FILES" ]; then
38
+ BAD_DBCONTEXT=$(grep -Pn 'private\s+readonly\s+\w*DbContext|DbContext\s+\w+[,)]' $CTRL_FILES 2>/dev/null || true)
39
+ if [ -n "$BAD_DBCONTEXT" ]; then
40
+ echo "BLOCKING: Controller injects DbContext directly — violates Clean Architecture"
41
+ echo "Controllers must use Application services, not access the database directly"
42
+ echo "$BAD_DBCONTEXT"
43
+ echo "Fix: Create an Application service with the required business logic and inject it instead"
44
+ FAIL=true
45
+ fi
46
+ fi
47
+
48
+ # POST-CHECK A4: API must return DTOs, not Domain entities (WARNING)
49
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
50
+ ENTITY_FILES=$(find src/ -path "*/Domain/Entities/*" -name "*.cs" 2>/dev/null)
51
+ if [ -n "$CTRL_FILES" ] && [ -n "$ENTITY_FILES" ]; then
52
+ ENTITY_NAMES=$(grep -ohP 'public\s+class\s+(\w+)\s*:' $ENTITY_FILES 2>/dev/null | grep -oP '\w+(?=\s*:)' | grep -v '^public$' | sort -u || true)
53
+ for ENTITY in $ENTITY_NAMES; do
54
+ BAD_RETURN=$(grep -Pn "ActionResult<$ENTITY>|ActionResult<IEnumerable<$ENTITY>>|ActionResult<List<$ENTITY>>" $CTRL_FILES 2>/dev/null || true)
55
+ if [ -n "$BAD_RETURN" ]; then
56
+ echo "WARNING: Controller returns Domain entity '$ENTITY' instead of a DTO"
57
+ echo "$BAD_RETURN"
58
+ echo "Fix: Return ${ENTITY}ResponseDto instead of $ENTITY"
59
+ fi
60
+ done
61
+ fi
62
+
63
+ # POST-CHECK A5: Service interfaces in Application, implementations in Infrastructure (WARNING)
64
+ APP_SERVICES=$(find src/ -path "*/Application/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
65
+ if [ -n "$APP_SERVICES" ]; then
66
+ for f in $APP_SERVICES; do
67
+ if grep -q 'public class.*Service' "$f" 2>/dev/null; then
68
+ echo "WARNING: Service implementation found in Application layer: $f"
69
+ echo "Fix: Move implementation to Infrastructure/Services/. Application should only contain interfaces."
70
+ fi
71
+ done
72
+ fi
73
+
74
+ DOMAIN_INTERFACES=$(find src/ -path "*/Domain/*" -name "I*Service.cs" 2>/dev/null)
75
+ API_INTERFACES=$(find src/ -path "*/Api/*" -name "I*Service.cs" 2>/dev/null)
76
+ for f in $DOMAIN_INTERFACES $API_INTERFACES; do
77
+ if [ -n "$f" ] && grep -q 'public interface.*Service' "$f" 2>/dev/null; then
78
+ echo "WARNING: Service interface found outside Application layer: $f"
79
+ echo "Fix: Move to Application/Interfaces/"
80
+ fi
81
+ done
82
+
83
+ # POST-CHECK A6: No EF Core attributes in Domain entities (BLOCKING)
84
+ DOMAIN_FILES=$(find src/ -path "*/Domain/*" -name "*.cs" 2>/dev/null)
85
+ if [ -n "$DOMAIN_FILES" ]; then
86
+ BAD_EF=$(grep -Pn '\[Table\(|\[Column\(|\[Index\(|using\s+Microsoft\.EntityFrameworkCore' $DOMAIN_FILES 2>/dev/null || true)
87
+ if [ -n "$BAD_EF" ]; then
88
+ echo "BLOCKING: EF Core attributes or using directives found in Domain layer"
89
+ echo "Domain entities must be persistence-ignorant — EF configuration belongs in Infrastructure"
90
+ echo "$BAD_EF"
91
+ echo "Fix: Move [Table], [Column], [Index] to IEntityTypeConfiguration<T> in Infrastructure/Persistence/Configurations/"
92
+ FAIL=true
93
+ fi
94
+ fi
95
+
96
+ # POST-CHECK A7: No direct repository usage in controllers (WARNING)
97
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
98
+ if [ -n "$CTRL_FILES" ]; then
99
+ BAD_REPO=$(grep -Pn 'IRepository<|IGenericRepository<|private\s+readonly\s+IRepository|private\s+readonly\s+IGenericRepository' $CTRL_FILES 2>/dev/null || true)
100
+ if [ -n "$BAD_REPO" ]; then
101
+ echo "WARNING: Controller injects repository directly — should use Application services"
102
+ echo "$BAD_REPO"
103
+ echo "Fix: Controllers should depend on Application services (I*Service), not repositories"
104
+ fi
105
+ fi
106
+
107
+ # POST-CHECK A8: API endpoints must match handoff apiEndpointSummary (BLOCKING)
108
+ PRD_FILE=".ralph/prd.json"
109
+ if [ ! -f "$PRD_FILE" ]; then
110
+ if [ -f ".ralph/modules-queue.json" ]; then
111
+ PRD_FILE=$(cat .ralph/modules-queue.json | grep -o '"prdFile":"[^"]*"' | tail -1 | cut -d'"' -f4)
112
+ fi
113
+ fi
114
+
115
+ if [ -f "$PRD_FILE" ]; then
116
+ OPERATIONS=$(cat "$PRD_FILE" | grep -o '"operation"\s*:\s*"[^"]*"' | cut -d'"' -f4 2>/dev/null || true)
117
+
118
+ if [ -n "$OPERATIONS" ]; then
119
+ CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
120
+ MISSING_OPS=""
121
+ TOTAL_OPS=0
122
+ FOUND_OPS=0
123
+
124
+ for op in $OPERATIONS; do
125
+ TOTAL_OPS=$((TOTAL_OPS + 1))
126
+ FOUND=false
127
+ if [ -n "$CTRL_FILES" ]; then
128
+ for f in $CTRL_FILES; do
129
+ if grep -q "$op" "$f" 2>/dev/null; then
130
+ FOUND=true
131
+ break
132
+ fi
133
+ done
134
+ fi
135
+ if [ "$FOUND" = true ]; then
136
+ FOUND_OPS=$((FOUND_OPS + 1))
137
+ else
138
+ MISSING_OPS="$MISSING_OPS $op"
139
+ fi
140
+ done
141
+
142
+ if [ -n "$MISSING_OPS" ]; then
143
+ echo "BLOCKING: API endpoints missing from controllers (handoff contract violation)"
144
+ echo "Found: $FOUND_OPS/$TOTAL_OPS operations"
145
+ echo "Missing operations:$MISSING_OPS"
146
+ echo "Fix: Implement missing endpoints in the appropriate Controller"
147
+ FAIL=true
148
+ fi
149
+ fi
150
+ fi
151
+
152
+ if [ "$FAIL" = true ]; then
153
+ exit 1
154
+ fi