swallowkit 1.0.1 → 1.0.3
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/__tests__/fixtures.d.ts.map +1 -1
- package/dist/__tests__/fixtures.js +3 -0
- package/dist/__tests__/fixtures.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +2 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +24 -1
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/core/operations/file-session.d.ts +9 -0
- package/dist/core/operations/file-session.d.ts.map +1 -1
- package/dist/core/operations/file-session.js +7 -0
- package/dist/core/operations/file-session.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
- package/dist/core/operations/scaffold-apply.js +15 -1
- package/dist/core/operations/scaffold-apply.js.map +1 -1
- package/dist/core/operations/scaffold-machine.d.ts +2 -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 +3 -1
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +7 -0
- package/dist/core/operations/scaffold-plan.js.map +1 -1
- package/dist/core/project/drift.d.ts.map +1 -1
- package/dist/core/project/drift.js +6 -4
- package/dist/core/project/drift.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +10 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +182 -60
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +61 -13
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +2 -0
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +26 -8
- package/dist/core/verify/index.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +16 -9
- package/dist/machine/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/agent-loop-phase34.test.ts +35 -4
- package/src/__tests__/fixtures.ts +3 -0
- package/src/__tests__/model-parser.test.ts +90 -0
- package/src/__tests__/plan-apply.test.ts +182 -0
- package/src/cli/commands/scaffold.ts +30 -2
- package/src/core/operations/file-session.ts +16 -0
- package/src/core/operations/scaffold-apply.ts +21 -1
- package/src/core/operations/scaffold-machine.ts +2 -0
- package/src/core/operations/scaffold-plan.ts +10 -0
- package/src/core/project/drift.ts +9 -5
- package/src/core/scaffold/model-parser.ts +199 -67
- package/src/core/scaffold/native-schema-generator.ts +69 -13
- package/src/core/verify/index.ts +29 -8
- package/src/machine/index.ts +18 -4
|
@@ -4,11 +4,13 @@ import * as path from "path";
|
|
|
4
4
|
import * as childProcess from "child_process";
|
|
5
5
|
import { EventEmitter } from "events";
|
|
6
6
|
import { runMachineCli } from "../machine";
|
|
7
|
+
import { getCSharpSchemaModelPath } from "../core/scaffold/native-schema-generator";
|
|
7
8
|
|
|
8
9
|
jest.mock("child_process", () => {
|
|
9
10
|
const actual = jest.requireActual<typeof import("child_process")>("child_process");
|
|
10
11
|
return {
|
|
11
12
|
...actual,
|
|
13
|
+
execFileSync: jest.fn(actual.execFileSync),
|
|
12
14
|
spawn: jest.fn(),
|
|
13
15
|
spawnSync: jest.fn(),
|
|
14
16
|
};
|
|
@@ -143,6 +145,8 @@ describe("plan / apply / drift / verify machine commands", () => {
|
|
|
143
145
|
beforeEach(() => {
|
|
144
146
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-agent-"));
|
|
145
147
|
process.chdir(tempDir);
|
|
148
|
+
const actual = jest.requireActual<typeof import("child_process")>("child_process");
|
|
149
|
+
(childProcess.execFileSync as unknown as ReturnType<typeof jest.fn>).mockImplementation(actual.execFileSync);
|
|
146
150
|
});
|
|
147
151
|
|
|
148
152
|
afterEach(() => {
|
|
@@ -161,6 +165,16 @@ describe("plan / apply / drift / verify machine commands", () => {
|
|
|
161
165
|
expect(response.status).toBe("complete");
|
|
162
166
|
expect(response.data.planId).toMatch(/^[0-9a-f]{12}$/);
|
|
163
167
|
expect(response.data.requiresApproval).toBe(false);
|
|
168
|
+
expect(response.data.schemaAnalysis).toMatchObject({
|
|
169
|
+
parserMode: "static-ast",
|
|
170
|
+
semanticFingerprint: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
171
|
+
canonicalModel: expect.objectContaining({
|
|
172
|
+
name: "Todo",
|
|
173
|
+
fields: expect.arrayContaining([expect.objectContaining({ name: "id", type: "string" })]),
|
|
174
|
+
}),
|
|
175
|
+
warnings: [],
|
|
176
|
+
});
|
|
177
|
+
expect(response.data.diagnostics).toEqual([]);
|
|
164
178
|
expect(response.data.operations).toEqual(
|
|
165
179
|
expect.arrayContaining([
|
|
166
180
|
expect.objectContaining({ path: "app/api/todo/route.ts", action: "create", ownership: "managed" }),
|
|
@@ -309,6 +323,84 @@ describe("plan / apply / drift / verify machine commands", () => {
|
|
|
309
323
|
}
|
|
310
324
|
);
|
|
311
325
|
|
|
326
|
+
it("preserves enum and literal semantics in UI, OpenAPI, and C# artifacts", async () => {
|
|
327
|
+
createNativeBackendFixture(tempDir, "csharp");
|
|
328
|
+
writeFile(
|
|
329
|
+
path.join(tempDir, "shared", "models", "learning-question.ts"),
|
|
330
|
+
`import { z } from 'zod/v4';
|
|
331
|
+
export const LearningQuestion = z.object({
|
|
332
|
+
id: z.string(),
|
|
333
|
+
kind: z.enum(['grammar', 'in_progress', '123', '5級', 'a-b', 'a_b', 'class']),
|
|
334
|
+
examLevel: z.literal('5'),
|
|
335
|
+
createdAt: z.string().optional(),
|
|
336
|
+
updatedAt: z.string().optional(),
|
|
337
|
+
});
|
|
338
|
+
export type LearningQuestion = z.infer<typeof LearningQuestion>;
|
|
339
|
+
`
|
|
340
|
+
);
|
|
341
|
+
const restoreCodegenMocks = mockSuccessfulNativeCodegen();
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const applied = await runMachine(machineArgs("apply", "scaffold", "learning-question"));
|
|
345
|
+
expect(applied.response).toEqual(expect.objectContaining({ ok: true, status: "complete" }));
|
|
346
|
+
|
|
347
|
+
const form = fs.readFileSync(
|
|
348
|
+
path.join(tempDir, "app", "learning-question", "_components", "LearningQuestionForm.tsx"),
|
|
349
|
+
"utf-8"
|
|
350
|
+
);
|
|
351
|
+
expect(form).toContain('<select');
|
|
352
|
+
expect(form).toContain('<option value="grammar">grammar</option>');
|
|
353
|
+
expect(form).toContain('<option value="5">5</option>');
|
|
354
|
+
|
|
355
|
+
const openApi = JSON.parse(
|
|
356
|
+
fs.readFileSync(path.join(tempDir, "functions", "openapi", "learning-question.openapi.json"), "utf-8")
|
|
357
|
+
);
|
|
358
|
+
const properties = openApi.components.schemas.LearningQuestion.properties;
|
|
359
|
+
expect(properties.kind.enum).toEqual(["grammar", "in_progress", "123", "5級", "a-b", "a_b", "class"]);
|
|
360
|
+
expect(properties.examLevel.enum).toEqual(["5"]);
|
|
361
|
+
|
|
362
|
+
const csharpOutputDir = path.join(tempDir, "functions", "generated", "csharp-models");
|
|
363
|
+
const csharp = fs.readFileSync(getCSharpSchemaModelPath(csharpOutputDir, "LearningQuestion"), "utf-8");
|
|
364
|
+
expect(csharp).toContain("public enum KindEnum");
|
|
365
|
+
expect(csharp).toContain("public enum ExamLevelEnum");
|
|
366
|
+
expect(csharp).toContain("Grammar = 1");
|
|
367
|
+
expect(csharp).toContain("InProgress = 2");
|
|
368
|
+
expect(csharp).toContain("Value123 = 3");
|
|
369
|
+
expect(csharp).toContain("Value5U7D1A = 4");
|
|
370
|
+
expect(csharp).toContain("AB_612D62 = 5");
|
|
371
|
+
expect(csharp).toContain("AB_615F62 = 6");
|
|
372
|
+
expect(csharp).toContain("Class = 7");
|
|
373
|
+
expect(csharp).toContain("Value5 = 1");
|
|
374
|
+
expect(csharp).toContain('value.Equals("5", StringComparison.Ordinal)');
|
|
375
|
+
expect(csharp).toContain("return ExamLevelEnum.Value5;");
|
|
376
|
+
expect(csharp).toContain('return "5";');
|
|
377
|
+
expect(csharp).toContain('value.Equals("a-b", StringComparison.Ordinal)');
|
|
378
|
+
expect(csharp).toContain("return KindEnum.AB_612D62;");
|
|
379
|
+
expect(csharp).toContain("if (value == KindEnum.AB_615F62)");
|
|
380
|
+
expect(csharp).toContain('return "a_b";');
|
|
381
|
+
expect(csharp).not.toContain("Dictionary<string, object> Kind");
|
|
382
|
+
expect(csharp).not.toContain("Dictionary<string, object> ExamLevel");
|
|
383
|
+
|
|
384
|
+
writeFile(
|
|
385
|
+
path.join(csharpOutputDir, "SwallowKitBackendModels.csproj"),
|
|
386
|
+
`<Project Sdk="Microsoft.NET.Sdk">
|
|
387
|
+
<PropertyGroup>
|
|
388
|
+
<TargetFramework>net10.0</TargetFramework>
|
|
389
|
+
<Nullable>enable</Nullable>
|
|
390
|
+
</PropertyGroup>
|
|
391
|
+
</Project>
|
|
392
|
+
`
|
|
393
|
+
);
|
|
394
|
+
expect(() => childProcess.execFileSync("dotnet", ["build", "--nologo", "--verbosity", "quiet"], {
|
|
395
|
+
cwd: csharpOutputDir,
|
|
396
|
+
stdio: "pipe",
|
|
397
|
+
timeout: 120_000,
|
|
398
|
+
})).not.toThrow();
|
|
399
|
+
} finally {
|
|
400
|
+
restoreCodegenMocks();
|
|
401
|
+
}
|
|
402
|
+
}, 150_000);
|
|
403
|
+
|
|
312
404
|
it("leaves the project untouched when the native toolchain validation fails on apply", async () => {
|
|
313
405
|
createNativeBackendFixture(tempDir, "csharp");
|
|
314
406
|
const spawnSyncMock = childProcess.spawnSync as unknown as any;
|
|
@@ -356,6 +448,96 @@ describe("plan / apply / drift / verify machine commands", () => {
|
|
|
356
448
|
expect(fs.existsSync(path.join(tempDir, "app"))).toBe(false);
|
|
357
449
|
});
|
|
358
450
|
|
|
451
|
+
it("blocks planning without writing artifacts when dynamic schema evaluation cannot start", async () => {
|
|
452
|
+
createProjectFixture(tempDir);
|
|
453
|
+
writeFile(
|
|
454
|
+
path.join(tempDir, "shared", "models", "computed.ts"),
|
|
455
|
+
`import { z } from 'zod/v4';
|
|
456
|
+
const field = z.string();
|
|
457
|
+
export const Computed = z.object({ id: field.transform(value => value) });
|
|
458
|
+
`
|
|
459
|
+
);
|
|
460
|
+
const unavailable = Object.assign(new Error("spawn EPERM"), { code: "EPERM" });
|
|
461
|
+
(childProcess.execFileSync as unknown as ReturnType<typeof jest.fn>).mockImplementation(() => { throw unavailable; });
|
|
462
|
+
|
|
463
|
+
const { response, exitCode } = await runMachine(machineArgs("plan", "scaffold", "computed", "--api-only"));
|
|
464
|
+
|
|
465
|
+
expect(exitCode).toBe(1);
|
|
466
|
+
expect(response).toMatchObject({
|
|
467
|
+
ok: false,
|
|
468
|
+
status: "blocked",
|
|
469
|
+
error: {
|
|
470
|
+
code: "schema-evaluation-unavailable",
|
|
471
|
+
details: expect.objectContaining({ command: process.execPath, reason: "EPERM" }),
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
expect(fs.existsSync(path.join(tempDir, "app"))).toBe(false);
|
|
475
|
+
expect(fs.existsSync(path.join(tempDir, ".swallowkit", "artifacts.json"))).toBe(false);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("blocks apply when saved parser metadata differs from current analysis", async () => {
|
|
479
|
+
createProjectFixture(tempDir);
|
|
480
|
+
const plan = await runMachine(machineArgs("plan", "scaffold", "todo", "--api-only"));
|
|
481
|
+
const planPath = path.join(tempDir, ".swallowkit", "state", "plans", `${plan.response.data.planId}.json`);
|
|
482
|
+
const savedPlan = JSON.parse(fs.readFileSync(planPath, "utf-8"));
|
|
483
|
+
savedPlan.schemaAnalysis.parserMode = "dynamic";
|
|
484
|
+
fs.writeFileSync(planPath, JSON.stringify(savedPlan, null, 2));
|
|
485
|
+
|
|
486
|
+
const { response, exitCode } = await runMachine(
|
|
487
|
+
machineArgs("apply", "scaffold", "--plan", plan.response.data.planId)
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
expect(exitCode).toBe(1);
|
|
491
|
+
expect(response).toMatchObject({ ok: false, status: "blocked", error: { code: "schema-analysis-mismatch" } });
|
|
492
|
+
expect(fs.existsSync(path.join(tempDir, "app"))).toBe(false);
|
|
493
|
+
expect(fs.existsSync(path.join(tempDir, ".swallowkit", "artifacts.json"))).toBe(false);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("blocks apply without writes when dynamic evaluation becomes unavailable after planning", async () => {
|
|
497
|
+
createProjectFixture(tempDir);
|
|
498
|
+
writeFile(
|
|
499
|
+
path.join(tempDir, "shared", "models", "computed.ts"),
|
|
500
|
+
`import { z } from 'zod/v4';
|
|
501
|
+
const baseFields = { id: z.string(), kind: z.enum(['grammar', 'vocabulary']) };
|
|
502
|
+
export const Computed = z.object({ ...baseFields, level: z.literal('5') });
|
|
503
|
+
`
|
|
504
|
+
);
|
|
505
|
+
const plan = await runMachine(machineArgs("plan", "scaffold", "computed", "--api-only"));
|
|
506
|
+
expect(plan.response).toMatchObject({
|
|
507
|
+
ok: true,
|
|
508
|
+
data: { schemaAnalysis: { parserMode: "dynamic" } },
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const unavailable = Object.assign(new Error("spawn EPERM"), { code: "EPERM" });
|
|
512
|
+
(childProcess.execFileSync as unknown as ReturnType<typeof jest.fn>).mockImplementation(() => { throw unavailable; });
|
|
513
|
+
const applied = await runMachine(machineArgs("apply", "scaffold", "--plan", plan.response.data.planId));
|
|
514
|
+
|
|
515
|
+
expect(applied.exitCode).toBe(1);
|
|
516
|
+
expect(applied.response).toMatchObject({
|
|
517
|
+
ok: false,
|
|
518
|
+
status: "blocked",
|
|
519
|
+
error: { code: "schema-evaluation-unavailable" },
|
|
520
|
+
});
|
|
521
|
+
expect(fs.existsSync(path.join(tempDir, "app"))).toBe(false);
|
|
522
|
+
expect(fs.existsSync(path.join(tempDir, ".swallowkit", "artifacts.json"))).toBe(false);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("does not report schema drift for an unrelated exported response contract", async () => {
|
|
526
|
+
createProjectFixture(tempDir);
|
|
527
|
+
await runMachine(machineArgs("apply", "scaffold", "todo", "--api-only"));
|
|
528
|
+
fs.appendFileSync(
|
|
529
|
+
path.join(tempDir, "shared", "models", "todo.ts"),
|
|
530
|
+
"\nexport const TodoRevealFeedback = z.object({ message: z.string() });\n"
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
const drift = await runMachine(machineArgs("inspect", "drift"));
|
|
534
|
+
|
|
535
|
+
expect(drift.response.ok).toBe(true);
|
|
536
|
+
expect(
|
|
537
|
+
drift.response.data.findings.filter((finding: { kind: string }) => finding.kind === "schema-drift")
|
|
538
|
+
).toEqual([]);
|
|
539
|
+
});
|
|
540
|
+
|
|
359
541
|
it("rejects a native backend plan when a nested model changes after planning", async () => {
|
|
360
542
|
createNativeBackendFixture(tempDir, "csharp");
|
|
361
543
|
writeFile(path.join(tempDir, "shared", "models", "member.ts"), createModelSource("Member"));
|
|
@@ -8,6 +8,7 @@ import * as path from "path";
|
|
|
8
8
|
import { spawnSync } from "child_process";
|
|
9
9
|
import { getBackendLanguage, getConnectorDefinition, getAuthConfig, getFullConfig, getNormalizedAuthConfig, ensureSwallowKitProject, normalizeAuthConfig, validateConfig } from "../../core/config";
|
|
10
10
|
import { ModelInfo, parseModelFile, toKebabCase, toPascalCase, toCamelCase } from "../../core/scaffold/model-parser";
|
|
11
|
+
import { MachineCommandError } from "../../machine/errors";
|
|
11
12
|
import {
|
|
12
13
|
generateCSharpAzureFunctionsCRUD,
|
|
13
14
|
generateCompactAzureFunctionsCRUD,
|
|
@@ -47,7 +48,7 @@ import { generateBFFCallFunctionWithAuth, generateBFFCallFunctionWithMultipleAut
|
|
|
47
48
|
import {
|
|
48
49
|
FileOperationSession,
|
|
49
50
|
getActiveFileSession,
|
|
50
|
-
|
|
51
|
+
SchemaAnalysisSnapshot,
|
|
51
52
|
} from "../../core/operations/file-session";
|
|
52
53
|
import { recordSessionOperations } from "../../core/project/artifacts";
|
|
53
54
|
|
|
@@ -58,6 +59,7 @@ interface ScaffoldOptions {
|
|
|
58
59
|
functionsDir?: string; // Azure Functions のディレクトリ(デフォルト: "functions")
|
|
59
60
|
apiDir?: string; // Next.js API routes のディレクトリ(デフォルト: "app/api")
|
|
60
61
|
apiOnly?: boolean; // true の場合、UI を生成しない(デフォルト: false)
|
|
62
|
+
expectedSchemaAnalysis?: SchemaAnalysisSnapshot;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
function getMachineAwareStdio(): "inherit" | "pipe" {
|
|
@@ -100,6 +102,31 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
100
102
|
// 2. Parse model file
|
|
101
103
|
console.log("🔍 Parsing model file...");
|
|
102
104
|
const modelInfo = await parseModelFile(modelPath);
|
|
105
|
+
const schemaAnalysis: SchemaAnalysisSnapshot = {
|
|
106
|
+
parserMode: modelInfo.parserMode,
|
|
107
|
+
semanticFingerprint: modelInfo.semanticFingerprint,
|
|
108
|
+
canonicalModel: {
|
|
109
|
+
name: modelInfo.name,
|
|
110
|
+
schemaName: modelInfo.schemaName,
|
|
111
|
+
fields: modelInfo.fields,
|
|
112
|
+
nestedSchemaRefs: modelInfo.nestedSchemaRefs,
|
|
113
|
+
partitionKey: modelInfo.partitionKey,
|
|
114
|
+
connectorConfig: modelInfo.connectorConfig,
|
|
115
|
+
authPolicy: modelInfo.authPolicy,
|
|
116
|
+
},
|
|
117
|
+
warnings: modelInfo.parserWarnings,
|
|
118
|
+
};
|
|
119
|
+
if (options.expectedSchemaAnalysis &&
|
|
120
|
+
(options.expectedSchemaAnalysis.parserMode !== schemaAnalysis.parserMode ||
|
|
121
|
+
options.expectedSchemaAnalysis.semanticFingerprint !== schemaAnalysis.semanticFingerprint)) {
|
|
122
|
+
throw new MachineCommandError(
|
|
123
|
+
"schema-analysis-mismatch",
|
|
124
|
+
"Schema analysis changed between plan and apply; no artifacts were written.",
|
|
125
|
+
{ expected: options.expectedSchemaAnalysis, actual: schemaAnalysis, nextAction: "Re-run plan scaffold in the same environment, then apply the new plan." },
|
|
126
|
+
"blocked"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
session.setSchemaAnalysis(schemaAnalysis);
|
|
103
130
|
console.log(`✅ Model parsed: ${modelInfo.name} (${modelInfo.schemaName})`);
|
|
104
131
|
|
|
105
132
|
const backendLanguage = getBackendLanguage();
|
|
@@ -199,7 +226,7 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
199
226
|
|
|
200
227
|
if (session.mode === "commit") {
|
|
201
228
|
recordSessionOperations(session.operations, {
|
|
202
|
-
schemaHash:
|
|
229
|
+
schemaHash: modelInfo.semanticFingerprint,
|
|
203
230
|
sourceModel: modelInfo.name,
|
|
204
231
|
});
|
|
205
232
|
await syncProjectManifest();
|
|
@@ -223,6 +250,7 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
223
250
|
);
|
|
224
251
|
console.log(` ${options.apiOnly ? (backendLanguage === "typescript" ? "4" : "5") : (backendLanguage === "typescript" ? "6" : "7")}. Run '${getCommands(detectFromProject()).exec} swallowkit dev' to test the generated code`);
|
|
225
252
|
} catch (error: any) {
|
|
253
|
+
if (error instanceof MachineCommandError) throw error;
|
|
226
254
|
console.error("\n❌ Scaffold failed:", error.message);
|
|
227
255
|
process.exit(1);
|
|
228
256
|
}
|
|
@@ -43,6 +43,13 @@ export interface RecordedFileOperation {
|
|
|
43
43
|
newHash: string | null;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export interface SchemaAnalysisSnapshot {
|
|
47
|
+
parserMode: string;
|
|
48
|
+
semanticFingerprint: string;
|
|
49
|
+
canonicalModel: unknown;
|
|
50
|
+
warnings: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
export function normalizeContentForHash(content: string): string {
|
|
47
54
|
return content.replace(/\r\n/g, "\n");
|
|
48
55
|
}
|
|
@@ -78,6 +85,7 @@ export class FileOperationSession {
|
|
|
78
85
|
private readonly inputFingerprintsByPath = new Map<string, string | null>();
|
|
79
86
|
/** overlay: absolute normalized path -> content (null = deleted in session) */
|
|
80
87
|
private readonly overlay = new Map<string, string | null>();
|
|
88
|
+
private schemaAnalysisValue: SchemaAnalysisSnapshot | null = null;
|
|
81
89
|
|
|
82
90
|
constructor(mode: FileSessionMode, rootDirectory: string = process.cwd()) {
|
|
83
91
|
this.mode = mode;
|
|
@@ -94,6 +102,14 @@ export class FileOperationSession {
|
|
|
94
102
|
);
|
|
95
103
|
}
|
|
96
104
|
|
|
105
|
+
get schemaAnalysis(): SchemaAnalysisSnapshot | null {
|
|
106
|
+
return this.schemaAnalysisValue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
setSchemaAnalysis(analysis: SchemaAnalysisSnapshot): void {
|
|
110
|
+
this.schemaAnalysisValue = analysis;
|
|
111
|
+
}
|
|
112
|
+
|
|
97
113
|
addWarning(message: string): void {
|
|
98
114
|
if (!this.warnings.includes(message)) {
|
|
99
115
|
this.warnings.push(message);
|
|
@@ -75,6 +75,23 @@ export async function applyScaffoldOperation(options: ApplyScaffoldOptions): Pro
|
|
|
75
75
|
// 常に最新状態で collect し、競合を検出する(plan 指定時も現況を再確認)
|
|
76
76
|
const currentPlan = await planScaffoldOperation(effectiveOptions);
|
|
77
77
|
|
|
78
|
+
if (basePlan &&
|
|
79
|
+
(!basePlan.schemaAnalysis ||
|
|
80
|
+
basePlan.schemaAnalysis.parserMode !== currentPlan.schemaAnalysis.parserMode ||
|
|
81
|
+
basePlan.schemaAnalysis.semanticFingerprint !== currentPlan.schemaAnalysis.semanticFingerprint)) {
|
|
82
|
+
throw new MachineCommandError(
|
|
83
|
+
"schema-analysis-mismatch",
|
|
84
|
+
`Plan "${basePlan.planId}" used different schema analysis metadata; no artifacts were written.`,
|
|
85
|
+
{
|
|
86
|
+
planId: basePlan.planId,
|
|
87
|
+
expected: basePlan.schemaAnalysis,
|
|
88
|
+
actual: currentPlan.schemaAnalysis,
|
|
89
|
+
nextAction: "Re-run plan scaffold in the same environment, then apply the new plan.",
|
|
90
|
+
},
|
|
91
|
+
"blocked"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
78
95
|
if (currentPlan.requiresApproval && !options.approve) {
|
|
79
96
|
// 承認待ちの plan は残し、参照できるようにする
|
|
80
97
|
throw new MachineCommandError(
|
|
@@ -88,7 +105,10 @@ export async function applyScaffoldOperation(options: ApplyScaffoldOptions): Pro
|
|
|
88
105
|
);
|
|
89
106
|
}
|
|
90
107
|
|
|
91
|
-
const result = await runMachineScaffoldOperation(
|
|
108
|
+
const result = await runMachineScaffoldOperation({
|
|
109
|
+
...effectiveOptions,
|
|
110
|
+
expectedSchemaAnalysis: currentPlan.schemaAnalysis,
|
|
111
|
+
});
|
|
92
112
|
|
|
93
113
|
// 適用済み plan の状態を破棄
|
|
94
114
|
deletePlanState(currentPlan.planId, projectRoot);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { scaffoldCommand } from "../../cli/commands/scaffold";
|
|
2
|
+
import { SchemaAnalysisSnapshot } from "./file-session";
|
|
2
3
|
import {
|
|
3
4
|
captureConsoleMessagesWithError,
|
|
4
5
|
deriveCapturedErrorMessage,
|
|
@@ -12,6 +13,7 @@ export interface MachineScaffoldOperationOptions {
|
|
|
12
13
|
functionsDir?: string;
|
|
13
14
|
apiDir?: string;
|
|
14
15
|
apiOnly?: boolean;
|
|
16
|
+
expectedSchemaAnalysis?: SchemaAnalysisSnapshot;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface MachineScaffoldOperationResult {
|
|
@@ -12,6 +12,7 @@ import { savePlanState } from "../project/state";
|
|
|
12
12
|
import {
|
|
13
13
|
ArtifactOwnership,
|
|
14
14
|
FileOperationSession,
|
|
15
|
+
SchemaAnalysisSnapshot,
|
|
15
16
|
hashFileIfExists,
|
|
16
17
|
runWithFileSession,
|
|
17
18
|
} from "./file-session";
|
|
@@ -52,6 +53,8 @@ export interface ScaffoldPlanData {
|
|
|
52
53
|
swallowkitVersion: string;
|
|
53
54
|
operations: ScaffoldPlanOperation[];
|
|
54
55
|
warnings: string[];
|
|
56
|
+
diagnostics: string[];
|
|
57
|
+
schemaAnalysis: SchemaAnalysisSnapshot;
|
|
55
58
|
conflicts: ScaffoldPlanOperation[];
|
|
56
59
|
requiresApproval: boolean;
|
|
57
60
|
/** project-relative path -> pre-plan content hash (null = did not exist). Used for stale-plan detection. */
|
|
@@ -112,6 +115,10 @@ export async function planScaffoldOperation(options: PlanScaffoldOptions): Promi
|
|
|
112
115
|
const operations: ScaffoldPlanOperation[] = [];
|
|
113
116
|
const fingerprints: Record<string, string | null> = { ...session.inputFingerprints };
|
|
114
117
|
const warnings = [...session.warnings];
|
|
118
|
+
const schemaAnalysis = session.schemaAnalysis;
|
|
119
|
+
if (!schemaAnalysis) {
|
|
120
|
+
throw new Error("Scaffold completed without recording schema analysis metadata.");
|
|
121
|
+
}
|
|
115
122
|
|
|
116
123
|
for (const op of session.operations) {
|
|
117
124
|
fingerprints[op.path] = op.previousHash;
|
|
@@ -173,6 +180,7 @@ export async function planScaffoldOperation(options: PlanScaffoldOptions): Promi
|
|
|
173
180
|
options: { functionsDir: options.functionsDir, apiDir: options.apiDir, apiOnly: options.apiOnly },
|
|
174
181
|
operations: operations.map((op) => ({ path: op.path, action: op.action })),
|
|
175
182
|
fingerprints,
|
|
183
|
+
schemaAnalysis,
|
|
176
184
|
})
|
|
177
185
|
)
|
|
178
186
|
.digest("hex")
|
|
@@ -191,6 +199,8 @@ export async function planScaffoldOperation(options: PlanScaffoldOptions): Promi
|
|
|
191
199
|
swallowkitVersion: getSwallowKitVersion(),
|
|
192
200
|
operations,
|
|
193
201
|
warnings,
|
|
202
|
+
diagnostics: schemaAnalysis.warnings.map((warning) => `warning:${warning}`),
|
|
203
|
+
schemaAnalysis,
|
|
194
204
|
conflicts,
|
|
195
205
|
requiresApproval: conflicts.length > 0,
|
|
196
206
|
fingerprints,
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import * as path from "path";
|
|
13
13
|
import { getSwallowKitVersion } from "../../version";
|
|
14
14
|
import { hashFileIfExists } from "../operations/file-session";
|
|
15
|
+
import { parseModelFile } from "../scaffold/model-parser";
|
|
15
16
|
import { ArtifactRecord, loadArtifactLedger } from "./artifacts";
|
|
16
17
|
import { buildProjectManifest, readProjectManifest, SWALLOWKIT_MANIFEST_PATH } from "./manifest";
|
|
17
18
|
|
|
@@ -84,11 +85,11 @@ function detectArtifactDrift(
|
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
function detectSchemaDrift(
|
|
88
|
+
async function detectSchemaDrift(
|
|
88
89
|
records: ArtifactRecord[],
|
|
89
90
|
projectRoot: string,
|
|
90
91
|
findings: DriftFinding[]
|
|
91
|
-
): void {
|
|
92
|
+
): Promise<void> {
|
|
92
93
|
const byModel = new Map<string, ArtifactRecord[]>();
|
|
93
94
|
|
|
94
95
|
for (const record of records) {
|
|
@@ -110,7 +111,10 @@ function detectSchemaDrift(
|
|
|
110
111
|
if (currentSchemaHash === null) {
|
|
111
112
|
continue; // モデルファイルが見つからない場合は manifest-drift 側で検出される
|
|
112
113
|
}
|
|
113
|
-
const
|
|
114
|
+
const semanticSchemaHash = (await parseModelFile(modelPath)).semanticFingerprint;
|
|
115
|
+
const staleRecords = modelRecords.filter(
|
|
116
|
+
(record) => record.schemaHash !== semanticSchemaHash && record.schemaHash !== currentSchemaHash
|
|
117
|
+
);
|
|
114
118
|
if (staleRecords.length > 0) {
|
|
115
119
|
const staleSchemaHashes = [...new Set(staleRecords.map((record) => record.schemaHash!))].sort();
|
|
116
120
|
findings.push({
|
|
@@ -120,7 +124,7 @@ function detectSchemaDrift(
|
|
|
120
124
|
path: `shared/models/${kebab}.ts`,
|
|
121
125
|
entity: modelName,
|
|
122
126
|
expected: staleSchemaHashes.join(", "),
|
|
123
|
-
actual:
|
|
127
|
+
actual: semanticSchemaHash,
|
|
124
128
|
repairAction: `Run "swallowkit machine plan scaffold ${kebab}" then apply to regenerate artifacts for ${modelName}.`,
|
|
125
129
|
});
|
|
126
130
|
}
|
|
@@ -182,7 +186,7 @@ export async function detectDrift(projectRoot: string = process.cwd()): Promise<
|
|
|
182
186
|
for (const record of ledger.artifacts) {
|
|
183
187
|
detectArtifactDrift(record, projectRoot, findings);
|
|
184
188
|
}
|
|
185
|
-
detectSchemaDrift(ledger.artifacts, projectRoot, findings);
|
|
189
|
+
await detectSchemaDrift(ledger.artifacts, projectRoot, findings);
|
|
186
190
|
detectGeneratorDrift(ledger.artifacts, findings);
|
|
187
191
|
} else {
|
|
188
192
|
findings.push({
|