swallowkit 2.0.0-alpha.2 → 2.0.0-alpha.4

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 (176) hide show
  1. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  2. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  3. package/dist/cli/commands/configure-mcp.js +42 -0
  4. package/dist/cli/commands/configure-mcp.js.map +1 -0
  5. package/dist/cli/commands/dev.d.ts +2 -2
  6. package/dist/cli/commands/dev.d.ts.map +1 -1
  7. package/dist/cli/commands/dev.js +39 -27
  8. package/dist/cli/commands/dev.js.map +1 -1
  9. package/dist/cli/commands/init.d.ts +6 -2
  10. package/dist/cli/commands/init.d.ts.map +1 -1
  11. package/dist/cli/commands/init.js +43 -122
  12. package/dist/cli/commands/init.js.map +1 -1
  13. package/dist/cli/commands/migrations.d.ts.map +1 -1
  14. package/dist/cli/commands/migrations.js +25 -0
  15. package/dist/cli/commands/migrations.js.map +1 -1
  16. package/dist/cli/commands/provision.d.ts +2 -0
  17. package/dist/cli/commands/provision.d.ts.map +1 -1
  18. package/dist/cli/commands/provision.js +45 -16
  19. package/dist/cli/commands/provision.js.map +1 -1
  20. package/dist/cli/commands/scaffold.d.ts +4 -0
  21. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  22. package/dist/cli/commands/scaffold.js +40 -2
  23. package/dist/cli/commands/scaffold.js.map +1 -1
  24. package/dist/cli/index.d.ts +2 -0
  25. package/dist/cli/index.d.ts.map +1 -1
  26. package/dist/cli/index.js +27 -2
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/core/operations/model-migration.d.ts +22 -0
  29. package/dist/core/operations/model-migration.d.ts.map +1 -0
  30. package/dist/core/operations/model-migration.js +36 -0
  31. package/dist/core/operations/model-migration.js.map +1 -0
  32. package/dist/core/operations/provision-operations.d.ts +19 -1
  33. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  34. package/dist/core/operations/provision-operations.js +62 -35
  35. package/dist/core/operations/provision-operations.js.map +1 -1
  36. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  37. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  38. package/dist/core/operations/scaffold-apply.js +1 -0
  39. package/dist/core/operations/scaffold-apply.js.map +1 -1
  40. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  41. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  42. package/dist/core/operations/scaffold-machine.js.map +1 -1
  43. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  44. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  45. package/dist/core/operations/scaffold-plan.js +3 -1
  46. package/dist/core/operations/scaffold-plan.js.map +1 -1
  47. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  48. package/dist/core/project/agent-instructions.js +4 -1
  49. package/dist/core/project/agent-instructions.js.map +1 -1
  50. package/dist/core/project/boundaries.d.ts +5 -2
  51. package/dist/core/project/boundaries.d.ts.map +1 -1
  52. package/dist/core/project/boundaries.js +49 -18
  53. package/dist/core/project/boundaries.js.map +1 -1
  54. package/dist/core/project/capabilities.d.ts +1 -0
  55. package/dist/core/project/capabilities.d.ts.map +1 -1
  56. package/dist/core/project/capabilities.js +20 -2
  57. package/dist/core/project/capabilities.js.map +1 -1
  58. package/dist/core/project/home-page.d.ts +4 -0
  59. package/dist/core/project/home-page.d.ts.map +1 -0
  60. package/dist/core/project/home-page.js +113 -0
  61. package/dist/core/project/home-page.js.map +1 -0
  62. package/dist/core/project/mcp-config.d.ts +26 -0
  63. package/dist/core/project/mcp-config.d.ts.map +1 -0
  64. package/dist/core/project/mcp-config.js +117 -0
  65. package/dist/core/project/mcp-config.js.map +1 -0
  66. package/dist/core/project/react-project.d.ts +1 -1
  67. package/dist/core/project/react-project.d.ts.map +1 -1
  68. package/dist/core/project/react-project.js +45 -24
  69. package/dist/core/project/react-project.js.map +1 -1
  70. package/dist/core/project/workflows.d.ts.map +1 -1
  71. package/dist/core/project/workflows.js +9 -1
  72. package/dist/core/project/workflows.js.map +1 -1
  73. package/dist/core/scaffold/auth-generator.js +8 -8
  74. package/dist/core/scaffold/model-parser.d.ts +23 -0
  75. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  76. package/dist/core/scaffold/model-parser.js +407 -137
  77. package/dist/core/scaffold/model-parser.js.map +1 -1
  78. package/dist/core/scaffold/native-schema-generator.d.ts +11 -0
  79. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  80. package/dist/core/scaffold/native-schema-generator.js +279 -91
  81. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  82. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  83. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  84. package/dist/core/scaffold/openapi-generator.js +53 -3
  85. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  86. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  87. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  88. package/dist/core/scaffold/ui-generator.js +68 -7
  89. package/dist/core/scaffold/ui-generator.js.map +1 -1
  90. package/dist/core/verify/index.d.ts.map +1 -1
  91. package/dist/core/verify/index.js +24 -1
  92. package/dist/core/verify/index.js.map +1 -1
  93. package/dist/machine/index.d.ts.map +1 -1
  94. package/dist/machine/index.js +39 -15
  95. package/dist/machine/index.js.map +1 -1
  96. package/dist/mcp/index.d.ts +3 -2
  97. package/dist/mcp/index.d.ts.map +1 -1
  98. package/dist/mcp/index.js +137 -13
  99. package/dist/mcp/index.js.map +1 -1
  100. package/dist/mcp/version-selection.d.ts +55 -0
  101. package/dist/mcp/version-selection.d.ts.map +1 -0
  102. package/dist/mcp/version-selection.js +152 -0
  103. package/dist/mcp/version-selection.js.map +1 -0
  104. package/dist/utils/azure-cli.d.ts +44 -0
  105. package/dist/utils/azure-cli.d.ts.map +1 -0
  106. package/dist/utils/azure-cli.js +164 -0
  107. package/dist/utils/azure-cli.js.map +1 -0
  108. package/dist/utils/package-manager.js +1 -1
  109. package/dist/utils/package-manager.js.map +1 -1
  110. package/dist/utils/python-uv.d.ts +5 -7
  111. package/dist/utils/python-uv.d.ts.map +1 -1
  112. package/dist/utils/python-uv.js +50 -24
  113. package/dist/utils/python-uv.js.map +1 -1
  114. package/dist/utils/react-dev.d.ts +4 -0
  115. package/dist/utils/react-dev.d.ts.map +1 -0
  116. package/dist/utils/react-dev.js +35 -0
  117. package/dist/utils/react-dev.js.map +1 -0
  118. package/dist/utils/semver.d.ts +14 -0
  119. package/dist/utils/semver.d.ts.map +1 -0
  120. package/dist/utils/semver.js +61 -0
  121. package/dist/utils/semver.js.map +1 -0
  122. package/package.json +1 -1
  123. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  124. package/src/__tests__/agent-loop-phase34.test.ts +29 -0
  125. package/src/__tests__/auth.test.ts +9 -0
  126. package/src/__tests__/azure-cli.test.ts +199 -0
  127. package/src/__tests__/boundaries.test.ts +42 -0
  128. package/src/__tests__/csharp-schema.test.ts +261 -0
  129. package/src/__tests__/dev.test.ts +13 -6
  130. package/src/__tests__/home-page.test.ts +50 -0
  131. package/src/__tests__/init.test.ts +57 -0
  132. package/src/__tests__/mcp-version.test.ts +262 -0
  133. package/src/__tests__/mcp.test.ts +49 -3
  134. package/src/__tests__/package-manager.test.ts +2 -2
  135. package/src/__tests__/plan-apply.test.ts +68 -0
  136. package/src/__tests__/python-uv.test.ts +50 -14
  137. package/src/__tests__/python-venv.test.ts +48 -0
  138. package/src/__tests__/react-dev.test.ts +72 -0
  139. package/src/__tests__/react.test.ts +19 -0
  140. package/src/__tests__/schema-analysis.test.ts +329 -0
  141. package/src/cli/commands/configure-mcp.ts +54 -0
  142. package/src/cli/commands/dev.ts +40 -31
  143. package/src/cli/commands/init.ts +48 -125
  144. package/src/cli/commands/migrations.ts +22 -0
  145. package/src/cli/commands/provision.ts +53 -17
  146. package/src/cli/commands/scaffold.ts +40 -2
  147. package/src/cli/index.ts +26 -3
  148. package/src/core/operations/model-migration.ts +52 -0
  149. package/src/core/operations/provision-operations.ts +86 -42
  150. package/src/core/operations/scaffold-apply.ts +2 -0
  151. package/src/core/operations/scaffold-machine.ts +1 -0
  152. package/src/core/operations/scaffold-plan.ts +5 -1
  153. package/src/core/project/agent-instructions.ts +4 -1
  154. package/src/core/project/agent-templates/AGENTS.md +39 -3
  155. package/src/core/project/agent-templates/CLAUDE.md +3 -1
  156. package/src/core/project/agent-templates/copilot-instructions.md +3 -1
  157. package/src/core/project/boundaries.ts +60 -18
  158. package/src/core/project/capabilities.ts +22 -2
  159. package/src/core/project/home-page.ts +114 -0
  160. package/src/core/project/mcp-config.ts +84 -0
  161. package/src/core/project/react-project.ts +49 -27
  162. package/src/core/project/workflows.ts +9 -1
  163. package/src/core/scaffold/auth-generator.ts +8 -8
  164. package/src/core/scaffold/model-parser.ts +413 -129
  165. package/src/core/scaffold/native-schema-generator.ts +305 -105
  166. package/src/core/scaffold/openapi-generator.ts +63 -5
  167. package/src/core/scaffold/ui-generator.ts +70 -8
  168. package/src/core/verify/index.ts +23 -1
  169. package/src/machine/index.ts +42 -18
  170. package/src/mcp/index.ts +156 -14
  171. package/src/mcp/version-selection.ts +163 -0
  172. package/src/utils/azure-cli.ts +154 -0
  173. package/src/utils/package-manager.ts +1 -1
  174. package/src/utils/python-uv.ts +53 -33
  175. package/src/utils/react-dev.ts +31 -0
  176. package/src/utils/semver.ts +59 -0
