swallowkit 2.0.0-alpha.1 → 2.0.0-alpha.10

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 (224) hide show
  1. package/README.ja.md +14 -0
  2. package/README.md +14 -0
  3. package/dist/cli/commands/add-auth.js +37 -6
  4. package/dist/cli/commands/add-auth.js.map +1 -1
  5. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  6. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  7. package/dist/cli/commands/configure-mcp.js +42 -0
  8. package/dist/cli/commands/configure-mcp.js.map +1 -0
  9. package/dist/cli/commands/dev.d.ts +2 -2
  10. package/dist/cli/commands/dev.d.ts.map +1 -1
  11. package/dist/cli/commands/dev.js +39 -27
  12. package/dist/cli/commands/dev.js.map +1 -1
  13. package/dist/cli/commands/init.d.ts +8 -2
  14. package/dist/cli/commands/init.d.ts.map +1 -1
  15. package/dist/cli/commands/init.js +67 -132
  16. package/dist/cli/commands/init.js.map +1 -1
  17. package/dist/cli/commands/migrations.d.ts.map +1 -1
  18. package/dist/cli/commands/migrations.js +25 -0
  19. package/dist/cli/commands/migrations.js.map +1 -1
  20. package/dist/cli/commands/provision.d.ts +5 -0
  21. package/dist/cli/commands/provision.d.ts.map +1 -1
  22. package/dist/cli/commands/provision.js +278 -55
  23. package/dist/cli/commands/provision.js.map +1 -1
  24. package/dist/cli/commands/scaffold.d.ts +4 -0
  25. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  26. package/dist/cli/commands/scaffold.js +85 -3
  27. package/dist/cli/commands/scaffold.js.map +1 -1
  28. package/dist/cli/index.d.ts +2 -0
  29. package/dist/cli/index.d.ts.map +1 -1
  30. package/dist/cli/index.js +43 -5
  31. package/dist/cli/index.js.map +1 -1
  32. package/dist/core/operations/create-model.d.ts +2 -0
  33. package/dist/core/operations/create-model.d.ts.map +1 -1
  34. package/dist/core/operations/create-model.js +60 -8
  35. package/dist/core/operations/create-model.js.map +1 -1
  36. package/dist/core/operations/migration-generator.d.ts.map +1 -1
  37. package/dist/core/operations/migration-generator.js +12 -1
  38. package/dist/core/operations/migration-generator.js.map +1 -1
  39. package/dist/core/operations/model-migration.d.ts +22 -0
  40. package/dist/core/operations/model-migration.d.ts.map +1 -0
  41. package/dist/core/operations/model-migration.js +36 -0
  42. package/dist/core/operations/model-migration.js.map +1 -0
  43. package/dist/core/operations/provision-deployment.d.ts +52 -0
  44. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  45. package/dist/core/operations/provision-deployment.js +164 -0
  46. package/dist/core/operations/provision-deployment.js.map +1 -0
  47. package/dist/core/operations/provision-operations.d.ts +62 -6
  48. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  49. package/dist/core/operations/provision-operations.js +653 -115
  50. package/dist/core/operations/provision-operations.js.map +1 -1
  51. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  52. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  53. package/dist/core/operations/scaffold-apply.js +1 -0
  54. package/dist/core/operations/scaffold-apply.js.map +1 -1
  55. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  56. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  57. package/dist/core/operations/scaffold-machine.js.map +1 -1
  58. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  59. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  60. package/dist/core/operations/scaffold-plan.js +3 -1
  61. package/dist/core/operations/scaffold-plan.js.map +1 -1
  62. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  63. package/dist/core/project/agent-instructions.js +4 -1
  64. package/dist/core/project/agent-instructions.js.map +1 -1
  65. package/dist/core/project/boundaries.d.ts +5 -2
  66. package/dist/core/project/boundaries.d.ts.map +1 -1
  67. package/dist/core/project/boundaries.js +51 -20
  68. package/dist/core/project/boundaries.js.map +1 -1
  69. package/dist/core/project/capabilities.d.ts +10 -0
  70. package/dist/core/project/capabilities.d.ts.map +1 -1
  71. package/dist/core/project/capabilities.js +38 -6
  72. package/dist/core/project/capabilities.js.map +1 -1
  73. package/dist/core/project/home-page.d.ts +4 -0
  74. package/dist/core/project/home-page.d.ts.map +1 -0
  75. package/dist/core/project/home-page.js +113 -0
  76. package/dist/core/project/home-page.js.map +1 -0
  77. package/dist/core/project/mcp-config.d.ts +26 -0
  78. package/dist/core/project/mcp-config.d.ts.map +1 -0
  79. package/dist/core/project/mcp-config.js +117 -0
  80. package/dist/core/project/mcp-config.js.map +1 -0
  81. package/dist/core/project/react-project.d.ts +1 -1
  82. package/dist/core/project/react-project.d.ts.map +1 -1
  83. package/dist/core/project/react-project.js +46 -25
  84. package/dist/core/project/react-project.js.map +1 -1
  85. package/dist/core/project/state.d.ts +7 -0
  86. package/dist/core/project/state.d.ts.map +1 -1
  87. package/dist/core/project/state.js +86 -0
  88. package/dist/core/project/state.js.map +1 -1
  89. package/dist/core/project/workflows.d.ts.map +1 -1
  90. package/dist/core/project/workflows.js +53 -7
  91. package/dist/core/project/workflows.js.map +1 -1
  92. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  93. package/dist/core/scaffold/auth-generator.js +26 -27
  94. package/dist/core/scaffold/auth-generator.js.map +1 -1
  95. package/dist/core/scaffold/model-parser.d.ts +23 -0
  96. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  97. package/dist/core/scaffold/model-parser.js +407 -137
  98. package/dist/core/scaffold/model-parser.js.map +1 -1
  99. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  100. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  101. package/dist/core/scaffold/native-schema-generator.js +294 -91
  102. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  103. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  104. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  105. package/dist/core/scaffold/openapi-generator.js +53 -3
  106. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  107. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  108. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  109. package/dist/core/scaffold/ui-generator.js +68 -7
  110. package/dist/core/scaffold/ui-generator.js.map +1 -1
  111. package/dist/core/verify/index.d.ts +4 -3
  112. package/dist/core/verify/index.d.ts.map +1 -1
  113. package/dist/core/verify/index.js +122 -5
  114. package/dist/core/verify/index.js.map +1 -1
  115. package/dist/machine/contracts.d.ts +1 -0
  116. package/dist/machine/contracts.d.ts.map +1 -1
  117. package/dist/machine/errors.d.ts +3 -2
  118. package/dist/machine/errors.d.ts.map +1 -1
  119. package/dist/machine/errors.js +2 -1
  120. package/dist/machine/errors.js.map +1 -1
  121. package/dist/machine/index.d.ts.map +1 -1
  122. package/dist/machine/index.js +59 -28
  123. package/dist/machine/index.js.map +1 -1
  124. package/dist/mcp/index.d.ts +6 -4
  125. package/dist/mcp/index.d.ts.map +1 -1
  126. package/dist/mcp/index.js +210 -25
  127. package/dist/mcp/index.js.map +1 -1
  128. package/dist/mcp/version-selection.d.ts +55 -0
  129. package/dist/mcp/version-selection.d.ts.map +1 -0
  130. package/dist/mcp/version-selection.js +152 -0
  131. package/dist/mcp/version-selection.js.map +1 -0
  132. package/dist/utils/azure-cli.d.ts +56 -0
  133. package/dist/utils/azure-cli.d.ts.map +1 -0
  134. package/dist/utils/azure-cli.js +326 -0
  135. package/dist/utils/azure-cli.js.map +1 -0
  136. package/dist/utils/azure-diagnostics.d.ts +16 -0
  137. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  138. package/dist/utils/azure-diagnostics.js +68 -0
  139. package/dist/utils/azure-diagnostics.js.map +1 -0
  140. package/dist/utils/package-manager.js +2 -2
  141. package/dist/utils/package-manager.js.map +1 -1
  142. package/dist/utils/python-uv.d.ts +5 -7
  143. package/dist/utils/python-uv.d.ts.map +1 -1
  144. package/dist/utils/python-uv.js +50 -24
  145. package/dist/utils/python-uv.js.map +1 -1
  146. package/dist/utils/react-dev.d.ts +4 -0
  147. package/dist/utils/react-dev.d.ts.map +1 -0
  148. package/dist/utils/react-dev.js +35 -0
  149. package/dist/utils/react-dev.js.map +1 -0
  150. package/dist/utils/semver.d.ts +14 -0
  151. package/dist/utils/semver.d.ts.map +1 -0
  152. package/dist/utils/semver.js +61 -0
  153. package/dist/utils/semver.js.map +1 -0
  154. package/package.json +6 -3
  155. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  156. package/src/__tests__/agent-instructions.test.ts +5 -0
  157. package/src/__tests__/agent-loop-phase34.test.ts +143 -8
  158. package/src/__tests__/auth.test.ts +9 -0
  159. package/src/__tests__/azure-cli.test.ts +202 -0
  160. package/src/__tests__/boundaries.test.ts +42 -0
  161. package/src/__tests__/csharp-schema.test.ts +261 -0
  162. package/src/__tests__/dev.test.ts +13 -6
  163. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  164. package/src/__tests__/fixtures/azure-cli.cjs +153 -0
  165. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  166. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  167. package/src/__tests__/home-page.test.ts +50 -0
  168. package/src/__tests__/init.test.ts +116 -0
  169. package/src/__tests__/mcp-version.test.ts +262 -0
  170. package/src/__tests__/mcp.test.ts +120 -3
  171. package/src/__tests__/multi-auth.test.ts +115 -4
  172. package/src/__tests__/package-manager.test.ts +17 -2
  173. package/src/__tests__/plan-apply.test.ts +179 -0
  174. package/src/__tests__/provision-runtime.test.ts +722 -0
  175. package/src/__tests__/python-uv.test.ts +50 -14
  176. package/src/__tests__/python-venv.test.ts +48 -0
  177. package/src/__tests__/react-dev.test.ts +72 -0
  178. package/src/__tests__/react.test.ts +19 -0
  179. package/src/__tests__/schema-analysis.test.ts +329 -0
  180. package/src/cli/commands/add-auth.ts +36 -6
  181. package/src/cli/commands/configure-mcp.ts +54 -0
  182. package/src/cli/commands/dev.ts +40 -31
  183. package/src/cli/commands/init.ts +73 -138
  184. package/src/cli/commands/migrations.ts +22 -0
  185. package/src/cli/commands/provision.ts +294 -56
  186. package/src/cli/commands/scaffold.ts +105 -3
  187. package/src/cli/index.ts +40 -7
  188. package/src/core/operations/create-model.ts +51 -13
  189. package/src/core/operations/migration-generator.ts +14 -1
  190. package/src/core/operations/model-migration.ts +52 -0
  191. package/src/core/operations/provision-deployment.ts +183 -0
  192. package/src/core/operations/provision-operations.ts +662 -154
  193. package/src/core/operations/scaffold-apply.ts +2 -0
  194. package/src/core/operations/scaffold-machine.ts +1 -0
  195. package/src/core/operations/scaffold-plan.ts +5 -1
  196. package/src/core/project/agent-instructions.ts +4 -1
  197. package/src/core/project/agent-templates/AGENTS.md +89 -6
  198. package/src/core/project/agent-templates/CLAUDE.md +14 -1
  199. package/src/core/project/agent-templates/copilot-instructions.md +15 -1
  200. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  201. package/src/core/project/boundaries.ts +62 -20
  202. package/src/core/project/capabilities.ts +49 -6
  203. package/src/core/project/home-page.ts +114 -0
  204. package/src/core/project/mcp-config.ts +84 -0
  205. package/src/core/project/react-project.ts +50 -28
  206. package/src/core/project/state.ts +70 -0
  207. package/src/core/project/workflows.ts +53 -7
  208. package/src/core/scaffold/auth-generator.ts +26 -27
  209. package/src/core/scaffold/model-parser.ts +413 -129
  210. package/src/core/scaffold/native-schema-generator.ts +319 -103
  211. package/src/core/scaffold/openapi-generator.ts +63 -5
  212. package/src/core/scaffold/ui-generator.ts +70 -8
  213. package/src/core/verify/index.ts +118 -6
  214. package/src/machine/contracts.ts +1 -0
  215. package/src/machine/errors.ts +4 -2
  216. package/src/machine/index.ts +65 -34
  217. package/src/mcp/index.ts +221 -31
  218. package/src/mcp/version-selection.ts +163 -0
  219. package/src/utils/azure-cli.ts +315 -0
  220. package/src/utils/azure-diagnostics.ts +75 -0
  221. package/src/utils/package-manager.ts +2 -2
  222. package/src/utils/python-uv.ts +53 -33
  223. package/src/utils/react-dev.ts +31 -0
  224. package/src/utils/semver.ts +59 -0
