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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist/cli/commands/add-auth.js +37 -6
  2. package/dist/cli/commands/add-auth.js.map +1 -1
  3. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  4. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  5. package/dist/cli/commands/configure-mcp.js +42 -0
  6. package/dist/cli/commands/configure-mcp.js.map +1 -0
  7. package/dist/cli/commands/init.d.ts +6 -1
  8. package/dist/cli/commands/init.d.ts.map +1 -1
  9. package/dist/cli/commands/init.js +19 -5
  10. package/dist/cli/commands/init.js.map +1 -1
  11. package/dist/cli/commands/migrations.d.ts.map +1 -1
  12. package/dist/cli/commands/migrations.js +25 -0
  13. package/dist/cli/commands/migrations.js.map +1 -1
  14. package/dist/cli/commands/provision.d.ts +2 -0
  15. package/dist/cli/commands/provision.d.ts.map +1 -1
  16. package/dist/cli/commands/provision.js +45 -16
  17. package/dist/cli/commands/provision.js.map +1 -1
  18. package/dist/cli/commands/scaffold.d.ts +4 -0
  19. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  20. package/dist/cli/commands/scaffold.js +85 -3
  21. package/dist/cli/commands/scaffold.js.map +1 -1
  22. package/dist/cli/index.d.ts +2 -0
  23. package/dist/cli/index.d.ts.map +1 -1
  24. package/dist/cli/index.js +29 -4
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/core/operations/create-model.d.ts +2 -0
  27. package/dist/core/operations/create-model.d.ts.map +1 -1
  28. package/dist/core/operations/create-model.js +60 -8
  29. package/dist/core/operations/create-model.js.map +1 -1
  30. package/dist/core/operations/model-migration.d.ts +22 -0
  31. package/dist/core/operations/model-migration.d.ts.map +1 -0
  32. package/dist/core/operations/model-migration.js +36 -0
  33. package/dist/core/operations/model-migration.js.map +1 -0
  34. package/dist/core/operations/provision-operations.d.ts +19 -1
  35. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  36. package/dist/core/operations/provision-operations.js +62 -35
  37. package/dist/core/operations/provision-operations.js.map +1 -1
  38. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  39. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  40. package/dist/core/operations/scaffold-apply.js +1 -0
  41. package/dist/core/operations/scaffold-apply.js.map +1 -1
  42. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  43. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  44. package/dist/core/operations/scaffold-machine.js.map +1 -1
  45. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  46. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  47. package/dist/core/operations/scaffold-plan.js +3 -1
  48. package/dist/core/operations/scaffold-plan.js.map +1 -1
  49. package/dist/core/project/boundaries.d.ts +5 -2
  50. package/dist/core/project/boundaries.d.ts.map +1 -1
  51. package/dist/core/project/boundaries.js +51 -20
  52. package/dist/core/project/boundaries.js.map +1 -1
  53. package/dist/core/project/capabilities.d.ts +2 -0
  54. package/dist/core/project/capabilities.d.ts.map +1 -1
  55. package/dist/core/project/capabilities.js +29 -5
  56. package/dist/core/project/capabilities.js.map +1 -1
  57. package/dist/core/project/mcp-config.d.ts +26 -0
  58. package/dist/core/project/mcp-config.d.ts.map +1 -0
  59. package/dist/core/project/mcp-config.js +117 -0
  60. package/dist/core/project/mcp-config.js.map +1 -0
  61. package/dist/core/project/react-project.js +1 -1
  62. package/dist/core/project/workflows.d.ts.map +1 -1
  63. package/dist/core/project/workflows.js +23 -5
  64. package/dist/core/project/workflows.js.map +1 -1
  65. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  66. package/dist/core/scaffold/auth-generator.js +18 -19
  67. package/dist/core/scaffold/auth-generator.js.map +1 -1
  68. package/dist/core/scaffold/model-parser.d.ts +23 -0
  69. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  70. package/dist/core/scaffold/model-parser.js +407 -137
  71. package/dist/core/scaffold/model-parser.js.map +1 -1
  72. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  73. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  74. package/dist/core/scaffold/native-schema-generator.js +289 -76
  75. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  76. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  77. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  78. package/dist/core/scaffold/openapi-generator.js +53 -3
  79. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  80. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  81. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  82. package/dist/core/scaffold/ui-generator.js +68 -7
  83. package/dist/core/scaffold/ui-generator.js.map +1 -1
  84. package/dist/core/verify/index.d.ts +4 -3
  85. package/dist/core/verify/index.d.ts.map +1 -1
  86. package/dist/core/verify/index.js +122 -5
  87. package/dist/core/verify/index.js.map +1 -1
  88. package/dist/machine/contracts.d.ts +1 -0
  89. package/dist/machine/contracts.d.ts.map +1 -1
  90. package/dist/machine/errors.d.ts +3 -2
  91. package/dist/machine/errors.d.ts.map +1 -1
  92. package/dist/machine/errors.js +2 -1
  93. package/dist/machine/errors.js.map +1 -1
  94. package/dist/machine/index.d.ts.map +1 -1
  95. package/dist/machine/index.js +42 -17
  96. package/dist/machine/index.js.map +1 -1
  97. package/dist/mcp/index.d.ts +4 -2
  98. package/dist/mcp/index.d.ts.map +1 -1
  99. package/dist/mcp/index.js +154 -15
  100. package/dist/mcp/index.js.map +1 -1
  101. package/dist/mcp/version-selection.d.ts +55 -0
  102. package/dist/mcp/version-selection.d.ts.map +1 -0
  103. package/dist/mcp/version-selection.js +152 -0
  104. package/dist/mcp/version-selection.js.map +1 -0
  105. package/dist/utils/azure-cli.d.ts +44 -0
  106. package/dist/utils/azure-cli.d.ts.map +1 -0
  107. package/dist/utils/azure-cli.js +164 -0
  108. package/dist/utils/azure-cli.js.map +1 -0
  109. package/dist/utils/semver.d.ts +14 -0
  110. package/dist/utils/semver.d.ts.map +1 -0
  111. package/dist/utils/semver.js +61 -0
  112. package/dist/utils/semver.js.map +1 -0
  113. package/package.json +6 -3
  114. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  115. package/src/__tests__/agent-loop-phase34.test.ts +124 -0
  116. package/src/__tests__/azure-cli.test.ts +199 -0
  117. package/src/__tests__/boundaries.test.ts +42 -0
  118. package/src/__tests__/csharp-schema.test.ts +261 -0
  119. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  120. package/src/__tests__/mcp-version.test.ts +262 -0
  121. package/src/__tests__/mcp.test.ts +64 -3
  122. package/src/__tests__/multi-auth.test.ts +115 -4
  123. package/src/__tests__/plan-apply.test.ts +170 -0
  124. package/src/__tests__/schema-analysis.test.ts +329 -0
  125. package/src/cli/commands/add-auth.ts +36 -6
  126. package/src/cli/commands/configure-mcp.ts +54 -0
  127. package/src/cli/commands/init.ts +23 -8
  128. package/src/cli/commands/migrations.ts +22 -0
  129. package/src/cli/commands/provision.ts +53 -17
  130. package/src/cli/commands/scaffold.ts +105 -3
  131. package/src/cli/index.ts +28 -5
  132. package/src/core/operations/create-model.ts +51 -13
  133. package/src/core/operations/model-migration.ts +52 -0
  134. package/src/core/operations/provision-operations.ts +86 -42
  135. package/src/core/operations/scaffold-apply.ts +2 -0
  136. package/src/core/operations/scaffold-machine.ts +1 -0
  137. package/src/core/operations/scaffold-plan.ts +5 -1
  138. package/src/core/project/agent-templates/AGENTS.md +67 -6
  139. package/src/core/project/agent-templates/CLAUDE.md +7 -1
  140. package/src/core/project/agent-templates/copilot-instructions.md +7 -1
  141. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  142. package/src/core/project/boundaries.ts +62 -20
  143. package/src/core/project/capabilities.ts +32 -5
  144. package/src/core/project/mcp-config.ts +84 -0
  145. package/src/core/project/react-project.ts +1 -1
  146. package/src/core/project/workflows.ts +23 -5
  147. package/src/core/scaffold/auth-generator.ts +18 -19
  148. package/src/core/scaffold/model-parser.ts +413 -129
  149. package/src/core/scaffold/native-schema-generator.ts +313 -78
  150. package/src/core/scaffold/openapi-generator.ts +63 -5
  151. package/src/core/scaffold/ui-generator.ts +70 -8
  152. package/src/core/verify/index.ts +118 -6
  153. package/src/machine/contracts.ts +1 -0
  154. package/src/machine/errors.ts +4 -2
  155. package/src/machine/index.ts +45 -20
  156. package/src/mcp/index.ts +173 -18
  157. package/src/mcp/version-selection.ts +163 -0
  158. package/src/utils/azure-cli.ts +154 -0
  159. package/src/utils/semver.ts +59 -0
