swallowkit 2.0.0-alpha.3 → 2.0.0-alpha.5

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 (159) hide show
  1. package/dist/cli/commands/add-auth.js +37 -6
  2. package/dist/cli/commands/add-auth.js.map +1 -1
  3. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  4. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  5. package/dist/cli/commands/configure-mcp.js +42 -0
  6. package/dist/cli/commands/configure-mcp.js.map +1 -0
  7. package/dist/cli/commands/init.d.ts +6 -1
  8. package/dist/cli/commands/init.d.ts.map +1 -1
  9. package/dist/cli/commands/init.js +19 -5
  10. package/dist/cli/commands/init.js.map +1 -1
  11. package/dist/cli/commands/migrations.d.ts.map +1 -1
  12. package/dist/cli/commands/migrations.js +25 -0
  13. package/dist/cli/commands/migrations.js.map +1 -1
  14. package/dist/cli/commands/provision.d.ts +2 -0
  15. package/dist/cli/commands/provision.d.ts.map +1 -1
  16. package/dist/cli/commands/provision.js +45 -16
  17. package/dist/cli/commands/provision.js.map +1 -1
  18. package/dist/cli/commands/scaffold.d.ts +4 -0
  19. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  20. package/dist/cli/commands/scaffold.js +85 -3
  21. package/dist/cli/commands/scaffold.js.map +1 -1
  22. package/dist/cli/index.d.ts +2 -0
  23. package/dist/cli/index.d.ts.map +1 -1
  24. package/dist/cli/index.js +29 -4
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/core/operations/create-model.d.ts +2 -0
  27. package/dist/core/operations/create-model.d.ts.map +1 -1
  28. package/dist/core/operations/create-model.js +60 -8
  29. package/dist/core/operations/create-model.js.map +1 -1
  30. package/dist/core/operations/model-migration.d.ts +22 -0
  31. package/dist/core/operations/model-migration.d.ts.map +1 -0
  32. package/dist/core/operations/model-migration.js +36 -0
  33. package/dist/core/operations/model-migration.js.map +1 -0
  34. package/dist/core/operations/provision-operations.d.ts +19 -1
  35. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  36. package/dist/core/operations/provision-operations.js +62 -35
  37. package/dist/core/operations/provision-operations.js.map +1 -1
  38. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  39. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  40. package/dist/core/operations/scaffold-apply.js +1 -0
  41. package/dist/core/operations/scaffold-apply.js.map +1 -1
  42. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  43. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  44. package/dist/core/operations/scaffold-machine.js.map +1 -1
  45. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  46. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  47. package/dist/core/operations/scaffold-plan.js +3 -1
  48. package/dist/core/operations/scaffold-plan.js.map +1 -1
  49. package/dist/core/project/boundaries.d.ts +5 -2
  50. package/dist/core/project/boundaries.d.ts.map +1 -1
  51. package/dist/core/project/boundaries.js +51 -20
  52. package/dist/core/project/boundaries.js.map +1 -1
  53. package/dist/core/project/capabilities.d.ts +2 -0
  54. package/dist/core/project/capabilities.d.ts.map +1 -1
  55. package/dist/core/project/capabilities.js +29 -5
  56. package/dist/core/project/capabilities.js.map +1 -1
  57. package/dist/core/project/mcp-config.d.ts +26 -0
  58. package/dist/core/project/mcp-config.d.ts.map +1 -0
  59. package/dist/core/project/mcp-config.js +117 -0
  60. package/dist/core/project/mcp-config.js.map +1 -0
  61. package/dist/core/project/react-project.js +1 -1
  62. package/dist/core/project/workflows.d.ts.map +1 -1
  63. package/dist/core/project/workflows.js +23 -5
  64. package/dist/core/project/workflows.js.map +1 -1
  65. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  66. package/dist/core/scaffold/auth-generator.js +18 -19
  67. package/dist/core/scaffold/auth-generator.js.map +1 -1
  68. package/dist/core/scaffold/model-parser.d.ts +23 -0
  69. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  70. package/dist/core/scaffold/model-parser.js +407 -137
  71. package/dist/core/scaffold/model-parser.js.map +1 -1
  72. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  73. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  74. package/dist/core/scaffold/native-schema-generator.js +289 -76
  75. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  76. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  77. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  78. package/dist/core/scaffold/openapi-generator.js +53 -3
  79. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  80. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  81. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  82. package/dist/core/scaffold/ui-generator.js +68 -7
  83. package/dist/core/scaffold/ui-generator.js.map +1 -1
  84. package/dist/core/verify/index.d.ts +4 -3
  85. package/dist/core/verify/index.d.ts.map +1 -1
  86. package/dist/core/verify/index.js +122 -5
  87. package/dist/core/verify/index.js.map +1 -1
  88. package/dist/machine/contracts.d.ts +1 -0
  89. package/dist/machine/contracts.d.ts.map +1 -1
  90. package/dist/machine/errors.d.ts +3 -2
  91. package/dist/machine/errors.d.ts.map +1 -1
  92. package/dist/machine/errors.js +2 -1
  93. package/dist/machine/errors.js.map +1 -1
  94. package/dist/machine/index.d.ts.map +1 -1
  95. package/dist/machine/index.js +42 -17
  96. package/dist/machine/index.js.map +1 -1
  97. package/dist/mcp/index.d.ts +4 -2
  98. package/dist/mcp/index.d.ts.map +1 -1
  99. package/dist/mcp/index.js +154 -15
  100. package/dist/mcp/index.js.map +1 -1
  101. package/dist/mcp/version-selection.d.ts +55 -0
  102. package/dist/mcp/version-selection.d.ts.map +1 -0
  103. package/dist/mcp/version-selection.js +152 -0
  104. package/dist/mcp/version-selection.js.map +1 -0
  105. package/dist/utils/azure-cli.d.ts +44 -0
  106. package/dist/utils/azure-cli.d.ts.map +1 -0
  107. package/dist/utils/azure-cli.js +164 -0
  108. package/dist/utils/azure-cli.js.map +1 -0
  109. package/dist/utils/semver.d.ts +14 -0
  110. package/dist/utils/semver.d.ts.map +1 -0
  111. package/dist/utils/semver.js +61 -0
  112. package/dist/utils/semver.js.map +1 -0
  113. package/package.json +6 -3
  114. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  115. package/src/__tests__/agent-loop-phase34.test.ts +124 -0
  116. package/src/__tests__/azure-cli.test.ts +199 -0
  117. package/src/__tests__/boundaries.test.ts +42 -0
  118. package/src/__tests__/csharp-schema.test.ts +261 -0
  119. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  120. package/src/__tests__/mcp-version.test.ts +262 -0
  121. package/src/__tests__/mcp.test.ts +64 -3
  122. package/src/__tests__/multi-auth.test.ts +115 -4
  123. package/src/__tests__/plan-apply.test.ts +170 -0
  124. package/src/__tests__/schema-analysis.test.ts +329 -0
  125. package/src/cli/commands/add-auth.ts +36 -6
  126. package/src/cli/commands/configure-mcp.ts +54 -0
  127. package/src/cli/commands/init.ts +23 -8
  128. package/src/cli/commands/migrations.ts +22 -0
  129. package/src/cli/commands/provision.ts +53 -17
  130. package/src/cli/commands/scaffold.ts +105 -3
  131. package/src/cli/index.ts +28 -5
  132. package/src/core/operations/create-model.ts +51 -13
  133. package/src/core/operations/model-migration.ts +52 -0
  134. package/src/core/operations/provision-operations.ts +86 -42
  135. package/src/core/operations/scaffold-apply.ts +2 -0
  136. package/src/core/operations/scaffold-machine.ts +1 -0
  137. package/src/core/operations/scaffold-plan.ts +5 -1
  138. package/src/core/project/agent-templates/AGENTS.md +67 -6
  139. package/src/core/project/agent-templates/CLAUDE.md +7 -1
  140. package/src/core/project/agent-templates/copilot-instructions.md +7 -1
  141. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  142. package/src/core/project/boundaries.ts +62 -20
  143. package/src/core/project/capabilities.ts +32 -5
  144. package/src/core/project/mcp-config.ts +84 -0
  145. package/src/core/project/react-project.ts +1 -1
  146. package/src/core/project/workflows.ts +23 -5
  147. package/src/core/scaffold/auth-generator.ts +18 -19
  148. package/src/core/scaffold/model-parser.ts +413 -129
  149. package/src/core/scaffold/native-schema-generator.ts +313 -78
  150. package/src/core/scaffold/openapi-generator.ts +63 -5
  151. package/src/core/scaffold/ui-generator.ts +70 -8
  152. package/src/core/verify/index.ts +118 -6
  153. package/src/machine/contracts.ts +1 -0
  154. package/src/machine/errors.ts +4 -2
  155. package/src/machine/index.ts +45 -20
  156. package/src/mcp/index.ts +173 -18
  157. package/src/mcp/version-selection.ts +163 -0
  158. package/src/utils/azure-cli.ts +154 -0
  159. package/src/utils/semver.ts +59 -0
