@atlashub/smartstack-cli 3.31.0 → 3.32.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/.documentation/installation.html +7 -2
- package/README.md +7 -1
- package/dist/index.js +33 -37
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +547 -97
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/scripts/health-check.sh +2 -1
- package/templates/mcp-scaffolding/controller.cs.hbs +10 -7
- package/templates/mcp-scaffolding/entity-extension.cs.hbs +132 -124
- package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +4 -4
- package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +38 -15
- package/templates/mcp-scaffolding/tests/service.test.cs.hbs +20 -8
- package/templates/skills/apex/SKILL.md +7 -9
- package/templates/skills/apex/_shared.md +9 -2
- package/templates/skills/apex/references/code-generation.md +412 -0
- package/templates/skills/apex/references/post-checks.md +377 -37
- package/templates/skills/apex/references/smartstack-api.md +229 -5
- package/templates/skills/apex/references/smartstack-frontend.md +368 -11
- package/templates/skills/apex/references/smartstack-layers.md +54 -7
- package/templates/skills/apex/steps/step-00-init.md +1 -2
- package/templates/skills/apex/steps/step-01-analyze.md +45 -2
- package/templates/skills/apex/steps/step-02-plan.md +23 -2
- package/templates/skills/apex/steps/step-03-execute.md +195 -5
- package/templates/skills/apex/steps/step-04-examine.md +18 -5
- package/templates/skills/apex/steps/step-05-deep-review.md +9 -11
- package/templates/skills/apex/steps/step-06-resolve.md +5 -9
- package/templates/skills/apex/steps/step-07-tests.md +66 -1
- package/templates/skills/apex/steps/step-08-run-tests.md +12 -3
- package/templates/skills/application/references/provider-template.md +62 -39
- package/templates/skills/application/templates-backend.md +3 -3
- package/templates/skills/application/templates-frontend.md +12 -12
- package/templates/skills/application/templates-seed.md +14 -4
- package/templates/skills/business-analyse/SKILL.md +9 -6
- package/templates/skills/business-analyse/questionnaire/04-data.md +8 -0
- package/templates/skills/business-analyse/references/agent-module-prompt.md +84 -5
- package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +83 -19
- package/templates/skills/business-analyse/references/consolidation-structural-checks.md +6 -2
- package/templates/skills/business-analyse/references/team-orchestration.md +443 -110
- package/templates/skills/business-analyse/references/validation-checklist.md +5 -4
- package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +44 -0
- package/templates/skills/business-analyse/steps/step-03a2-analysis.md +72 -1
- package/templates/skills/business-analyse/steps/step-03c-compile.md +93 -7
- package/templates/skills/business-analyse/steps/step-03d-validate.md +34 -2
- package/templates/skills/business-analyse/steps/step-04b-analyze.md +40 -0
- package/templates/skills/controller/references/controller-code-templates.md +2 -2
- package/templates/skills/controller/templates.md +12 -12
- package/templates/skills/feature-full/steps/step-01-implementation.md +4 -4
- package/templates/skills/ralph-loop/references/category-rules.md +44 -2
- package/templates/skills/ralph-loop/references/compact-loop.md +37 -0
- package/templates/skills/ralph-loop/references/core-seed-data.md +51 -20
- package/templates/skills/review-code/references/owasp-api-top10.md +1 -1
package/package.json
CHANGED
package/scripts/health-check.sh
CHANGED
|
@@ -107,7 +107,8 @@ if command -v claude &> /dev/null; then
|
|
|
107
107
|
fi
|
|
108
108
|
else
|
|
109
109
|
log_error "Claude Code CLI not found"
|
|
110
|
-
log_info " Install:
|
|
110
|
+
log_info " Install: curl -fsSL https://claude.ai/install.sh | bash"
|
|
111
|
+
log_info " More info: https://code.claude.com/docs/en/setup"
|
|
111
112
|
fi
|
|
112
113
|
|
|
113
114
|
echo ""
|
|
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization;
|
|
|
6
6
|
using Microsoft.AspNetCore.Mvc;
|
|
7
7
|
using Microsoft.Extensions.Logging;
|
|
8
8
|
using SmartStack.Api.Authorization;
|
|
9
|
+
using SmartStack.Application.Common.Models;
|
|
9
10
|
|
|
10
11
|
namespace {{namespace}}.Controllers;
|
|
11
12
|
|
|
@@ -34,24 +35,26 @@ public class {{name}}Controller : ControllerBase
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/// <summary>
|
|
37
|
-
/// Get all {{namePlural}}
|
|
38
|
+
/// Get all {{namePlural}} with pagination
|
|
38
39
|
/// </summary>
|
|
39
|
-
/// <param name="cancellationToken">Cancellation token</param>
|
|
40
|
-
/// <returns>List of {{namePlural}}</returns>
|
|
41
40
|
[HttpGet]
|
|
42
41
|
{{#if navRoute}}
|
|
43
42
|
[RequirePermission("{{navRoute}}.read")]
|
|
44
43
|
{{/if}}
|
|
45
|
-
[ProducesResponseType(typeof(
|
|
46
|
-
public async Task<ActionResult<
|
|
44
|
+
[ProducesResponseType(typeof(PaginatedResult<{{name}}Dto>), 200)]
|
|
45
|
+
public async Task<ActionResult<PaginatedResult<{{name}}Dto>>> GetAll(
|
|
46
|
+
[FromQuery] string? search = null,
|
|
47
|
+
[FromQuery] int page = 1,
|
|
48
|
+
[FromQuery] int pageSize = 20,
|
|
49
|
+
CancellationToken cancellationToken = default)
|
|
47
50
|
{
|
|
48
51
|
_logger.LogInformation("Getting all {{namePlural}}");
|
|
49
52
|
|
|
50
53
|
// TODO: Implement using service
|
|
51
|
-
// var result = await _{{nameCamel}}Service.GetAllAsync(cancellationToken);
|
|
54
|
+
// var result = await _{{nameCamel}}Service.GetAllAsync(search, page, pageSize, cancellationToken);
|
|
52
55
|
// return Ok(result);
|
|
53
56
|
|
|
54
|
-
return Ok(
|
|
57
|
+
return Ok(PaginatedResult<{{name}}Dto>.Empty(page, pageSize));
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
/// <summary>
|
|
@@ -1,20 +1,64 @@
|
|
|
1
1
|
using System;
|
|
2
|
-
using System.ComponentModel.DataAnnotations;
|
|
3
2
|
using SmartStack.Domain.Common;
|
|
4
|
-
using SmartStack.Domain.Common.Interfaces;
|
|
5
3
|
|
|
6
4
|
namespace {{namespace}};
|
|
7
5
|
|
|
8
6
|
/// <summary>
|
|
9
7
|
/// {{name}} entity{{#if baseEntity}} extending {{baseEntity}}{{/if}}
|
|
8
|
+
{{#if isStrict}}
|
|
9
|
+
/// Tenant-scoped: data is isolated per tenant (ITenantEntity)
|
|
10
|
+
{{else if isOptional}}
|
|
11
|
+
/// Cross-tenant: data can be shared across tenants or tenant-specific (IOptionalTenantEntity)
|
|
12
|
+
{{else if isScoped}}
|
|
13
|
+
/// Scoped: data visibility controlled by EntityScope (IScopedTenantEntity)
|
|
14
|
+
{{else}}
|
|
15
|
+
/// Platform-level entity: no tenant isolation
|
|
16
|
+
{{/if}}
|
|
10
17
|
/// </summary>
|
|
11
|
-
{{#if
|
|
12
|
-
public class {{name}} :
|
|
18
|
+
{{#if isStrict}}
|
|
19
|
+
public class {{name}} : BaseEntity, ITenantEntity, IAuditableEntity
|
|
20
|
+
{{else if isOptional}}
|
|
21
|
+
public class {{name}} : BaseEntity, IOptionalTenantEntity, IAuditableEntity
|
|
22
|
+
{{else if isScoped}}
|
|
23
|
+
public class {{name}} : BaseEntity, IScopedTenantEntity, IAuditableEntity
|
|
13
24
|
{{else}}
|
|
14
|
-
public class {{name}} : BaseEntity
|
|
25
|
+
public class {{name}} : BaseEntity, IAuditableEntity
|
|
15
26
|
{{/if}}
|
|
16
27
|
{
|
|
28
|
+
{{#if hasTenantId}}
|
|
29
|
+
// === MULTI-TENANT ===
|
|
30
|
+
|
|
31
|
+
{{#if isNullableTenant}}
|
|
32
|
+
/// <summary>
|
|
33
|
+
/// Tenant identifier — null means shared across all tenants
|
|
34
|
+
/// </summary>
|
|
35
|
+
public Guid? TenantId { get; private set; }
|
|
36
|
+
{{else}}
|
|
37
|
+
/// <summary>
|
|
38
|
+
/// Tenant identifier for multi-tenant isolation (required)
|
|
39
|
+
/// </summary>
|
|
40
|
+
public Guid TenantId { get; private set; }
|
|
41
|
+
{{/if}}
|
|
42
|
+
|
|
43
|
+
{{/if}}
|
|
44
|
+
{{#if hasScope}}
|
|
45
|
+
/// <summary>
|
|
46
|
+
/// Visibility scope: Tenant (tenant-specific), Shared (all tenants), Platform (admin only)
|
|
47
|
+
/// </summary>
|
|
48
|
+
public EntityScope Scope { get; private set; }
|
|
49
|
+
|
|
50
|
+
{{/if}}
|
|
51
|
+
// === AUDIT ===
|
|
52
|
+
|
|
53
|
+
/// <summary>User who created this entity</summary>
|
|
54
|
+
public string? CreatedBy { get; set; }
|
|
55
|
+
|
|
56
|
+
/// <summary>User who last updated this entity</summary>
|
|
57
|
+
public string? UpdatedBy { get; set; }
|
|
58
|
+
|
|
17
59
|
{{#if baseEntity}}
|
|
60
|
+
// === RELATIONSHIPS ===
|
|
61
|
+
|
|
18
62
|
/// <summary>
|
|
19
63
|
/// Foreign key to {{baseEntity}}
|
|
20
64
|
/// </summary>
|
|
@@ -23,7 +67,7 @@ public class {{name}} : BaseEntity
|
|
|
23
67
|
/// <summary>
|
|
24
68
|
/// Navigation property to {{baseEntity}}
|
|
25
69
|
/// </summary>
|
|
26
|
-
public
|
|
70
|
+
public {{baseEntity}}? {{baseEntity}} { get; private set; }
|
|
27
71
|
|
|
28
72
|
{{/if}}
|
|
29
73
|
// === BUSINESS PROPERTIES ===
|
|
@@ -37,130 +81,95 @@ public class {{name}} : BaseEntity
|
|
|
37
81
|
/// <summary>
|
|
38
82
|
/// Factory method to create a new {{name}}
|
|
39
83
|
/// </summary>
|
|
40
|
-
{{#if
|
|
84
|
+
{{#if isStrict}}
|
|
85
|
+
/// <param name="tenantId">Required tenant identifier</param>
|
|
41
86
|
public static {{name}} Create(
|
|
42
|
-
|
|
87
|
+
Guid tenantId,
|
|
43
88
|
string? createdBy = null)
|
|
44
89
|
{
|
|
90
|
+
if (tenantId == Guid.Empty)
|
|
91
|
+
throw new ArgumentException("TenantId is required", nameof(tenantId));
|
|
92
|
+
|
|
45
93
|
return new {{name}}
|
|
46
94
|
{
|
|
47
95
|
Id = Guid.NewGuid(),
|
|
48
|
-
|
|
96
|
+
TenantId = tenantId,
|
|
49
97
|
CreatedAt = DateTime.UtcNow,
|
|
50
98
|
CreatedBy = createdBy
|
|
51
99
|
};
|
|
52
100
|
}
|
|
53
|
-
{{else}}
|
|
101
|
+
{{else if isOptional}}
|
|
102
|
+
/// <param name="tenantId">Tenant identifier — null for shared (cross-tenant) data</param>
|
|
54
103
|
public static {{name}} Create(
|
|
55
|
-
Guid tenantId,
|
|
56
|
-
string code,
|
|
104
|
+
Guid? tenantId = null,
|
|
57
105
|
string? createdBy = null)
|
|
58
106
|
{
|
|
59
107
|
return new {{name}}
|
|
60
108
|
{
|
|
61
109
|
Id = Guid.NewGuid(),
|
|
62
110
|
TenantId = tenantId,
|
|
63
|
-
Code = code.ToLowerInvariant(),
|
|
64
111
|
CreatedAt = DateTime.UtcNow,
|
|
65
112
|
CreatedBy = createdBy
|
|
66
113
|
};
|
|
67
114
|
}
|
|
68
|
-
{{
|
|
69
|
-
|
|
70
|
-
/// <
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
115
|
+
{{else if isScoped}}
|
|
116
|
+
/// <param name="tenantId">Tenant identifier — null for shared/platform data</param>
|
|
117
|
+
/// <param name="scope">Visibility scope (default: Tenant)</param>
|
|
118
|
+
public static {{name}} Create(
|
|
119
|
+
Guid? tenantId = null,
|
|
120
|
+
EntityScope scope = EntityScope.Tenant,
|
|
121
|
+
string? createdBy = null)
|
|
74
122
|
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
123
|
+
if (scope == EntityScope.Tenant && tenantId == null)
|
|
124
|
+
throw new ArgumentException("TenantId is required when scope is Tenant", nameof(tenantId));
|
|
78
125
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
126
|
+
return new {{name}}
|
|
127
|
+
{
|
|
128
|
+
Id = Guid.NewGuid(),
|
|
129
|
+
TenantId = tenantId,
|
|
130
|
+
Scope = scope,
|
|
131
|
+
CreatedAt = DateTime.UtcNow,
|
|
132
|
+
CreatedBy = createdBy
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
{{else}}
|
|
136
|
+
public static {{name}} Create(
|
|
137
|
+
string? createdBy = null)
|
|
83
138
|
{
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
139
|
+
return new {{name}}
|
|
140
|
+
{
|
|
141
|
+
Id = Guid.NewGuid(),
|
|
142
|
+
CreatedAt = DateTime.UtcNow,
|
|
143
|
+
CreatedBy = createdBy
|
|
144
|
+
};
|
|
87
145
|
}
|
|
146
|
+
{{/if}}
|
|
88
147
|
|
|
89
148
|
/// <summary>
|
|
90
|
-
///
|
|
149
|
+
/// Update the entity
|
|
91
150
|
/// </summary>
|
|
92
|
-
public void
|
|
151
|
+
public void Update(string? updatedBy = null)
|
|
93
152
|
{
|
|
94
|
-
IsDeleted = false;
|
|
95
|
-
DeletedAt = null;
|
|
96
|
-
DeletedBy = null;
|
|
97
153
|
UpdatedAt = DateTime.UtcNow;
|
|
98
|
-
UpdatedBy =
|
|
154
|
+
UpdatedBy = updatedBy;
|
|
99
155
|
}
|
|
100
156
|
}
|
|
101
157
|
|
|
102
158
|
// ============================================================================
|
|
103
|
-
// Base Entity Classes (SmartStack.Domain.Common)
|
|
159
|
+
// Base Entity Classes (SmartStack.Domain.Common) — Reference Only
|
|
104
160
|
// ============================================================================
|
|
105
161
|
/*
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
public Guid Id { get; set; } = Guid.NewGuid();
|
|
112
|
-
public Guid TenantId { get; set; }
|
|
113
|
-
|
|
114
|
-
private string _code = string.Empty;
|
|
115
|
-
public string Code
|
|
116
|
-
{
|
|
117
|
-
get => _code;
|
|
118
|
-
set => _code = value?.ToLowerInvariant() ?? string.Empty;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
122
|
-
public DateTime? UpdatedAt { get; set; }
|
|
123
|
-
public string? CreatedBy { get; set; }
|
|
124
|
-
public string? UpdatedBy { get; set; }
|
|
125
|
-
|
|
126
|
-
public bool IsDeleted { get; set; }
|
|
127
|
-
public DateTime? DeletedAt { get; set; }
|
|
128
|
-
public string? DeletedBy { get; set; }
|
|
129
|
-
|
|
130
|
-
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/// <summary>
|
|
134
|
-
/// Base entity for system-wide entities (no tenant isolation)
|
|
135
|
-
/// </summary>
|
|
136
|
-
public abstract class SystemEntity : ISystemEntity, IHasCode, ISoftDeletable, IVersioned
|
|
137
|
-
{
|
|
138
|
-
public Guid Id { get; set; } = Guid.NewGuid();
|
|
139
|
-
|
|
140
|
-
private string _code = string.Empty;
|
|
141
|
-
public string Code
|
|
142
|
-
{
|
|
143
|
-
get => _code;
|
|
144
|
-
set => _code = value?.ToLowerInvariant() ?? string.Empty;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
148
|
-
public DateTime? UpdatedAt { get; set; }
|
|
149
|
-
public string? CreatedBy { get; set; }
|
|
150
|
-
public string? UpdatedBy { get; set; }
|
|
151
|
-
|
|
152
|
-
public bool IsDeleted { get; set; }
|
|
153
|
-
public DateTime? DeletedAt { get; set; }
|
|
154
|
-
public string? DeletedBy { get; set; }
|
|
155
|
-
|
|
156
|
-
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
|
|
157
|
-
}
|
|
162
|
+
BaseEntity: Id (Guid), CreatedAt (DateTime), UpdatedAt (DateTime?)
|
|
163
|
+
ITenantEntity: TenantId (Guid) — strict tenant isolation
|
|
164
|
+
IOptionalTenantEntity: TenantId (Guid?) — cross-tenant shared data (null = shared)
|
|
165
|
+
IScopedTenantEntity: TenantId (Guid?) + Scope — with EntityScope (Tenant/Shared/Platform)
|
|
166
|
+
IAuditableEntity: CreatedBy (string?), UpdatedBy (string?)
|
|
158
167
|
*/
|
|
159
168
|
|
|
160
169
|
// ============================================================================
|
|
161
170
|
// EF Core Configuration
|
|
162
171
|
// ============================================================================
|
|
163
|
-
namespace {{infrastructureNamespace}}.Persistence.Configurations;
|
|
172
|
+
namespace {{infrastructureNamespace}}.Persistence.Configurations{{#if configNamespaceSuffix}}.{{configNamespaceSuffix}}{{/if}};
|
|
164
173
|
|
|
165
174
|
using Microsoft.EntityFrameworkCore;
|
|
166
175
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
@@ -170,62 +179,61 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
|
170
179
|
{
|
|
171
180
|
public void Configure(EntityTypeBuilder<{{name}}> builder)
|
|
172
181
|
{
|
|
173
|
-
// Table name with schema and domain prefix
|
|
174
182
|
builder.ToTable("{{tablePrefix}}{{name}}s", "{{schema}}");
|
|
175
183
|
|
|
176
|
-
// Primary key
|
|
177
184
|
builder.HasKey(e => e.Id);
|
|
178
185
|
|
|
179
|
-
{{#
|
|
180
|
-
// Multi-tenant
|
|
186
|
+
{{#if isStrict}}
|
|
187
|
+
// Multi-tenant (strict: TenantId required)
|
|
181
188
|
builder.Property(e => e.TenantId).IsRequired();
|
|
182
|
-
builder.HasIndex(e => e.TenantId)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
.HasDatabaseName("IX_{{tablePrefix}}{{name}}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
builder.
|
|
194
|
-
|
|
195
|
-
.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
189
|
+
builder.HasIndex(e => e.TenantId)
|
|
190
|
+
.HasDatabaseName("IX_{{tablePrefix}}{{name}}s_TenantId");
|
|
191
|
+
|
|
192
|
+
{{else if isOptional}}
|
|
193
|
+
// Multi-tenant (optional: TenantId nullable — null = shared across tenants)
|
|
194
|
+
builder.Property(e => e.TenantId).IsRequired(false);
|
|
195
|
+
builder.HasIndex(e => e.TenantId)
|
|
196
|
+
.HasDatabaseName("IX_{{tablePrefix}}{{name}}s_TenantId");
|
|
197
|
+
|
|
198
|
+
{{else if isScoped}}
|
|
199
|
+
// Multi-tenant (scoped: TenantId nullable + EntityScope)
|
|
200
|
+
builder.Property(e => e.TenantId).IsRequired(false);
|
|
201
|
+
builder.HasIndex(e => e.TenantId)
|
|
202
|
+
.HasDatabaseName("IX_{{tablePrefix}}{{name}}s_TenantId");
|
|
203
|
+
|
|
204
|
+
builder.Property(e => e.Scope)
|
|
205
|
+
.IsRequired()
|
|
206
|
+
.HasConversion<string>()
|
|
207
|
+
.HasMaxLength(20);
|
|
208
|
+
builder.HasIndex(e => new { e.TenantId, e.Scope })
|
|
209
|
+
.HasDatabaseName("IX_{{tablePrefix}}{{name}}s_TenantId_Scope");
|
|
210
|
+
|
|
211
|
+
{{/if}}
|
|
212
|
+
// Audit fields (from IAuditableEntity)
|
|
203
213
|
builder.Property(e => e.CreatedBy).HasMaxLength(256);
|
|
204
214
|
builder.Property(e => e.UpdatedBy).HasMaxLength(256);
|
|
205
|
-
builder.Property(e => e.DeletedBy).HasMaxLength(256);
|
|
206
215
|
|
|
207
216
|
{{#if baseEntity}}
|
|
208
|
-
// Relationship to {{baseEntity}}
|
|
217
|
+
// Relationship to {{baseEntity}}
|
|
209
218
|
builder.HasOne(e => e.{{baseEntity}})
|
|
210
|
-
.
|
|
211
|
-
.HasForeignKey
|
|
212
|
-
.OnDelete(DeleteBehavior.
|
|
219
|
+
.WithMany()
|
|
220
|
+
.HasForeignKey(e => e.{{baseEntity}}Id)
|
|
221
|
+
.OnDelete(DeleteBehavior.Restrict);
|
|
213
222
|
|
|
214
|
-
// Index on foreign key
|
|
215
223
|
builder.HasIndex(e => e.{{baseEntity}}Id)
|
|
216
|
-
.
|
|
224
|
+
.HasDatabaseName("IX_{{tablePrefix}}{{name}}s_{{baseEntity}}Id");
|
|
217
225
|
{{/if}}
|
|
218
226
|
|
|
219
|
-
// TODO: Add
|
|
227
|
+
// TODO: Add business property configurations (HasMaxLength, IsRequired, indexes)
|
|
220
228
|
}
|
|
221
229
|
}
|
|
222
230
|
|
|
223
231
|
// ============================================================================
|
|
224
|
-
// DbContext Update (add to
|
|
232
|
+
// DbContext Update (add to your DbContext interface + implementation)
|
|
225
233
|
// ============================================================================
|
|
226
234
|
// public DbSet<{{name}}> {{name}}s => Set<{{name}}>();
|
|
227
235
|
|
|
228
236
|
// ============================================================================
|
|
229
237
|
// Migration Command
|
|
230
238
|
// ============================================================================
|
|
231
|
-
// dotnet ef migrations add core_vX.X.X_XXX_Add{{name}} --context
|
|
239
|
+
// dotnet ef migrations add core_vX.X.X_XXX_Add{{name}} --context CoreDbContext
|
|
@@ -16,8 +16,8 @@ import type {
|
|
|
16
16
|
{{name}}CreateRequest,
|
|
17
17
|
{{name}}UpdateRequest,
|
|
18
18
|
{{name}}ListResponse,
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
PaginationParams,
|
|
20
|
+
PaginatedResult
|
|
21
21
|
} from '../types/{{nameLower}}';
|
|
22
22
|
|
|
23
23
|
const ROUTE = getRoute('{{navRoute}}');
|
|
@@ -27,7 +27,7 @@ export const {{nameLower}}Api = {
|
|
|
27
27
|
/**
|
|
28
28
|
* Get all {{name}}s with pagination
|
|
29
29
|
*/
|
|
30
|
-
async getAll(params?:
|
|
30
|
+
async getAll(params?: PaginationParams): Promise<PaginatedResult<{{name}}>> {
|
|
31
31
|
const response = await apiClient.get<{{name}}ListResponse>(ROUTE.api, { params });
|
|
32
32
|
return response.data;
|
|
33
33
|
},
|
|
@@ -76,7 +76,7 @@ export const {{nameLower}}Api = {
|
|
|
76
76
|
/**
|
|
77
77
|
* Search {{name}}s
|
|
78
78
|
*/
|
|
79
|
-
async search(query: string, params?:
|
|
79
|
+
async search(query: string, params?: PaginationParams): Promise<PaginatedResult<{{name}}>> {
|
|
80
80
|
const response = await apiClient.get<{{name}}ListResponse>(`${ROUTE.api}/search`, {
|
|
81
81
|
params: { q: query, ...params }
|
|
82
82
|
});
|
|
@@ -53,12 +53,18 @@ public class {{name}}ControllerTests : IClassFixture<WebApplicationFactory<Progr
|
|
|
53
53
|
public async Task GetAll_WhenCalled_ShouldReturn200WithList()
|
|
54
54
|
{
|
|
55
55
|
// Arrange
|
|
56
|
-
var items = new
|
|
56
|
+
var items = new PaginatedResult<{{name}}Response>
|
|
57
57
|
{
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
Items = new List<{{name}}Response>
|
|
59
|
+
{
|
|
60
|
+
new() { Id = Guid.NewGuid(), Code = "TEST-001" },
|
|
61
|
+
new() { Id = Guid.NewGuid(), Code = "TEST-002" }
|
|
62
|
+
},
|
|
63
|
+
Total = 2,
|
|
64
|
+
Page = 1,
|
|
65
|
+
PageSize = 10
|
|
60
66
|
};
|
|
61
|
-
_mockService.Setup(x => x.GetAllAsync(It.IsAny<CancellationToken>()))
|
|
67
|
+
_mockService.Setup(x => x.GetAllAsync(It.IsAny<PaginationParams>(), It.IsAny<CancellationToken>()))
|
|
62
68
|
.ReturnsAsync(items);
|
|
63
69
|
|
|
64
70
|
// Act
|
|
@@ -66,8 +72,9 @@ public class {{name}}ControllerTests : IClassFixture<WebApplicationFactory<Progr
|
|
|
66
72
|
|
|
67
73
|
// Assert
|
|
68
74
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
69
|
-
var result = await response.Content.ReadFromJsonAsync<
|
|
70
|
-
result.Should().
|
|
75
|
+
var result = await response.Content.ReadFromJsonAsync<PaginatedResult<{{name}}Response>>();
|
|
76
|
+
result.Should().NotBeNull();
|
|
77
|
+
result!.Items.Should().HaveCount(2);
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
[Fact]
|
|
@@ -297,11 +304,17 @@ public class {{name}}ControllerTests : IClassFixture<WebApplicationFactory<Progr
|
|
|
297
304
|
{
|
|
298
305
|
// Arrange
|
|
299
306
|
var tenantAId = Guid.NewGuid();
|
|
300
|
-
var tenantAItems = new
|
|
307
|
+
var tenantAItems = new PaginatedResult<{{name}}Response>
|
|
301
308
|
{
|
|
302
|
-
|
|
309
|
+
Items = new List<{{name}}Response>
|
|
310
|
+
{
|
|
311
|
+
new() { Id = Guid.NewGuid(), TenantId = tenantAId, Code = "A-001" }
|
|
312
|
+
},
|
|
313
|
+
Total = 1,
|
|
314
|
+
Page = 1,
|
|
315
|
+
PageSize = 10
|
|
303
316
|
};
|
|
304
|
-
_mockService.Setup(x => x.GetAllAsync(It.IsAny<CancellationToken>()))
|
|
317
|
+
_mockService.Setup(x => x.GetAllAsync(It.IsAny<PaginationParams>(), It.IsAny<CancellationToken>()))
|
|
305
318
|
.ReturnsAsync(tenantAItems);
|
|
306
319
|
|
|
307
320
|
// Act
|
|
@@ -309,8 +322,9 @@ public class {{name}}ControllerTests : IClassFixture<WebApplicationFactory<Progr
|
|
|
309
322
|
|
|
310
323
|
// Assert
|
|
311
324
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
312
|
-
var result = await response.Content.ReadFromJsonAsync<
|
|
313
|
-
result.Should().
|
|
325
|
+
var result = await response.Content.ReadFromJsonAsync<PaginatedResult<{{name}}Response>>();
|
|
326
|
+
result.Should().NotBeNull();
|
|
327
|
+
result!.Items.Should().OnlyContain(x => x.TenantId == tenantAId);
|
|
314
328
|
}
|
|
315
329
|
|
|
316
330
|
[Fact]
|
|
@@ -377,16 +391,25 @@ public class {{name}}ControllerTests : IClassFixture<WebApplicationFactory<Progr
|
|
|
377
391
|
var items = Enumerable.Range(1, 100)
|
|
378
392
|
.Select(i => new {{name}}Response { Id = Guid.NewGuid(), Code = $"TEST-{i:D3}" })
|
|
379
393
|
.ToList();
|
|
380
|
-
|
|
381
|
-
|
|
394
|
+
var paginatedResult = new PaginatedResult<{{name}}Response>
|
|
395
|
+
{
|
|
396
|
+
Items = items.Take(10).ToList(),
|
|
397
|
+
Total = 100,
|
|
398
|
+
Page = 1,
|
|
399
|
+
PageSize = 10
|
|
400
|
+
};
|
|
401
|
+
_mockService.Setup(x => x.GetAllAsync(It.IsAny<PaginationParams>(), It.IsAny<CancellationToken>()))
|
|
402
|
+
.ReturnsAsync(paginatedResult);
|
|
382
403
|
|
|
383
404
|
// Act
|
|
384
405
|
var response = await _client.GetAsync("/api/{{lowerName}}?page=1&pageSize=10");
|
|
385
406
|
|
|
386
407
|
// Assert
|
|
387
408
|
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
388
|
-
var result = await response.Content.ReadFromJsonAsync<
|
|
389
|
-
result.Should().
|
|
409
|
+
var result = await response.Content.ReadFromJsonAsync<PaginatedResult<{{name}}Response>>();
|
|
410
|
+
result.Should().NotBeNull();
|
|
411
|
+
result!.Items.Should().HaveCount(10);
|
|
412
|
+
result.Total.Should().Be(100);
|
|
390
413
|
}
|
|
391
414
|
|
|
392
415
|
#endregion
|
|
@@ -87,28 +87,40 @@ public class {{name}}ServiceTests
|
|
|
87
87
|
CreateValid{{name}}(),
|
|
88
88
|
CreateValid{{name}}()
|
|
89
89
|
};
|
|
90
|
-
_mockRepository.Setup(x => x.GetAllAsync(It.IsAny<CancellationToken>()))
|
|
91
|
-
.ReturnsAsync(
|
|
90
|
+
_mockRepository.Setup(x => x.GetAllAsync(It.IsAny<PaginationParams>(), It.IsAny<CancellationToken>()))
|
|
91
|
+
.ReturnsAsync(new PaginatedResult<{{name}}>
|
|
92
|
+
{
|
|
93
|
+
Items = entities,
|
|
94
|
+
Total = 2,
|
|
95
|
+
Page = 1,
|
|
96
|
+
PageSize = 10
|
|
97
|
+
});
|
|
92
98
|
|
|
93
99
|
// Act
|
|
94
|
-
var result = await _sut.GetAllAsync();
|
|
100
|
+
var result = await _sut.GetAllAsync(new PaginationParams { Page = 1, PageSize = 10 });
|
|
95
101
|
|
|
96
102
|
// Assert
|
|
97
|
-
result.Should().HaveCount(2);
|
|
103
|
+
result.Items.Should().HaveCount(2);
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
[Fact]
|
|
101
107
|
public async Task GetAllAsync_WhenNoEntities_ShouldReturnEmptyList()
|
|
102
108
|
{
|
|
103
109
|
// Arrange
|
|
104
|
-
_mockRepository.Setup(x => x.GetAllAsync(It.IsAny<CancellationToken>()))
|
|
105
|
-
.ReturnsAsync(new
|
|
110
|
+
_mockRepository.Setup(x => x.GetAllAsync(It.IsAny<PaginationParams>(), It.IsAny<CancellationToken>()))
|
|
111
|
+
.ReturnsAsync(new PaginatedResult<{{name}}>
|
|
112
|
+
{
|
|
113
|
+
Items = new List<{{name}}>(),
|
|
114
|
+
Total = 0,
|
|
115
|
+
Page = 1,
|
|
116
|
+
PageSize = 10
|
|
117
|
+
});
|
|
106
118
|
|
|
107
119
|
// Act
|
|
108
|
-
var result = await _sut.GetAllAsync();
|
|
120
|
+
var result = await _sut.GetAllAsync(new PaginationParams { Page = 1, PageSize = 10 });
|
|
109
121
|
|
|
110
122
|
// Assert
|
|
111
|
-
result.Should().BeEmpty();
|
|
123
|
+
result.Items.Should().BeEmpty();
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
#endregion
|
|
@@ -7,7 +7,7 @@ description: |
|
|
|
7
7
|
- Developing feature by feature without /business-analyse
|
|
8
8
|
- Fixing or extending code generated by /ralph-loop
|
|
9
9
|
- Any incremental SmartStack development task
|
|
10
|
-
argument-hint: "[-a] [-x] [-s] [-
|
|
10
|
+
argument-hint: "[-a] [-x] [-s] [-e] [-r] [-pr] <task description>"
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
<objective>
|
|
@@ -21,7 +21,7 @@ Execute incremental SmartStack development using the APEX methodology. This skil
|
|
|
21
21
|
```bash
|
|
22
22
|
/apex add absence management to HR module # Add section to module
|
|
23
23
|
/apex -a fix the leave request controller # Auto fix
|
|
24
|
-
/apex -a -x -s -
|
|
24
|
+
/apex -a -x -s -pr add PDF export to Orders # Full workflow
|
|
25
25
|
/apex -e add status field to Project entity # Economy (no agents)
|
|
26
26
|
/apex -r # Resume previous
|
|
27
27
|
```
|
|
@@ -35,7 +35,6 @@ Execute incremental SmartStack development using the APEX methodology. This skil
|
|
|
35
35
|
| `-a` | Auto mode: skip confirmations |
|
|
36
36
|
| `-x` | Deep review: adversarial code review (beyond step-04 automated checks) |
|
|
37
37
|
| `-s` | Save mode: output to `.claude/output/apex/` |
|
|
38
|
-
| `-t` | Test mode: scaffold + run tests via MCP |
|
|
39
38
|
| `-e` | Economy mode: no subagents |
|
|
40
39
|
| `-r` | Resume: continue from previous state |
|
|
41
40
|
| `-pr` | PR mode: create pull request at end |
|
|
@@ -51,7 +50,6 @@ Execute incremental SmartStack development using the APEX methodology. This skil
|
|
|
51
50
|
| `{auto_mode}` | boolean | Skip confirmations |
|
|
52
51
|
| `{examine_mode}` | boolean | Adversarial review |
|
|
53
52
|
| `{save_mode}` | boolean | Save outputs to `.claude/output/apex/{task-id}/` |
|
|
54
|
-
| `{test_mode}` | boolean | Include test steps (07-08) |
|
|
55
53
|
| `{economy_mode}` | boolean | No subagents |
|
|
56
54
|
| `{pr_mode}` | boolean | Create PR at end |
|
|
57
55
|
| `{context_code}` | string | "business", "platform", "personal" |
|
|
@@ -88,8 +86,8 @@ Execute incremental SmartStack development using the APEX methodology. This skil
|
|
|
88
86
|
| 04 | `steps/step-04-examine.md` | Opus | eXamine: MCP validation, build, 21 POST-CHECKs, acceptance criteria |
|
|
89
87
|
| 05 | `steps/step-05-deep-review.md` | Opus | Deep Review: adversarial code review (if -x) |
|
|
90
88
|
| 06 | `steps/step-06-resolve.md` | Opus | Fix BLOCKING findings (if any) |
|
|
91
|
-
| 07 | `steps/step-07-tests.md` | Opus | Scaffold tests via MCP
|
|
92
|
-
| 08 | `steps/step-08-run-tests.md` | Opus | Run tests until 100% pass
|
|
89
|
+
| 07 | `steps/step-07-tests.md` | Opus | Scaffold tests via MCP |
|
|
90
|
+
| 08 | `steps/step-08-run-tests.md` | Opus | Run tests until 100% pass |
|
|
93
91
|
</step_files>
|
|
94
92
|
|
|
95
93
|
<apex_phases>
|
|
@@ -101,10 +99,10 @@ Execute incremental SmartStack development using the APEX methodology. This skil
|
|
|
101
99
|
| **A** — Analyze | 01 | Yes | Explore existing code |
|
|
102
100
|
| **P** — Plan | 02 | Yes | File-by-file plan with skill/MCP mapping |
|
|
103
101
|
| **E** — Execute | 03 | Yes | Orchestrate creation via skills and MCP |
|
|
104
|
-
| **X** — eXamine | 04 | Yes |
|
|
102
|
+
| **X** — eXamine | 04 | Yes | 30 POST-CHECKs, MCP validation, build, acceptance criteria |
|
|
105
103
|
| *Deep Review* | 05 (if -x) | No | Adversarial code review beyond automated checks |
|
|
106
104
|
| *Resolve* | 06 (if BLOCKING) | No | Fix BLOCKING findings |
|
|
107
|
-
| *Tests* | 07-08
|
|
105
|
+
| *Tests* | 07-08 | **Yes** | Scaffold and run tests |
|
|
108
106
|
</apex_phases>
|
|
109
107
|
|
|
110
108
|
<reference_files>
|
|
@@ -157,6 +155,6 @@ Execute incremental SmartStack development using the APEX methodology. This skil
|
|
|
157
155
|
- MCP validate_conventions: 0 errors
|
|
158
156
|
- Build: dotnet build PASS + npm run typecheck PASS (if frontend)
|
|
159
157
|
- Seed data complete (navigation, permissions, roles, provider) if required
|
|
160
|
-
- Tests: 100% pass, >= 80% coverage
|
|
158
|
+
- Tests: 100% pass, >= 80% coverage
|
|
161
159
|
- Commits atomic per layer
|
|
162
160
|
</success_criteria>
|