@@ -62,9 +62,35 @@ interface ScaffoldOptions {
62
62
  functionsDir?: string; // Azure Functions のディレクトリ(デフォルト: "functions")
63
63
  apiDir?: string; // BFF directory (Next.js: app/api, React: bff)
64
64
  apiOnly?: boolean; // true の場合、UI を生成しない(デフォルト: false)
65
+ skipInfra?: boolean; // true の場合、infra/ 配下(コンテナー migration)を変更しない
65
66
  expectedSchemaAnalysis?: SchemaAnalysisSnapshot;
66
67
  }
67
68
 
69
+ export function buildInfraSkippedWarning(modelName: string, modelArgument: string): string {
70
+ return `infra-skipped: The Cosmos DB container migration for ${modelName} was not generated (--skip-infra); infra/ was left unchanged. ` +
71
+ `Generate it later with "swallowkit migrations add-model ${modelArgument}", or re-plan scaffold without --skip-infra.`;
72
+ }
73
+
74
+ const USER_SCOPE_FIELD = /^(user|owner|learner|member|account|customer|student|author|creator|profile|person|employee|patient)Id$/i;
75
+
76
+ function isAuthenticationConfigured(authConfig: AuthConfig | undefined): boolean {
77
+ if (!authConfig || authConfig.provider === "none") return false;
78
+ return Boolean(authConfig.provider) || Object.keys(authConfig.schemes ?? {}).length > 0;
79
+ }
80
+
81
+ /** Warn when a model looks per-user (partition key or owner-like id field) but no authentication is configured. */
82
+ export function buildUserScopedDataWarning(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): string | undefined {
83
+ if (isAuthenticationConfigured(authConfig)) return undefined;
84
+ const partitionField = modelInfo.partitionKey.replace(/^\//, "");
85
+ const fieldNames = modelInfo.fields.filter((field) => field.type === "string" && !field.isArray).map((field) => field.name);
86
+ const ownerField = [partitionField, ...fieldNames].find((name) => USER_SCOPE_FIELD.test(name) && fieldNames.includes(name));
87
+ if (!ownerField) return undefined;
88
+ const reason = ownerField === partitionField ? `is the partition key and looks like a per-user owner id` : `looks like a per-user owner id`;
89
+ return `user-scoped-data-without-auth: ${modelInfo.name}.${ownerField} ${reason}, but authentication is not configured. ` +
90
+ `Generated CRUD does not scope data to a principal, so any caller can read and write every ${modelInfo.name} document. ` +
91
+ `Add authentication with "plan auth", restrict or remove the generated write endpoints, and add ai-authored endpoints that derive ${ownerField} from the verified principal instead of the request body.`;
92
+ }
93
+
68
94
  function getMachineAwareStdio(): "inherit" | "pipe" {
69
95
  return process.env.SWALLOWKIT_MACHINE_OUTPUT === "1" ? "pipe" : "inherit";
70
96
  }
@@ -132,6 +158,12 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
132
158
  session.setSchemaAnalysis(schemaAnalysis);
133
159
  console.log(`✅ Model parsed: ${modelInfo.name} (${modelInfo.schemaName})`);
134
160
 
161
+ const userScopeWarning = buildUserScopedDataWarning(modelInfo, getAuthConfig());
162
+ if (userScopeWarning) {
163
+ session.addWarning(userScopeWarning);
164
+ console.warn(`⚠️ ${userScopeWarning}`);
165
+ }
166
+
135
167
  const backendLanguage = getBackendLanguage();
136
168
  const frontend = getFrontendFramework();
137
169
  const apiDir = options.apiDir || (frontend === "react" ? "bff" : "app/api");
@@ -210,9 +242,15 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
210
242
  await generateBFFRoutes(modelInfo, apiDir, sharedPackageName, session);
211
243
  }