@@ -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(),
@@ -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.
@@ -300,6 +319,20 @@ custom endpoints that derive the scoping key (e.g. `learnerId`) from the
300
319
  verified principal — never from the request body — and restrict the generated
301
320
  write endpoints to admin roles.
302
321
 
322
+ Configure authentication with `plan auth` / `apply auth` **before** declaring
323
+ `authPolicy`. Without authentication, an explicit policy
324
+ blocks plan/apply/scaffold with `auth-policy-without-auth` before file writes;
325
+ follow `nextActions`, then re-plan. Do not remove a required policy just to pass
326
+ generation.
327
+ An empty `authPolicy = {}` means no explicit policy; it does not enable authentication.
328
+
329
+ With named `auth.schemes`, define `auth.authorization.policies` selecting the
330
+ required schemes and roles, then use `authPolicy = { policy: 'adminOnly' }` (or
331
+ read/write policy names). Role-only declarations do not select a named scheme
332
+ and block with `auth-policy-requires-named-policy`. Conversely, single-provider
333
+ `auth.provider` supports role lists only; a named policy (including a named
334
+ `defaultPolicy`) blocks with `auth-named-policy-requires-named-scheme`.
335
+
303
336
  ## Verification Is a Completion Gate
304
337
 
305
338
  Before treating an agent-driven implementation as complete, run SwallowKit
