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.
- package/dist/cli/commands/add-auth.js +37 -6
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/configure-mcp.d.ts +12 -0
- package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
- package/dist/cli/commands/configure-mcp.js +42 -0
- package/dist/cli/commands/configure-mcp.js.map +1 -0
- package/dist/cli/commands/init.d.ts +6 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +19 -5
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrations.d.ts.map +1 -1
- package/dist/cli/commands/migrations.js +25 -0
- package/dist/cli/commands/migrations.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +2 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +45 -16
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +4 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +85 -3
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +29 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/core/operations/create-model.d.ts +2 -0
- package/dist/core/operations/create-model.d.ts.map +1 -1
- package/dist/core/operations/create-model.js +60 -8
- package/dist/core/operations/create-model.js.map +1 -1
- package/dist/core/operations/model-migration.d.ts +22 -0
- package/dist/core/operations/model-migration.d.ts.map +1 -0
- package/dist/core/operations/model-migration.js +36 -0
- package/dist/core/operations/model-migration.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +19 -1
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +62 -35
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +1 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
- package/dist/core/operations/scaffold-apply.js +1 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -1
- package/dist/core/operations/scaffold-machine.d.ts +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +2 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +3 -1
- package/dist/core/operations/scaffold-plan.js.map +1 -1
- package/dist/core/project/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +51 -20
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +2 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +29 -5
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/mcp-config.d.ts +26 -0
- package/dist/core/project/mcp-config.d.ts.map +1 -0
- package/dist/core/project/mcp-config.js +117 -0
- package/dist/core/project/mcp-config.js.map +1 -0
- package/dist/core/project/react-project.js +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +23 -5
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +18 -19
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +23 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +407 -137
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +289 -76
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +5 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
- package/dist/core/scaffold/openapi-generator.js +53 -3
- package/dist/core/scaffold/openapi-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +0 -3
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +68 -7
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +4 -3
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +122 -5
- package/dist/core/verify/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +1 -0
- package/dist/machine/contracts.d.ts.map +1 -1
- package/dist/machine/errors.d.ts +3 -2
- package/dist/machine/errors.d.ts.map +1 -1
- package/dist/machine/errors.js +2 -1
- package/dist/machine/errors.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +42 -17
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +4 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +154 -15
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/version-selection.d.ts +55 -0
- package/dist/mcp/version-selection.d.ts.map +1 -0
- package/dist/mcp/version-selection.js +152 -0
- package/dist/mcp/version-selection.js.map +1 -0
- package/dist/utils/azure-cli.d.ts +44 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +164 -0
- package/dist/utils/azure-cli.js.map +1 -0
- package/dist/utils/semver.d.ts +14 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +61 -0
- package/dist/utils/semver.js.map +1 -0
- package/package.json +6 -3
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-loop-phase34.test.ts +124 -0
- package/src/__tests__/azure-cli.test.ts +199 -0
- package/src/__tests__/boundaries.test.ts +42 -0
- package/src/__tests__/csharp-schema.test.ts +261 -0
- package/src/__tests__/generated-project-matrix.test.ts +789 -0
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +64 -3
- package/src/__tests__/multi-auth.test.ts +115 -4
- package/src/__tests__/plan-apply.test.ts +170 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/add-auth.ts +36 -6
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/init.ts +23 -8
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +53 -17
- package/src/cli/commands/scaffold.ts +105 -3
- package/src/cli/index.ts +28 -5
- package/src/core/operations/create-model.ts +51 -13
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-operations.ts +86 -42
- package/src/core/operations/scaffold-apply.ts +2 -0
- package/src/core/operations/scaffold-machine.ts +1 -0
- package/src/core/operations/scaffold-plan.ts +5 -1
- package/src/core/project/agent-templates/AGENTS.md +67 -6
- package/src/core/project/agent-templates/CLAUDE.md +7 -1
- package/src/core/project/agent-templates/copilot-instructions.md +7 -1
- package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
- package/src/core/project/boundaries.ts +62 -20
- package/src/core/project/capabilities.ts +32 -5
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +1 -1
- package/src/core/project/workflows.ts +23 -5
- package/src/core/scaffold/auth-generator.ts +18 -19
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +313 -78
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +118 -6
- package/src/machine/contracts.ts +1 -0
- package/src/machine/errors.ts +4 -2
- package/src/machine/index.ts +45 -20
- package/src/mcp/index.ts +173 -18
- package/src/mcp/version-selection.ts +163 -0
- package/src/utils/azure-cli.ts +154 -0
- 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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
547
|
-
{ planId:
|
|
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
|
-
`
|
|
557
|
-
{ 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",
|
|
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(
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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 指定時も現況を再確認)
|
|
@@ -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
|
-
|
|
315
|
-
|
|
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`,
|
|
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
|
-
-
|
|
22
|
-
|
|
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
|