212
244
 
213
- // 8. Generate Cosmos DB container Bicep file (skip for connector models)
245
+ // 8. Generate Cosmos DB container Bicep file (skip for connector models or when infra changes are excluded)
214
246
  if (!isConnectorModel) {
215
- await generateCosmosContainer(modelInfo, session);
247
+ if (options.skipInfra) {
248
+ const warning = buildInfraSkippedWarning(modelInfo.name, toKebabCase(modelInfo.name));
249
+ session.addWarning(warning);
250
+ console.warn(`\n⚠️ ${warning}`);
251
+ } else {
252
+ await generateCosmosContainer(modelInfo, session);
253
+ }
216
254
  }
217
255
 
218
256
  // 9. Generate UI components (unless --api-only)
package/src/cli/index.ts CHANGED
@@ -8,6 +8,7 @@ import { initCommand, devCommand, devSeedsCommand, scaffoldCommand, createModelC
8
8
  import { provisionCommand } from "./commands/provision";
9
9
  import { createMigrationCommand } from "./commands/create-migration";
10
10
  import { migrationsCommand } from "./commands/migrations";
11
+ import { configureMcpCommand } from "./commands/configure-mcp";
11
12
  import { addConnectorCommand } from "./commands/add-connector";
12
13
  import { addAuthCommand } from "./commands/add-auth";
13
14
  import { planScaffoldOperation } from "../core/operations/scaffold-plan";
@@ -15,6 +16,7 @@ import { inspectArtifacts } from "../core/project/artifacts";
15
16
  import { detectDrift } from "../core/project/drift";
16
17
  import { runVerify } from "../core/verify";
17
18
  import { isMachineCommand, runMachineCli } from "../machine";
19
+ import { MachineCommandError } from "../machine/errors";
18
20
 
19
21
  const packageJsonPath = path.resolve(__dirname, "../../package.json");
20
22
  export const CLI_VERSION = JSON.parse(readFileSync(packageJsonPath, "utf8")).version as string;
@@ -140,6 +142,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
140
142
  program.addCommand(provisionCommand);
141
143
  program.addCommand(migrationsCommand);
142
144
  program.addCommand(createMigrationCommand);
145
+ program.addCommand(configureMcpCommand);
143
146
 
144
147
  program
145
148
  .command("create-model <names...>")
@@ -160,6 +163,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
160
163
  .option("--functions-dir <dir>", "Azure Functions directory", "functions")
161
164
  .option("--api-dir <dir>", "BFF routes directory (Next.js: app/api; React: bff)")
162
165
  .option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets", false)
166
+ .option("--skip-infra", "Do not create or update anything under infra/ (generate the container migration later with migrations add-model)", false)
163
167
  .option("--dry-run", "Show planned file changes without writing anything", false)
164
168
  .action(async (model, options) => {
165
169
  if (options.dryRun) {
@@ -168,6 +172,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
168
172
  functionsDir: options.functionsDir,
169
173
  apiDir: options.apiDir,
170
174
  apiOnly: options.apiOnly,
175
+ skipInfra: options.skipInfra,
171
176
  });
172
177
  return;
173
178
  }
@@ -177,6 +182,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
177
182
  functionsDir: options.functionsDir,
178
183
  apiDir: options.apiDir,
179
184
  apiOnly: options.apiOnly,
185
+ skipInfra: options.skipInfra,
180
186
  });
181
187
  });
182
188
 
@@ -226,7 +232,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
226
232
  return program;
227
233
  }
228
234
 
229
- async function runScaffoldDryRun(options: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean }): Promise<void> {
235
+ async function runScaffoldDryRun(options: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }): Promise<void> {
230
236
  console.log("🔍 Scaffold dry-run — no files will be written.\n");
231
237
 
232
238
  const plan = await planScaffoldOperation(options);
@@ -264,7 +270,7 @@ async function runScaffoldDryRun(options: { model: string; functionsDir?: string
264
270
  console.log(`⚠️ ${plan.conflicts.length} file(s) were modified after generation and would be overwritten.`);
265
271
  console.log(` Review the conflicts, then run: swallowkit scaffold ${options.model} (or machine apply scaffold --plan ${plan.planId} --approve)`);
266
272
  } else {
267
- console.log(` Run without --dry-run to apply: swallowkit scaffold ${options.model}`);
273
+ console.log(` Run without --dry-run to apply: swallowkit scaffold ${options.model}${options.skipInfra ? " --skip-infra" : ""}`);
268
274
  }
269
275
  }
270
276
 
@@ -351,9 +357,26 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
351
357
  await createProgram().parseAsync(normalizeDevCommandArgv(argv));
352
358
  }
