swallowkit 1.0.0-beta.54 → 1.0.0-beta.56

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 (138) hide show
  1. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  2. package/dist/cli/commands/add-auth.js +154 -157
  3. package/dist/cli/commands/add-auth.js.map +1 -1
  4. package/dist/cli/commands/init.d.ts +5 -0
  5. package/dist/cli/commands/init.d.ts.map +1 -1
  6. package/dist/cli/commands/init.js +178 -25
  7. package/dist/cli/commands/init.js.map +1 -1
  8. package/dist/cli/commands/scaffold.d.ts +7 -2
  9. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  10. package/dist/cli/commands/scaffold.js +111 -123
  11. package/dist/cli/commands/scaffold.js.map +1 -1
  12. package/dist/cli/index.d.ts.map +1 -1
  13. package/dist/cli/index.js +127 -2
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/core/config.d.ts +1 -1
  16. package/dist/core/config.d.ts.map +1 -1
  17. package/dist/core/config.js +8 -4
  18. package/dist/core/config.js.map +1 -1
  19. package/dist/core/operations/auth-operations.d.ts +57 -0
  20. package/dist/core/operations/auth-operations.d.ts.map +1 -0
  21. package/dist/core/operations/auth-operations.js +269 -0
  22. package/dist/core/operations/auth-operations.js.map +1 -0
  23. package/dist/core/operations/create-model.d.ts.map +1 -1
  24. package/dist/core/operations/create-model.js +23 -13
  25. package/dist/core/operations/create-model.js.map +1 -1
  26. package/dist/core/operations/file-session.d.ts +53 -0
  27. package/dist/core/operations/file-session.d.ts.map +1 -0
  28. package/dist/core/operations/file-session.js +195 -0
  29. package/dist/core/operations/file-session.js.map +1 -0
  30. package/dist/core/operations/provision-operations.d.ts +52 -0
  31. package/dist/core/operations/provision-operations.d.ts.map +1 -0
  32. package/dist/core/operations/provision-operations.js +211 -0
  33. package/dist/core/operations/provision-operations.js.map +1 -0
  34. package/dist/core/operations/runtime.d.ts +5 -0
  35. package/dist/core/operations/runtime.d.ts.map +1 -1
  36. package/dist/core/operations/runtime.js +23 -0
  37. package/dist/core/operations/runtime.js.map +1 -1
  38. package/dist/core/operations/scaffold-apply.d.ts +21 -0
  39. package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
  40. package/dist/core/operations/scaffold-apply.js +57 -0
  41. package/dist/core/operations/scaffold-apply.js.map +1 -0
  42. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  43. package/dist/core/operations/scaffold-machine.js +1 -13
  44. package/dist/core/operations/scaffold-machine.js.map +1 -1
  45. package/dist/core/operations/scaffold-plan.d.ts +43 -0
  46. package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
  47. package/dist/core/operations/scaffold-plan.js +195 -0
  48. package/dist/core/operations/scaffold-plan.js.map +1 -0
  49. package/dist/core/project/artifacts.d.ts +58 -0
  50. package/dist/core/project/artifacts.d.ts.map +1 -0
  51. package/dist/core/project/artifacts.js +179 -0
  52. package/dist/core/project/artifacts.js.map +1 -0
  53. package/dist/core/project/boundaries.d.ts +50 -0
  54. package/dist/core/project/boundaries.d.ts.map +1 -0
  55. package/dist/core/project/boundaries.js +117 -0
  56. package/dist/core/project/boundaries.js.map +1 -0
  57. package/dist/core/project/drift.d.ts +29 -0
  58. package/dist/core/project/drift.d.ts.map +1 -0
  59. package/dist/core/project/drift.js +188 -0
  60. package/dist/core/project/drift.js.map +1 -0
  61. package/dist/core/project/infra.d.ts +49 -0
  62. package/dist/core/project/infra.d.ts.map +1 -0
  63. package/dist/core/project/infra.js +167 -0
  64. package/dist/core/project/infra.js.map +1 -0
  65. package/dist/core/project/state.d.ts +15 -0
  66. package/dist/core/project/state.d.ts.map +1 -0
  67. package/dist/core/project/state.js +106 -0
  68. package/dist/core/project/state.js.map +1 -0
  69. package/dist/core/project/workflows.d.ts +43 -0
  70. package/dist/core/project/workflows.d.ts.map +1 -0
  71. package/dist/core/project/workflows.js +255 -0
  72. package/dist/core/project/workflows.js.map +1 -0
  73. package/dist/core/scaffold/auth-generator.d.ts +1 -1
  74. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  75. package/dist/core/scaffold/auth-generator.js +108 -52
  76. package/dist/core/scaffold/auth-generator.js.map +1 -1
  77. package/dist/core/scaffold/functions-generator.js +9 -9
  78. package/dist/core/scaffold/nextjs-generator.js +6 -6
  79. package/dist/core/scaffold/ui-generator.js +19 -19
  80. package/dist/core/scaffold/ui-generator.js.map +1 -1
  81. package/dist/core/verify/index.d.ts +62 -0
  82. package/dist/core/verify/index.d.ts.map +1 -0
  83. package/dist/core/verify/index.js +343 -0
  84. package/dist/core/verify/index.js.map +1 -0
  85. package/dist/machine/contracts.d.ts +8 -0
  86. package/dist/machine/contracts.d.ts.map +1 -1
  87. package/dist/machine/errors.d.ts +4 -1
  88. package/dist/machine/errors.d.ts.map +1 -1
  89. package/dist/machine/errors.js +9 -1
  90. package/dist/machine/errors.js.map +1 -1
  91. package/dist/machine/index.d.ts.map +1 -1
  92. package/dist/machine/index.js +350 -16
  93. package/dist/machine/index.js.map +1 -1
  94. package/dist/mcp/index.d.ts.map +1 -1
  95. package/dist/mcp/index.js +202 -1
  96. package/dist/mcp/index.js.map +1 -1
  97. package/dist/types/index.d.ts +14 -0
  98. package/dist/types/index.d.ts.map +1 -1
  99. package/package.json +1 -1
  100. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +17 -17
  101. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +6 -6
  102. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +11 -11
  103. package/src/__tests__/agent-loop-phase34.test.ts +449 -0
  104. package/src/__tests__/bff-swa-auth.test.ts +275 -0
  105. package/src/__tests__/file-session.test.ts +98 -0
  106. package/src/__tests__/init.test.ts +50 -0
  107. package/src/__tests__/mcp.test.ts +113 -5
  108. package/src/__tests__/multi-auth.test.ts +1 -1
  109. package/src/__tests__/plan-apply.test.ts +294 -0
  110. package/src/cli/commands/add-auth.ts +164 -164
  111. package/src/cli/commands/init.ts +186 -24
  112. package/src/cli/commands/scaffold.ts +128 -129
  113. package/src/cli/index.ts +148 -2
  114. package/src/core/config.ts +5 -4
  115. package/src/core/operations/auth-operations.ts +353 -0
  116. package/src/core/operations/create-model.ts +25 -13
  117. package/src/core/operations/file-session.ts +210 -0
  118. package/src/core/operations/provision-operations.ts +297 -0
  119. package/src/core/operations/runtime.ts +23 -0
  120. package/src/core/operations/scaffold-apply.ts +106 -0
  121. package/src/core/operations/scaffold-machine.ts +2 -16
  122. package/src/core/operations/scaffold-plan.ts +224 -0
  123. package/src/core/project/artifacts.ts +205 -0
  124. package/src/core/project/boundaries.ts +170 -0
  125. package/src/core/project/drift.ts +202 -0
  126. package/src/core/project/infra.ts +196 -0
  127. package/src/core/project/state.ts +77 -0
  128. package/src/core/project/workflows.ts +249 -0
  129. package/src/core/scaffold/auth-generator.ts +119 -60
  130. package/src/core/scaffold/functions-generator.ts +9 -9
  131. package/src/core/scaffold/nextjs-generator.ts +6 -6
  132. package/src/core/scaffold/ui-generator.ts +19 -19
  133. package/src/core/verify/index.ts +392 -0
  134. package/src/machine/contracts.ts +16 -0
  135. package/src/machine/errors.ts +12 -1
  136. package/src/machine/index.ts +444 -15
  137. package/src/mcp/index.ts +198 -2
  138. package/src/types/index.ts +17 -0
