@atlashub/smartstack-cli 3.34.0 → 3.36.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 (39) hide show
  1. package/.documentation/init.html +409 -0
  2. package/dist/index.js +35 -3
  3. package/dist/index.js.map +1 -1
  4. package/dist/mcp-entry.mjs +118 -70
  5. package/dist/mcp-entry.mjs.map +1 -1
  6. package/package.json +1 -2
  7. package/templates/mcp-scaffolding/controller.cs.hbs +5 -1
  8. package/templates/skills/apex/SKILL.md +6 -3
  9. package/templates/skills/apex/references/post-checks.md +225 -0
  10. package/templates/skills/apex/references/smartstack-api.md +29 -1
  11. package/templates/skills/apex/references/smartstack-frontend.md +27 -0
  12. package/templates/skills/apex/references/smartstack-layers.md +18 -2
  13. package/templates/skills/apex/steps/step-00-init.md +77 -1
  14. package/templates/skills/apex/steps/step-01-analyze.md +21 -0
  15. package/templates/skills/apex/steps/step-03-execute.md +94 -5
  16. package/templates/skills/apex/steps/step-04-examine.md +7 -1
  17. package/templates/skills/business-analyse/SKILL.md +4 -3
  18. package/templates/skills/business-analyse/_shared.md +9 -0
  19. package/templates/skills/business-analyse/schemas/application-schema.json +13 -0
  20. package/templates/skills/business-analyse/steps/step-00-init.md +190 -34
  21. package/templates/skills/business-analyse/steps/step-01-cadrage.md +129 -10
  22. package/templates/skills/business-analyse/steps/step-01b-applications.md +184 -13
  23. package/templates/skills/business-analyse/steps/step-03c-compile.md +14 -2
  24. package/templates/skills/business-analyse/steps/step-03d-validate.md +5 -1
  25. package/templates/skills/documentation/SKILL.md +175 -9
  26. package/templates/skills/efcore/steps/squash/step-03-create.md +6 -4
  27. package/templates/skills/gitflow/_shared.md +3 -1
  28. package/templates/skills/gitflow/steps/step-pr.md +34 -0
  29. package/templates/skills/ralph-loop/SKILL.md +31 -2
  30. package/templates/skills/ralph-loop/references/category-rules.md +29 -0
  31. package/templates/skills/ralph-loop/references/compact-loop.md +85 -2
  32. package/templates/skills/ralph-loop/references/section-splitting.md +439 -0
  33. package/templates/skills/ralph-loop/references/team-orchestration.md +331 -14
  34. package/templates/skills/ralph-loop/steps/step-00-init.md +4 -0
  35. package/templates/skills/ralph-loop/steps/step-01-task.md +27 -0
  36. package/templates/skills/ralph-loop/steps/step-02-execute.md +206 -1
  37. package/templates/skills/ralph-loop/steps/step-05-report.md +19 -0
  38. package/scripts/health-check.sh +0 -168
  39. package/scripts/postinstall.js +0 -18
