@atlashub/smartstack-cli 4.35.0 → 4.37.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 (49) hide show
  1. package/dist/index.js +54 -100
  2. package/dist/index.js.map +1 -1
  3. package/dist/mcp-entry.mjs +54 -11
  4. package/dist/mcp-entry.mjs.map +1 -1
  5. package/package.json +1 -1
  6. package/templates/agents/efcore/migration.md +43 -0
  7. package/templates/agents/efcore/rebase-snapshot.md +36 -0
  8. package/templates/agents/efcore/squash.md +36 -0
  9. package/templates/skills/apex/references/checks/seed-checks.sh +1 -1
  10. package/templates/skills/apex/references/core-seed-data.md +39 -21
  11. package/templates/skills/application/references/application-roles-template.md +14 -8
  12. package/templates/skills/application/references/provider-template.md +32 -20
  13. package/templates/skills/application/templates-frontend.md +294 -2
  14. package/templates/skills/application/templates-seed.md +23 -11
  15. package/templates/skills/audit-route/SKILL.md +107 -0
  16. package/templates/skills/audit-route/references/routing-pattern.md +129 -0
  17. package/templates/skills/audit-route/steps/step-00-init.md +128 -0
  18. package/templates/skills/audit-route/steps/step-01-inventory.md +157 -0
  19. package/templates/skills/audit-route/steps/step-02-conformity.md +193 -0
  20. package/templates/skills/audit-route/steps/step-03-report.md +201 -0
  21. package/templates/skills/dev-start/SKILL.md +12 -2
  22. package/templates/skills/efcore/SKILL.md +219 -67
  23. package/templates/agents/efcore/conflicts.md +0 -114
  24. package/templates/agents/efcore/db-deploy.md +0 -86
  25. package/templates/agents/efcore/db-reset.md +0 -98
  26. package/templates/agents/efcore/db-seed.md +0 -73
  27. package/templates/agents/efcore/db-status.md +0 -97
  28. package/templates/agents/efcore/scan.md +0 -124
  29. package/templates/skills/efcore/references/both-contexts.md +0 -32
  30. package/templates/skills/efcore/references/destructive-operations.md +0 -38
  31. package/templates/skills/efcore/steps/db/step-deploy.md +0 -217
  32. package/templates/skills/efcore/steps/db/step-reset.md +0 -186
  33. package/templates/skills/efcore/steps/db/step-seed.md +0 -166
  34. package/templates/skills/efcore/steps/db/step-status.md +0 -173
  35. package/templates/skills/efcore/steps/migration/step-00-init.md +0 -102
  36. package/templates/skills/efcore/steps/migration/step-01-check.md +0 -164
  37. package/templates/skills/efcore/steps/migration/step-02-create.md +0 -160
  38. package/templates/skills/efcore/steps/migration/step-03-validate.md +0 -168
  39. package/templates/skills/efcore/steps/rebase-snapshot/step-00-init.md +0 -173
  40. package/templates/skills/efcore/steps/rebase-snapshot/step-01-backup.md +0 -100
  41. package/templates/skills/efcore/steps/rebase-snapshot/step-02-fetch.md +0 -115
  42. package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +0 -112
  43. package/templates/skills/efcore/steps/rebase-snapshot/step-04-validate.md +0 -157
  44. package/templates/skills/efcore/steps/shared/step-00-init.md +0 -131
  45. package/templates/skills/efcore/steps/squash/step-00-init.md +0 -141
  46. package/templates/skills/efcore/steps/squash/step-01-backup.md +0 -120
  47. package/templates/skills/efcore/steps/squash/step-02-fetch.md +0 -168
  48. package/templates/skills/efcore/steps/squash/step-03-create.md +0 -184
  49. package/templates/skills/efcore/steps/squash/step-04-validate.md +0 -174
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlashub/smartstack-cli",
3
- "version": "4.35.0",
3
+ "version": "4.37.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -114,6 +114,49 @@ rm Persistence/Migrations/*${OLD_NAME}*.cs
114
114
 
115
115
  > **Note:** `$MCP_NAME` is obtained via MCP call, never computed locally.
116
116
 