353
359
 
360
+ /** Human-readable error output for regular CLI commands (machine commands print JSON). */
361
+ export function formatCliError(error: unknown): string {
362
+ if (error instanceof MachineCommandError) {
363
+ const lines = [`❌ ${error.message}`, ` (code: ${error.code})`];
364
+ const details = error.details as Record<string, unknown> | undefined;
365
+ if (details && typeof details === "object") {
366
+ if (typeof details.nextAction === "string") lines.push(` Next: ${details.nextAction}`);
367
+ if (Array.isArray(details.changedFiles)) lines.push(` Changed files: ${details.changedFiles.join(", ")}`);
368
+ }
369
+ return lines.join("\n");
370
+ }
371
+ if (error instanceof Error) {
372
+ return process.env.SWALLOWKIT_DEBUG ? String(error.stack ?? error.message) : `❌ ${error.message}`;
373
+ }
374
+ return `❌ ${String(error)}`;
375
+ }
376
+
354
377
  if (require.main === module) {
355
378
  void runCli().catch((error) => {
356
- console.error(error);
379
+ console.error(formatCliError(error));
357
380
  process.exitCode = 1;
358
381
  });
359
382
  }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Generate only the Cosmos DB container migration for a model. Used after
3
+ * `scaffold --skip-infra`, when infrastructure changes become allowed.
4
+ */
5
+
6
+ import { resolveModelPath } from "../../cli/commands/scaffold";
7
+ import { MachineCommandError } from "../../machine/errors";
8
+ import { parseModelFile } from "../scaffold/model-parser";
9
+ import { recordSessionOperations } from "../project/artifacts";
10
+ import { FileOperationSession } from "./file-session";
11
+ import { generateModelContainerMigration } from "./migration-generator";
12
+
13
+ export interface ModelMigrationResult {
14
+ model: string;
15
+ dryRun: boolean;
16
+ migration: { version: number; template: string } | null;
17
+ reason?: string;
18
+ operations: Array<{ path: string; action: string }>;
19
+ }
20
+
21
+ export async function addModelContainerMigration(
22
+ modelInput: string,
23
+ options: { dryRun?: boolean; projectRoot?: string } = {}
24
+ ): Promise<ModelMigrationResult> {
25
+ const projectRoot = options.projectRoot ?? process.cwd();
26
+ const modelInfo = await parseModelFile(resolveModelPath(modelInput));
27
+ if (modelInfo.connectorConfig) {
28
+ throw new MachineCommandError(
29
+ "connector-model-has-no-container",
30
+ `${modelInfo.name} uses connector ${modelInfo.connectorConfig.connector}; connector models do not get a Cosmos DB container.`,
31
+ undefined,
32
+ "failed"
33
+ );
34
+ }
35
+
36
+ const session = new FileOperationSession(options.dryRun ? "collect" : "commit", projectRoot);
37
+ const migration = generateModelContainerMigration(modelInfo, session);
38
+ if (!options.dryRun) {
39
+ recordSessionOperations(session.operations, {
40
+ schemaHash: modelInfo.semanticFingerprint,
41
+ sourceModel: modelInfo.name,
42
+ }, projectRoot);
43
+ }
44
+
45
+ return {
46
+ model: modelInfo.name,
47
+ dryRun: Boolean(options.dryRun),
48
+ migration: migration ? { version: migration.version, template: migration.template } : null,
49
+ ...(migration ? {} : { reason: "The container is already owned by the legacy migration baseline." }),
50
+ operations: session.operations.map((operation) => ({ path: operation.path, action: operation.action })),
51
+ };
52
+ }
@@ -5,12 +5,14 @@
5
5
  * ネットワークアクセスや az login を必要とする操作は行わない。
6
6
  * --what-if を明示した場合のみ az deployment group what-if を実行する。
7
7
  * - apply provision: 常に承認(--approve)必須。承認なしでは requires-human で停止する。
8
+ * - 対話的な `swallowkit provision` は同じ計画をメモリ上で作成・表示し、確認後に
9
+ * executeProvisionPlan で同じ安全確認を経て実行する(計画ファイルは保存しない)。
8
10
  */
9
11
 
10
12
  import * as crypto from "crypto";
11
13
  import * as fs from "fs";
14
+ import { AzCliAvailability, checkAzCliAvailability, runAzCli } from "../../utils/azure-cli";
12
15
  import * as path from "path";
13
- import { spawnSync } from "child_process";
14
16
  import { MachineCommandError } from "../../machine/errors";
15
17
  import { getSwallowKitVersion } from "../../version";
16
18
  import { inspectInfra, InfraInspection } from "../project/infra";
@@ -30,7 +32,6 @@ import {
30
32
  getMigrationTagKey,
31
33
  loadMigrationManifest,
32
34
  replaceMigrationManifest,
33
- resolveMigrations,
34
35
  selectPendingMigrations,
35
36
  } from "../project/migrations";
36
37
 
@@ -113,13 +114,10 @@ function validateProvisionInputs(options: PlanProvisionOptions): void {
113
114
  }
114
115
  }
115
116
 