@@ -26396,6 +26396,7 @@ var init_config = __esm({
26396
26396
  ],
26397
26397
  customTablePrefixes: [],
26398
26398
  scopeTypes: ["Core", "Extension", "Partner", "Community"],
26399
+ // Incremental: {context}_v{version}_{sequence}_{Description} | Squash: {context}_v{version}
26399
26400
  migrationFormat: "{context}_v{version}_{sequence}_{Description}",
26400
26401
  namespaces: {
26401
26402
  // Empty = auto-detect from .csproj files
@@ -26847,32 +26848,39 @@ async function validateMigrationNaming(structure, _config, result) {
26847
26848
  return;
26848
26849
  }
26849
26850
  const migrationFiles = await findFiles("*.cs", { cwd: structure.migrations });
26850
- const migrationPattern = /^(\w+)_v(\d+\.\d+\.\d+)_(\d{3})_(.+)\.cs$/;
26851
+ const incrementalPattern = /^(\w+)_v(\d+\.\d+\.\d+)_(\d{3})_(.+)\.cs$/;
26852
+ const squashPattern = /^(\w+)_v(\d+\.\d+\.\d+)\.cs$/;
26851
26853
  const designerPattern = /\.Designer\.cs$/;
26852
26854
  for (const file of migrationFiles) {
26853
26855
  const fileName = path8.basename(file);
26854
26856
  if (designerPattern.test(fileName) || fileName.includes("ModelSnapshot")) {
26855
26857
  continue;
26856
26858
  }
26857
- if (!migrationPattern.test(fileName)) {
26859
+ if (!incrementalPattern.test(fileName) && !squashPattern.test(fileName)) {
26858
26860
  result.errors.push({
26859
26861
  type: "error",
26860
26862
  category: "migrations",
26861
26863
  message: `Migration "${fileName}" does not follow naming convention`,
26862
26864
  file: path8.relative(structure.root, file),
26863
- suggestion: `Expected format: {context}_v{version}_{sequence}_{Description}.cs (e.g., core_v1.0.0_001_CreateAuthUsers.cs)`
26865
+ suggestion: `Expected format: {context}_v{version}_{sequence}_{Description}.cs (incremental) or {context}_v{version}.cs (squash)`
26864
26866
  });
26865
26867
  }
26866
26868
  }
26867
- const orderedMigrations = migrationFiles.map((f) => path8.basename(f)).filter((f) => migrationPattern.test(f) && !f.includes("Designer")).sort();
26869
+ const isValidMigration = (f) => (incrementalPattern.test(f) || squashPattern.test(f)) && !f.includes("Designer");
26870
+ const getVersion = (f) => {
26871
+ const inc = incrementalPattern.exec(f);
26872
+ if (inc) return inc[2];
26873
+ const sq = squashPattern.exec(f);
26874
+ if (sq) return sq[2];
26875
+ return "0.0.0";
26876
+ };
26877
+ const orderedMigrations = migrationFiles.map((f) => path8.basename(f)).filter(isValidMigration).sort();
26868
26878
  for (let i = 1; i < orderedMigrations.length; i++) {
26869
26879
  const prev = orderedMigrations[i - 1];
26870
26880
  const curr = orderedMigrations[i];
26871
- const prevMatch = migrationPattern.exec(prev);
26872
- const currMatch = migrationPattern.exec(curr);
26873
- if (prevMatch && currMatch) {
26874
- const prevVersion = prevMatch[2];
26875
- const currVersion = currMatch[2];
26881
+ const prevVersion = getVersion(prev);
26882
+ const currVersion = getVersion(curr);
26883
+ if (prevVersion !== "0.0.0" && currVersion !== "0.0.0") {
26876
26884
  if (currVersion < prevVersion) {
26877
26885
  result.warnings.push({
26878
26886
  type: "warning",
@@ -27176,7 +27184,6 @@ async function validateControllerRoutes(structure, _config, result) {
27176
27184
  const navRouteMatch = content.match(/\[NavRoute\s*\(\s*"([^"]+)"(?:\s*,\s*Suffix\s*=\s*"([^"]+)")?\s*\)\]/);
27177
27185
  if (navRouteMatch) {
27178
27186
  const routePath = navRouteMatch[1];
27179
- const suffix = navRouteMatch[2];
27180
27187
  const parts = routePath.split(".");
27181
27188
  if (parts.length < 2) {
27182
27189
  result.warnings.push({
@@ -27197,26 +27204,14 @@ async function validateControllerRoutes(structure, _config, result) {
27197
27204
  suggestion: 'NavRoute paths must be lowercase (e.g., "platform.administration.users")'
27198
27205
  });
27199
27206
  }
27200
- const expectedRoute = `api/${routePath.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`;
27201
27207
  const routeAttrMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
27202
27208
  if (routeAttrMatch) {
27203
- const actualRoute = routeAttrMatch[1];
27204
- if (actualRoute !== expectedRoute) {
27205
- result.errors.push({
27206
- type: "error",
27207
- category: "controllers",
27208
- message: `Controller "${fileName}" has [Route("${actualRoute}")] that doesn't match NavRoute "${routePath}". Expected [Route("${expectedRoute}")]`,
27209
- file: path8.relative(structure.root, file),
27210
- suggestion: `Change [Route] to [Route("${expectedRoute}")] to match the NavRoute convention`
27211
- });
27212
- }
27213
- } else {
27214
- result.warnings.push({
27215
- type: "warning",
27209
+ result.errors.push({
27210
+ type: "error",
27216
27211
  category: "controllers",
27217
- message: `Controller "${fileName}" has [NavRoute] but no explicit [Route] attribute`,
27212
+ message: `Controller "${fileName}" has BOTH [Route("${routeAttrMatch[1]}")] and [NavRoute("${routePath}")]. Only [NavRoute] should be used.`,
27218
27213
  file: path8.relative(structure.root, file),
27219
- suggestion: `Add [Route("${expectedRoute}")] for deterministic routing`
27214
+ suggestion: `Remove [Route("${routeAttrMatch[1]}")] \u2014 [NavRoute("${routePath}")] resolves the route dynamically from the database at startup`
27220
27215
  });
27221
27216
  }
27222
27217
  }
@@ -28276,26 +28271,34 @@ async function handleCheckMigrations(args, config2) {
28276
28271
  async function parseMigrations(migrationsPath, rootPath) {
28277
28272
  const files = await findFiles("*.cs", { cwd: migrationsPath });
28278
28273
  const migrations = [];
28279
- const pattern = /^(\w+)_v(\d+\.\d+\.\d+)_(\d{3})_(.+)\.cs$/;
28274
+ const incrementalPattern = /^(\w+)_v(\d+\.\d+\.\d+)_(\d{3})_(.+)\.cs$/;
28275
+ const squashPattern = /^(\w+)_v(\d+\.\d+\.\d+)\.cs$/;
28280
28276
  for (const file of files) {
28281
28277
  const fileName = path9.basename(file);
28282
28278
  if (fileName.includes(".Designer.") || fileName.includes("ModelSnapshot")) {
28283
28279
  continue;
28284
28280
  }
28285
- const match2 = pattern.exec(fileName);
28286
- if (match2) {
28281
+ const incrementalMatch = incrementalPattern.exec(fileName);
28282
+ const squashMatch = squashPattern.exec(fileName);
28283
+ if (incrementalMatch) {
28287
28284
  migrations.push({
28288
28285
  name: fileName.replace(".cs", ""),
28289
- context: match2[1],
28290
- // DbContext (core, extensions, etc.)
28291
- version: match2[2],
28292
- // Semver version (1.0.0, 1.2.0, etc.)
28293
- sequence: match2[3],
28294
- // Sequence number (001, 002, etc.)
28295
- description: match2[4],
28286
+ context: incrementalMatch[1],
28287
+ version: incrementalMatch[2],
28288
+ sequence: incrementalMatch[3],
28289
+ description: incrementalMatch[4],
28290
+ file: path9.relative(rootPath, file),
28291
+ applied: true
28292
+ });
28293
+ } else if (squashMatch) {
28294
+ migrations.push({
28295
+ name: fileName.replace(".cs", ""),
28296
+ context: squashMatch[1],
28297
+ version: squashMatch[2],
28298
+ sequence: "000",
28299
+ description: "Squash",
28296
28300
  file: path9.relative(rootPath, file),
28297
28301
  applied: true
28298
- // We'd need DB connection to check this
28299
28302
  });
28300
28303
  } else {
28301
28304
  migrations.push({
@@ -28322,7 +28325,7 @@ function checkNamingConventions(result, _config) {
28322
28325
  type: "naming",
28323
28326
  description: `Migration "${migration.name}" does not follow naming convention`,
28324
28327
  files: [migration.file],
28325
- resolution: `Rename to format: {context}_v{version}_{sequence}_{Description} (e.g., core_v1.0.0_001_CreateAuthUsers)`
28328
+ resolution: `Rename to format: {context}_v{version}_{sequence}_{Description} (incremental) or {context}_v{version} (squash)`
28326
28329
  });
28327
28330
  }
28328
28331
  if (migration.version === "0.0.0") {
@@ -28330,7 +28333,7 @@ function checkNamingConventions(result, _config) {
28330
28333
  type: "naming",
28331
28334
  description: `Migration "${migration.name}" missing version number`,
28332
28335
  files: [migration.file],
28333
- resolution: `Use format: {context}_v{version}_{sequence}_{Description} where version is semver (1.0.0, 1.2.0, etc.)`
28336
+ resolution: `Use format: {context}_v{version}_{sequence}_{Description} (incremental) or {context}_v{version} (squash) where version is semver (1.0.0, 1.2.0, etc.)`
28334
28337
  });
28335
28338
  }
28336
28339
  if (migration.version !== "0.0.0" && !parseSemver(migration.version)) {
@@ -28438,7 +28441,7 @@ function generateSuggestions(result) {
28438
28441
  }
28439
28442
  if (result.conflicts.some((c) => c.type === "naming")) {
28440
28443
  result.suggestions.push(
28441
- "Use convention: {context}_v{version}_{sequence}_{Description} for migration naming (e.g., core_v1.0.0_001_CreateAuthUsers)"
28444
+ "Use convention: {context}_v{version}_{sequence}_{Description} for incremental migrations (e.g., core_v1.0.0_001_CreateAuthUsers) or {context}_v{version} for squash migrations (e.g., core_v1.0.0)"
28442
28445
  );
28443
28446
  }
28444
28447
  if (result.conflicts.some((c) => c.type === "order")) {
@@ -52792,19 +52795,24 @@ async function handleSuggestMigration(args, config2) {
52792
52795
  } else {
52793
52796
  version2 = version2 || "1.0.0";
52794
52797
  }
52795
- const pascalDescription = toPascalCase(sanitizedDescription);
52796
- if (!pascalDescription || !/^[A-Z][a-zA-Z0-9]*$/.test(pascalDescription)) {
52797
- throw new Error(`Invalid migration description after PascalCase conversion: "${pascalDescription}"`);
52798
+ let migrationName;
52799
+ if (input.squash) {
52800
+ migrationName = `${context}_v${version2}`;
52801
+ } else {
52802
+ const pascalDescription = toPascalCase(sanitizedDescription);
52803
+ if (!pascalDescription || !/^[A-Z][a-zA-Z0-9]*$/.test(pascalDescription)) {
52804
+ throw new Error(`Invalid migration description after PascalCase conversion: "${pascalDescription}"`);
52805
+ }
52806
+ const sequenceStr = sequence.toString().padStart(3, "0");
52807
+ migrationName = `${context}_v${version2}_${sequenceStr}_${pascalDescription}`;
52798
52808
  }
52799
- const sequenceStr = sequence.toString().padStart(3, "0");
52800
- const migrationName = `${context}_v${version2}_${sequenceStr}_${pascalDescription}`;
52801
52809
  const dbContextName = context === "core" ? "CoreDbContext" : "ExtensionsDbContext";
52802
52810
  const outputPath = context === "extensions" ? "Persistence/Migrations/Extensions" : "Persistence/Migrations";
52803
52811
  const command = `dotnet ef migrations add ${migrationName} --context ${dbContextName} --project ../SmartStack.Infrastructure -o ${outputPath}`;
52804
52812
  const lines = [];
52805
52813
  lines.push("# Migration Name Suggestion");
52806
52814
  lines.push("");
52807
- lines.push("## Suggested Name");
52815
+ lines.push(`## Suggested Name (${input.squash ? "Squash" : "Incremental"})`);
52808
52816
  lines.push("```");
52809
52817
  lines.push(migrationName);
52810
52818
  lines.push("```");
@@ -52822,8 +52830,13 @@ async function handleSuggestMigration(args, config2) {
52822
52830
  lines.push(`| DbContext | \`${dbContextName}\` | EF Core DbContext to use |`);
52823
52831
  lines.push(`| Schema | \`${context}\` | Database schema for tables |`);
52824
52832
  lines.push(`| Version | \`v${version2}\` | Semver version |`);
52825
- lines.push(`| Sequence | \`${sequenceStr}\` | Order in version |`);
52826
- lines.push(`| Description | \`${pascalDescription}\` | Migration description |`);
52833
+ if (!input.squash) {
52834
+ const sequenceStr = sequence.toString().padStart(3, "0");
52835
+ lines.push(`| Sequence | \`${sequenceStr}\` | Order in version |`);
52836
+ lines.push(`| Description | \`${toPascalCase(sanitizedDescription)}\` | Migration description |`);
52837
+ } else {
52838
+ lines.push(`| Mode | \`squash\` | Consolidated baseline (no sequence/description) |`);
52839
+ }
52827
52840
  lines.push("");
52828
52841
  lines.push("> **Note**: Migrations are stored in separate history tables:");
52829
52842
  lines.push(`> - Core: \`core.__EFMigrationsHistory\``);
@@ -52845,15 +52858,17 @@ async function findExistingMigrations(structure, config2, context) {
52845
52858
  const migrationsPath = path12.join(infraPath, "Persistence", "Migrations");
52846
52859
  try {
52847
52860
  const migrationFiles = await findFiles("*.cs", { cwd: migrationsPath });
52848
- const migrationPattern = /^(\w+)_v(\d+\.\d+\.\d+)_(\d+)_(\w+)\.cs$/;
52861
+ const incrementalPattern = /^(\w+)_v(\d+\.\d+\.\d+)_(\d+)_(\w+)\.cs$/;
52862
+ const squashPattern = /^(\w+)_v(\d+\.\d+\.\d+)\.cs$/;
52849
52863
  for (const file of migrationFiles) {
52850
52864
  const fileName = path12.basename(file);
52851
52865
  if (fileName.includes(".Designer.") || fileName.includes("ModelSnapshot")) {
52852
52866
  continue;
52853
52867
  }
52854
- const match2 = fileName.match(migrationPattern);
52855
- if (match2) {
52856
- const [, ctx, ver, seq, desc] = match2;
52868
+ const incrementalMatch = fileName.match(incrementalPattern);
52869
+ const squashMatch = fileName.match(squashPattern);
52870
+ if (incrementalMatch) {
52871
+ const [, ctx, ver, seq, desc] = incrementalMatch;
52857
52872
  if (ctx === context || !context) {
52858
52873
  migrations.push({
52859
52874
  name: fileName.replace(".cs", ""),
@@ -52863,6 +52878,17 @@ async function findExistingMigrations(structure, config2, context) {
52863
52878
  description: desc
52864
52879
  });
52865
52880
  }
52881
+ } else if (squashMatch) {
52882
+ const [, ctx, ver] = squashMatch;
52883
+ if (ctx === context || !context) {
52884
+ migrations.push({
52885
+ name: fileName.replace(".cs", ""),
52886
+ context: ctx,
52887
+ version: ver,
52888
+ sequence: 0,
52889
+ description: "Squash"
52890
+ });
52891
+ }
52866
52892
  }
52867
52893
  }
52868
52894
  migrations.sort((a, b) => {
@@ -52895,7 +52921,7 @@ var init_suggest_migration = __esm({
52895
52921
  init_logger();
52896
52922
  suggestMigrationTool = {
52897
52923
  name: "suggest_migration",
52898
- description: "Suggest a migration name following SmartStack conventions ({context}_v{version}_{sequence}_{Description})",
52924
+ description: "Suggest a migration name following SmartStack conventions ({context}_v{version}_{sequence}_{Description} for incremental, {context}_v{version} for squash)",
52899
52925
  inputSchema: {
52900
52926
  type: "object",
52901
52927
  properties: {
@@ -52911,6 +52937,10 @@ var init_suggest_migration = __esm({
52911
52937
  version: {
52912
52938
  type: "string",
52913
52939
  description: 'Semver version (e.g., "1.0.0", "1.2.0"). If not provided, uses latest from existing migrations.'
52940
+ },
52941
+ squash: {
52942
+ type: "boolean",
52943
+ description: "If true, generates squash format: {context}_v{version} (no sequence/description). Used before merge to consolidate feature migrations."
52914
52944
  }
52915
52945
  },
52916
52946
  required: ["description"]
@@ -52919,7 +52949,8 @@ var init_suggest_migration = __esm({
52919
52949
  SuggestMigrationInputSchema2 = external_exports.object({
52920
52950
  description: external_exports.string().min(3, "Migration description must be at least 3 characters").max(100, "Migration description must be at most 100 characters").describe("Description of what the migration does"),
52921
52951
  context: external_exports.enum(["core", "extensions"]).optional().describe("DbContext name (default: auto-detected from project config)"),
52922
- version: external_exports.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format (e.g., "1.0.0")').optional().describe('Semver version (e.g., "1.0.0")')
52952
+ version: external_exports.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format (e.g., "1.0.0")').optional().describe('Semver version (e.g., "1.0.0")'),
52953
+ squash: external_exports.boolean().optional().describe("If true, generates squash format: {context}_v{version} (no sequence/description)")
52923
52954
  });
52924
52955
  }
52925
52956
  });
@@ -57631,7 +57662,6 @@ async function discoverNavRoutes(structure, scope, warnings) {
57631
57662
  permissions.push(match2[1]);
57632
57663
  }
57633
57664
  const fullNavRoute = suffix ? `${navRoute}.${suffix}` : navRoute;
57634
- const expectedRoute = `api/${navRouteToUrlPath(navRoute)}${suffix ? `/${toKebabCase(suffix)}` : ""}`;
57635
57665
  routes.push({
57636
57666
  navRoute: fullNavRoute,
57637
57667
  apiPath: `/api/${navRouteToUrlPath(navRoute)}${suffix ? `/${toKebabCase(suffix)}` : ""}`,
@@ -57642,12 +57672,9 @@ async function discoverNavRoutes(structure, scope, warnings) {
57642
57672
  });
57643
57673
  const routeAttrMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
57644
57674
  if (routeAttrMatch && warnings) {
57645
- const actualRoute = routeAttrMatch[1];
57646
- if (actualRoute !== expectedRoute && actualRoute !== "api/[controller]") {
57647
- warnings.push(
57648
- `WARNING: ${controllerName}Controller has [Route("${actualRoute}")] that doesn't match NavRoute "${navRoute}". Expected [Route("${expectedRoute}")]`
57649
- );
57650
- }
57675
+ warnings.push(
57676
+ `WARNING: ${controllerName}Controller has [Route("${routeAttrMatch[1]}")] alongside [NavRoute]. Remove [Route] \u2014 NavRoute resolves routes dynamically from the database.`
57677
+ );
57651
57678
  }
57652
57679
  }
57653
57680
  } catch {
@@ -64918,8 +64945,9 @@ builder.HasOne(e => e.User)
64918
64945
 
64919
64946
  ### Naming Format
64920
64947
 
64921
- Migrations MUST follow this naming pattern:
64948
+ Migrations follow two naming patterns depending on context:
64922
64949
 
64950
+ **Incremental (during development):**
64923
64951
  \`\`\`
64924
64952
  ${migrationFormat}
64925
64953
  \`\`\`
@@ -64931,30 +64959,50 @@ ${migrationFormat}
64931
64959
  | \`{sequence}\` | Order in version | \`001\`, \`002\` |
64932
64960
  | \`{Description}\` | Action (PascalCase) | \`CreateAuthUsers\` |
64933
64961
 
64962
+ **Squash (before merge, consolidated baseline):**
64963
+ \`\`\`
64964
+ {context}_v{version}
64965
+ \`\`\`
64966
+
64967
+ The squash format has no sequence or description \u2014 it represents the complete state at a version.
64968
+
64934
64969
  **Examples:**
64935
- - \`core_v1.0.0_001_InitialSchema.cs\`
64936
- - \`core_v1.0.0_002_CreateAuthUsers.cs\`
64937
- - \`core_v1.2.0_001_AddUserProfiles.cs\`
64938
- - \`extensions_v1.0.0_001_AddClientFeatures.cs\`
64970
+ - \`core_v1.0.0_001_InitialSchema.cs\` (incremental)
64971
+ - \`core_v1.0.0_002_CreateAuthUsers.cs\` (incremental)
64972
+ - \`core_v1.2.0_001_AddUserProfiles.cs\` (incremental)
64973
+ - \`extensions_v1.0.0_001_AddClientFeatures.cs\` (incremental)
64974
+ - \`core_v1.0.0.cs\` (squash \u2014 consolidated baseline)
64939
64975
 
64940
64976
  ### Creating Migrations
64941
64977
 
64942
64978
  \`\`\`bash
64943
- # Create a new migration
64979
+ # Create an incremental migration
64944
64980
  dotnet ef migrations add core_v1.0.0_001_InitialSchema
64945
64981
 
64946
64982
  # With context specified
64947
64983
  dotnet ef migrations add core_v1.2.0_001_AddUserProfiles --context ApplicationDbContext
64948
64984
  \`\`\`
64949
64985
 
64986
+ ### Squash Workflow
64987
+
64988
+ Before creating a PR, squash all feature migrations into one:
64989
+
64990
+ \`\`\`bash
64991
+ # Run /efcore squash \u2014 produces: core_v1.0.0 (no sequence/description)
64992
+ dotnet ef migrations add core_v1.0.0 --context CoreDbContext
64993
+ \`\`\`
64994
+
64995
+ After a squash at \`core_v1.0.0\`, the next incremental migration starts at \`core_v1.0.0_001_*\`.
64996
+
64950
64997
  ### Migration Rules
64951
64998
 
64952
- 1. **One migration per feature** - Group related changes in a single migration
64999
+ 1. **One migration per feature** - Squash before merge to ensure a single migration per feature branch
64953
65000
  2. **Version-based naming** - Use semver (v1.0.0, v1.2.0) to link migrations to releases
64954
- 3. **Sequence numbers** - Use NNN (001, 002, etc.) for migrations in the same version
65001
+ 3. **Sequence numbers** - Use NNN (001, 002, etc.) for incremental migrations in the same version
64955
65002
  4. **Context prefix** - Use \`core_\` for platform tables, \`extensions_\` for client extensions
64956
- 5. **Descriptive names** - Use clear PascalCase descriptions (CreateAuthUsers, AddUserProfiles, etc.)
65003
+ 5. **Descriptive names** - Use clear PascalCase descriptions for incremental (CreateAuthUsers, AddUserProfiles, etc.)
64957
65004
  6. **Schema must be specified** - All tables must specify their schema in ToTable()
65005
+ 7. **Squash before PR** - Feature branches must have exactly 1 migration (squashed) before creating a PR
64958
65006
 
64959
65007
  ---
64960
65008