117
+ ## SQL Objects Post-Injection (MANDATORY after every migration creation)
118
+
119
+ **This step is NON-OPTIONAL.** After EVERY `dotnet ef migrations add`, you MUST check for SQL objects and inject them into the generated migration. Skipping this step causes runtime 500 errors on ALL endpoints (TVFs like `fn_GetUserGroupHierarchy` won't exist in the database).
120
+
121
+ ```bash
122
+ # Detect the generated migration file (the main .cs, not .Designer.cs)
123
+ MIGRATION_FILE=$(find "$MIGRATIONS_DIR" -name "*${MCP_NAME}.cs" ! -name "*.Designer.cs" | head -1)
124
+ SQL_OBJECTS_DIR="$INFRA_PROJECT_DIR/Persistence/SqlObjects"
125
+ SQL_FILES=$(find "$SQL_OBJECTS_DIR" -name "*.sql" 2>/dev/null | wc -l)
126
+
127
+ if [ "$SQL_FILES" -gt 0 ]; then
128
+ echo "Found $SQL_FILES SQL object(s) — injecting SqlObjectHelper.ApplyAll()..."
129
+
130
+ # Add using directive if not present
131
+ if ! grep -q "using SmartStack.Infrastructure.Persistence.SqlObjects;" "$MIGRATION_FILE"; then
132
+ sed -i '1s/^/using SmartStack.Infrastructure.Persistence.SqlObjects;\n/' "$MIGRATION_FILE"
133
+ fi
134
+
135
+ # Add SqlObjectHelper.ApplyAll at the end of Up() method
136
+ # Use Edit tool to insert before the closing brace of Up()
137
+ # Target: the last " }" before "protected override void Down"
138
+ sed -i '/protected override void Down/i\
139
+ \ // Apply SQL objects (TVF, Views, SP) from embedded resources\
140
+ \ SqlObjectHelper.ApplyAll(migrationBuilder);' "$MIGRATION_FILE"
141
+
142
+ # VERIFY injection succeeded — FAIL if not
143
+ if ! grep -q "SqlObjectHelper.ApplyAll" "$MIGRATION_FILE"; then
144
+ echo "ERROR: Auto-injection failed. Use Edit tool to manually add:"
145
+ echo " 1. Add 'using SmartStack.Infrastructure.Persistence.SqlObjects;' at top"
146
+ echo " 2. Add 'SqlObjectHelper.ApplyAll(migrationBuilder);' at end of Up() method"
147
+ echo " File: $MIGRATION_FILE"
148
+ # DO NOT CONTINUE — fix this before proceeding
149
+ else
150
+ echo " SqlObjectHelper.ApplyAll(migrationBuilder) injected successfully"
151
+ find "$SQL_OBJECTS_DIR" -name "*.sql" -exec basename {} \; | while read f; do echo " - $f"; done
152
+ fi
153
+ else
154
+ echo "No SQL objects found in SqlObjects/ — skipping injection"
155
+ fi
156
+ ```
157
+
158
+ > **CRITICAL:** If the bash `sed` injection fails (indentation mismatch), use the **Edit tool** to manually insert the lines. NEVER skip this step — it causes cascading 500 errors at runtime.
159
+
117
160
  ## Context Detection
118
161
 
119
162
  If unable to auto-detect:
@@ -70,6 +70,42 @@ mcp__smartstack__suggest_migration({ description: "...", context: DBCONTEXT_TYPE
70
70
 
71
71
  dotnet ef migrations add "$MIGRATION_NAME" --context "$DBCONTEXT"
72
72
 
73
+ # ═══════════════════════════════════════════════════════════════════════════
74
+ # MANDATORY: SQL Objects Post-Injection
75
+ # Without this, TVFs (fn_GetUserGroupHierarchy etc.) won't exist in DB
76
+ # and ALL endpoints will return 500 at runtime
77
+ # ═══════════════════════════════════════════════════════════════════════════
78
+ MIGRATION_FILE=$(find Migrations -name "*${MIGRATION_NAME}.cs" ! -name "*.Designer.cs" | head -1)
79
+ SQL_OBJECTS_DIR="$INFRA_PROJECT_DIR/Persistence/SqlObjects"
80
+ SQL_FILES=$(find "$SQL_OBJECTS_DIR" -name "*.sql" 2>/dev/null | wc -l)
81
+
82
+ if [ "$SQL_FILES" -gt 0 ]; then
83
+ echo "Found $SQL_FILES SQL object(s) — injecting SqlObjectHelper.ApplyAll()..."
84
+
85
+ # Add using directive if not present
86
+ if ! grep -q "using SmartStack.Infrastructure.Persistence.SqlObjects;" "$MIGRATION_FILE"; then
87
+ sed -i '1s/^/using SmartStack.Infrastructure.Persistence.SqlObjects;\n/' "$MIGRATION_FILE"
88
+ fi
89
+
90
+ # Inject SqlObjectHelper.ApplyAll before Down() method
91
+ sed -i '/protected override void Down/i\
92
+ \ // Apply SQL objects (TVF, Views, SP) from embedded resources\
93
+ \ SqlObjectHelper.ApplyAll(migrationBuilder);' "$MIGRATION_FILE"
94
+
95
+ # VERIFY — FAIL if injection didn't work
96
+ if ! grep -q "SqlObjectHelper.ApplyAll" "$MIGRATION_FILE"; then
97
+ echo "ERROR: Auto-injection failed. Use Edit tool to manually add:"
98
+ echo " 1. 'using SmartStack.Infrastructure.Persistence.SqlObjects;' at top"
99
+ echo " 2. 'SqlObjectHelper.ApplyAll(migrationBuilder);' at end of Up()"
100
+ echo " File: $MIGRATION_FILE"
101
+ else
102
+ echo " SqlObjectHelper.ApplyAll(migrationBuilder) injected"
103
+ find "$SQL_OBJECTS_DIR" -name "*.sql" -exec basename {} \; | while read f; do echo " - $f"; done
104
+ fi
105
+ else
106
+ echo "No SQL objects found — skipping injection"
107
+ fi
108
+
73
109
  # Validate
74
110
  dotnet build
75
111
  ```
@@ -100,6 +100,42 @@ mcp__smartstack__suggest_migration({ description: "...", context: DBCONTEXT_TYPE
100
100
 
101
101
  dotnet ef migrations add "$MIGRATION_NAME_FROM_MCP" --context "$DBCONTEXT"
102
102
 
103
+ # ═══════════════════════════════════════════════════════════════════════════
104
+ # MANDATORY: SQL Objects Post-Injection
105
+ # Without this, TVFs (fn_GetUserGroupHierarchy etc.) won't exist in DB
106
+ # and ALL endpoints will return 500 at runtime
107
+ # ═══════════════════════════════════════════════════════════════════════════
108
+ MIGRATION_FILE=$(find Migrations -name "*${MIGRATION_NAME_FROM_MCP}.cs" ! -name "*.Designer.cs" | head -1)
109
+ SQL_OBJECTS_DIR="$INFRA_PROJECT_DIR/Persistence/SqlObjects"
110
+ SQL_FILES=$(find "$SQL_OBJECTS_DIR" -name "*.sql" 2>/dev/null | wc -l)
111
+
112
+ if [ "$SQL_FILES" -gt 0 ]; then
113
+ echo "Found $SQL_FILES SQL object(s) — injecting SqlObjectHelper.ApplyAll()..."
114
+
115
+ # Add using directive if not present
116
+ if ! grep -q "using SmartStack.Infrastructure.Persistence.SqlObjects;" "$MIGRATION_FILE"; then
117
+ sed -i '1s/^/using SmartStack.Infrastructure.Persistence.SqlObjects;\n/' "$MIGRATION_FILE"
118
+ fi
119
+
120
+ # Inject SqlObjectHelper.ApplyAll before Down() method
121
+ sed -i '/protected override void Down/i\
122
+ \ // Apply SQL objects (TVF, Views, SP) from embedded resources\
123
+ \ SqlObjectHelper.ApplyAll(migrationBuilder);' "$MIGRATION_FILE"
124
+
125
+ # VERIFY — FAIL if injection didn't work
126
+ if ! grep -q "SqlObjectHelper.ApplyAll" "$MIGRATION_FILE"; then
127
+ echo "ERROR: Auto-injection failed. Use Edit tool to manually add:"
128
+ echo " 1. 'using SmartStack.Infrastructure.Persistence.SqlObjects;' at top"
129
+ echo " 2. 'SqlObjectHelper.ApplyAll(migrationBuilder);' at end of Up()"
130
+ echo " File: $MIGRATION_FILE"
131
+ else
132
+ echo " SqlObjectHelper.ApplyAll(migrationBuilder) injected"
133
+ find "$SQL_OBJECTS_DIR" -name "*.sql" -exec basename {} \; | while read f; do echo " - $f"; done
134
+ fi
135
+ else
136
+ echo "No SQL objects found — skipping injection"
137
+ fi
138
+
103
139
  # Validate
104
140
  dotnet build && dotnet ef migrations script --idempotent > /dev/null
105
141
  ```
@@ -249,7 +249,7 @@ if [ -n "$PROVIDER" ]; then
249
249
  echo "CRITICAL: Translation seed data inserts without idempotency guard in $PROVIDER"
250
250
  echo "Fix: Before each NavigationTranslations.Add block, check existence:"
251
251
  echo " if (!await context.NavigationTranslations.AnyAsync("
252
- echo " t => t.EntityId == {Module}NavigationSeedData.{Module}ModuleId"
252
+ echo " t => t.EntityId == mod1Entity.Id // actual DB ID, NOT seed-time GUID"
253
253
  echo " && t.EntityType == NavigationEntityType.Module, ct))"
254
254
  echo " { foreach (var t in ...) { context.NavigationTranslations.Add(...); } }"
255
255
  echo "The unique index IX_nav_Translations_EntityType_EntityId_LanguageCode will crash on duplicates."
@@ -109,6 +109,7 @@ public static class NavigationApplicationSeedData
109
109
  return new NavigationApplicationSeedEntry
110
110
  {
111
111
  Id = ApplicationId,
112
+ Zone = ApplicationZone.Business,
112
113
  Code = "{appCode}",
113
114
  Label = "{appLabel_en}",
114
115
  Description = "{appDesc_en}",
@@ -198,6 +199,7 @@ public static class NavigationApplicationSeedData
198
199
  public class NavigationApplicationSeedEntry
199
200
  {
200
201
  public Guid Id { get; init; }
202
+ public ApplicationZone Zone { get; init; }
201
203
  public string Code { get; init; } = null!;
202
204
  public string Label { get; init; } = null!;
203
205
  public string? Description { get; init; }
@@ -1007,11 +1009,12 @@ public class RolePermissionSeedEntry
1007
1009
 
1008
1010
  | Rule | Description |
1009
1011
  |------|-------------|
1010
- | Factory methods | `NavigationModule.Create(...)`, `Role.Create(...)`, `Permission.CreateForModule(...)`, `RolePermission.Create(...)`do not use `new Entity()` |
1012
+ | Factory methods | `NavigationApplication.Create(zone, ...)`, `Role.Create(name, shortName, category, ...)`, `Permission.CreateForModule(...)` — NEVER `new Entity()` |
1011
1013
  | Idempotence | Each Seed method checks existence before inserting |
1012
1014
  | Execution order | Navigation → Roles → Permissions → RolePermissions (roles MUST exist before mapping) |
1013
1015
  | SaveChanges per group | Navigation -> save -> Roles -> save -> Permissions -> save -> RolePermissions -> save |
1014
- | FK resolution by Code | Parent entities (modules, roles) found by `Code`, not hardcoded GUID |
1016
+ | FK resolution by Code | Parent entities (applications, modules, roles) found by `Code` from DB, not seed-time GUID |
1017
+ | Translation EntityId | ALWAYS use actual DB ID (`app.Id`, `mod1Entity.Id`, `actualSection.Id`), NEVER seed-time GUID from SeedData classes |
1015
1018
  | DI registration | `services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>()` |
1016
1019
 
1017
1020
  ### Template
@@ -1054,7 +1057,7 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1054
1057
  else
1055
1058
  {
1056
1059
  app = NavigationApplication.Create(
1057
- appEntry.Code, appEntry.Label,
1060
+ appEntry.Zone, appEntry.Code, appEntry.Label,
1058
1061
  appEntry.Description, appEntry.Icon, appEntry.IconType,
1059
1062
  appEntry.Route, appEntry.DisplayOrder);
1060
1063
  context.NavigationApplications.Add(app);
@@ -1064,7 +1067,7 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1064
1067
  foreach (var t in NavigationApplicationSeedData.GetTranslationEntries())
1065
1068
  {
1066
1069
  context.NavigationTranslations.Add(
1067
- NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
1070
+ NavigationTranslation.Create(t.EntityType, app.Id, t.LanguageCode, t.Label, t.Description));
1068
1071
  }
1069
1072
  await ((DbContext)context).SaveChangesAsync(ct);
1070
1073
  }
@@ -1097,14 +1100,15 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1097
1100
  // --- Module translations (idempotent — unique index IX_nav_Translations_EntityType_EntityId_LanguageCode) ---
1098
1101
  // Always check existence before inserting translations to avoid duplicate key errors
1099
1102
  // on re-runs, partial failures, or DB reset scenarios.
1103
+ // CRITICAL: Use mod1Entity.Id (actual DB ID), NOT seed-time GUID from SeedData class.
1100
1104
  if (!await context.NavigationTranslations.AnyAsync(
1101
- t => t.EntityId == {Module1Pascal}NavigationSeedData.{Module1Pascal}ModuleId
1105
+ t => t.EntityId == mod1Entity.Id
1102
1106
  && t.EntityType == NavigationEntityType.Module, ct))
1103
1107
  {
1104
1108
  foreach (var t in {Module1Pascal}NavigationSeedData.GetTranslationEntries())
1105
1109
  {
1106
1110
  context.NavigationTranslations.Add(
1107
- NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
1111
+ NavigationTranslation.Create(t.EntityType, mod1Entity.Id, t.LanguageCode, t.Label, t.Description));
1108
1112
  }
1109
1113
  }
1110
1114
  // Repeat for each module...
@@ -1129,16 +1133,21 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1129
1133
  await ((DbContext)context).SaveChangesAsync(ct);
1130
1134
 
1131
1135
  // --- Section translations (idempotent — check before inserting) ---
1136
+ // CRITICAL: secEntry.Id is a seed-time GUID ≠ actual DB ID.
1137
+ // Resolve actual section from DB by Code+ModuleId to get the real ID.
1132
1138
  foreach (var secEntry in {Module1Pascal}NavigationSeedData.GetSectionEntries(mod1Entity.Id))
1133
1139
  {
1140
+ var actualSection = await context.NavigationSections
1141
+ .FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == mod1Entity.Id, ct);
1142
+
1134
1143
  if (!await context.NavigationTranslations.AnyAsync(
1135
- t => t.EntityId == secEntry.Id && t.EntityType == NavigationEntityType.Section, ct))
1144
+ t => t.EntityId == actualSection.Id && t.EntityType == NavigationEntityType.Section, ct))
1136
1145
  {
1137
1146
  foreach (var t in {Module1Pascal}NavigationSeedData.GetSectionTranslationEntries()
1138
1147
  .Where(st => st.EntityId == secEntry.Id))
1139
1148
  {
1140
1149
  context.NavigationTranslations.Add(
1141
- NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
1150
+ NavigationTranslation.Create(t.EntityType, actualSection.Id, t.LanguageCode, t.Label, t.Description));
1142
1151
  }
1143
1152
  }
1144
1153
  }
@@ -1174,10 +1183,13 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1174
1183
 
1175
1184
  public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct)
1176
1185
  {
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
1186
+ // Resolve application from DB by Code NOT seed-time GUID
1187
+ var app = await context.NavigationApplications
1188
+ .FirstAsync(a => a.Code == "{appCode}", ct);
1189
+
1190
+ // Check idempotence — verify by Code, scoped by actual DB ApplicationId
1179
1191
  var existingRoleCodes = await context.Roles
1180
- .Where(r => r.ApplicationId == ApplicationRolesSeedData.ApplicationId
1192
+ .Where(r => r.ApplicationId == app.Id
1181
1193
  && (r.Code == "admin" || r.Code == "manager" || r.Code == "contributor" || r.Code == "viewer"))
1182
1194
  .Select(r => r.Code)
1183
1195
  .ToListAsync(ct);
@@ -1188,11 +1200,13 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1188
1200
  if (existingRoleCodes.Contains(entry.Code)) continue; // Skip if already exists
1189
1201
 
1190
1202
  var role = Role.Create(
1191
- entry.Code,
1192
- entry.Name,
1193
- entry.Description,
1194
- entry.ApplicationId,
1195
- entry.IsSystem);
1203
+ name: entry.Name,
1204
+ shortName: entry.Code,
1205
+ category: RoleCategory.Application,
1206
+ description: entry.Description,
1207
+ isSystem: entry.IsSystem,
1208
+ applicationId: app.Id,
1209
+ code: entry.Code);
1196
1210
  context.Roles.Add(role);
1197
1211
  }
1198
1212
  await ((DbContext)context).SaveChangesAsync(ct);
@@ -1231,13 +1245,17 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1231
1245
  .AnyAsync(rp => rp.Permission!.Path.StartsWith("{appCode}."), ct);
1232
1246
  if (exists) return;
1233
1247
 
1248
+ // Resolve application from DB by Code — NOT seed-time GUID
1249
+ var app = await context.NavigationApplications
1250
+ .FirstAsync(a => a.Code == "{appCode}", ct);
1251
+
1234
1252
  // Resolve roles by Code from DB — do not use hardcoded GUIDs.
1235
1253
  // Application-scoped roles (admin, manager, contributor, viewer) are created by
1236
1254
  // SeedRolesAsync() above. System roles use their own IDs that do not match
1237
1255
  // DeterministicGuid("role:admin").
1238
1256
  // Load THIS application's roles + system roles only
1239
1257
  var roles = await context.Roles
1240
- .Where(r => r.ApplicationId == ApplicationRolesSeedData.ApplicationId || r.IsSystem)
1258
+ .Where(r => r.ApplicationId == app.Id || r.IsSystem)
1241
1259
  .ToListAsync(ct);
1242
1260
 
1243
1261
  // Resolve permissions
@@ -1333,11 +1351,11 @@ Before marking the task as completed, verify ALL:
1333
1351
 
1334
1352
  **Application-Level (FIRST — before modules):**
1335
1353
  - [ ] `NavigationApplicationSeedData.cs` created (once per application, at `Infrastructure/Persistence/Seeding/Data/`)
1336
- - [ ] Application GUID is deterministic (SHA256 of `"navigation-application-{appCode}"`)
1337
- - [ ] GetApplicationEntry() takes no parameters (no contextId)
1338
- - [ ] Application translations created (4 languages: fr, en, it, de, EntityType = Application)
1354
+ - [ ] Application GUID is random (`Guid.NewGuid()`) — FK resolution is by Code lookup, not fixed ID
1355
+ - [ ] GetApplicationEntry() takes no parameters (no contextId), includes `Zone = ApplicationZone.Business`
1356
+ - [ ] Application translations created (4 languages: fr, en, it, de, EntityType = Application), using `app.Id` (actual DB ID) for EntityId
1339
1357
  - [ ] `IClientSeedDataProvider.SeedNavigationAsync()` uses `NavigationApplicationSeedData` (NO hardcoded `{appLabel_en}` / `{appIcon}` placeholders)
1340
- - [ ] `ApplicationRolesSeedData.ApplicationId` references `NavigationApplicationSeedData.ApplicationId` (NO `{ApplicationGuid}` placeholder)
1358
+ - [ ] `ApplicationRolesSeedData.ApplicationId` references `NavigationApplicationSeedData.ApplicationId` (DTO only — provider code resolves from DB by Code)
1341
1359
 
1342
1360
  **Module-Level:**
1343
1361
  - [ ] Random GUIDs via `Guid.NewGuid()` (no deterministic/sequential/fixed values)
@@ -147,20 +147,26 @@ Add a new method `SeedRolesAsync()` to the provider:
147
147
  ```csharp
148
148
  public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct)
149
149
  {
150
- // Check idempotence
150
+ // Resolve application from DB by Code — NOT seed-time GUID
151
+ var app = await context.NavigationApplications
152
+ .FirstAsync(a => a.Code == "{app_code}", ct);
153
+
154
+ // Check idempotence — scoped by actual DB ApplicationId
151
155
  var exists = await context.Roles
152
- .AnyAsync(r => r.ApplicationId == ApplicationRolesSeedData.ApplicationId, ct);
156
+ .AnyAsync(r => r.ApplicationId == app.Id, ct);
153
157
  if (exists) return;
154
158
 
155
159
  // Create application-scoped roles using factory method
156
160
  foreach (var entry in ApplicationRolesSeedData.GetRoleEntries())
157
161
  {
158
162
  var role = Role.Create(
159
- entry.Code,
160
- entry.Name,
161
- entry.Description,
162
- entry.ApplicationId,
163
- entry.IsSystem);
163
+ name: entry.Name,
164
+ shortName: entry.Code,
165
+ category: RoleCategory.Application,
166
+ description: entry.Description,
167
+ isSystem: entry.IsSystem,
168
+ applicationId: app.Id,
169
+ code: entry.Code);
164
170
 
165
171
  context.Roles.Add(role);
166
172
  }
@@ -205,7 +211,7 @@ Before marking the task as completed, verify:
205
211
  ## Notes
206
212
 
207
213
  - **Application ID source:** Read from the navigation application created in `SeedNavigationAsync()` or from `{AppPascal}NavigationSeedData.cs`
208
- - **Role factory method:** Use `Role.Create(code, name, description, applicationId, isSystem)` from SmartStack.Domain
214
+ - **Role factory method:** Use `Role.Create(name, shortName, category, description, isSystem, applicationId: ..., code: ...)` from SmartStack.Domain
209
215
  - **Code uniqueness:** Role codes must be unique within the application scope
210
216
  - **System roles:** These are NOT system roles (IsSystem = false) - they are application-scoped roles
211
217
  - **Tenant isolation:** Application-scoped roles are automatically tenant-isolated via the Core authorization system
@@ -43,7 +43,7 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
43
43
  else
44
44
  {
45
45
  app = NavigationApplication.Create(
46
- appEntry.Code, appEntry.Label,
46
+ ApplicationZone.Business, appEntry.Code, appEntry.Label,
47
47
  appEntry.Description, appEntry.Icon, appEntry.IconType,
48
48
  appEntry.Route, appEntry.DisplayOrder);
49
49
  context.NavigationApplications.Add(app);
@@ -53,7 +53,7 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
53
53
  foreach (var t in NavigationApplicationSeedData.GetTranslationEntries())
54
54
  {
55
55
  context.NavigationTranslations.Add(
56
- NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
56
+ NavigationTranslation.Create(t.EntityType, app.Id, t.LanguageCode, t.Label, t.Description));
57
57
  }
58
58
  await ((DbContext)context).SaveChangesAsync(ct);
59
59
  }
@@ -72,18 +72,24 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
72
72
  // --- Module translations (IDEMPOTENT — check before inserting) ---
73
73
  // CRITICAL: nav_Translations has unique index IX_nav_Translations_EntityType_EntityId_LanguageCode
74
74
  // Always guard with AnyAsync to prevent duplicate key errors on re-runs.
75
+ // CRITICAL: Use modEntity.Id (actual DB ID), NOT seed-time GUID from SeedData class.
75
76
  // if (!await context.NavigationTranslations.AnyAsync(
76
- // t => t.EntityId == {Module}NavigationSeedData.{Module}ModuleId
77
+ // t => t.EntityId == mod1Entity.Id
77
78
  // && t.EntityType == NavigationEntityType.Module, ct))
78
- // { foreach (var t in {Module}NavigationSeedData.GetTranslationEntries()) { ... } }
79
+ // { foreach (var t in {Module}NavigationSeedData.GetTranslationEntries())
80
+ // { context.NavigationTranslations.Add(
81
+ // NavigationTranslation.Create(t.EntityType, mod1Entity.Id, t.LanguageCode, t.Label, t.Description)); } }
79
82
  await ((DbContext)context).SaveChangesAsync(ct);
80
83
 
81
84
  // --- Sections (idempotent per-section) ---
82
85
  // Check each section before inserting: AnyAsync(s => s.Code == secEntry.Code && s.ModuleId == ...)
83
86
 
84
87
  // --- Section translations (IDEMPOTENT — same guard pattern as module translations) ---
88
+ // CRITICAL: secEntry.Id is a seed-time GUID ≠ actual DB ID.
89
+ // Resolve actual section from DB: var actualSection = await context.NavigationSections
90
+ // .FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == modEntity.Id, ct);
85
91
  // if (!await context.NavigationTranslations.AnyAsync(
86
- // t => t.EntityId == secEntry.Id && t.EntityType == NavigationEntityType.Section, ct))
92
+ // t => t.EntityId == actualSection.Id && t.EntityType == NavigationEntityType.Section, ct))
87
93
 
88
94
  // --- Resources (idempotent — use ACTUAL section IDs from DB) ---
89
95
  // CRITICAL: NavigationSection.Create() generates its own random ID.
@@ -95,10 +101,13 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
95
101
 
96
102
  public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct)
97
103
  {
98
- // Check idempotence
99
- var applicationId = ApplicationRolesSeedData.ApplicationId;
104
+ // Resolve application from DB by Code — NOT seed-time GUID
105
+ var app = await context.NavigationApplications
106
+ .FirstAsync(a => a.Code == "{app_code}", ct);
107
+
108
+ // Check idempotence — scoped by actual DB ApplicationId
100
109
  var exists = await context.Roles
101
- .AnyAsync(r => r.ApplicationId == applicationId, ct);
110
+ .AnyAsync(r => r.ApplicationId == app.Id, ct);
102
111
  if (exists) return;
103
112
 
104
113
  // Create application-scoped roles (Admin, Manager, Contributor, Viewer)
@@ -106,11 +115,13 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
106
115
  foreach (var entry in ApplicationRolesSeedData.GetRoleEntries())
107
116
  {
108
117
  var role = Role.Create(
109
- entry.Code,
110
- entry.Name,
111
- entry.Description,
112
- entry.ApplicationId,
113
- entry.IsSystem);
118
+ name: entry.Name,
119
+ shortName: entry.Code,
120
+ category: RoleCategory.Application,
121
+ description: entry.Description,
122
+ isSystem: entry.IsSystem,
123
+ applicationId: app.Id,
124
+ code: entry.Code);
114
125
  context.Roles.Add(role);
115
126
  }
116
127
  await ((DbContext)context).SaveChangesAsync(ct);
@@ -167,11 +178,12 @@ services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>();
167
178
 
168
179
  ## Critical Rules
169
180
 
170
- 1. **Factory methods mandatory**: `NavigationModule.Create(...)`, `Role.Create(...)`, `Permission.CreateForModule(...)`, `RolePermission.Create(...)` — NEVER `new Entity()`
181
+ 1. **Factory methods mandatory**: `NavigationApplication.Create(zone, ...)`, `NavigationModule.Create(...)`, `Role.Create(name, shortName, category, ...)`, `Permission.CreateForModule(...)`, `RolePermission.Create(...)` — NEVER `new Entity()`
171
182
  2. **Idempotence**: Each Seed method checks existence before inserting. **Translations require explicit `AnyAsync` guard** — unique index `IX_nav_Translations_EntityType_EntityId_LanguageCode` causes crashes on duplicate inserts
172
- 3. **Translation guard pattern**: `if (!await context.NavigationTranslations.AnyAsync(t => t.EntityId == {id} && t.EntityType == ..., ct))` MANDATORY before every translation batch insert
173
- 4. **SaveChangesAsync per group**: Navigation save Roles save Permissions save RolePermissions save
174
- 4. **Execution order**: `SeedRolesAsync()` MUST be called BEFORE `SeedRolePermissionsAsync()` (roles must exist before mapping)
175
- 5. **Random GUIDs**: ALWAYS use `Guid.NewGuid()` resolve FKs by Code lookup, not fixed IDs
176
- 6. **Resolve FK by Code**: Parent modules and roles are found by `Code`, not hardcoded GUID
177
- 7. **Order property**: Use `100` as default. If multiple providers exist, they run in Order sequence
183
+ 3. **Translation EntityId**: ALWAYS use actual DB ID (e.g. `app.Id`, `mod1Entity.Id`, `actualSection.Id`), NEVER seed-time GUID from SeedData classes
184
+ 4. **Translation guard pattern**: `if (!await context.NavigationTranslations.AnyAsync(t => t.EntityId == {actualDbId} && t.EntityType == ..., ct))` MANDATORY before every translation batch insert
185
+ 5. **SaveChangesAsync per group**: Navigation save Roles save Permissions save → RolePermissions → save
186
+ 6. **Execution order**: `SeedRolesAsync()` MUST be called BEFORE `SeedRolePermissionsAsync()` (roles must exist before mapping)
187
+ 7. **Random GUIDs**: ALWAYS use `Guid.NewGuid()` resolve FKs by Code lookup, not fixed IDs
188
+ 8. **Resolve FK by Code**: Parent entities (applications, modules, roles) are found by `Code` from DB, not by seed-time GUID
189
+ 9. **Order property**: Use `100` as default. If multiple providers exist, they run in Order sequence