@@ -5,12 +5,12 @@ import { spawn, spawnSync } from "child_process";
5
5
  import { BackendLanguage } from "../../types";
6
6
  import { FileOperationSession } from "../operations/file-session";
7
7
  import { ModelInfo, toKebabCase } from "./model-parser";
8
- import { generateOpenApiDocument } from "./openapi-generator";
8
+ import { expandInlineObjectModels, generateOpenApiDocument } from "./openapi-generator";
9
9
  import {
10
- buildProjectLocalUvEnv,
11
10
  buildProjectLocalUvInstallerEnv,
12
11
  buildUvPipInstallArgs,
13
- buildUvVenvArgs,
12
+ buildPythonCodegenInstallEnv,
13
+ ensureUvVirtualEnvironment,
14
14
  getProjectLocalUvInstallerCommand,
15
15
  getProjectLocalUvPaths,
16
16
  getPythonProjectRoot,
@@ -19,6 +19,8 @@ import {
19
19
  export const NSWAG_CONSOLECORE_VERSION = "14.7.1";
20
20
  export const PYTHON_SCHEMA_CODEGEN_REQUIREMENT = "datamodel-code-generator>=0.44.0,<1.0.0";
21
21
  const PYTHON_OUTPUT_MODEL_TYPE = "pydantic_v2.BaseModel";
22
+ // Preserve model syntax compatibility with existing deployments, not the tool's interpreter.
23
+ const PYTHON_MODEL_SYNTAX_VERSION = "3.11";
22
24
 
23
25
  function getMachineAwareStdio(): "inherit" | "pipe" {
24
26
  return process.env.SWALLOWKIT_MACHINE_OUTPUT === "1" ? "pipe" : "inherit";
@@ -152,8 +154,22 @@ function csharpStringLiteral(value: string): string {
152
154
  return JSON.stringify(value);
153
155
  }
154
156
 
155
- function isDateLikeField(field: Pick<ModelInfo["fields"][number], "name" | "type">): boolean {
156
- return field.type === "date" || (field.type === "string" && field.name.toLowerCase().endsWith("at"));
157
+ /** Date/time types are used only when the Zod schema declares them (z.iso.datetime(), z.iso.date(), z.date()). */
158
+ function getCSharpScalarType(field: ModelInfo["fields"][number]): string {
159
+ switch (field.type) {
160
+ case "string":
161
+ return field.format === "date-time" ? "DateTime" : field.format === "date" ? "DateOnly" : "string";
162
+ case "number":
163
+ return field.isInteger ? "long" : "double";
164
+ case "boolean":
165
+ return "bool";
166
+ case "date":
167
+ return "DateTime";
168
+ case "object":
169
+ return "Dictionary<string, object>";
170
+ default:
171
+ return "object";
172
+ }
157
173
  }
158
174
 
159
175
  export function getCSharpSchemaModelPath(outputDir: string, modelName: string): string {
@@ -201,7 +217,7 @@ export function getPythonNativeGeneratorArgs(specPath: string, outputPath: strin
201
217
  "--output-model-type",
202
218
  PYTHON_OUTPUT_MODEL_TYPE,
203
219
  "--target-python-version",
204
- "3.11",
220
+ PYTHON_MODEL_SYNTAX_VERSION,
205
221
  "--disable-timestamp",
206
222
  "--use-union-operator",
207
223
  "--collapse-root-models",
@@ -209,6 +225,11 @@ export function getPythonNativeGeneratorArgs(specPath: string, outputPath: strin
209
225
  }
210
226
 
211
227
  function getCSharpFieldBaseType(field: ModelInfo["fields"][number]): string {
228
+ // Arrays first: arrays of nested models or enums are lists of the element type.
229
+ if (field.isArray) {
230
+ return `List<${getCSharpArrayElementType(field)}>`;
231
+ }
232
+
212
233
  if (field.isNestedSchema && field.nestedModelName) {
213
234
  return field.nestedModelName;
214
235
  }
@@ -217,24 +238,7 @@ function getCSharpFieldBaseType(field: ModelInfo["fields"][number]): string {
217
238
  return `${toPascalIdentifier(field.name)}Enum`;
218
239
  }
219
240
 
220
- if (field.isArray) {
221
- return `List<${getCSharpArrayElementType(field)}>`;
222
- }
223
-
224
- switch (field.type) {
225
- case "string":
226
- return isDateLikeField(field) ? "DateTime" : "string";
227
- case "number":
228
- return "decimal";
229
- case "boolean":
230
- return "bool";
231
- case "date":
232
- return "DateTime";
233
- case "object":
234
- return "Dictionary<string, object>";
235
- default:
236
- return "object";
237
- }
241
+ return getCSharpScalarType(field);
238
242
  }
239
243
 
240
244
  function getCSharpArrayElementType(field: ModelInfo["fields"][number]): string {
@@ -246,25 +250,12 @@ function getCSharpArrayElementType(field: ModelInfo["fields"][number]): string {
246
250
  return `${toPascalIdentifier(field.name)}Enum`;
247
251
  }
248
252
 
249
- switch (field.type) {
250
- case "string":
251
- return isDateLikeField(field) ? "DateTime" : "string";
252
- case "number":
253
- return "decimal";
254
- case "boolean":
255
- return "bool";
256
- case "date":
257
- return "DateTime";
258
- case "object":
259
- return "Dictionary<string, object>";
260
- default:
261
- return "object";
262
- }
253
+ return getCSharpScalarType(field);
263
254
  }
264
255
 
265
256
  function getCSharpPropertyType(field: ModelInfo["fields"][number]): string {
266
257
  const baseType = getCSharpFieldBaseType(field);
267
- return field.isOptional ? `${baseType}?` : baseType;
258
+ return field.isOptional || field.isNullable ? `${baseType}?` : baseType;
268
259
  }
269
260
 
270
261
  function getCSharpOptionType(field: ModelInfo["fields"][number]): string {
@@ -331,12 +322,120 @@ function buildCSharpEnumToJsonCases(field: ModelInfo["fields"][number], members:
331
322
  .join("\n\n");
332
323
  }
333
324
 
325
+ function csharpNumberLiteral(value: number): string {
326
+ return Number.isInteger(value) ? `${value}` : `${value}d`;
327
+ }
328
+
329
+ function csharpRegexOptions(flags: string | undefined): string {
330
+ const options = [
331
+ flags?.includes("i") ? "RegexOptions.IgnoreCase" : null,
332
+ flags?.includes("m") ? "RegexOptions.Multiline" : null,
333
+ flags?.includes("s") ? "RegexOptions.Singleline" : null,
334
+ ].filter(Boolean);
335
+ return options.length > 0 ? options.join(" | ") : "RegexOptions.None";
336
+ }
337
+
338
+ function isCSharpReferenceType(field: ModelInfo["fields"][number]): boolean {
339
+ if (field.isArray || field.isNestedSchema || field.type === "object") return true;
340
+ if (field.enumValues?.length) return false;
341
+ return field.type === "string" && field.format !== "date-time" && field.format !== "date";
342
+ }
343
+
344
+ /**
345
+ * Translate Zod constraints into IValidatableObject checks. Regexes use search semantics like
346
+ * Zod's RegExp.test. String formats (email/uuid/url) and value-type presence stay with Zod.
347
+ */
348
+ function buildCSharpValueChecks(
349
+ field: ModelInfo["fields"][number],
350
+ accessor: string,
351
+ member: string,
352
+ optionalValue: boolean
353
+ ): string[] {
354
+ const constraints = field.constraints ?? {};
355
+ const checks: string[] = [];
356
+ const add = (condition: string, message: string) =>
357
+ checks.push(`if (${condition})\n yield return new ValidationResult(${csharpStringLiteral(message)}, new[] { ${member} });`);
358
+
359
+ if (field.isNestedSchema && field.nestedModelName) {
360
+ // Pattern variables in an if condition stay in scope for the enclosing block, so keep names unique.
361
+ const variable = `validated${toPascalIdentifier(field.name)}${accessor === "item" ? "Item" : ""}`;
362
+ checks.push(`if (${accessor} is IValidatableObject ${variable})\n foreach (var ${variable}Result in ${variable}.Validate(new ValidationContext(${accessor})))\n yield return new ValidationResult(${variable}Result.ErrorMessage, ${variable}Result.MemberNames.Select(name => ${member} + "." + name).ToArray());`);
363
+ return checks;
364
+ }
365
+ if (field.enumValues?.length) return checks;
366
+
367
+ if (field.type === "string" && field.format !== "date-time" && field.format !== "date") {
368
+ if (constraints.minLength !== undefined) add(`${accessor} != null && ${accessor}.Length < ${constraints.minLength}`, `${field.name} must contain at least ${constraints.minLength} character(s).`);
369
+ if (constraints.maxLength !== undefined) add(`${accessor} != null && ${accessor}.Length > ${constraints.maxLength}`, `${field.name} must contain at most ${constraints.maxLength} character(s).`);
370
+ if (constraints.pattern !== undefined) {
371
+ add(
372
+ `${accessor} != null && !Regex.IsMatch(${accessor}, ${csharpStringLiteral(constraints.pattern)}, ${csharpRegexOptions(constraints.patternFlags)})`,
373
+ `${field.name} must match /${constraints.pattern}/${constraints.patternFlags ?? ""}.`
374
+ );
375
+ }
376
+ }
377
+
378
+ if (field.type === "number") {
379
+ const value = optionalValue ? `${accessor}.Value` : accessor;
380
+ const guard = optionalValue ? `${accessor}.HasValue && ` : "";
381
+ if (constraints.minimum !== undefined) {
382
+ add(`${guard}${value} ${constraints.exclusiveMinimum ? "<=" : "<"} ${csharpNumberLiteral(constraints.minimum)}`,
383
+ `${field.name} must be ${constraints.exclusiveMinimum ? "greater than" : "greater than or equal to"} ${constraints.minimum}.`);
384
+ }
385
+ if (constraints.maximum !== undefined) {
386
+ add(`${guard}${value} ${constraints.exclusiveMaximum ? ">=" : ">"} ${csharpNumberLiteral(constraints.maximum)}`,
387
+ `${field.name} must be ${constraints.exclusiveMaximum ? "less than" : "less than or equal to"} ${constraints.maximum}.`);
388
+ }
389
+ }
390
+ return checks;
391
+ }
392
+
393
+ function buildCSharpValidationBody(model: ModelInfo): string {
394
+ const blocks: string[] = [];
395
+ for (const field of model.fields) {
396
+ const propertyName = toPascalIdentifier(field.name);
397
+ const member = csharpStringLiteral(field.name);
398
+ const constraints = field.constraints ?? {};
399
+
400
+ // .nullable() fields accept null in Zod, so only non-nullable required references are rejected.
401
+ if (!field.isOptional && !field.isNullable && isCSharpReferenceType(field)) {
402
+ blocks.push(`if (${propertyName} is null)\n yield return new ValidationResult(${csharpStringLiteral(`${field.name} is required.`)}, new[] { ${member} });`);
403
+ }
404
+
405
+ if (field.isArray) {
406
+ if (constraints.minItems !== undefined) {
407
+ blocks.push(`if (${propertyName} != null && ${propertyName}.Count < ${constraints.minItems})\n yield return new ValidationResult(${csharpStringLiteral(`${field.name} must contain at least ${constraints.minItems} item(s).`)}, new[] { ${member} });`);
408
+ }
409
+ if (constraints.maxItems !== undefined) {
410
+ blocks.push(`if (${propertyName} != null && ${propertyName}.Count > ${constraints.maxItems})\n yield return new ValidationResult(${csharpStringLiteral(`${field.name} must contain at most ${constraints.maxItems} item(s).`)}, new[] { ${member} });`);
411
+ }
412
+ const itemChecks = buildCSharpValueChecks(
413
+ { ...field, isArray: false },
414
+ "item",
415
+ `${member} + "[" + index + "]"`,
416
+ false
417
+ );
418
+ if (itemChecks.length > 0) {
419
+ blocks.push(`if (${propertyName} != null)\n {\n for (var index = 0; index < ${propertyName}.Count; index++)\n {\n var item = ${propertyName}[index];\n${itemChecks.map((check) => ` ${check.replace(/\n {16}/g, "\n ").replace(/\n {20}/g, "\n ")}`).join("\n")}\n }\n }`);
420
+ }
421
+ continue;
422
+ }
423
+
424
+ const optionalValue = (field.isOptional || field.isNullable === true) && !isCSharpReferenceType(field);
425
+ blocks.push(...buildCSharpValueChecks(field, propertyName, member, optionalValue));
426
+ }
427
+
428
+ return blocks.length > 0
429
+ ? blocks.map((block) => ` ${block}`).join("\n\n")
430
+ : " yield break;";
431
+ }
432
+
334
433
  function generateLegacyCompatibleCSharpModelSource(model: ModelInfo): string {
335
434
  const requiredFields = model.fields.filter((field) => !field.isOptional);
336
435
  const optionalFields = model.fields.filter((field) => field.isOptional);
337
436
 
338
437
  const constructorParams = [
339
- ...requiredFields.map((field) => `${getCSharpFieldBaseType(field)} ${field.name}`),
438
+ ...requiredFields.map((field) => `${getCSharpPropertyType(field)} ${field.name}`),
340
439
  ...optionalFields.map((field) => `${getCSharpOptionType(field)} ${field.name} = default`),
341
440
  ].join(", ");
342
441
 
@@ -354,7 +453,7 @@ function generateLegacyCompatibleCSharpModelSource(model: ModelInfo): string {
354
453
  return ` /// <summary>
355
454
  /// Defines ${toPascalIdentifier(field.name)}
356
455
  /// </summary>
357
- [JsonConverter(typeof(JsonStringEnumConverter))]
456
+ [JsonConverter(typeof(${enumType}JsonConverter))]
358
457
  public enum ${enumType}
359
458
  {
360
459
  ${buildCSharpEnumMembers(enumMembers)}
@@ -375,6 +474,22 @@ ${buildCSharpEnumFromStringCases(field, enumMembers, true)}
375
474
  ${buildCSharpEnumToJsonCases(field, enumMembers)}
376
475
 
377
476
  throw new NotImplementedException($"Value could not be handled: '{value}'");
477
+ }
478
+
479
+ /// <summary>
480
+ /// Reads and writes ${enumType} with the exact Zod enum values.
481
+ /// </summary>
482
+ public sealed class ${enumType}JsonConverter : JsonConverter<${enumType}>
483
+ {
484
+ public override ${enumType} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
485
+ {
486
+ var value = reader.GetString();
487
+ var parsed = value == null ? null : ${enumType}FromStringOrDefault(value);
488
+ return parsed ?? throw new JsonException($"Unsupported ${enumType} value: '{value}'");
489
+ }
490
+
491
+ public override void Write(Utf8JsonWriter writer, ${enumType} value, JsonSerializerOptions options)
492
+ => writer.WriteStringValue(${enumType}ToJsonValue(value));
378
493
  }`;
379
494
  })
380
495
  .join("\n\n");
@@ -402,7 +517,8 @@ ${buildCSharpEnumToJsonCases(field, enumMembers)}
402
517
  /// <summary>
403
518
  /// Gets or Sets ${propertyName}
404
519
  /// </summary>
405
- [JsonPropertyName("${field.name}")]
520
+ [JsonPropertyName("${field.name}")]${field.isNullable ? "" : `
521
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`}
406
522
  public ${propertyType} ${propertyName} { get { return this.${propertyName}Option; } set { this.${propertyName}Option = new(value); } }`;
407
523
  })
408
524
  .join("\n\n");
@@ -426,8 +542,11 @@ ${buildCSharpEnumToJsonCases(field, enumMembers)}
426
542
  using System;
427
543
  using System.Collections.Generic;
428
544
  using System.ComponentModel.DataAnnotations;
545
+ using System.Linq;
429
546
  using System.Text;
547
+ using System.Text.Json;
430
548
  using System.Text.Json.Serialization;
549
+ using System.Text.RegularExpressions;
431
550
  using SwallowKitBackendModels.Client;
432
551
 
433
552
  namespace SwallowKitBackendModels.Model
@@ -437,12 +556,22 @@ namespace SwallowKitBackendModels.Model
437
556
  /// </summary>
438
557
  public partial class ${model.name} : IValidatableObject
439
558
  {
559
+ /// <summary>
560
+ /// Used by System.Text.Json. Optional values are marked as set only when present in the JSON.
561
+ /// </summary>
562
+ #pragma warning disable CS8618
440
563
  [JsonConstructor]
564
+ public ${model.name}()
565
+ {
566
+ OnCreated();
567
+ }
568
+ #pragma warning restore CS8618
569
+ ${constructorParams ? `
441
570
  public ${model.name}(${constructorParams})
442
571
  {
443
572
  ${constructorAssignments}
444
573
  }
445
-
574
+ ` : ""}
446
575
  partial void OnCreated();
447
576
  ${enumBlocks ? `\n\n${enumBlocks}` : ""}
448
577
 
@@ -459,27 +588,19 @@ ${toStringBody}
459
588
 
460
589
  IEnumerable<ValidationResult> IValidatableObject.Validate(ValidationContext validationContext)
461
590
  {
462
- yield break;
591
+ ${buildCSharpValidationBody(model)}
463
592
  }
464
593
  }
465
594
  }
466
595
  `;
467
596
  }
468
597
 
469
- function getPythonTypeName(field: ModelInfo["fields"][number]): string {
470
- if (field.isNestedSchema && field.nestedModelName) {
471
- return field.nestedModelName;
472
- }
473
-
474
- if (field.isArray) {
475
- return `List[${getPythonArrayElementType(field)}]`;
476
- }
477
-
598
+ function getPythonScalarType(field: ModelInfo["fields"][number]): string {
478
599
  switch (field.type) {
479
600
  case "string":
480
- return isDateLikeField(field) ? "datetime" : "StrictStr";
601
+ return field.format === "date-time" ? "datetime" : field.format === "date" ? "date" : "StrictStr";
481
602
  case "number":
482
- return "Union[StrictFloat, StrictInt]";
603
+ return field.isInteger ? "StrictInt" : "Union[StrictFloat, StrictInt]";
483
604
  case "boolean":
484
605
  return "StrictBool";
485
606
  case "date":
@@ -491,43 +612,137 @@ function getPythonTypeName(field: ModelInfo["fields"][number]): string {
491
612
  }
492
613
  }
493
614
 
615
+ /** pydantic Field bounds for a scalar value (string length or numeric range). */
616
+ function getPythonValueConstraintArgs(field: ModelInfo["fields"][number]): string[] {
617
+ const constraints = field.constraints ?? {};
618
+ if (field.enumValues?.length || field.isNestedSchema) return [];
619
+ const args: string[] = [];
620
+ if (field.type === "string" && field.format !== "date-time" && field.format !== "date") {
621
+ if (constraints.minLength !== undefined) args.push(`min_length=${constraints.minLength}`);
622
+ if (constraints.maxLength !== undefined) args.push(`max_length=${constraints.maxLength}`);
623
+ }
624
+ if (field.type === "number") {
625
+ if (constraints.minimum !== undefined) args.push(`${constraints.exclusiveMinimum ? "gt" : "ge"}=${constraints.minimum}`);
626
+ if (constraints.maximum !== undefined) args.push(`${constraints.exclusiveMaximum ? "lt" : "le"}=${constraints.maximum}`);
627
+ }
628
+ return args;
629
+ }
630
+
494
631
  function getPythonArrayElementType(field: ModelInfo["fields"][number]): string {
495
632
  if (field.isNestedSchema && field.nestedModelName) {
496
633
  return field.nestedModelName;
497
634
  }
498
635
 
499
- switch (field.type) {
500
- case "string":
501
- return isDateLikeField(field) ? "datetime" : "StrictStr";
502
- case "number":
503
- return "Union[StrictFloat, StrictInt]";
504
- case "boolean":
505
- return "StrictBool";
506
- case "date":
507
- return "datetime";
508
- case "object":
509
- return "Dict[str, Any]";
510
- default:
511
- return "Any";
636
+ const elementType = getPythonScalarType(field);
637
+ const elementArgs = getPythonValueConstraintArgs(field);
638
+ return elementArgs.length > 0 ? `Annotated[${elementType}, Field(${elementArgs.join(", ")})]` : elementType;
639
+ }
640
+
641
+ function getPythonTypeName(field: ModelInfo["fields"][number]): string {
642
+ if (field.isArray) {
643
+ return `List[${getPythonArrayElementType(field)}]`;
512
644
  }
645
+
646
+ if (field.isNestedSchema && field.nestedModelName) {
647
+ return field.nestedModelName;
648
+ }
649
+
650
+ return getPythonScalarType(field);
513
651
  }
514
652
 
515
653
  function buildPythonFieldDeclaration(field: ModelInfo["fields"][number]): string {
516
654
  const pythonName = toSnakeCase(field.name);
517
655
  const typeName = field.isOptional ? `Optional[${getPythonTypeName(field)}]` : getPythonTypeName(field);
518
- const aliasSuffix = pythonName !== field.name ? `, alias="${field.name}"` : "";
656
+ const fieldArgs = [
657
+ ...(field.isOptional ? ["default=None"] : []),
658
+ ...(pythonName !== field.name ? [`alias="${field.name}"`] : []),
659
+ ...(field.isArray
660
+ ? [
661
+ ...(field.constraints?.minItems !== undefined ? [`min_length=${field.constraints.minItems}`] : []),
662
+ ...(field.constraints?.maxItems !== undefined ? [`max_length=${field.constraints.maxItems}`] : []),
663
+ ]
664
+ : getPythonValueConstraintArgs(field)),
665
+ ];
519
666
 
520
- if (field.isOptional) {
521
- return `${pythonName}: ${typeName} = Field(default=None${aliasSuffix})`;
522
- }
667
+ return fieldArgs.length > 0
668
+ ? `${pythonName}: ${typeName} = Field(${fieldArgs.join(", ")})`
669
+ : `${pythonName}: ${typeName}`;
670
+ }
671
+
672
+ function buildPythonRegexFlags(flags: string | undefined): string {
673
+ const values = [
674
+ flags?.includes("i") ? "re.IGNORECASE" : null,
675
+ flags?.includes("m") ? "re.MULTILINE" : null,
676
+ flags?.includes("s") ? "re.DOTALL" : null,
677
+ ].filter(Boolean);
678
+ return values.length > 0 ? values.join(" | ") : "0";
679
+ }
523
680
 
524
- if (aliasSuffix) {
525
- return `${pythonName}: ${typeName} = Field(${aliasSuffix.slice(2)})`;
681
+ export type PythonRegexTranslation = { pattern: string } | { unsupported: string };
682
+
683
+ /**
684
+ * Translates a JavaScript regex (as used by Zod) to Python `re` syntax. Constructs Python cannot
685
+ * express identically are reported as unsupported so the validator is skipped instead of breaking imports.
686
+ */
687
+ export function toPythonRegex(pattern: string, flags = ""): PythonRegexTranslation {
688
+ const unsupportedFlags = [...flags].filter((flag) => !"imsgu".includes(flag));
689
+ if (unsupportedFlags.length > 0) return { unsupported: `flag(s) ${unsupportedFlags.join("")} have no Python equivalent` };
690
+ const checks: Array<[RegExp, string]> = [
691
+ [/\\[pP]\{/, "Unicode property escapes (\\p{...})"],
692
+ [/\\u\{/, "code point escapes (\\u{...})"],
693
+ [/\\c[A-Za-z]/, "control escapes (\\cX)"],
694
+ [/\(\?<[=!]/, "lookbehind (JavaScript allows variable width; Python does not)"],
695
+ [/\[\^?\]/, "empty character classes ([] / [^])"],
696
+ ];
697
+ for (const [construct, label] of checks) {
698
+ if (construct.test(pattern)) return { unsupported: label };
526
699
  }
700
+ return {
701
+ pattern: pattern
702
+ .replace(/\(\?<([A-Za-z_][A-Za-z0-9_]*)>/g, "(?P<$1>")
703
+ .replace(/\\k<([A-Za-z_][A-Za-z0-9_]*)>/g, "(?P=$1)"),
704
+ };
705
+ }
706
+
707
+ function pythonRegexFields(model: ModelInfo): Array<{ field: ModelInfo["fields"][number]; translation: PythonRegexTranslation }> {
708
+ return model.fields
709
+ .filter((field) => field.type === "string" && !field.enumValues?.length && field.constraints?.pattern !== undefined)
710
+ .map((field) => ({ field, translation: toPythonRegex(field.constraints!.pattern!, field.constraints!.patternFlags) }));
711
+ }
527
712
 
528
- return `${pythonName}: ${typeName}`;
713
+ export function getPythonRegexWarnings(model: ModelInfo): string[] {
714
+ return pythonRegexFields(model)
715
+ .filter((entry): entry is { field: ModelInfo["fields"][number]; translation: { unsupported: string } } => "unsupported" in entry.translation)
716
+ .map(({ field, translation }) =>
717
+ `python-regex-not-enforced: ${model.name}.${field.name} uses ${translation.unsupported}; the Python model does not enforce /${field.constraints!.pattern}/. The BFF Zod schema still validates it.`);
529
718
  }
530
719
 
720
+ /**
721
+ * Zod regex checks use RegExp.test (search), so validators use re.search instead of pydantic's anchored pattern.
722
+ * They run before type conversion so date/time strings are checked as the JSON text Zod sees; non-string
723
+ * values are left to the field's own type validation.
724
+ */
725
+ function buildPythonPatternValidators(model: ModelInfo): string {
726
+ return pythonRegexFields(model)
727
+ .filter((entry): entry is { field: ModelInfo["fields"][number]; translation: { pattern: string } } => "pattern" in entry.translation)
728
+ .map(({ field, translation }) => {
729
+ const pythonName = toSnakeCase(field.name);
730
+ const pattern = JSON.stringify(translation.pattern);
731
+ const flags = buildPythonRegexFlags(field.constraints!.patternFlags);
732
+ const message = JSON.stringify(`${field.name} must match /${field.constraints!.pattern}/${field.constraints!.patternFlags ?? ""}`);
733
+ return ` @field_validator('${pythonName}', mode='before')
734
+ def ${pythonName}_validate_pattern(cls, value):
735
+ """Validates the Zod regex constraint"""
736
+ if value is None:
737
+ return value
738
+
739
+ for item in (value if isinstance(value, list) else [value]):
740
+ if isinstance(item, str) and not re.search(${pattern}, item, ${flags}):
741
+ raise ValueError(${message})
742
+ return value`;
743
+ })
744
+ .join("\n\n");
745
+ }
531
746
  function buildPythonEnumValidators(model: ModelInfo): string {
532
747
  return model.fields
533
748
  .filter((field) => field.enumValues?.length)
@@ -564,7 +779,7 @@ function buildPythonModelImports(model: ModelInfo): string {
564
779
  function generateLegacyCompatiblePythonModelSource(model: ModelInfo): string {
565
780
  const fieldDeclarations = model.fields.map((field) => ` ${buildPythonFieldDeclaration(field)}`).join("\n");
566
781
  const propertyNames = model.fields.map((field) => `"${field.name}"`).join(", ");
567
- const validators = buildPythonEnumValidators(model);
782
+ const validators = [buildPythonEnumValidators(model), buildPythonPatternValidators(model)].filter(Boolean).join("\n\n");
568
783
  const dictAssignments = model.fields
569
784
  .map((field) => ` "${field.name}": obj.get("${field.name}")`)
570
785
  .join(",\n");
@@ -588,9 +803,9 @@ import pprint
588
803
  import re # noqa: F401
589
804
  import json
590
805
 
591
- from datetime import datetime
806
+ from datetime import date, datetime
592
807
  from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictFloat, StrictInt, StrictStr, field_validator
593
- from typing import Any, ClassVar, Dict, List, Optional, Set, Union
808
+ from typing import Annotated, Any, ClassVar, Dict, List, Optional, Set, Union
594
809
  from typing_extensions import Self
595
810
 
596
811
  ${buildPythonModelImports(model)}class ${model.name}(BaseModel):
@@ -614,7 +829,7 @@ ${validators ? `\n\n${validators}` : ""}
614
829
 
615
830
  def to_json(self) -> str:
616
831
  """Returns the JSON representation of the model using alias"""
617
- return json.dumps(self.to_dict())
832
+ return self.model_dump_json(by_alias=True, exclude_none=True)
618
833
 
619
834
  @classmethod
620
835
  def from_json(cls, json_str: str) -> Optional[Self]:
@@ -724,14 +939,23 @@ function ensurePythonCodegenProjectFiles(
724
939
  return requirementsPath;
725
940
  }
726
941
 
727
- function getVirtualEnvPythonPath(venvDir: string): string {
728
- return process.platform === "win32"
729
- ? path.join(venvDir, "Scripts", "python.exe")
730
- : path.join(venvDir, "bin", "python");
942
+ /** Generated backend_models import pydantic v2 at runtime, so the deployed Functions app must install it. */
943
+ export const PYTHON_SCHEMA_RUNTIME_REQUIREMENT = "pydantic>=2.6,<3";
944
+
945
+ function ensurePythonRuntimeRequirement(functionsRoot: string, session: FileOperationSession): void {
946
+ const requirementsPath = path.join(functionsRoot, "requirements.txt");
947
+ const current = session.fileExists(requirementsPath) ? session.readFile(requirementsPath) : "";
948
+ const declared = current.split(/\r?\n/).some((line) => line.trim().split(/[<>=!~;[ ]/)[0].toLowerCase() === "pydantic");
949
+ if (declared) return;
950
+ const prefix = current && !current.endsWith("\n") ? `${current}\n` : current;
951
+ session.writeFile(requirementsPath, `${prefix}${PYTHON_SCHEMA_RUNTIME_REQUIREMENT}\n`, {
952
+ ownership: "extension-point",
953
+ generator: "native-schema",
954
+ });
731
955
  }
732
956
 
733
957
  async function ensureProjectLocalUvCommand(projectRoot: string): Promise<{ command: string; env: NodeJS.ProcessEnv }> {
734
- const uvEnv = buildProjectLocalUvEnv(process.env, projectRoot);
958
+ const uvEnv = { ...process.env };
735
959
  const { localUvExecutable } = getProjectLocalUvPaths(projectRoot);
736
960
 
737
961
  if (canRun("uv", ["--version"], projectRoot)) {
@@ -762,22 +986,7 @@ async function ensurePythonCodegenEnvironment(functionsRoot: string, requirement
762
986
  const projectRoot = getPythonProjectRoot(functionsRoot);
763
987
  const { command: uvCommand, env: uvEnv } = await ensureProjectLocalUvCommand(projectRoot);
764
988
  const venvDir = path.join(functionsRoot, ".codegen-venv");
765
- const venvPython = getVirtualEnvPythonPath(venvDir);
766
-
767
- if (!canRun(venvPython, ["--version"], functionsRoot, uvEnv)) {
768
- const venvArgs = buildUvVenvArgs(venvDir);
769
- if (fs.existsSync(venvDir)) {
770
- venvArgs.push("--clear");
771
- }
772
-
773
- await runCommand(
774
- uvCommand,
775
- venvArgs,
776
- functionsRoot,
777
- "Failed to create the Python schema code generation virtual environment.",
778
- uvEnv
779
- );
780
- }
989
+ const venvPython = await ensureUvVirtualEnvironment(uvCommand, venvDir, functionsRoot, uvEnv);
781
990
 
782
991
  if (!canRun(venvPython, ["-c", "import datamodel_code_generator"], functionsRoot, uvEnv)) {
783
992
  await runCommand(
@@ -785,7 +994,7 @@ async function ensurePythonCodegenEnvironment(functionsRoot: string, requirement
785
994
  buildUvPipInstallArgs(venvPython, requirementsPath),
786
995
  functionsRoot,
787
996
  "Failed to install Python schema generation dependencies.",
788
- uvEnv
997
+ buildPythonCodegenInstallEnv(uvEnv)
789
998
  );
790
999
  }
791
1000
 
@@ -888,6 +1097,7 @@ async function generatePythonSchemaArtifacts(
888
1097
  sourceModel: string
889
1098
  ): Promise<void> {
890
1099
  ensurePythonCodegenProjectFiles(functionsRoot, session, sourceModel);
1100
+ ensurePythonRuntimeRequirement(functionsRoot, session);
891
1101
 
892
1102
  const packageRoot = path.join(outputDir, "backend_models");
893
1103
  const modelsRoot = path.join(packageRoot, "models");
@@ -905,6 +1115,10 @@ async function generatePythonSchemaArtifacts(
905
1115
  );
906
1116
 
907
1117
  for (const model of models) {
1118
+ for (const warning of getPythonRegexWarnings(model)) {
1119
+ session.addWarning(warning);
1120
+ console.warn(`⚠️ ${warning}`);
1121
+ }
908
1122
  const modelPath = getPythonSchemaModelPath(outputDir, model.name);
909
1123
  session.writeFile(modelPath, generateLegacyCompatiblePythonModelSource(model), {
910
1124
  ownership: "managed",
@@ -946,10 +1160,12 @@ export async function generateLanguageSchemaArtifacts(
946
1160
  backendLanguage === "csharp" ? "csharp-models" : "python-models"
947
1161
  );
948
1162
 
1163
+ // Inline z.object fields become named classes (e.g. ProbeItemNested) so their shape is preserved.
1164
+ const nativeModels = expandInlineObjectModels(models);
949
1165
  if (backendLanguage === "csharp") {
950
- await generateCSharpSchemaArtifacts(models, outputDir, functionsRoot, session, rootModel.name);
1166
+ await generateCSharpSchemaArtifacts(nativeModels, outputDir, functionsRoot, session, rootModel.name);
951
1167
  } else {
952
- await generatePythonSchemaArtifacts(models, outputDir, functionsRoot, session, rootModel.name);
1168
+ await generatePythonSchemaArtifacts(nativeModels, outputDir, functionsRoot, session, rootModel.name);
953
1169
  }
954
1170
 
955
1171
  console.log(