@@ -311,8 +344,17 @@ verification:
311
344
 
312
345
  or the equivalent MCP operation.
313
346
 
314
- Verification covers SwallowKit structural correctness, drift, type correctness,
315
- and configured project checks.
347
+ Default checks are `structure`, `drift`, `typecheck`, `backend-build`, plus
348
+ configured project checks. `backend-build` checks standalone `functions/`:
349
+ C# uses `dotnet build`, TypeScript uses the project's `tsc --noEmit`, and Python
350
+ uses Python 3 `compileall` (syntax only, not imports or type checking), preferring
351
+ `functions/.venv` over PATH. Missing toolchains are `skip` with an explanation;
352
+ a skipped backend check is **not** evidence that the backend works. Restore the
353
+ toolchain and re-run it before claiming backend validation.
354
+
355
+ Root `build` scripts do not replace standalone backend verification. To include
356
+ frontend production builds, use `--checks "structure,drift,typecheck,backend-build,build"`.
357
+ Quote comma-separated values in PowerShell.
316
358
 
317
359
  When verification fails, use the structured failure explanation:
318
360
 
@@ -455,12 +497,24 @@ same generated structure.
455
497
  - Model schema files use `shared/models/{kebab-case}.ts`.
456
498
  - The Zod schema constant and inferred TypeScript type use the same PascalCase