116
- function isAzCliAvailable(): boolean {
117
- try {
118
- const result = spawnSync("az", ["--version"], { shell: false, stdio: "ignore", timeout: 30_000 });
119
- return result.status === 0;
120
- } catch {
121
- return false;
122
- }
117
+ function azCliUnavailableWarning(availability: Exclude<AzCliAvailability, { available: true }>): string {
118
+ return availability.reason === "not-found"
119
+ ? "az-cli-not-found: Azure CLI was not found on PATH. Install it (https://aka.ms/azure-cli) or add it to PATH, then try again."
120
+ : `az-cli-not-runnable: Azure CLI was found but could not be started. ${availability.detail}`;
123
121
  }
124
122
 
125
123
  function displayCommand(command: ProvisionCommand): string {
@@ -133,14 +131,27 @@ interface AzResult {
133
131
  }
134
132
 
135
133
  function runAz(args: string[], timeout = 300_000): AzResult {
136
- const result = spawnSync("az", args, { shell: false, encoding: "utf-8", timeout });
134
+ const result = runAzCli(args, { timeout });
137
135
  return {
138
136
  status: result.status,
139
- stdout: result.stdout ?? "",
140
- stderr: result.stderr ?? "",
137
+ stdout: result.stdout,
138
+ stderr: result.stderr || (result.error ? result.error.message : ""),
141
139
  };
142
140
  }
143
141
 
142
+ function azQueryError(code: string, message: string, result: AzResult): MachineCommandError {
143
+ const output = (result.stderr || result.stdout || "").trim();
144
+ if (/az login|not logged in|AADSTS|expired/i.test(output)) {
145
+ return new MachineCommandError(
146
+ "az-not-logged-in",
147
+ `Azure CLI is not signed in (or the session expired). Run "az login", then try again.\n${output.slice(0, 1000)}`,
148
+ undefined,
149
+ "requires-human"
150
+ );
151
+ }
152
+ return new MachineCommandError(code, `${message}: ${output}`, undefined, "blocked");
153
+ }
154
+
144
155
  function subscriptionArgs(subscription?: string): string[] {
145
156
  return subscription ? ["--subscription", subscription] : [];
146
157
  }
@@ -176,12 +187,12 @@ interface RemoteGroupState {
176
187
  function inspectRemoteGroup(options: PlanProvisionOptions): RemoteGroupState {
177
188
  const existsResult = runAz(["group", "exists", "--name", options.resourceGroup, ...subscriptionArgs(options.subscription)], 60_000);
178
189
  if (existsResult.status !== 0) {
179
- throw new MachineCommandError("azure-state-query-failed", `Could not query resource group: ${(existsResult.stderr || existsResult.stdout || "").trim()}`, undefined, "blocked");
190
+ throw azQueryError("azure-state-query-failed", "Could not query resource group", existsResult);
180
191
  }
181
192
  if ((existsResult.stdout || "").trim() !== "true") return { exists: false, tags: {} };
182
193
  const showResult = runAz(["group", "show", "--name", options.resourceGroup, ...subscriptionArgs(options.subscription), "--query", "{id:id,tags:tags}", "-o", "json"], 60_000);
183
194
  if (showResult.status !== 0) {
184
- throw new MachineCommandError("azure-state-query-failed", `Could not read resource group tags: ${(showResult.stderr || showResult.stdout || "").trim()}`, undefined, "blocked");
195
+ throw azQueryError("azure-state-query-failed", "Could not read resource group tags", showResult);
185
196
  }
186
197
  const parsed = JSON.parse(showResult.stdout || "{}");
187
198
  return { exists: true, id: parsed.id, tags: parsed.tags ?? {} };
@@ -231,7 +242,14 @@ function assertAdoptionWhatIfIsSafe(whatIfResult: string): void {
231
242
  }
232
243
  }
233
244
 
234
- export async function planProvisionOperation(options: PlanProvisionOptions): Promise<ProvisionPlanData> {
245
+ /**
246
+ * Builds the provision plan. Machine/MCP callers persist it for a later `apply provision --plan`;
247
+ * the interactive `swallowkit provision` command keeps it in memory and executes it after confirmation.
248
+ */
249
+ export async function planProvisionOperation(
250
+ options: PlanProvisionOptions,
251
+ planOptions: { persist?: boolean } = {}
252
+ ): Promise<ProvisionPlanData> {
235
253
  const projectRoot = process.cwd();
236
254
  validateProvisionInputs(options);
237
255
 
@@ -250,9 +268,10 @@ export async function planProvisionOperation(options: PlanProvisionOptions): Pro
250
268
  warnings.push("infra/main.parameters.json not found; deployment will fail until it is created.");
251
269
  }
252
270
 
253
- const azCliAvailable = isAzCliAvailable();
254
- if (!azCliAvailable) {
255
- warnings.push("az-cli-not-found: Azure CLI is not installed. Install it before applying: https://aka.ms/azure-cli");
271
+ const azCli = checkAzCliAvailability();
272
+ const azCliAvailable = azCli.available;
273
+ if (!azCli.available) {
274
+ warnings.push(azCliUnavailableWarning(azCli));
256
275
  }
257
276
 
258
277
  const mainBicepPath = path.join(projectRoot, "infra", "main.bicep");
@@ -396,7 +415,13 @@ export async function planProvisionOperation(options: PlanProvisionOptions): Pro
396
415
  }
397
416
  }
398
417
  } else if (options.whatIf || options.adoptExisting || options.reconcile) {
399
- throw new MachineCommandError("az-cli-not-found", "Azure CLI is required for --what-if, --adopt-existing, and --reconcile.", undefined, "blocked");
418
+ const detail = azCli.available ? "" : ` ${azCliUnavailableWarning(azCli)}`;
419
+ throw new MachineCommandError(
420
+ !azCli.available && azCli.reason === "not-runnable" ? "az-cli-not-runnable" : "az-cli-not-found",
421
+ `Azure CLI is required for --what-if, --adopt-existing, and --reconcile.${detail}`,
422
+ undefined,
423
+ "blocked"
424
+ );
400
425
  }
401
426
 
402
427
  const fingerprints: Record<string, string | null> = {
@@ -460,7 +485,7 @@ export async function planProvisionOperation(options: PlanProvisionOptions): Pro
460
485
  ...(baselineSnapshotChecksum ? { baselineSnapshotChecksum } : {}),
461
486
  };
462
487
 
463
- savePlanState(plan, projectRoot);
488
+ if (planOptions.persist !== false) savePlanState(plan, projectRoot);
464
489
  return plan;
465
490
  }
466
491
 
@@ -490,37 +515,40 @@ function readRemoteMigrationState(plan: ProvisionPlanData): { group: RemoteGroup
490
515
  return { group, state: tagValue ? decodeMigrationState(tagValue) : null };
491
516
  }
492
517
 
493
- function assertPlanRemoteState(plan: ProvisionPlanData, expected: MigrationState | null | undefined): void {
518
+ function assertPlanRemoteState(plan: ProvisionPlanData, expected: MigrationState | null | undefined, retryHint: string): void {
494
519
  if (plan.mode === "offline") return;
495
520
  const current = readRemoteMigrationState(plan);
496
521
  if (plan.mode === "bootstrap") {
497
522
  if (current.group.exists) {
498
- throw new MachineCommandError("stale-azure-state", "The resource group was created after this plan. Re-run plan provision.", undefined, "blocked");
523
+ throw new MachineCommandError("stale-azure-state", `The resource group was created after the preview was computed. ${retryHint}`, undefined, "blocked");
499
524
  }
500
525
  return;
501
526
  }
502
527
  if (!current.group.exists || !statesEqual(current.state, expected)) {
503
528
  throw new MachineCommandError(
504
529
  "stale-azure-state",
505
- "Azure migration state changed after this plan was created. Re-run plan provision.",
530
+ `Azure migration state changed after the preview was computed. ${retryHint}`,
506
531
  { expected, actual: current.state, resourceGroupExists: current.group.exists },
507
532
  "blocked"
508
533
  );
509
534
  }
510
535
  }
511
536
 
512
- function assertTagExpectedState(plan: ProvisionPlanData, expected: MigrationState | null | undefined): void {
537
+ function assertTagExpectedState(plan: ProvisionPlanData, expected: MigrationState | null | undefined, retryHint: string): void {
513
538
  const current = readRemoteMigrationState(plan);
514
539
  if (!current.group.exists || !statesEqual(current.state, expected)) {
515
540
  throw new MachineCommandError(
516
541
  "stale-azure-state",
517
- "Azure migration state changed before its tag could be updated. Re-run plan provision.",
542
+ `Azure migration state changed before its tag could be updated. ${retryHint}`,
518
543
  { expected, actual: current.state, resourceGroupExists: current.group.exists },
519
544
  "blocked"
520
545
  );
521
546
  }
522
547
  }
523
548
 
549
+ const MACHINE_RETRY_HINT = "Re-run plan provision.";
550
+ export const INTERACTIVE_PROVISION_RETRY_HINT = 'Run "swallowkit provision" again to review the current state.';
551
+
524
552
  export async function applyProvisionOperation(options: ApplyProvisionOptions): Promise<ApplyProvisionResult> {
525
553
  const projectRoot = process.cwd();
526
554
 
@@ -534,6 +562,22 @@ export async function applyProvisionOperation(options: ApplyProvisionOptions): P
534
562
  );
535
563
  }
536
564
 
565
+ const result = await executeProvisionPlan(plan, { approve: options.approve, retryHint: MACHINE_RETRY_HINT, projectRoot });
566
+ deletePlanState(plan.planId, projectRoot);
567
+ return result;
568
+ }
569
+
570
+ /**
571
+ * Runs an approved provision plan with every safety check: local files and Azure state must still
572
+ * match the reviewed preview, and each migration/tag step re-checks Azure state before it runs.
573
+ */
574
+ export async function executeProvisionPlan(
575
+ plan: ProvisionPlanData,
576
+ options: { approve?: boolean; retryHint?: string; projectRoot?: string } = {}
577
+ ): Promise<ApplyProvisionResult> {
578
+ const projectRoot = options.projectRoot ?? process.cwd();
579
+ const retryHint = options.retryHint ?? MACHINE_RETRY_HINT;
580
+
537
581
  // Bicep が plan 後に変更されていないか確認
538
582
  const changedFiles: string[] = [];
539
583
  for (const [relativePath, expectedHash] of Object.entries(plan.fingerprints)) {
@@ -543,8 +587,8 @@ export async function applyProvisionOperation(options: ApplyProvisionOptions): P
543
587
  if (changedFiles.length > 0) {
544
588
  throw new MachineCommandError(
545
589
  "stale-plan",
546
- `Plan "${options.planId}" is stale: ${changedFiles.join(", ")} changed after the plan was created. Re-run "plan provision".`,
547
- { planId: options.planId, changedFiles },
590
+ `${changedFiles.join(", ")} changed after the preview was computed. ${retryHint}`,
591
+ { planId: plan.planId, changedFiles },
548
592
  "blocked"
549
593
  );
550
594
  }
@@ -553,8 +597,8 @@ export async function applyProvisionOperation(options: ApplyProvisionOptions): P
553
597
  if (currentBaselineChecksum !== plan.baselineSnapshotChecksum) {
554
598
  throw new MachineCommandError(
555
599
  "stale-plan",
556
- `Plan "${options.planId}" is stale: baseline infrastructure inventory changed after the plan was created.`,
557
- { planId: options.planId },
600
+ `Baseline infrastructure files changed after the preview was computed. ${retryHint}`,
601
+ { planId: plan.planId },
558
602
  "blocked"
559
603
  );
560
604
  }
@@ -569,10 +613,15 @@ export async function applyProvisionOperation(options: ApplyProvisionOptions): P
569
613
  );
570
614
  }
571
615
  if (!plan.mode || !plan.commandSpecs) {
572
- throw new MachineCommandError("incompatible-plan", "This provision plan was created by an older SwallowKit version. Re-run plan provision.", undefined, "blocked");
616
+ throw new MachineCommandError("incompatible-plan", `This provision plan was created by an older SwallowKit version. ${retryHint}`, undefined, "blocked");
573
617
  }
574
618
  if (plan.mode === "offline") {
575
- throw new MachineCommandError("online-plan-required", "Provision apply requires a fresh plan created while Azure CLI is available and authenticated.", undefined, "blocked");
619
+ throw new MachineCommandError(
620
+ "online-plan-required",
621
+ `Azure state was not read because Azure CLI was unavailable, so nothing can be applied. Make az available and signed in (az login). ${retryHint}`,
622
+ { planId: plan.planId, warnings: plan.warnings },
623
+ "blocked"
624
+ );
576
625
  }
577
626
 
578
627
  if (plan.mode === "adoption-required") {
@@ -584,27 +633,23 @@ export async function applyProvisionOperation(options: ApplyProvisionOptions): P
584
633
  );
585
634
  }
