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.
Files changed (53) hide show
  1. package/dist/__tests__/fixtures.d.ts.map +1 -1
  2. package/dist/__tests__/fixtures.js +3 -0
  3. package/dist/__tests__/fixtures.js.map +1 -1
  4. package/dist/cli/commands/scaffold.d.ts +2 -1
  5. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  6. package/dist/cli/commands/scaffold.js +24 -1
  7. package/dist/cli/commands/scaffold.js.map +1 -1
  8. package/dist/core/operations/file-session.d.ts +9 -0
  9. package/dist/core/operations/file-session.d.ts.map +1 -1
  10. package/dist/core/operations/file-session.js +7 -0
  11. package/dist/core/operations/file-session.js.map +1 -1
  12. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  13. package/dist/core/operations/scaffold-apply.js +15 -1
  14. package/dist/core/operations/scaffold-apply.js.map +1 -1
  15. package/dist/core/operations/scaffold-machine.d.ts +2 -0
  16. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  17. package/dist/core/operations/scaffold-machine.js.map +1 -1
  18. package/dist/core/operations/scaffold-plan.d.ts +3 -1
  19. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  20. package/dist/core/operations/scaffold-plan.js +7 -0
  21. package/dist/core/operations/scaffold-plan.js.map +1 -1
  22. package/dist/core/project/drift.d.ts.map +1 -1
  23. package/dist/core/project/drift.js +6 -4
  24. package/dist/core/project/drift.js.map +1 -1
  25. package/dist/core/scaffold/model-parser.d.ts +10 -1
  26. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  27. package/dist/core/scaffold/model-parser.js +182 -60
  28. package/dist/core/scaffold/model-parser.js.map +1 -1
  29. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  30. package/dist/core/scaffold/native-schema-generator.js +61 -13
  31. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  32. package/dist/core/verify/index.d.ts +2 -0
  33. package/dist/core/verify/index.d.ts.map +1 -1
  34. package/dist/core/verify/index.js +26 -8
  35. package/dist/core/verify/index.js.map +1 -1
  36. package/dist/machine/index.d.ts.map +1 -1
  37. package/dist/machine/index.js +16 -9
  38. package/dist/machine/index.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/__tests__/agent-loop-phase34.test.ts +35 -4
  41. package/src/__tests__/fixtures.ts +3 -0
  42. package/src/__tests__/model-parser.test.ts +90 -0
  43. package/src/__tests__/plan-apply.test.ts +182 -0
  44. package/src/cli/commands/scaffold.ts +30 -2
  45. package/src/core/operations/file-session.ts +16 -0
  46. package/src/core/operations/scaffold-apply.ts +21 -1
  47. package/src/core/operations/scaffold-machine.ts +2 -0
  48. package/src/core/operations/scaffold-plan.ts +10 -0
  49. package/src/core/project/drift.ts +9 -5
  50. package/src/core/scaffold/model-parser.ts +199 -67
  51. package/src/core/scaffold/native-schema-generator.ts +69 -13
  52. package/src/core/verify/index.ts +29 -8
  53. 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
- hashFileIfExists,
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: hashFileIfExists(modelPath) ?? undefined,
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(effectiveOptions);
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 staleRecords = modelRecords.filter((record) => record.schemaHash !== currentSchemaHash);
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: currentSchemaHash,
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({