457
499
  name: `export const Todo = z.object(...); export type Todo = z.infer<typeof Todo>`.
458
- - Re-export models from `shared/index.ts`.
500
+ - Re-export models from `shared/index.ts`. create-model and plan/apply scaffold
501
+ append missing exports, including manually authored models. Manual exports
502
+ are allowed; this file has no managed markers and existing content is preserved.
459
503
  - `id` is a required `z.string()`; only `createdAt` / `updatedAt` are optional
460
504
  backend-managed fields.
461
- - Models may declare `partitionKey`, `authPolicy`, `displayName`, and
462
- `connectorConfig` as exported constants. Exact formats:
505
+ - Models may declare `partitionKey`, `authPolicy`, `displayName`,
506
+ `references`, and `connectorConfig` as exported constants. Exact formats:
463
507
  `{{runCmd}} swallowkit machine inspect capabilities` (modelDeclarations).
508
+ - A string field named `<model>Id` is treated as a foreign key only when that
509
+ model exists in `shared/models`. Declare other references explicitly with
510
+ `export const references = { authorId: 'User' };` (map a field to `false`
511
+ to opt out).
512
+ - Native C#/Python schema types come from Zod, not field names: use
513
+ `z.iso.datetime()` / `z.iso.date()` for date values (`createdAt` stays a
514
+ string), `z.number().int()` for integers, and inline `z.object` for nested
515
+ classes. Length/range/regex/item constraints are enforced natively; formats
516
+ such as email remain Zod (BFF) validation. See `inspect capabilities`
517
+ (sharedModels.nativeSchemaMapping).
464
518
  - Cosmos DB containers use PascalCase plural names and default to `/id` as the
465
519
  partition key unless the model declares another key, e.g.