586
635
 
587
- if (!isAzCliAvailable()) {
588
- throw new MachineCommandError(
589
- "az-cli-not-found",
590
- "Azure CLI is not installed. Install it first: https://aka.ms/azure-cli",
591
- undefined,
592
- "blocked"
593
- );
636
+ const azCli = checkAzCliAvailability();
637
+ if (!azCli.available) {
638
+ throw new MachineCommandError(azCli.reason === "not-found" ? "az-cli-not-found" : "az-cli-not-runnable", azCliUnavailableWarning(azCli), undefined, "blocked");
594
639
  }
595
640
 
596
641
  const executedCommands: string[] = [];
597
642
  const warnings = [...plan.warnings];
598
643
  let deploymentOutputs: Record<string, unknown> | undefined;
599
644
 
600
- assertPlanRemoteState(plan, plan.remoteState);
645
+ assertPlanRemoteState(plan, plan.remoteState, retryHint);
601
646
 
602
647
  for (const command of plan.commandSpecs) {
603
648
  if (command.kind === "migration") {
604
- assertPlanRemoteState(plan, command.expectedState);
649
+ assertPlanRemoteState(plan, command.expectedState, retryHint);
605
650
  }
606
651
  if (command.kind === "tag") {
607
- assertTagExpectedState(plan, command.expectedState);
652
+ assertTagExpectedState(plan, command.expectedState, retryHint);
608
653
  }
609
654
  const result = runAz(command.args);
610
655
  const commandText = displayCommand(command);
@@ -630,7 +675,6 @@ export async function applyProvisionOperation(options: ApplyProvisionOptions): P
630
675
  if (plan.manifestAfterApply) {
631
676
  replaceMigrationManifest(plan.manifestAfterApply, projectRoot);
632
677
  }
633
- deletePlanState(plan.planId, projectRoot);
634
678
 
635
679
  return {
636
680
  planId: plan.planId,
@@ -19,6 +19,7 @@ export interface ApplyScaffoldOptions {
19
19
  functionsDir?: string;
20
20
  apiDir?: string;
21
21
  apiOnly?: boolean;
22
+ skipInfra?: boolean;
22
23
  }
23
24
 
24
25
  export interface ApplyScaffoldResult extends MachineScaffoldOperationResult {
@@ -70,6 +71,7 @@ export async function applyScaffoldOperation(options: ApplyScaffoldOptions): Pro
70
71
  functionsDir: options.functionsDir ?? basePlan?.options.functionsDir,
71
72
  apiDir: options.apiDir ?? basePlan?.options.apiDir,
72
73
  apiOnly: options.apiOnly ?? basePlan?.options.apiOnly,
74
+ skipInfra: options.skipInfra ?? basePlan?.options.skipInfra,
73
75
  };
74
76
 
75
77
  // 常に最新状態で collect し、競合を検出する(plan 指定時も現況を再確認)
@@ -13,6 +13,7 @@ export interface MachineScaffoldOperationOptions {
13
13
  functionsDir?: string;
14
14
  apiDir?: string;
15
15
  apiOnly?: boolean;
16
+ skipInfra?: boolean;
16
17
  expectedSchemaAnalysis?: SchemaAnalysisSnapshot;
17
18
  }
18
19
 
@@ -29,6 +29,7 @@ export interface PlanScaffoldOptions {
29
29
  functionsDir?: string;
30
30
  apiDir?: string;
31
31
  apiOnly?: boolean;
32
+ skipInfra?: boolean;
32
33
  }
33
34
 
34
35
  export type ScaffoldPlanAction = "create" | "update" | "overwrite" | "append" | "delete" | "skip";
@@ -59,6 +60,7 @@ export interface ScaffoldPlanData {
59
60
  functionsDir?: string;
60
61
  apiDir?: string;
61
62
  apiOnly?: boolean;
63
+ skipInfra?: boolean;
62
64
  };
63
65
  createdAt: string;
64
66
  swallowkitVersion: string;
@@ -93,6 +95,7 @@ async function collectScaffoldOperations(options: PlanScaffoldOptions): Promise<
93
95
  functionsDir: options.functionsDir,
94
96
  apiDir: options.apiDir,
95
97
  apiOnly: options.apiOnly,
98
+ skipInfra: options.skipInfra,
96
99
  });
97
100
  });