@@ -71,7 +71,7 @@ export function generateListPage(model: ModelInfo, sharedPackageName: string, au
71
71
  const refModelCamel = toCamelCase(refModel);
72
72
  return ` fetch('/api/${refModelCamel}')
73
73
  .then(res => res.json())
74
- .then((data: any[]) => {
74
+ .then((data: Array<{ id: string; name?: string; title?: string }>) => {
75
75
  const map: Record<string, string> = {};
76
76
  data.forEach(item => {
77
77
  // name または title フィールドを表示用文字列として使用
@@ -138,8 +138,8 @@ ${hasForeignKeys ? foreignKeyFetches : ''}
138
138
  if (!res.ok) throw new Error('Failed to delete ${modelCamel}');
139
139
 
140
140
  set${modelName}s(${modelCamel}s.filter((item) => item.id !== id));
141
- } catch (err: any) {
142
- alert(\`Error: \${err.message}\`);
141
+ } catch (err) {
142
+ alert(\`Error: \${err instanceof Error ? err.message : String(err)}\`);
143
143
  }
144
144
  };
145
145
 
@@ -216,7 +216,7 @@ ${displayFields.map(f => {
216
216
  const displayField = f.nestedDisplayField || 'name';
217
217
  if (f.isArray) {
218
218
  return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
219
- {Array.isArray(item.${f.name}) ? item.${f.name}.map((ref: any) => ref?.${displayField} || '-').join(', ') : '-'}
219
+ {Array.isArray(item.${f.name}) ? item.${f.name}.map((ref: unknown) => (ref as { ${displayField}?: string } | null)?.${displayField} || '-').join(', ') : '-'}
220
220
  </td>`;
221
221
  } else {
222
222
  return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
@@ -317,7 +317,7 @@ export function generateDetailPage(model: ModelInfo, sharedPackageName: string,
317
317
  const refModelCamel = toCamelCase(refModel);
318
318
  return ` fetch('/api/${refModelCamel}')
319
319
  .then(res => res.json())
320
- .then((data: any[]) => {
320
+ .then((data: Array<{ id: string; name?: string; title?: string }>) => {
321
321
  const map: Record<string, string> = {};
322
322
  data.forEach(item => {
323
323
  map[item.id] = item.name || item.title || item.id;
@@ -389,8 +389,8 @@ ${hasForeignKeys ? foreignKeyFetches : ''}
389
389
  if (!res.ok) throw new Error('Failed to delete ${modelCamel}');
390
390
 
391
391
  router.push('/${modelKebab}');
392
- } catch (err: any) {
393
- alert(\`Error: \${err.message}\`);
392
+ } catch (err) {
393
+ alert(\`Error: \${err instanceof Error ? err.message : String(err)}\`);
394
394
  }
395
395
  };
396
396
 
@@ -455,7 +455,7 @@ ${model.fields.map(f => {
455
455
  if (f.isArray) {
456
456
  return ` <div>
457
457
  <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${label}</dt>
458
- <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{Array.isArray(${modelCamel}.${f.name}) ? ${modelCamel}.${f.name}.map((ref: any) => ref?.${displayField} || '-').join(', ') : '-'}</dd>
458
+ <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{Array.isArray(${modelCamel}.${f.name}) ? ${modelCamel}.${f.name}.map((ref: unknown) => (ref as { ${displayField}?: string } | null)?.${displayField} || '-').join(', ') : '-'}</dd>
459
459
  </div>`;
460
460
  } else {
461
461
  return ` <div>
@@ -547,7 +547,7 @@ export function generateFormComponent(model: ModelInfo, sharedPackageName: strin
547
547
  return ` // ${refModelPascal} の一覧を取得
548
548
  fetch('/api/${refModelCamel}')
549
549
  .then(res => res.json())
550
- .then(data => set${refModelPascal}Options(data.map((item: any) => ({ id: item.id, name: item.${displayField} || item.name || item.title || item.id }))))
550
+ .then(data => set${refModelPascal}Options(data.map((item: Record<string, string>) => ({ id: item.id, name: item.${displayField} || item.name || item.title || item.id }))))
551
551
  .catch(err => console.error('Failed to load ${refModelPascal} options:', err));`;
552
552
  }).join('\n');
553
553
 
@@ -581,7 +581,7 @@ ${formFields.map(f => {
581
581
  // ネストスキーマの場合は参照ID(単一: string, 配列: string[])を管理
582
582
  if (f.isNestedSchema) {
583
583
  if (f.isArray) {
584
- return ` ${f.name}Ids: initialData?.${f.name} ? (Array.isArray(initialData.${f.name}) ? initialData.${f.name}.map((item: any) => item.id) : []) : [] as string[],`;
584
+ return ` ${f.name}Ids: initialData?.${f.name} ? (Array.isArray(initialData.${f.name}) ? initialData.${f.name}.map((item: { id: string }) => item.id) : []) : [] as string[],`;
585
585
  } else {
586
586
  return ` ${f.name}Id: initialData?.${f.name}?.id ?? '',`;
587
587
  }
@@ -615,7 +615,7 @@ ${needsUseEffect ? `
615
615
  ${hasForignKeys ? foreignKeyFields.map(f => ` // ${f.referencedModel} の一覧を取得
616
616
  fetch('/api/${toKebabCase(f.referencedModel!)}')
617
617
  .then(res => res.json())
618
- .then(data => set${f.referencedModel}Options(data.map((item: any) => ({ id: item.id, name: item.name || item.title || item.id }))))
618
+ .then(data => set${f.referencedModel}Options(data.map((item: Record<string, string>) => ({ id: item.id, name: item.name || item.title || item.id }))))
619
619
  .catch(err => console.error('Failed to load ${f.referencedModel} options:', err));`).join('\n') : ''}
620
620
  ${hasNestedSchemas ? nestedSchemaFetches : ''}
621
621
  }, []);
@@ -627,7 +627,7 @@ ${hasNestedSchemas ? nestedSchemaFetches : ''}
627
627
 
628
628
  try {
629
629
  // Array フィールドをカンマ区切りから配列に変換
630
- const submitData: any = { ...formData };
630
+ const submitData: Record<string, unknown> = { ...formData };
631
631
  ${formFields.filter(f => f.isArray && !f.isNestedSchema).map(f => ` if (typeof submitData.${f.name} === 'string') {
632
632
  submitData.${f.name} = submitData.${f.name}.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0);
633
633
  }`).join('\n')}
@@ -651,7 +651,7 @@ ${nestedSchemaFields.map(f => {
651
651
  if (f.isArray) {
652
652
  return ` // ${refModelPascal} 配列参照の変換
653
653
  if (submitData.${f.name}Ids) {
654
- submitData.${f.name} = submitData.${f.name}Ids
654
+ submitData.${f.name} = (submitData.${f.name}Ids as string[])
655
655
  .map((refId: string) => ${refModelCamel}Options.find(opt => opt.id === refId))
656
656
  .filter(Boolean);
657
657
  delete submitData.${f.name}Ids;
@@ -688,17 +688,17 @@ ${nestedSchemaFields.map(f => {
688
688
  }
689
689
 
690
690
  router.push('/${toKebabCase(modelName)}');
691
- } catch (err: any) {
692
- if (err.issues) {
691
+ } catch (err) {
692
+ if (err instanceof z.ZodError) {
693
693
  // Zod validation errors
694
694
  const fieldErrors: Record<string, string> = {};
695
- err.issues.forEach((error: any) => {
696
- const field = error.path[0];
697
- fieldErrors[field] = error.message;
695
+ err.issues.forEach((issue) => {
696
+ const field = String(issue.path[0]);
697
+ fieldErrors[field] = issue.message;
698
698
  });
699
699
  setErrors(fieldErrors);
700
700
  } else {
701
- alert(\`Error: \${err.message}\`);
701
+ alert(\`Error: \${err instanceof Error ? err.message : String(err)}\`);
702
702
  }
703
703
  setLoading(false);
704
704
  }
@@ -0,0 +1,392 @@
1
+ /**
2
+ * Verify — 生成後の検証チェックを実行し、機械可読な結果を返す。
3
+ *
4
+ * 提供チェック:
5
+ * - structure: プロジェクト構造規約 (validateProject)
6
+ * - drift: 生成物と現在状態の乖離 (detectDrift)
7
+ * - typecheck: TypeScript 型チェック (tsc --noEmit / typecheck script)
8
+ * - build/lint/test: package.json scripts に同名 script がある場合のみ実行 (なければ skip)
9
+ * - custom: swallowkit.config の verify.checks で定義されたプロジェクト固有チェック
10
+ *
11
+ * 既定実行セットは structure/drift/typecheck + custom。build/lint/test は --checks で明示指定。
12
+ */
13
+
14
+ import * as fs from "fs";
15
+ import * as path from "path";
16
+ import { spawnSync } from "child_process";
17
+ import { loadConfig } from "../config";
18
+ import { detectDrift, DriftFinding } from "../project/drift";
19
+ import { saveLastVerifyState } from "../project/state";
20
+ import { ProjectViolation, validateProject } from "../project/validation";
21
+ import { CustomVerifyCheck } from "../../types";
22
+ import { detectFromProject, getCommands } from "../../utils/package-manager";
23
+
24
+ export type VerifyCheckStatus = "pass" | "fail" | "skip" | "error";
25
+
26
+ export interface VerifyCheckEvidence {
27
+ command?: string;
28
+ exitCode?: number;
29
+ violations?: ProjectViolation[];
30
+ findings?: DriftFinding[];
31
+ logTail?: string[];
32
+ }
33
+
34
+ export interface VerifyCheckResult {
35
+ id: string;
36
+ title: string;
37
+ status: VerifyCheckStatus;
38
+ durationMs: number;
39
+ fixable: boolean;
40
+ evidence: VerifyCheckEvidence;
41
+ suggestedActions: string[];
42
+ }
43
+
44
+ export interface VerifySummary {
45
+ passed: number;
46
+ failed: number;
47
+ skipped: number;
48
+ errors: number;
49
+ /** true = すべてのチェックが pass または skip */
50
+ done: boolean;
51
+ }
52
+
53
+ export interface VerifyResult {
54
+ checks: VerifyCheckResult[];
55
+ summary: VerifySummary;
56
+ verifiedAt: string;
57
+ }
58
+
59
+ export const AVAILABLE_VERIFY_CHECKS = ["structure", "drift", "typecheck", "build", "lint", "test"] as const;
60
+ export type VerifyCheckId = (typeof AVAILABLE_VERIFY_CHECKS)[number];
61
+
62
+ /** 既定で実行される built-in チェック。build/lint/test は明示指定時のみ。 */
63
+ export const DEFAULT_VERIFY_CHECKS = ["structure", "drift", "typecheck"] as const;
64
+
65
+ async function runStructureCheck(projectRoot: string): Promise<VerifyCheckResult> {
66
+ const startedAt = Date.now();
67
+ try {
68
+ const result = await validateProject(projectRoot);
69
+ const errors = result.violations.filter((violation) => violation.severity === "error");
70
+ return {
71
+ id: "structure",
72
+ title: "Project structure conventions",
73
+ status: errors.length === 0 ? "pass" : "fail",
74
+ durationMs: Date.now() - startedAt,
75
+ fixable: true,
76
+ evidence: { violations: result.violations },
77
+ suggestedActions:
78
+ errors.length === 0
79
+ ? []
80
+ : errors.map((violation) => violation.suggestedFix ?? violation.message),
81
+ };
82
+ } catch (error) {
83
+ return {
84
+ id: "structure",
85
+ title: "Project structure conventions",
86
+ status: "error",
87
+ durationMs: Date.now() - startedAt,
88
+ fixable: false,
89
+ evidence: { logTail: [error instanceof Error ? error.message : String(error)] },
90
+ suggestedActions: ["Run this command inside a SwallowKit project root."],
91
+ };
92
+ }
93
+ }
94
+
95
+ async function runDriftCheck(projectRoot: string): Promise<VerifyCheckResult> {
96
+ const startedAt = Date.now();
97
+ try {
98
+ const result = await detectDrift(projectRoot);
99
+ const errors = result.findings.filter((finding) => finding.severity === "error");
100
+ return {
101
+ id: "drift",
102
+ title: "Generated artifact drift",
103
+ status: errors.length === 0 ? "pass" : "fail",
104
+ durationMs: Date.now() - startedAt,
105
+ fixable: true,
106
+ evidence: { findings: result.findings },
107
+ suggestedActions: errors.map((finding) => finding.repairAction),
108
+ };
109
+ } catch (error) {
110
+ return {
111
+ id: "drift",
112
+ title: "Generated artifact drift",
113
+ status: "error",
114
+ durationMs: Date.now() - startedAt,
115
+ fixable: false,
116
+ evidence: { logTail: [error instanceof Error ? error.message : String(error)] },
117
+ suggestedActions: [],
118
+ };
119
+ }
120
+ }
121
+
122
+ function tailLines(text: string, count: number): string[] {
123
+ return text
124
+ .split(/\r?\n/)
125
+ .filter((line) => line.trim().length > 0)
126
+ .slice(-count);
127
+ }
128
+
129
+ interface CommandCheckSpec {
130
+ id: string;
131
+ title: string;
132
+ command: string;
133
+ args: string[];
134
+ timeoutMs?: number;
135
+ failAction: string;
136
+ }
137
+
138
+ function runCommandCheck(projectRoot: string, spec: CommandCheckSpec): VerifyCheckResult {
139
+ const startedAt = Date.now();
140
+ const commandLabel = [spec.command, ...spec.args].join(" ").trim();
141
+ try {
142
+ const result = spawnSync(spec.command, spec.args, {
143
+ cwd: projectRoot,
144
+ shell: true,
145
+ encoding: "utf-8",
146
+ timeout: spec.timeoutMs ?? 5 * 60 * 1000,
147
+ });
148
+
149
+ const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
150
+ const exitCode = typeof result.status === "number" ? result.status : -1;
151
+
152
+ if (result.error) {
153
+ return {
154
+ id: spec.id,
155
+ title: spec.title,
156
+ status: "error",
157
+ durationMs: Date.now() - startedAt,
158
+ fixable: false,
159
+ evidence: { command: commandLabel, logTail: [result.error.message] },
160
+ suggestedActions: [`Ensure "${commandLabel}" can run in this project.`],
161
+ };
162
+ }
163
+
164
+ return {
165
+ id: spec.id,
166
+ title: spec.title,
167
+ status: exitCode === 0 ? "pass" : "fail",
168
+ durationMs: Date.now() - startedAt,
169
+ fixable: true,
170
+ evidence: {
171
+ command: commandLabel,
172
+ exitCode,
173
+ logTail: tailLines(output, 50),
174
+ },
175
+ suggestedActions: exitCode === 0 ? [] : [spec.failAction],
176
+ };
177
+ } catch (error) {
178
+ return {
179
+ id: spec.id,
180
+ title: spec.title,
181
+ status: "error",
182
+ durationMs: Date.now() - startedAt,
183
+ fixable: false,
184
+ evidence: { command: commandLabel, logTail: [error instanceof Error ? error.message : String(error)] },
185
+ suggestedActions: [],
186
+ };
187
+ }
188
+ }
189
+
190
+ function readPackageScripts(projectRoot: string): Record<string, string> {
191
+ const packageJsonPath = path.join(projectRoot, "package.json");
192
+ if (!fs.existsSync(packageJsonPath)) return {};
193
+ try {
194
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as { scripts?: Record<string, string> };
195
+ return pkg.scripts ?? {};
196
+ } catch {
197
+ return {};
198
+ }
199
+ }
200
+
201
+ function runTypecheckCheck(projectRoot: string): VerifyCheckResult {
202
+ const startedAt = Date.now();
203
+
204
+ if (!fs.existsSync(path.join(projectRoot, "tsconfig.json"))) {
205
+ return {
206
+ id: "typecheck",
207
+ title: "TypeScript type check",
208
+ status: "skip",
209
+ durationMs: Date.now() - startedAt,
210
+ fixable: false,
211
+ evidence: { logTail: ["tsconfig.json not found; skipping typecheck."] },
212
+ suggestedActions: [],
213
+ };
214
+ }
215
+
216
+ const scripts = readPackageScripts(projectRoot);
217
+ let command: string;
218
+ let args: string[];
219
+ if (typeof scripts.typecheck === "string") {
220
+ const pm = detectFromProject(projectRoot);
221
+ command = getCommands(pm).name;
222
+ args = ["run", "typecheck"];
223
+ } else {
224
+ command = "npx";
225
+ args = ["tsc", "--noEmit"];
226
+ }
227
+
228
+ return runCommandCheck(projectRoot, {
229
+ id: "typecheck",
230
+ title: "TypeScript type check",
231
+ command,
232
+ args,
233
+ failAction: "Fix the TypeScript errors listed in evidence.logTail, then re-run verify.",
234
+ });
235
+ }
236
+
237
+ const SCRIPT_CHECK_TITLES: Record<string, string> = {
238
+ build: "Project build",
239
+ lint: "Lint",
240
+ test: "Test suite",
241
+ };
242
+
243
+ function runScriptCheck(projectRoot: string, scriptName: "build" | "lint" | "test"): VerifyCheckResult {
244
+ const startedAt = Date.now();
245
+ const scripts = readPackageScripts(projectRoot);
246
+ const title = SCRIPT_CHECK_TITLES[scriptName];
247
+
248
+ if (typeof scripts[scriptName] !== "string") {
249
+ return {
250
+ id: scriptName,
251
+ title,
252
+ status: "skip",
253
+ durationMs: Date.now() - startedAt,
254
+ fixable: false,
255
+ evidence: { logTail: [`No "${scriptName}" script in package.json; skipping.`] },
256
+ suggestedActions: [],
257
+ };
258
+ }
259
+
260
+ const pm = detectFromProject(projectRoot);
261
+ return runCommandCheck(projectRoot, {
262
+ id: scriptName,
263
+ title,
264
+ command: getCommands(pm).name,
265
+ args: ["run", scriptName],
266
+ timeoutMs: 10 * 60 * 1000,
267
+ failAction: `Fix the ${scriptName} failures listed in evidence.logTail, then re-run verify.`,
268
+ });
269
+ }
270
+
271
+ const CUSTOM_CHECK_ID = /^[a-z][a-z0-9-]*$/;
272
+
273
+ /** swallowkit.config の verify.checks を projectRoot 基準で静かに読み込む。 */
274
+ export function loadCustomVerifyChecks(projectRoot: string): CustomVerifyCheck[] {
275
+ const configNames = ["swallowkit.config.json", "swallowkit.config.js", ".swallowkitrc.json"];
276
+ for (const name of configNames) {
277
+ const configPath = path.join(projectRoot, name);
278
+ if (!fs.existsSync(configPath)) continue;
279
+ try {
280
+ const checks = loadConfig(configPath, false, true).verify?.checks ?? [];
281
+ return checks.filter(
282
+ (check) =>
283
+ typeof check?.id === "string" &&
284
+ CUSTOM_CHECK_ID.test(check.id) &&
285
+ !(AVAILABLE_VERIFY_CHECKS as readonly string[]).includes(check.id) &&
286
+ typeof check.command === "string" &&
287
+ check.command.trim().length > 0
288
+ );
289
+ } catch {
290
+ return [];
291
+ }
292
+ }
293
+ return [];
294
+ }
295
+
296
+ function runCustomCheck(projectRoot: string, check: CustomVerifyCheck): VerifyCheckResult {
297
+ return runCommandCheck(projectRoot, {
298
+ id: check.id,
299
+ title: check.title ?? `Custom check: ${check.id}`,
300
+ command: check.command,
301
+ args: [],
302
+ timeoutMs: check.timeoutMs,
303
+ failAction: `Fix the failures reported by "${check.command}", then re-run verify.`,
304
+ });
305
+ }
306
+
307
+ export async function runVerify(
308
+ checkIds?: string[],
309
+ projectRoot: string = process.cwd()
310
+ ): Promise<VerifyResult> {
311
+ const customChecks = loadCustomVerifyChecks(projectRoot);
312
+ const customIds = customChecks.map((check) => check.id);
313
+ const availableIds = [...AVAILABLE_VERIFY_CHECKS, ...customIds];
314
+
315
+ const requested = (checkIds && checkIds.length > 0
316
+ ? checkIds
317
+ : [...DEFAULT_VERIFY_CHECKS, ...customIds]
318
+ ).map((id) => id.trim()).filter(Boolean);
319
+
320
+ const unknown = requested.filter((id) => !availableIds.includes(id));
321
+ if (unknown.length > 0) {
322
+ throw new Error(
323
+ `Unknown verify check(s): ${unknown.join(", ")}. Available checks: ${availableIds.join(", ")}`
324
+ );
325
+ }
326
+
327
+ const checks: VerifyCheckResult[] = [];
328
+ for (const id of requested) {
329
+ if (id === "structure") {
330
+ checks.push(await runStructureCheck(projectRoot));
331
+ } else if (id === "drift") {
332
+ checks.push(await runDriftCheck(projectRoot));
333
+ } else if (id === "typecheck") {
334
+ checks.push(runTypecheckCheck(projectRoot));
335
+ } else if (id === "build" || id === "lint" || id === "test") {
336
+ checks.push(runScriptCheck(projectRoot, id));
337
+ } else {
338
+ const custom = customChecks.find((check) => check.id === id);
339
+ if (custom) checks.push(runCustomCheck(projectRoot, custom));
340
+ }
341
+ }
342
+
343
+ const summary: VerifySummary = {
344
+ passed: checks.filter((check) => check.status === "pass").length,
345
+ failed: checks.filter((check) => check.status === "fail").length,
346
+ skipped: checks.filter((check) => check.status === "skip").length,
347
+ errors: checks.filter((check) => check.status === "error").length,
348
+ done: checks.every((check) => check.status === "pass" || check.status === "skip"),
349
+ };
350
+
351
+ const result: VerifyResult = {
352
+ checks,
353
+ summary,
354
+ verifiedAt: new Date().toISOString(),
355
+ };
356
+
357
+ saveLastVerifyState(result, projectRoot);
358
+ return result;
359
+ }
360
+
361
+ export interface FailureExplanation {
362
+ checkId: string;
363
+ title: string;
364
+ status: VerifyCheckStatus;
365
+ verifiedAt: string;
366
+ evidence: VerifyCheckEvidence;
367
+ suggestedActions: string[];
368
+ }
369
+
370
+ export function explainVerifyFailure(
371
+ lastVerify: VerifyResult,
372
+ checkId?: string
373
+ ): FailureExplanation[] {
374
+ const targets = checkId
375
+ ? lastVerify.checks.filter((check) => check.id === checkId)
376
+ : lastVerify.checks.filter((check) => check.status === "fail" || check.status === "error");
377
+
378
+ if (checkId && targets.length === 0) {
379
+ throw new Error(
380
+ `Unknown check "${checkId}" in the last verify result. Available: ${lastVerify.checks.map((check) => check.id).join(", ")}`
381
+ );
382
+ }
383
+
384
+ return targets.map((check) => ({
385
+ checkId: check.id,
386
+ title: check.title,
387
+ status: check.status,
388
+ verifiedAt: lastVerify.verifiedAt,
389
+ evidence: check.evidence,
390
+ suggestedActions: check.suggestedActions,
391
+ }));
392
+ }
@@ -1,12 +1,28 @@
1
+ export type MachineOperationStatus =
2
+ | "complete"
3
+ | "in-progress"
4
+ | "blocked"
5
+ | "requires-human"
6
+ | "unsafe"
7
+ | "failed";
8
+
9
+ export interface MachineNextAction {
10
+ command: string;
11
+ description: string;
12
+ }
13
+
1
14
  export interface MachineSuccessResponse<TData> {
2
15
  ok: true;
3
16
  command: string;
17
+ status?: MachineOperationStatus;
18
+ nextActions?: MachineNextAction[];
4
19
  data: TData;
5
20
  }
6
21
 
7
22
  export interface MachineErrorResponse {
8
23
  ok: false;
9
24
  command: string;
25
+ status?: MachineOperationStatus;
10
26
  error: {
11
27
  code: string;
12
28
  message: string;
@@ -1,13 +1,24 @@
1
+ import type { MachineOperationStatus } from "./contracts";
2
+
1
3
  export class MachineCommandError extends Error {
2
4
  readonly code: string;
3
5
  readonly details?: unknown;
6
+ readonly status?: MachineOperationStatus;
4
7
 
5
- constructor(code: string, message: string, details?: unknown) {
8
+ constructor(code: string, message: string, details?: unknown, status?: MachineOperationStatus) {
6
9
  super(message);
7
10
  this.name = "MachineCommandError";
8
11
  this.code = code;
9
12
  this.details = details;
13
+ this.status = status;
14
+ }
15
+ }
16
+
17
+ export function resolveMachineErrorStatus(error: unknown): MachineOperationStatus {
18
+ if (error instanceof MachineCommandError && error.status) {
19
+ return error.status;
10
20
  }
21
+ return "failed";
11
22
  }
12
23
 
13
24
  export function toMachineError(error: unknown): { code: string; message: string; details?: unknown } {