@@ -3,7 +3,7 @@
3
3
  * CRUD 画面(一覧、詳細、新規作成、編集)を生成する
4
4
  */
5
5
 
6
- import { ModelInfo, toCamelCase, toKebabCase } from "./model-parser";
6
+ import { FieldInfo, ModelInfo, toCamelCase, toKebabCase } from "./model-parser";
7
7
  import { ModelAuthPolicy } from "../../types";
8
8
 
9
9
  /** auth が有効かつ authPolicy が存在する場合に渡されるオプション */
@@ -18,6 +18,21 @@ export interface UIAuthOptions {
18
18
  /**
19
19
  * 認証付きCRUDルートを、ページ側のuseAuthと同じAuthProviderで囲むレイアウトを生成する。
20
20
  */
21
+ /** Lookup state and fetches are per referenced model; several fields may reference the same model. */
22
+ function uniqueByReferencedModel(fields: FieldInfo[]): FieldInfo[] {
23
+ const seen = new Set<string>();
24
+ return fields.filter((field) => {
25
+ if (!field.referencedModel || seen.has(field.referencedModel)) return false;
26
+ seen.add(field.referencedModel);
27
+ return true;
28
+ });
29
+ }
30
+
31
+ /** Untyped or inline z.object fields (not references to another model) are edited and shown as JSON. */
32
+ function isJsonField(f: FieldInfo): boolean {
33
+ return f.type === "object" && !f.isNestedSchema;
34
+ }
35
+
21
36
  export function generateAuthLayout(model: ModelInfo, authOptions?: UIAuthOptions): string | undefined {
22
37
  if (!authOptions) return undefined;
23
38
 
@@ -59,14 +74,14 @@ export function generateListPage(model: ModelInfo, sharedPackageName: string, au
59
74
  const hasForeignKeys = foreignKeyFields.length > 0;
60
75
 
61
76
  // 外部キー用のstate定義を生成
62
- const foreignKeyStates = foreignKeyFields.map(f => {
77
+ const foreignKeyStates = uniqueByReferencedModel(foreignKeyFields).map(f => {
63
78
  const refModel = f.referencedModel!;
64
79
  const refModelCamel = toCamelCase(refModel);
65
80
  return ` const [${refModelCamel}Map, set${refModel}Map] = useState<Record<string, string>>({});`;
66
81
  }).join('\n');
67
82
 
68
83
  // 外部キーデータのフェッチロジックを生成
69
- const foreignKeyFetches = foreignKeyFields.map(f => {
84
+ const foreignKeyFetches = uniqueByReferencedModel(foreignKeyFields).map(f => {
70
85
  const refModel = f.referencedModel!;
71
86
  const refModelCamel = toCamelCase(refModel);
72
87
  return ` fetch('/api/${refModelCamel}')
@@ -234,6 +249,10 @@ ${displayFields.map(f => {
234
249
  return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
235
250
  {${refModelCamel}Map[item.${f.name}] || item.${f.name}}
236
251
  </td>`;
252
+ } else if (isJsonField(f)) {
253
+ return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
254
+ {item.${f.name} === undefined ? '-' : JSON.stringify(item.${f.name})}
255
+ </td>`;
237
256
  } else {
238
257
  return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
239
258
  {String(item.${f.name})}
@@ -305,14 +324,14 @@ export function generateDetailPage(model: ModelInfo, sharedPackageName: string,
305
324
  const hasForeignKeys = foreignKeyFields.length > 0;
306
325
 
307
326
  // 外部キー用のstate定義を生成
308
- const foreignKeyStates = foreignKeyFields.map(f => {
327
+ const foreignKeyStates = uniqueByReferencedModel(foreignKeyFields).map(f => {
309
328
  const refModel = f.referencedModel!;
310
329
  const refModelCamel = toCamelCase(refModel);
311
330
  return ` const [${refModelCamel}Map, set${refModel}Map] = useState<Record<string, string>>({});`;
312
331
  }).join('\n');
313
332
 
314
333
  // 外部キーデータのフェッチロジックを生成
315
- const foreignKeyFetches = foreignKeyFields.map(f => {
334
+ const foreignKeyFetches = uniqueByReferencedModel(foreignKeyFields).map(f => {
316
335
  const refModel = f.referencedModel!;
317
336
  const refModelCamel = toCamelCase(refModel);
318
337
  return ` fetch('/api/${refModelCamel}')
@@ -476,6 +495,11 @@ ${model.fields.map(f => {
476
495
  <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${refModel}</dt>
477
496
  <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{${refModelCamel}Map[${modelCamel}.${f.name}] || ${modelCamel}.${f.name}}</dd>
478
497
  </div>`;
498
+ } else if (isJsonField(f)) {
499
+ return ` <div>
500
+ <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${f.name}</dt>
501
+ <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100"><pre className="whitespace-pre-wrap">{${modelCamel}.${f.name} === undefined ? '-' : JSON.stringify(${modelCamel}.${f.name}, null, 2)}</pre></dd>
502
+ </div>`;
479
503
  } else {
480
504
  return ` <div>
481
505
  <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${f.name}</dt>
@@ -573,7 +597,7 @@ export default function ${modelName}Form({ initialData, isEdit = false }: ${mode
573
597
  const router = useRouter();
574
598
  const [loading, setLoading] = useState(false);
575
599
  const [errors, setErrors] = useState<Record<string, string>>({});
576
- ${hasForignKeys ? foreignKeyFields.map(f => ` const [${toCamelCase(f.referencedModel!)}Options, set${f.referencedModel}Options] = useState<Array<{ id: string; name: string }>>([]);`).join('\n') : ''}
600
+ ${hasForignKeys ? uniqueByReferencedModel(foreignKeyFields).map(f => ` const [${toCamelCase(f.referencedModel!)}Options, set${f.referencedModel}Options] = useState<Array<{ id: string; name: string }>>([]);`).join('\n') : ''}
577
601
  ${hasNestedSchemas ? nestedSchemaStates : ''}
578
602
 
579
603
  const [formData, setFormData] = useState({
@@ -587,6 +611,11 @@ ${formFields.map(f => {
587
611
  }
588
612
  }
589
613
 
614
+ // インラインオブジェクトは JSON テキストとして編集する
615
+ if (isJsonField(f)) {
616
+ return ` ${f.name}: initialData?.${f.name} !== undefined ? JSON.stringify(initialData.${f.name}, null, 2) : '',`;
617
+ }
618
+
590
619
  let defaultValue = "''";
591
620
  if (f.isArray) {
592
621
  // Array の場合は空文字列(カンマ区切りで入力するため)
@@ -612,7 +641,7 @@ ${formFields.map(f => {
612
641
  ${needsUseEffect ? `
613
642
  // 参照データの選択肢を取得
614
643
  useEffect(() => {
615
- ${hasForignKeys ? foreignKeyFields.map(f => ` // ${f.referencedModel} の一覧を取得
644
+ ${hasForignKeys ? uniqueByReferencedModel(foreignKeyFields).map(f => ` // ${f.referencedModel} の一覧を取得
616
645
  fetch('/api/${toKebabCase(f.referencedModel!)}')
617
646
  .then(res => res.json())
618
647
  .then(data => set${f.referencedModel}Options(data.map((item: Record<string, string>) => ({ id: item.id, name: item.name || item.title || item.id }))))
@@ -628,7 +657,19 @@ ${hasNestedSchemas ? nestedSchemaFetches : ''}
628
657
  try {
629
658
  // Array フィールドをカンマ区切りから配列に変換
630
659
  const submitData: Record<string, unknown> = { ...formData };
631
- ${formFields.filter(f => f.isArray && !f.isNestedSchema).map(f => ` if (typeof submitData.${f.name} === 'string') {
660
+ ${formFields.filter(isJsonField).length > 0 ? ` // JSON フィールド(インラインオブジェクト)を解析
661
+ ${formFields.filter(isJsonField).map(f => ` if (submitData.${f.name} === '') {
662
+ submitData.${f.name} = undefined;
663
+ } else if (typeof submitData.${f.name} === 'string') {
664
+ try {
665
+ submitData.${f.name} = JSON.parse(submitData.${f.name});
666
+ } catch {
667
+ setErrors({ ${f.name}: 'Invalid JSON' });
668
+ setLoading(false);
669
+ return;
670
+ }
671
+ }`).join('\n')}
672
+ ` : ''}${formFields.filter(f => f.isArray && !f.isNestedSchema && !isJsonField(f)).map(f => ` if (typeof submitData.${f.name} === 'string') {
632
673
  submitData.${f.name} = submitData.${f.name}.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0);
633
674
  }`).join('\n')}
634
675
  ${formFields.filter(f => f.enumValues && f.enumValues.length > 0).length > 0 ? `
@@ -833,6 +874,27 @@ ${f.enumValues.map(v => ` <option value="${v}">${v}</option>`).join('\n
833
874
  </div>`;
834
875
  }
835
876
 
877
+ // インラインオブジェクトは JSON テキストエリア
878
+ if (isJsonField(f)) {
879
+ return ` <div>
880
+ <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
881
+ ${f.name}${!f.isOptional ? ' *' : ''} <span className="text-xs text-gray-500">(JSON)</span>
882
+ </label>
883
+ <textarea
884
+ id="${f.name}"
885
+ name="${f.name}"
886
+ rows={4}
887
+ value={formData.${f.name}}
888
+ onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
889
+ className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 font-mono focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
890
+ ${!f.isOptional ? 'required' : ''}
891
+ />
892
+ {errors.${f.name} && (
893
+ <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
894
+ )}
895
+ </div>`;
896
+ }
897
+
836
898
  // Array の場合はカンマ区切りテキスト
837
899
  if (f.isArray) {
838
900
  return ` <div>
@@ -5,16 +5,17 @@
5
5
  * - structure: プロジェクト構造規約 (validateProject)
6
6
  * - drift: 生成物と現在状態の乖離 (detectDrift)
7
7
  * - typecheck: TypeScript 型チェック (tsc --noEmit / typecheck script)
8
+ * - backend-build: standalone Functions のコンパイル (C#/TypeScript) または構文検証 (Python)
8
9
  * - build/lint/test: package.json scripts に同名 script がある場合のみ実行 (なければ skip)
9
10
  * - custom: swallowkit.config の verify.checks で定義されたプロジェクト固有チェック
10
11
  *
11
- * 既定実行セットは structure/drift/typecheck + custom。build/lint/test は --checks で明示指定。
12
+ * 既定実行セットは structure/drift/typecheck/backend-build + custom。build/lint/test は --checks で明示指定。
12
13
  */
13
14
 
14
15
  import * as fs from "fs";
15
16
  import * as path from "path";
16
17
  import { spawnSync } from "child_process";
17
- import { loadConfig } from "../config";
18
+ import { loadConfig, loadConfigFromEnv } from "../config";
18
19
  import { detectDrift, DriftFinding } from "../project/drift";
19
20
  import { saveLastVerifyState } from "../project/state";
20
21
  import { ProjectViolation, validateProject } from "../project/validation";
@@ -60,11 +61,11 @@ export interface VerifyResult {
60
61
  verifiedAt: string;
61
62
  }
62
63
 
63
- export const AVAILABLE_VERIFY_CHECKS = ["structure", "drift", "typecheck", "build", "lint", "test"] as const;
64
+ export const AVAILABLE_VERIFY_CHECKS = ["structure", "drift", "typecheck", "backend-build", "build", "lint", "test"] as const;
64
65
  export type VerifyCheckId = (typeof AVAILABLE_VERIFY_CHECKS)[number];
65
66
 
66
67
  /** 既定で実行される built-in チェック。build/lint/test は明示指定時のみ。 */
67
- export const DEFAULT_VERIFY_CHECKS = ["structure", "drift", "typecheck"] as const;
68
+ export const DEFAULT_VERIFY_CHECKS = ["structure", "drift", "typecheck", "backend-build"] as const;
68
69
 
69
70
  async function runStructureCheck(projectRoot: string): Promise<VerifyCheckResult> {
70
71
  const startedAt = Date.now();
@@ -137,6 +138,7 @@ interface CommandCheckSpec {
137
138
  args: string[];
138
139
  timeoutMs?: number;
139
140
  failAction: string;
141
+ shell?: boolean;
140
142
  }
141
143
 
142
144
  function runCommandCheck(projectRoot: string, spec: CommandCheckSpec): VerifyCheckResult {
@@ -145,9 +147,10 @@ function runCommandCheck(projectRoot: string, spec: CommandCheckSpec): VerifyChe
145
147
  try {
146
148
  const result = spawnSync(spec.command, spec.args, {
147
149
  cwd: projectRoot,
148
- shell: true,
150
+ shell: spec.shell ?? true,
149
151
  encoding: "utf-8",
150
152
  timeout: spec.timeoutMs ?? 5 * 60 * 1000,
153
+ maxBuffer: 10 * 1024 * 1024,
151
154
  });
152
155
 
153
156
  const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
@@ -180,6 +183,11 @@ function runCommandCheck(projectRoot: string, spec: CommandCheckSpec): VerifyChe
180
183
  command: commandLabel,
181
184
  exitCode,
182
185
  logTail: tailLines(output, 50),
186
+ ...(spec.id === "backend-build" ? {
187
+ diagnostics: [...new Set(output.split(/\r?\n/).filter((line) =>
188
+ /\(\d+,\d+\):\s*error\s+\w+/.test(line) ||
189
+ /^\s*(File ".+", line \d+|(?:Sorry: )?(?:Syntax|Indentation|Tab)Error:|\*\*\* Error compiling)/.test(line)))],
190
+ } : {}),
183
191
  },
184
192
  suggestedActions: exitCode === 0 ? [] : [spec.failAction],
185
193
  };
@@ -201,6 +209,86 @@ function runCommandCheck(projectRoot: string, spec: CommandCheckSpec): VerifyChe
201
209
  }
202
210
  }
203
211
 
212
+ function backendUnavailable(reason: string): VerifyCheckResult {
213
+ return {
214
+ id: "backend-build",
215
+ title: "Standalone Functions compilation",
216
+ status: "skip",
217
+ durationMs: 0,
218
+ fixable: false,
219
+ evidence: { logTail: [reason], diagnostics: [reason] },
220
+ suggestedActions: ["Install or restore the backend toolchain, then re-run verify with --checks \"backend-build\". A skipped check is not backend validation."],
221
+ };
222
+ }
223
+
224
+ function runBackendBuildCheck(projectRoot: string): VerifyCheckResult {
225
+ const functionsRoot = path.join(projectRoot, "functions");
226
+ if (!fs.existsSync(functionsRoot)) return backendUnavailable("functions/ not found; no standalone backend was checked.");
227
+ const configPath = ["swallowkit.config.json", "swallowkit.config.js", ".swallowkitrc.json"]
228
+ .map((name) => path.join(projectRoot, name)).find((file) => fs.existsSync(file));
229
+ if (!configPath) return backendUnavailable("SwallowKit configuration not found; backend language is unknown.");
230
+ // Same effective language as generation (getBackendLanguage): env override, then file, invalid -> typescript.
231
+ const configured = loadConfigFromEnv().backend?.language ?? loadConfig(configPath, true, true).backend?.language;
232
+ const language = configured && ["typescript", "csharp", "python"].includes(configured) ? configured : "typescript";
233
+ let command: string;
234
+ let args: string[];
235
+ if (language === "csharp") {
236
+ command = "dotnet";
237
+ args = ["build", "--nologo"];
238
+ const probe = spawnSync(command, ["--version"], { cwd: functionsRoot, encoding: "utf-8", timeout: 30_000 });
239
+ if (probe.error && "code" in probe.error && probe.error.code === "ENOENT") return backendUnavailable("dotnet SDK is not installed or is not on PATH; C# compilation was skipped.");
240
+ } else if (language === "python") {
241
+ const venvPython = process.platform === "win32"
242
+ ? path.join(functionsRoot, ".venv", "Scripts", "python.exe")
243
+ : path.join(functionsRoot, ".venv", "bin", "python");
244
+ const candidates = fs.existsSync(venvPython)
245
+ ? [{ command: venvPython, args: [] as string[] }]
246
+ : process.platform === "win32"
247
+ ? [{ command: "python", args: [] }, { command: "python3", args: [] }, { command: "py", args: ["-3"] }]
248
+ : [{ command: "python3", args: [] }, { command: "python", args: [] }];
249
+ const python = candidates.find((candidate) => {
250
+ const probe = spawnSync(candidate.command, [...candidate.args, "-c", "import sys; sys.exit(0 if sys.version_info.major == 3 else 1)"], {
251
+ cwd: functionsRoot, encoding: "utf-8", timeout: 30_000,
252
+ });
253
+ // Preserve execution errors for an installed interpreter instead of silently falling back.
254
+ return probe.status === 0 || (probe.error && (!("code" in probe.error) || probe.error.code !== "ENOENT"));
255
+ });
256
+ if (!python) return backendUnavailable("Python 3 is not available in functions/.venv or on PATH; Python syntax compilation was skipped.");
257
+ command = python.command;
258
+ // -f: never trust cached bytecode, which can survive same-size edits within one mtime second.
259
+ args = [...python.args, "-m", "compileall", "-q", "-f", "-x", "[/\\\\](?:\\.venv|\\.codegen-venv|node_modules|__pycache__)(?:[/\\\\]|$)", "."];
260
+ } else {
261
+ let tscPath: string;
262
+ try {
263
+ tscPath = require.resolve("typescript/bin/tsc", { paths: [functionsRoot] });
264
+ } catch (error) {
265
+ if (error instanceof Error && "code" in error && error.code === "MODULE_NOT_FOUND") {
266
+ return backendUnavailable("The project's TypeScript compiler is not installed; restore dependencies before verifying functions/.");
267
+ }
268
+ throw error;
269
+ }
270
+ const sharedBuild = runSharedBuildIfPresent(projectRoot);
271
+ if (sharedBuild.ran && !sharedBuild.ok) {
272
+ return {
273
+ id: "backend-build", title: "Standalone Functions compilation", status: sharedBuild.unavailable ? "error" : "fail",
274
+ durationMs: 0, fixable: !sharedBuild.unavailable,
275
+ evidence: { command: sharedBuild.command, logTail: sharedBuild.logTail,
276
+ ...(sharedBuild.unavailable ? { errorCode: "verification-command-unavailable" as const } : {}) },
277
+ suggestedActions: ["Fix the shared package build before compiling the TypeScript backend."],
278
+ };
279
+ }
280
+ command = process.execPath;
281
+ args = [tscPath, "--project", "tsconfig.json", "--noEmit"];
282
+ }
283
+ const result = runCommandCheck(functionsRoot, {
284
+ id: "backend-build", title: "Standalone Functions compilation", command, args, shell: false,
285
+ timeoutMs: 10 * 60 * 1000,
286
+ failAction: "Fix the backend compilation errors in evidence.diagnostics / evidence.logTail, then re-run verify.",
287
+ });
288
+ result.evidence.command = `${result.evidence.command} (in functions/)`;
289
+ return result;
290
+ }
291
+
204
292
  function readPackageScripts(projectRoot: string): Record<string, string> {
205
293
  const packageJsonPath = path.join(projectRoot, "package.json");
206
294
  if (!fs.existsSync(packageJsonPath)) return {};
@@ -253,6 +341,8 @@ function runTypecheckCheck(projectRoot: string): VerifyCheckResult {
253
341
 
254
342
  const scripts = readPackageScripts(projectRoot);
255
343
  const pm = detectFromProject(projectRoot);
344
+ // Next.js App Router templates use generated route types (e.g. LayoutProps); create them before tsc.
345
+ const typegenNotes = runNextTypegenIfPresent(projectRoot, pm);
256
346
  let command: string;
257
347
  let args: string[];
258
348
  if (typeof scripts.typecheck === "string") {
@@ -263,13 +353,33 @@ function runTypecheckCheck(projectRoot: string): VerifyCheckResult {
263
353
  args = pm === "pnpm" ? ["exec", "tsc", "--noEmit"] : ["exec", "tsc", "--", "--noEmit"];
264
354
  }
265
355
 
266
- return runCommandCheck(projectRoot, {
356
+ const result = runCommandCheck(projectRoot, {
267
357
  id: "typecheck",
268
358
  title: "TypeScript type check",
269
359
  command,
270
360
  args,
271
361
  failAction: "Fix the TypeScript errors listed in evidence.logTail, then re-run verify.",
272
362
  });
363
+ if (result.status !== "pass" && typegenNotes.length > 0) {
364
+ result.evidence.logTail = [...typegenNotes, ...(result.evidence.logTail ?? [])];
365
+ }
366
+ return result;
367
+ }
368
+
369
+ const NEXT_CONFIG_FILES = ["next.config.ts", "next.config.js", "next.config.mjs", "next.config.cjs"];
370
+
371
+ function runNextTypegenIfPresent(projectRoot: string, pm: ReturnType<typeof detectFromProject>): string[] {
372
+ if (!NEXT_CONFIG_FILES.some((file) => fs.existsSync(path.join(projectRoot, file)))) return [];
373
+ if (!fs.existsSync(path.join(projectRoot, "node_modules", "next", "package.json"))) return [];
374
+ const args = pm === "pnpm" ? ["exec", "next", "typegen"] : ["exec", "--", "next", "typegen"];
375
+ const result = spawnSync(getCommands(pm).name, args, {
376
+ cwd: projectRoot,
377
+ shell: true,
378
+ encoding: "utf-8",
379
+ timeout: 2 * 60 * 1000,
380
+ });
381
+ if (!result.error && result.status === 0) return [];
382
+ return [`next typegen did not complete (${result.error?.message ?? `exit ${result.status}`}); route types such as LayoutProps may be missing.`];
273
383
  }
274
384
 
275
385
  const SCRIPT_CHECK_TITLES: Record<string, string> = {
@@ -408,6 +518,8 @@ export async function runVerify(
408
518
  checks.push(await runDriftCheck(projectRoot));
409
519
  } else if (id === "typecheck") {
410
520
  checks.push(runTypecheckCheck(projectRoot));
521
+ } else if (id === "backend-build") {
522
+ checks.push(runBackendBuildCheck(projectRoot));
411
523
  } else if (id === "build" || id === "lint" || id === "test") {
412
524
  checks.push(runScriptCheck(projectRoot, id));
413
525
  } else {
@@ -23,6 +23,7 @@ export interface MachineErrorResponse {
23
23
  ok: false;
24
24
  command: string;
25
25
  status?: MachineOperationStatus;
26
+ nextActions?: MachineNextAction[];
26
27
  error: {
27
28
  code: string;
28
29
  message: string;
@@ -1,16 +1,18 @@
1
- import type { MachineOperationStatus } from "./contracts";
1
+ import type { MachineNextAction, MachineOperationStatus } from "./contracts";
2
2
 
3
3
  export class MachineCommandError extends Error {
4
4
  readonly code: string;
5
5
  readonly details?: unknown;
6
6
  readonly status?: MachineOperationStatus;
7
+ readonly nextActions?: MachineNextAction[];
7
8
 
8
- constructor(code: string, message: string, details?: unknown, status?: MachineOperationStatus) {
9
+ constructor(code: string, message: string, details?: unknown, status?: MachineOperationStatus, nextActions?: MachineNextAction[]) {
9
10
  super(message);
10
11
  this.name = "MachineCommandError";
11
12
  this.code = code;
12
13
  this.details = details;
13
14
  this.status = status;
15
+ this.nextActions = nextActions;
14
16
  }
15
17
  }
16
18
 
@@ -58,6 +58,7 @@ function writeMachineError(command: string, error: unknown): void {
58
58
  ok: false,
59
59
  command,
60
60
  status: resolveMachineErrorStatus(error),
61
+ ...(error instanceof MachineCommandError && error.nextActions?.length ? { nextActions: error.nextActions } : {}),
61
62
  error: toMachineError(error),
62
63
  };
63
64
  writeMachineResponse(response);
@@ -274,7 +275,8 @@ function createMachineProgram(): Command {
274
275
  .option("--functions-dir <dir>", "Functions directory", "functions")
275
276
  .option("--api-dir <dir>", "BFF routes directory (Next.js: app/api; React: bff)")
276
277
  .option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets", false)
277
- .action(async (model: string, options: { functionsDir?: string; apiDir?: string; apiOnly?: boolean }) => {
278
+ .option("--skip-infra", "Do not create or update anything under infra/ (container migration is reported as pending)", false)
279
+ .action(async (model: string, options: { functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }) => {
278
280
  await handleMachineAction("generate-scaffold", async () => {
279
281
  ensureProjectForMachine();
280
282
  return runMachineScaffoldOperation({
@@ -282,6 +284,7 @@ function createMachineProgram(): Command {
282
284
  functionsDir: options.functionsDir,
283
285
  apiDir: options.apiDir,
284
286
  apiOnly: options.apiOnly,
287
+ skipInfra: options.skipInfra,
285
288
  });
286
289
  });
287
290
  });
@@ -294,7 +297,8 @@ function createMachineProgram(): Command {
294
297
  .option("--functions-dir <dir>", "Functions directory", "functions")
295
298
  .option("--api-dir <dir>", "BFF routes directory (Next.js: app/api; React: bff)")
296
299
  .option("--api-only", "Skip UI components in the plan", false)
297
- .action(async (models: string[], options: { functionsDir?: string; apiDir?: string; apiOnly?: boolean }) => {
300
+ .option("--skip-infra", "Exclude all infra/ changes (container migration) from the plan", false)
301
+ .action(async (models: string[], options: { functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }) => {
298
302
  await handleMachineAction(
299
303
  "plan-scaffold",
300
304
  async () => {
@@ -307,6 +311,7 @@ function createMachineProgram(): Command {
307
311
  functionsDir: options.functionsDir,
308
312
  apiDir: options.apiDir,
309
313
  apiOnly: options.apiOnly,
314
+ skipInfra: options.skipInfra,
310
315
  })
311
316
  );
312
317
  }
@@ -316,12 +321,20 @@ function createMachineProgram(): Command {
316
321
  const plans = "plans" in data ? data.plans : [data];
317
322
  return {
318
323
  status: plans.some((planData) => planData.requiresApproval) ? "requires-human" : "complete",
319
- nextActions: plans.map((planData) => ({
320
- command: `swallowkit machine apply scaffold --plan ${planData.planId}${planData.requiresApproval ? " --approve" : ""}`,
321
- description: planData.requiresApproval
322
- ? `Apply the plan for ${planData.model} after reviewing the listed conflicts (requires --approve).`
323
- : `Apply the plan for ${planData.model}.`,
324
- })),
324
+ nextActions: [
325
+ ...plans.map((planData) => ({
326
+ command: `swallowkit machine apply scaffold --plan ${planData.planId}${planData.requiresApproval ? " --approve" : ""}`,
327
+ description: planData.requiresApproval
328
+ ? `Apply the plan for ${planData.model} after reviewing the listed conflicts (requires --approve).`
329
+ : `Apply the plan for ${planData.model}.`,
330
+ })),
331
+ ...plans
332
+ .filter((planData) => planData.warnings.some((warning) => warning.startsWith("infra-skipped:")))
333
+ .map((planData) => ({
334
+ command: `swallowkit migrations add-model ${planData.model}`,
335
+ description: `Later, when infrastructure changes are allowed, generate only the Cosmos DB container migration for ${planData.model}.`,
336
+ })),
337
+ ],
325
338
  };
326
339
  }
327
340
  );
@@ -408,10 +421,11 @@ function createMachineProgram(): Command {
408
421
  .option("--functions-dir <dir>", "Functions directory")
409
422
  .option("--api-dir <dir>", "API routes directory")
410
423
  .option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets")
424
+ .option("--skip-infra", "Do not create or update anything under infra/ (container migration is reported as pending)")
411
425
  .action(
412
426
  async (
413
427
  models: string[],
414
- options: { plan?: string; approve?: boolean; functionsDir?: string; apiDir?: string; apiOnly?: boolean }
428
+ options: { plan?: string; approve?: boolean; functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }
415
429
  ) => {
416
430
  await handleMachineAction(
417
431
  "apply-scaffold",
@@ -433,6 +447,7 @@ function createMachineProgram(): Command {
433
447
  functionsDir: options.functionsDir,
434
448
  apiDir: options.apiDir,
435
449
  apiOnly: options.apiOnly,
450
+ skipInfra: options.skipInfra,
436
451
  });
437
452
  }
438
453
  const results = [];
@@ -444,20 +459,30 @@ function createMachineProgram(): Command {
444
459
  functionsDir: options.functionsDir,
445
460
  apiDir: options.apiDir,
446
461
  apiOnly: options.apiOnly,
462
+ skipInfra: options.skipInfra,
447
463
  })
448
464
  );
449
465
  }
450
466
  return { applyType: "scaffold-batch" as const, results };
451
467
  },
452
- () => ({
453
- status: "complete",
454
- nextActions: [
455
- {
456
- command: "swallowkit machine verify project",
457
- description: "Verify the project after applying scaffold changes.",
458
- },
459
- ],
460
- })
468
+ (data) => {
469
+ const results = "results" in data ? data.results : [data];
470
+ return {
471
+ status: "complete",
472
+ nextActions: [
473
+ {
474
+ command: "swallowkit machine verify project",
475
+ description: "Verify the project after applying scaffold changes.",
476
+ },
477
+ ...results
478
+ .filter((result) => result.warnings.some((warning) => warning.startsWith("infra-skipped:")))
479
+ .map((result) => ({
480
+ command: `swallowkit migrations add-model ${result.model}`,
481
+ description: `The Cosmos DB container migration for ${result.model} is still pending; generate it when infrastructure changes are allowed.`,
482
+ })),
483
+ ],
484
+ };
485
+ }
461
486
  );
462
487
  }
463
488
  );
@@ -543,10 +568,10 @@ function createMachineProgram(): Command {
543
568
  const verify = new Command("verify");
544
569
  verify
545
570
  .command("project")
546
- .description("Run verification checks (structure, drift, typecheck) with machine-readable evidence")
571
+ .description("Run verification checks (structure, drift, typecheck, backend-build) with machine-readable evidence")
547
572
  .option(
548
573
  "--checks <ids>",
549
- "Comma-separated check ids to run (structure,drift,typecheck,build,lint,test, plus custom ids from swallowkit.config verify.checks)"
574
+ "Comma-separated check ids to run (structure,drift,typecheck,backend-build,build,lint,test, plus custom ids from swallowkit.config verify.checks); quote CSV values in PowerShell"
550
575
  )
551
576
  .option("--compact", "Suppress info-severity findings/violations from evidence to keep agent-facing output small", false)
552
577
  .action(async (options: { checks?: string; compact?: boolean }) => {