98
101
  });
@@ -193,7 +196,7 @@ export async function planScaffoldOperation(options: PlanScaffoldOptions): Promi
193
196
  .update(
194
197
  JSON.stringify({
195
198
  model: options.model,
196
- options: { functionsDir: options.functionsDir, apiDir: options.apiDir, apiOnly: options.apiOnly },
199
+ options: { functionsDir: options.functionsDir, apiDir: options.apiDir, apiOnly: options.apiOnly, skipInfra: options.skipInfra },
197
200
  operations: operations.map((op) => ({ path: op.path, action: op.action })),
198
201
  fingerprints,
199
202
  schemaAnalysis,
@@ -210,6 +213,7 @@ export async function planScaffoldOperation(options: PlanScaffoldOptions): Promi
210
213
  ...(options.functionsDir ? { functionsDir: options.functionsDir } : {}),
211
214
  ...(options.apiDir ? { apiDir: options.apiDir } : {}),
212
215
  ...(options.apiOnly !== undefined ? { apiOnly: options.apiOnly } : {}),
216
+ ...(options.skipInfra ? { skipInfra: true } : {}),
213
217
  },
214
218
  createdAt: new Date().toISOString(),
215
219
  swallowkitVersion: getSwallowKitVersion(),
@@ -71,6 +71,9 @@ function getBackendRules(backendLanguage: BackendLanguage): string {
71
71
  "- All backend business logic and data access live in `functions/`.",
72
72
  `- SwallowKit derives OpenAPI and native ${BACKEND_LANGUAGE_LABELS[backendLanguage]} schema assets from the Zod source of truth.`,
73
73
  "- Keep custom business logic in ai-authored locations identified by the current boundary contract.",
74
+ backendLanguage === "python"
75
+ ? "- Use uv with the user's interpreter/version settings. Local Python is not pinned to the Azure deployment runtime. Preserve functions/.venv and functions/.codegen-venv; do not clear them or weaken OS application-control policy to fix interpreter errors."
76
+ : "- Start the isolated Functions backend with dotnet run, not func start from the source directory, so the SDK loads extensions from the build output.",
74
77
  ].join("\n");
75
78
  }
76
79
 
@@ -91,7 +94,7 @@ function interpolateTemplate(template: string, context: AgentInstructionContext)
91
94
  ? "├── src/ # React SPA (Vite + React Router)\n│ └── pages/ # generated/custom UI and route registrations\n├── bff/ # SWA managed HTTP-only Functions\n│ ├── src/ # BFF handlers and request-local helpers\n│ └── {function}/ # function.json registrations"
92
95
  : "├── app/ # Next.js App Router\n│ ├── api/ # BFF routes\n│ └── {model}/ # generated/custom UI",
93
96
  devGuidance: react
94
- ? "Use http://localhost:4280 for the app. `swallowkit dev` runs Vite (3000), the managed BFF (7073), standalone Functions (7071), and SWA CLI (4280). SWA forwards /api only to the managed BFF, never directly to standalone Functions. `--no-functions` leaves the BFF running; `--no-swa` removes same-origin routing/auth emulation. BFF local settings belong in bff/local.settings.json. Build bundles BFF dependencies and copies staticwebapp.config.json into dist/. Deploy dist/ + bff/ together, and functions/ separately."
97
+ ? "Use http://localhost:4280 for the app. `swallowkit dev` runs Vite (3000), the managed BFF (7073), standalone Functions (7071), and SWA CLI (4280). SWA forwards /api only to the managed BFF, never directly to standalone Functions. `--no-functions` leaves the BFF running; `--no-swa` removes same-origin routing/auth emulation. BFF local settings belong in bff/local.settings.json. Build bundles BFF dependencies and copies staticwebapp.config.json into dist/. Deploy dist/ + bff/ together, and functions/ separately. Generated home, CRUD, and login pages share the Next.js presentation templates. Preserve their Tailwind styles and the light/dark defaults in src/style.css when editing them."
95
98
  : "Use http://localhost:3000 for the app, or the SWA emulator port (4280) when testing SWA authentication. `swallowkit dev` runs Next.js and standalone Functions (7071). The BFF remains Next.js API routes.",
96
99
  pm: context.packageManager,
97
100
  runCmd,
@@ -41,7 +41,14 @@ When a SwallowKit capability, project structure, or ownership boundary affects
41
41
  a design decision, inspect the current project and the currently available
42
42
  SwallowKit interfaces before deciding.
43
43
 
44
- Prefer the `swallowkit_*` MCP tools when available.
44
+ Prefer the `swallowkit_*` MCP tools when available, with one exception: every
45
+ MCP response reports the SwallowKit version that answered in
46
+ `metadata.swallowkitVersion` and `metadata.mcp`. If that version is older than
47
+ the project's installed SwallowKit (`{{runCmd}} swallowkit --version`), or
48
+ `metadata.warnings` contains `mcp-older-than-project`, its capabilities,
49
+ boundaries, and plan/apply contracts may be outdated for this project. Use
50
+ `{{runCmd}} swallowkit machine ...` instead and follow the response's
51
+ `nextActions` (for example `{{runCmd}} swallowkit configure-mcp`).
45
52
 
46
53
  If MCP is unavailable, use the machine interface:
47
54
 
@@ -263,6 +270,18 @@ SwallowKit should generate framework-owned CRUD artifacts.
263
270
 
264
271
  Do not manually recreate boilerplate that SwallowKit owns.
265
272
 
273
+ When the task must not change Bicep or other infrastructure, still scaffold the
274
+ model: pass `--skip-infra` to `plan scaffold` / `apply scaffold` (MCP
275
+ `skipInfra: true`, or `swallowkit scaffold --skip-infra`). Nothing under
276
+ `infra/` changes, the plan reports `infra-skipped`, and `verify project` still
277
+ passes. Generate only the pending container migration later with
278
+ `{{runCmd}} swallowkit migrations add-model <model>`.
279
+
280
+ Treat plan `warnings` as decisions to make, not noise. In particular,
281
+ `user-scoped-data-without-auth` means the model looks per-user (for example a
282
+ `/userId` partition key) while authentication is not configured, so generated
283
+ CRUD would let any caller read and write every document.
284
+
266
285
  ## Authentication
267
286
 
268
287
  Authentication is a **plan/apply operation**, not a free-form config edit.
@@ -458,9 +477,19 @@ same generated structure.
458
477
  - Re-export models from `shared/index.ts`.
459
478
  - `id` is a required `z.string()`; only `createdAt` / `updatedAt` are optional
460
479
  backend-managed fields.
461
- - Models may declare `partitionKey`, `authPolicy`, `displayName`, and
462
- `connectorConfig` as exported constants. Exact formats:
480
+ - Models may declare `partitionKey`, `authPolicy`, `displayName`,
481
+ `references`, and `connectorConfig` as exported constants. Exact formats:
463
482
  `{{runCmd}} swallowkit machine inspect capabilities` (modelDeclarations).
483
+ - A string field named `<model>Id` is treated as a foreign key only when that
484
+ model exists in `shared/models`. Declare other references explicitly with
485
+ `export const references = { authorId: 'User' };` (map a field to `false`
486
+ to opt out).
487
+ - Native C#/Python schema types come from Zod, not field names: use
488
+ `z.iso.datetime()` / `z.iso.date()` for date values (`createdAt` stays a
489
+ string), `z.number().int()` for integers, and inline `z.object` for nested
490
+ classes. Length/range/regex/item constraints are enforced natively; formats
491
+ such as email remain Zod (BFF) validation. See `inspect capabilities`
492
+ (sharedModels.nativeSchemaMapping).
464
493
  - Cosmos DB containers use PascalCase plural names and default to `/id` as the
465
494
  partition key unless the model declares another key, e.g.
466
495
  `export const partitionKey = '/learnerId';` (the field must exist in the
@@ -517,3 +546,10 @@ Do:
517
546
  - verify before final semantic review
518
547
 
519
548
  Use `{{pm}}`, the package manager selected for this project. Do not introduce another package manager or lockfile. Use `{{runCmd}} swallowkit` for the project-local CLI. The MCP launcher intentionally resolves `latest` independently.
549
+ Exception for prerelease projects: when the project's SwallowKit is newer than
550
+ the published `latest` (for example `2.0.0-alpha.3` while `latest` is `1.1.2`),
551
+ `.mcp.json` may pin `SWALLOWKIT_MCP_VERSION` to the project version, and a
552
+ `latest` launcher that supports it hands the session to the project-installed
553
+ MCP. Run `{{runCmd}} swallowkit configure-mcp` to re-select the version (it
554
+ returns to `latest` once `latest` is at least the project version). Until MCP
555
+ reports the project version, prefer `{{runCmd}} swallowkit machine ...`.