466
520
  `export const partitionKey = '/learnerId';` (the field must exist in the
@@ -517,3 +571,10 @@ Do:
517
571
  - verify before final semantic review
518
572
 
519
573
  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.
574
+ Exception for prerelease projects: when the project's SwallowKit is newer than
575
+ the published `latest` (for example `2.0.0-alpha.3` while `latest` is `1.1.2`),
576
+ `.mcp.json` may pin `SWALLOWKIT_MCP_VERSION` to the project version, and a
577
+ `latest` launcher that supports it hands the session to the project-installed
578
+ MCP. Run `{{runCmd}} swallowkit configure-mcp` to re-select the version (it
579
+ returns to `latest` once `latest` is at least the project version). Until MCP
580
+ reports the project version, prefer `{{runCmd}} swallowkit machine ...`.
@@ -14,7 +14,9 @@ Always-on rules:
14
14
  - Inspect the current project and SwallowKit capabilities before making
15
15
  SwallowKit-dependent assumptions or asking questions the repository can answer.
16
16
  - Prefer `swallowkit_*` MCP tools.
17
- - Fall back to `{{runCmd}} swallowkit machine ...` when MCP is unavailable.
17
+ - Fall back to `{{runCmd}} swallowkit machine ...` when MCP is unavailable, or
18
+ when an MCP response's `metadata.swallowkitVersion` is older than the project's
19
+ SwallowKit (`metadata.warnings` contains `mcp-older-than-project`).
18
20
  - Inspect responsibility boundaries before editing framework-related files.
19
21
  - Never hand-edit deterministic SwallowKit-managed artifacts.
20
22
  - Add auth schemes only via plan/apply auth; never hand-write `auth.schemes`
@@ -23,6 +25,10 @@ Always-on rules:
23
25
  - Use plan/apply for deterministic generation.
24
26
  - Respect all `requires-human` states and provisioning approval gates.
25
27
  - Run SwallowKit verification before considering implementation complete.
28
+ - Default verification includes `backend-build` for standalone Functions.
29
+ Skipped toolchains are not validation; Python compilation checks syntax only.
30
+ - Configure auth via plan/apply auth before adding `authPolicy`; otherwise
31
+ scaffold blocks before file writes. Scaffold appends missing shared model exports.
26
32
 
27
33
  Task-specific SwallowKit Agent Skills are available under `.github/skills/`.
28
34
  Equivalent agent-agnostic runbooks are available under
@@ -13,7 +13,9 @@ Key rules:
13
13
  - Inspect current SwallowKit capabilities instead of assuming them.
14
14
  - Answer repository- and SwallowKit-inspectable questions before asking the human.
15
15
  - Prefer `swallowkit_*` MCP tools when available.
16
- - Otherwise use `{{runCmd}} swallowkit machine ...`.
16
+ - Otherwise use `{{runCmd}} swallowkit machine ...`. Also use it when an MCP
17
+ response's `metadata.swallowkitVersion` is older than the project's SwallowKit
18
+ or `metadata.warnings` contains `mcp-older-than-project`.
17
19
  - Inspect responsibility boundaries before editing framework-related files.
18
20
  - Never hand-edit deterministic SwallowKit-managed artifacts.
19
21
  - Add auth schemes only via plan/apply auth; never hand-write `auth.schemes`
@@ -22,6 +24,10 @@ Key rules:
22
24
  - Use plan/apply for deterministic generation.
23
25
  - Respect `requires-human` states and other human approval gates.
24
26
  - Run SwallowKit verification before completion.
27
+ - Default verification includes `backend-build` for standalone Functions.
28
+ Skipped toolchains are not validation; Python compilation checks syntax only.
29
+ - Configure auth via plan/apply auth before adding `authPolicy`; otherwise
30
+ scaffold blocks before file writes. Scaffold appends missing shared model exports.
25
31
 
26
32
  Task-specific SwallowKit Agent Skills live under `.github/skills/`.
27
33
  Agent-agnostic equivalents live under `.swallowkit/workflows/`.
@@ -18,8 +18,9 @@ source-of-truth models, subject to the current result of
18
18
  - `createdAt` and `updatedAt` are **optional** string fields
19
19
  (`z.string().optional()`). The backend manages their values.
20
20
  - Export a `displayName` string for UI display.
21
- - Re-export every model from `shared/index.ts`, outside managed markers where
22
- applicable.
21
+ - create-model and plan/apply scaffold append missing model exports to
22
+ `shared/index.ts`, including manually authored models. Manual exports are
23
+ allowed; there are no managed markers and existing content is preserved.
23
24
  - For relationships, use nested schemas rather than duplicating model shapes.
24
25
 
25
26
  ## Optional model declarations (exact formats)
@@ -30,7 +31,7 @@ These are the only declarations SwallowKit parses from a model file:
30
31
  // Cosmos DB partition key (default '/id'). Must reference a schema field.
31
32
  export const partitionKey = '/learnerId';
32
33
 
33
- // Role-based access control on generated CRUD.
34
+ // Role-based access control on generated CRUD (requires plan/apply auth first).
34
35
  export const authPolicy = { roles: ['admin'] };
35
36
  // or: { read: ['user'], write: ['admin'] } or: { policy: 'adminOnly' }
36
37
 
@@ -48,6 +49,13 @@ export const connectorConfig = {
48
49
  The full machine-readable contract:
49
50
  `{{runCmd}} swallowkit machine inspect capabilities`
50
51
 
52
+ An explicit `authPolicy` without authentication blocks plan/apply/scaffold with
53
+ `auth-policy-without-auth` before writing files. Follow `nextActions` to configure
54
+ auth, then re-plan. Do not drop a required guard to bypass the prerequisite.
55
+ For named `auth.schemes`, use named authorization policies selecting schemes
56
+ and roles, referenced by `policy` or read/write names. Role-only declarations
57
+ block with `auth-policy-requires-named-policy` rather than emitting missing helpers.
58
+
51
59
  After changing a model, do not manually patch generated artifacts.
52
60
 
53
61
  ## After changing a model