swallowkit 1.1.2 → 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 (262) hide show
  1. package/README.ja.md +61 -7
  2. package/README.md +61 -7
  3. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  4. package/dist/cli/commands/add-auth.js +103 -27
  5. package/dist/cli/commands/add-auth.js.map +1 -1
  6. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  7. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  8. package/dist/cli/commands/configure-mcp.js +42 -0
  9. package/dist/cli/commands/configure-mcp.js.map +1 -0
  10. package/dist/cli/commands/dev.d.ts +5 -2
  11. package/dist/cli/commands/dev.d.ts.map +1 -1
  12. package/dist/cli/commands/dev.js +143 -67
  13. package/dist/cli/commands/dev.js.map +1 -1
  14. package/dist/cli/commands/init.d.ts +14 -7
  15. package/dist/cli/commands/init.d.ts.map +1 -1
  16. package/dist/cli/commands/init.js +149 -170
  17. package/dist/cli/commands/init.js.map +1 -1
  18. package/dist/cli/commands/migrations.d.ts.map +1 -1
  19. package/dist/cli/commands/migrations.js +25 -0
  20. package/dist/cli/commands/migrations.js.map +1 -1
  21. package/dist/cli/commands/provision.d.ts +5 -0
  22. package/dist/cli/commands/provision.d.ts.map +1 -1
  23. package/dist/cli/commands/provision.js +278 -55
  24. package/dist/cli/commands/provision.js.map +1 -1
  25. package/dist/cli/commands/scaffold.d.ts +5 -1
  26. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  27. package/dist/cli/commands/scaffold.js +123 -18
  28. package/dist/cli/commands/scaffold.js.map +1 -1
  29. package/dist/cli/index.d.ts +2 -0
  30. package/dist/cli/index.d.ts.map +1 -1
  31. package/dist/cli/index.js +49 -8
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/core/config.d.ts +2 -1
  34. package/dist/core/config.d.ts.map +1 -1
  35. package/dist/core/config.js +19 -0
  36. package/dist/core/config.js.map +1 -1
  37. package/dist/core/operations/create-model.d.ts +2 -0
  38. package/dist/core/operations/create-model.d.ts.map +1 -1
  39. package/dist/core/operations/create-model.js +60 -8
  40. package/dist/core/operations/create-model.js.map +1 -1
  41. package/dist/core/operations/migration-generator.d.ts.map +1 -1
  42. package/dist/core/operations/migration-generator.js +12 -1
  43. package/dist/core/operations/migration-generator.js.map +1 -1
  44. package/dist/core/operations/model-migration.d.ts +22 -0
  45. package/dist/core/operations/model-migration.d.ts.map +1 -0
  46. package/dist/core/operations/model-migration.js +36 -0
  47. package/dist/core/operations/model-migration.js.map +1 -0
  48. package/dist/core/operations/provision-deployment.d.ts +52 -0
  49. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  50. package/dist/core/operations/provision-deployment.js +164 -0
  51. package/dist/core/operations/provision-deployment.js.map +1 -0
  52. package/dist/core/operations/provision-operations.d.ts +62 -6
  53. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  54. package/dist/core/operations/provision-operations.js +653 -115
  55. package/dist/core/operations/provision-operations.js.map +1 -1
  56. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  57. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  58. package/dist/core/operations/scaffold-apply.js +1 -0
  59. package/dist/core/operations/scaffold-apply.js.map +1 -1
  60. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  61. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  62. package/dist/core/operations/scaffold-machine.js.map +1 -1
  63. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  64. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  65. package/dist/core/operations/scaffold-plan.js +3 -1
  66. package/dist/core/operations/scaffold-plan.js.map +1 -1
  67. package/dist/core/project/agent-instructions.d.ts +2 -1
  68. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  69. package/dist/core/project/agent-instructions.js +15 -0
  70. package/dist/core/project/agent-instructions.js.map +1 -1
  71. package/dist/core/project/boundaries.d.ts +5 -2
  72. package/dist/core/project/boundaries.d.ts.map +1 -1
  73. package/dist/core/project/boundaries.js +51 -20
  74. package/dist/core/project/boundaries.js.map +1 -1
  75. package/dist/core/project/capabilities.d.ts +17 -0
  76. package/dist/core/project/capabilities.d.ts.map +1 -1
  77. package/dist/core/project/capabilities.js +45 -6
  78. package/dist/core/project/capabilities.js.map +1 -1
  79. package/dist/core/project/home-page.d.ts +4 -0
  80. package/dist/core/project/home-page.d.ts.map +1 -0
  81. package/dist/core/project/home-page.js +113 -0
  82. package/dist/core/project/home-page.js.map +1 -0
  83. package/dist/core/project/manifest.d.ts +3 -1
  84. package/dist/core/project/manifest.d.ts.map +1 -1
  85. package/dist/core/project/manifest.js +47 -25
  86. package/dist/core/project/manifest.js.map +1 -1
  87. package/dist/core/project/mcp-config.d.ts +26 -0
  88. package/dist/core/project/mcp-config.d.ts.map +1 -0
  89. package/dist/core/project/mcp-config.js +117 -0
  90. package/dist/core/project/mcp-config.js.map +1 -0
  91. package/dist/core/project/react-project.d.ts +26 -0
  92. package/dist/core/project/react-project.d.ts.map +1 -0
  93. package/dist/core/project/react-project.js +443 -0
  94. package/dist/core/project/react-project.js.map +1 -0
  95. package/dist/core/project/state.d.ts +7 -0
  96. package/dist/core/project/state.d.ts.map +1 -1
  97. package/dist/core/project/state.js +86 -0
  98. package/dist/core/project/state.js.map +1 -1
  99. package/dist/core/project/validation.d.ts.map +1 -1
  100. package/dist/core/project/validation.js +32 -10
  101. package/dist/core/project/validation.js.map +1 -1
  102. package/dist/core/project/workflows.d.ts.map +1 -1
  103. package/dist/core/project/workflows.js +53 -7
  104. package/dist/core/project/workflows.js.map +1 -1
  105. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  106. package/dist/core/scaffold/auth-generator.js +26 -27
  107. package/dist/core/scaffold/auth-generator.js.map +1 -1
  108. package/dist/core/scaffold/model-parser.d.ts +23 -0
  109. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  110. package/dist/core/scaffold/model-parser.js +407 -137
  111. package/dist/core/scaffold/model-parser.js.map +1 -1
  112. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  113. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  114. package/dist/core/scaffold/native-schema-generator.js +294 -91
  115. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  116. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  117. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  118. package/dist/core/scaffold/openapi-generator.js +53 -3
  119. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  120. package/dist/core/scaffold/react-bff-generator.d.ts +10 -0
  121. package/dist/core/scaffold/react-bff-generator.d.ts.map +1 -0
  122. package/dist/core/scaffold/react-bff-generator.js +252 -0
  123. package/dist/core/scaffold/react-bff-generator.js.map +1 -0
  124. package/dist/core/scaffold/react-ui-generator.d.ts +6 -0
  125. package/dist/core/scaffold/react-ui-generator.d.ts.map +1 -0
  126. package/dist/core/scaffold/react-ui-generator.js +41 -0
  127. package/dist/core/scaffold/react-ui-generator.js.map +1 -0
  128. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  129. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  130. package/dist/core/scaffold/ui-generator.js +68 -7
  131. package/dist/core/scaffold/ui-generator.js.map +1 -1
  132. package/dist/core/verify/index.d.ts +4 -3
  133. package/dist/core/verify/index.d.ts.map +1 -1
  134. package/dist/core/verify/index.js +122 -5
  135. package/dist/core/verify/index.js.map +1 -1
  136. package/dist/index.d.ts +3 -3
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +3 -2
  139. package/dist/index.js.map +1 -1
  140. package/dist/machine/contracts.d.ts +1 -0
  141. package/dist/machine/contracts.d.ts.map +1 -1
  142. package/dist/machine/errors.d.ts +3 -2
  143. package/dist/machine/errors.d.ts.map +1 -1
  144. package/dist/machine/errors.js +2 -1
  145. package/dist/machine/errors.js.map +1 -1
  146. package/dist/machine/index.d.ts.map +1 -1
  147. package/dist/machine/index.js +61 -30
  148. package/dist/machine/index.js.map +1 -1
  149. package/dist/mcp/index.d.ts +6 -4
  150. package/dist/mcp/index.d.ts.map +1 -1
  151. package/dist/mcp/index.js +210 -25
  152. package/dist/mcp/index.js.map +1 -1
  153. package/dist/mcp/version-selection.d.ts +55 -0
  154. package/dist/mcp/version-selection.d.ts.map +1 -0
  155. package/dist/mcp/version-selection.js +152 -0
  156. package/dist/mcp/version-selection.js.map +1 -0
  157. package/dist/types/index.d.ts +4 -0
  158. package/dist/types/index.d.ts.map +1 -1
  159. package/dist/utils/azure-cli.d.ts +56 -0
  160. package/dist/utils/azure-cli.d.ts.map +1 -0
  161. package/dist/utils/azure-cli.js +326 -0
  162. package/dist/utils/azure-cli.js.map +1 -0
  163. package/dist/utils/azure-diagnostics.d.ts +16 -0
  164. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  165. package/dist/utils/azure-diagnostics.js +68 -0
  166. package/dist/utils/azure-diagnostics.js.map +1 -0
  167. package/dist/utils/package-manager.d.ts +2 -2
  168. package/dist/utils/package-manager.d.ts.map +1 -1
  169. package/dist/utils/package-manager.js +6 -3
  170. package/dist/utils/package-manager.js.map +1 -1
  171. package/dist/utils/python-uv.d.ts +5 -7
  172. package/dist/utils/python-uv.d.ts.map +1 -1
  173. package/dist/utils/python-uv.js +50 -24
  174. package/dist/utils/python-uv.js.map +1 -1
  175. package/dist/utils/react-dev.d.ts +4 -0
  176. package/dist/utils/react-dev.d.ts.map +1 -0
  177. package/dist/utils/react-dev.js +35 -0
  178. package/dist/utils/react-dev.js.map +1 -0
  179. package/dist/utils/semver.d.ts +14 -0
  180. package/dist/utils/semver.d.ts.map +1 -0
  181. package/dist/utils/semver.js +61 -0
  182. package/dist/utils/semver.js.map +1 -0
  183. package/package.json +7 -4
  184. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  185. package/src/__tests__/agent-instructions.test.ts +6 -1
  186. package/src/__tests__/agent-loop-phase34.test.ts +143 -8
  187. package/src/__tests__/auth.test.ts +9 -0
  188. package/src/__tests__/azure-cli.test.ts +202 -0
  189. package/src/__tests__/boundaries.test.ts +42 -0
  190. package/src/__tests__/csharp-schema.test.ts +261 -0
  191. package/src/__tests__/dev.test.ts +13 -6
  192. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  193. package/src/__tests__/fixtures/azure-cli.cjs +153 -0
  194. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  195. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  196. package/src/__tests__/home-page.test.ts +50 -0
  197. package/src/__tests__/init.test.ts +145 -5
  198. package/src/__tests__/mcp-version.test.ts +262 -0
  199. package/src/__tests__/mcp.test.ts +120 -3
  200. package/src/__tests__/multi-auth.test.ts +115 -4
  201. package/src/__tests__/package-manager.test.ts +17 -2
  202. package/src/__tests__/plan-apply.test.ts +179 -0
  203. package/src/__tests__/provision-runtime.test.ts +722 -0
  204. package/src/__tests__/python-uv.test.ts +50 -14
  205. package/src/__tests__/python-venv.test.ts +48 -0
  206. package/src/__tests__/react-dev.test.ts +72 -0
  207. package/src/__tests__/react.test.ts +838 -0
  208. package/src/__tests__/release-metadata.test.ts +121 -0
  209. package/src/__tests__/schema-analysis.test.ts +329 -0
  210. package/src/cli/commands/add-auth.ts +103 -29
  211. package/src/cli/commands/configure-mcp.ts +54 -0
  212. package/src/cli/commands/dev.ts +145 -73
  213. package/src/cli/commands/init.ts +147 -172
  214. package/src/cli/commands/migrations.ts +22 -0
  215. package/src/cli/commands/provision.ts +294 -56
  216. package/src/cli/commands/scaffold.ts +145 -22
  217. package/src/cli/index.ts +46 -10
  218. package/src/core/config.ts +21 -1
  219. package/src/core/operations/create-model.ts +51 -13
  220. package/src/core/operations/migration-generator.ts +14 -1
  221. package/src/core/operations/model-migration.ts +52 -0
  222. package/src/core/operations/provision-deployment.ts +183 -0
  223. package/src/core/operations/provision-operations.ts +662 -154
  224. package/src/core/operations/scaffold-apply.ts +2 -0
  225. package/src/core/operations/scaffold-machine.ts +1 -0
  226. package/src/core/operations/scaffold-plan.ts +5 -1
  227. package/src/core/project/agent-instructions.ts +17 -1
  228. package/src/core/project/agent-templates/AGENTS.md +113 -15
  229. package/src/core/project/agent-templates/CLAUDE.md +14 -1
  230. package/src/core/project/agent-templates/bff-routes.instructions.md +5 -3
  231. package/src/core/project/agent-templates/copilot-instructions.md +15 -1
  232. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  233. package/src/core/project/boundaries.ts +62 -20
  234. package/src/core/project/capabilities.ts +63 -6
  235. package/src/core/project/home-page.ts +114 -0
  236. package/src/core/project/manifest.ts +52 -27
  237. package/src/core/project/mcp-config.ts +84 -0
  238. package/src/core/project/react-project.ts +529 -0
  239. package/src/core/project/state.ts +70 -0
  240. package/src/core/project/validation.ts +32 -10
  241. package/src/core/project/workflows.ts +53 -7
  242. package/src/core/scaffold/auth-generator.ts +26 -27
  243. package/src/core/scaffold/model-parser.ts +413 -129
  244. package/src/core/scaffold/native-schema-generator.ts +319 -103
  245. package/src/core/scaffold/openapi-generator.ts +63 -5
  246. package/src/core/scaffold/react-bff-generator.ts +292 -0
  247. package/src/core/scaffold/react-ui-generator.ts +51 -0
  248. package/src/core/scaffold/ui-generator.ts +70 -8
  249. package/src/core/verify/index.ts +118 -6
  250. package/src/index.ts +3 -1
  251. package/src/machine/contracts.ts +1 -0
  252. package/src/machine/errors.ts +4 -2
  253. package/src/machine/index.ts +67 -36
  254. package/src/mcp/index.ts +221 -31
  255. package/src/mcp/version-selection.ts +163 -0
  256. package/src/types/index.ts +4 -0
  257. package/src/utils/azure-cli.ts +315 -0
  258. package/src/utils/azure-diagnostics.ts +75 -0
  259. package/src/utils/package-manager.ts +7 -4
  260. package/src/utils/python-uv.ts +53 -33
  261. package/src/utils/react-dev.ts +31 -0
  262. package/src/utils/semver.ts +59 -0
@@ -0,0 +1,121 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { spawnSync } from "child_process";
4
+
5
+ describe("npm release metadata", () => {
6
+ const repository = process.cwd();
7
+ const script = path.join(repository, "scripts", "release-metadata.cjs");
8
+ let directory: string;
9
+
10
+ beforeEach(() => {
11
+ directory = fs.mkdtempSync(path.join(repository, ".tmp-release-test-"));
12
+ });
13
+
14
+ afterEach(() => {
15
+ fs.rmSync(directory, { recursive: true, force: true });
16
+ });
17
+
18
+ function run(
19
+ version: unknown,
20
+ options: {
21
+ refType?: string;
22
+ refName?: string;
23
+ lockVersion?: string;
24
+ rootVersion?: string;
25
+ noRootPackage?: boolean;
26
+ } = {}
27
+ ) {
28
+ fs.writeFileSync(
29
+ path.join(directory, "package.json"),
30
+ JSON.stringify({ name: "swallowkit", version })
31
+ );
32
+ fs.writeFileSync(
33
+ path.join(directory, "package-lock.json"),
34
+ JSON.stringify({
35
+ version: options.lockVersion ?? version,
36
+ packages: options.noRootPackage
37
+ ? {}
38
+ : { "": { version: options.rootVersion ?? version } },
39
+ })
40
+ );
41
+ const outputPath = path.join(directory, "output.txt");
42
+ const result = spawnSync(process.execPath, [script], {
43
+ cwd: directory,
44
+ encoding: "utf8",
45
+ env: {
46
+ ...process.env,
47
+ GITHUB_REF_TYPE: options.refType ?? "tag",
48
+ GITHUB_REF_NAME: options.refName ?? `v${version}`,
49
+ GITHUB_OUTPUT: outputPath,
50
+ },
51
+ });
52
+ expect(result.error).toBeUndefined();
53
+ const output = fs.existsSync(outputPath)
54
+ ? fs.readFileSync(outputPath, "utf8")
55
+ : "";
56
+ return { ...result, output };
57
+ }
58
+
59
+ it.each([
60
+ ["2.0.0", "latest"],
61
+ ["2.0.0-alpha.1", "alpha"],
62
+ ["2.0.0-alpha.2", "alpha"],
63
+ ["2.0.0-alpha", "alpha"],
64
+ ["2.0.0-beta.1", "beta"],
65
+ ["2.0.0-beta.12", "beta"],
66
+ ["2.0.0-rc.1", "next"],
67
+ ["2.0.0-next.1", "next"],
68
+ ["2.0.0-alphabet.1", "next"],
69
+ ["2.0.0-0", "next"],
70
+ ["2.0.0+build.1", "latest"],
71
+ ["2.0.0-alpha.1+build.1", "alpha"],
72
+ ])("publishes %s on the %s channel", (version, channel) => {
73
+ const result = run(version);
74
+ expect(result.status).toBe(0);
75
+ expect(result.stderr).toBe("");
76
+ expect(result.output).toBe(`version=${version}\ndist_tag=${channel}\n`);
77
+ expect(result.stdout).toBe(result.output);
78
+ });
79
+
80
+ it("supports manual branch dry runs without requiring a release tag", () => {
81
+ const result = run("2.0.0-alpha.1", {
82
+ refType: "branch",
83
+ refName: "main",
84
+ });
85
+ expect(result.status).toBe(0);
86
+ expect(result.output).toContain("dist_tag=alpha\n");
87
+ });
88
+
89
+ it.each([
90
+ { refName: "v2.0.0" },
91
+ { refName: "2.0.0-alpha.1" },
92
+ { lockVersion: "1.1.2" },
93
+ { rootVersion: "1.1.2" },
94
+ { noRootPackage: true },
95
+ ])("rejects inconsistent release metadata: %j", (options) => {
96
+ const result = run("2.0.0-alpha.1", options);
97
+ expect(result.status).not.toBe(0);
98
+ expect(result.stderr).toContain("must match");
99
+ expect(result.output).toBe("");
100
+ });
101
+
102
+ it.each([
103
+ undefined,
104
+ 2,
105
+ "",
106
+ "v2.0.0-alpha.1",
107
+ "2.0",
108
+ "02.0.0",
109
+ "2.0.0-",
110
+ "2.0.0-alpha.01",
111
+ "2.0.0-alpha..1",
112
+ "2.0.0-alpha_1",
113
+ "2.0.0\n",
114
+ "2.0.0-alpha.1\ndist_tag=latest",
115
+ ])("rejects invalid versions before emitting Actions outputs: %j", (version) => {
116
+ const result = run(version);
117
+ expect(result.status).not.toBe(0);
118
+ expect(result.stderr).toContain("must be a valid SemVer version");
119
+ expect(result.output).toBe("");
120
+ });
121
+ });
@@ -0,0 +1,329 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import { parseModelFile } from "../core/scaffold/model-parser";
5
+ import { generateOpenApiDocument } from "../core/scaffold/openapi-generator";
6
+ import { generateListPage, generateFormComponent } from "../core/scaffold/ui-generator";
7
+ import { generateLanguageSchemaArtifacts, getPythonRegexWarnings, toPythonRegex } from "../core/scaffold/native-schema-generator";
8
+ import { generateDetailPage } from "../core/scaffold/ui-generator";
9
+ import { spawnSync } from "child_process";
10
+ import { FileOperationSession } from "../core/operations/file-session";
11
+ import { buildUserScopedDataWarning } from "../cli/commands/scaffold";
12
+ import { inspectCapabilities } from "../core/project/capabilities";
13
+
14
+ const repoRoot = path.resolve(__dirname, "..", "..");
15
+
16
+ function model(name: string, fields: string, extra = ""): string {
17
+ const pascal = name.replace(/(^|-)([a-z])/g, (_, __, char: string) => char.toUpperCase());
18
+ return `import { z } from 'zod/v4';
19
+ export const ${pascal} = z.object({
20
+ id: z.string(),
21
+ ${fields}
22
+ });
23
+ export type ${pascal} = z.infer<typeof ${pascal}>;
24
+ ${extra}`;
25
+ }
26
+
27
+ describe("foreign-key inference", () => {
28
+ let root: string;
29
+ const write = (name: string, source: string) => {
30
+ const file = path.join(root, "shared", "models", `${name}.ts`);
31
+ fs.mkdirSync(path.dirname(file), { recursive: true });
32
+ fs.writeFileSync(file, source);
33
+ return file;
34
+ };
35
+
36
+ beforeEach(() => {
37
+ root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-fk-"));
38
+ });
39
+ afterEach(() => fs.rmSync(root, { recursive: true, force: true }));
40
+
41
+ it("does not treat userId as a foreign key when no User model exists", async () => {
42
+ const info = await parseModelFile(write("probe-item", model("probe-item", " userId: z.string(),\n title: z.string(),", "export const partitionKey = '/userId';\n")));
43
+ const userId = info.fields.find((field) => field.name === "userId")!;
44
+ expect(userId.isForeignKey).toBeUndefined();
45
+ expect(userId.referencedModel).toBeUndefined();
46
+ expect(info.parserWarnings).toEqual([]);
47
+
48
+ const list = generateListPage(info, "@sample/shared");
49
+ const form = generateFormComponent(info, "@sample/shared");
50
+ expect(list).not.toMatch(/api\/user|userMap/);
51
+ expect(form).not.toMatch(/api\/user|userOptions/);
52
+ const schema = JSON.parse(generateOpenApiDocument([info], info)).components.schemas;
53
+ expect(schema.ProbeItem.properties.userId).toEqual({ type: "string" });
54
+ expect(Object.keys(schema)).toEqual(["ProbeItem"]);
55
+ });
56
+
57
+ it("infers a foreign key when the referenced model exists", async () => {
58
+ write("category", model("category", " name: z.string(),"));
59
+ const info = await parseModelFile(write("todo", model("todo", " categoryId: z.string(),")));
60
+ expect(info.fields.find((field) => field.name === "categoryId")).toMatchObject({ isForeignKey: true, referencedModel: "Category" });
61
+ expect(generateListPage(info, "@sample/shared")).toContain("/api/category");
62
+ });
63
+
64
+ it("supports explicit references, opt-out, and warns about missing targets", async () => {
65
+ write("user", model("user", " name: z.string(),"));
66
+ const info = await parseModelFile(write("post", model(
67
+ "post",
68
+ " authorId: z.string(),\n userId: z.string(),\n reviewerId: z.string(),\n tagIds: z.array(z.string()),",
69
+ "export const references = { authorId: 'User', userId: false, reviewerId: 'Reviewer', tagIds: 'Tag', missingId: 'User' };\n"
70
+ )));
71
+ const field = (name: string) => info.fields.find((candidate) => candidate.name === name)!;
72
+ expect(field("authorId")).toMatchObject({ isForeignKey: true, referencedModel: "User" });
73
+ expect(field("userId").isForeignKey).toBeUndefined();
74
+ expect(field("reviewerId").isForeignKey).toBeUndefined();
75
+ expect(field("tagIds").isForeignKey).toBeUndefined();
76
+ expect(info.parserWarnings).toEqual([
77
+ "reference-model-not-found: Post.reviewerId references Reviewer, but shared/models has no Reviewer model; it is treated as a plain field.",
78
+ "reference-field-unsupported: Post.tagIds must be a single string id to reference Tag.",
79
+ "reference-field-not-found: Post.references declares missingId, but the schema has no such field.",
80
+ ]);
81
+ });
82
+
83
+ it("declares one lookup per referenced model when several fields reference it", async () => {
84
+ write("user", model("user", " name: z.string(),"));
85
+ const info = await parseModelFile(write("review", model(
86
+ "review",
87
+ " authorId: z.string(),\n reviewerId: z.string(),",
88
+ "export const references = { authorId: 'User', reviewerId: 'User' };\n"
89
+ )));
90
+ const count = (source: string, text: string) => source.split(text).length - 1;
91
+ for (const source of [generateListPage(info, "@sample/shared"), generateDetailPage(info, "@sample/shared")]) {
92
+ expect(count(source, "const [userMap, setUserMap]")).toBe(1);
93
+ expect(count(source, "fetch('/api/user')")).toBe(1);
94
+ expect(source).toContain("userMap[item.authorId]".replace("item", source.includes("ListPage") ? "item" : "review"));
95
+ }
96
+ const form = generateFormComponent(info, "@sample/shared");
97
+ expect(count(form, "const [userOptions, setUserOptions]")).toBe(1);
98
+ expect(count(form, "fetch('/api/user')")).toBe(1);
99
+ expect(form).toContain('id="authorId"');
100
+ expect(form).toContain('id="reviewerId"');
101
+ });
102
+
103
+ it("documents the references declaration in capabilities", () => {
104
+ expect(inspectCapabilities().modelDeclarations.map((declaration) => declaration.name)).toContain("references");
105
+ });
106
+ });
107
+
108
+ describe("dynamic schema evaluation metadata", () => {
109
+ let root: string;
110
+ beforeEach(() => {
111
+ root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-dynamic-"));
112
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "sample", type: "module" }));
113
+ fs.mkdirSync(path.join(root, "node_modules"), { recursive: true });
114
+ fs.symlinkSync(path.join(repoRoot, "node_modules", "zod"), path.join(root, "node_modules", "zod"), "junction");
115
+ });
116
+ afterEach(() => fs.rmSync(root, { recursive: true, force: true }));
117
+
118
+ it("extracts formats, integers, constraints, and inline objects from Zod v4 at runtime", async () => {
119
+ const file = path.join(root, "shared", "models", "probe-item.ts");
120
+ fs.mkdirSync(path.dirname(file), { recursive: true });
121
+ // startsWith() is outside the static subset, which forces runtime evaluation.
122
+ fs.writeFileSync(file, model("probe-item", [
123
+ " userId: z.string().startsWith('u'),",
124
+ " title: z.string().min(1).max(5),",
125
+ " code: z.string().regex(/^a\\d$/i),",
126
+ " tags: z.array(z.string().min(2)).min(1),",
127
+ " score: z.number().gt(0).lte(1),",
128
+ " edge: z.number().gt(0).min(0).lt(1).max(1),",
129
+ " count: z.int(),",
130
+ " publishedAt: z.iso.datetime().optional(),",
131
+ " createdAt: z.string().optional(),",
132
+ " nested: z.object({ x: z.string() }).optional(),",
133
+ ].join("\n")));
134
+
135
+ const info = await parseModelFile(file);
136
+ const field = (name: string) => info.fields.find((candidate) => candidate.name === name)!;
137
+ expect(info.parserMode).toBe("dynamic");
138
+ expect(field("userId").isForeignKey).toBeUndefined();
139
+ expect(field("title").constraints).toEqual({ minLength: 1, maxLength: 5 });
140
+ expect(field("code").constraints).toEqual({ pattern: "^a\\d$", patternFlags: "i" });
141
+ expect(field("tags")).toMatchObject({ isArray: true, constraints: { minLength: 2, minItems: 1 } });
142
+ expect(field("score").constraints).toEqual({ minimum: 0, exclusiveMinimum: true, maximum: 1 });
143
+ expect(field("edge").constraints).toEqual({ minimum: 0, exclusiveMinimum: true, maximum: 1, exclusiveMaximum: true });
144
+ expect(field("count").isInteger).toBe(true);
145
+ expect(field("publishedAt")).toMatchObject({ format: "date-time", isOptional: true });
146
+ expect(field("createdAt").format).toBeUndefined();
147
+ expect(field("nested")).toMatchObject({ type: "object", isOptional: true, objectFields: [expect.objectContaining({ name: "x", type: "string" })] });
148
+ });
149
+
150
+ it("extracts the same metadata from Zod v3 schemas", async () => {
151
+ const file = path.join(root, "shared", "models", "legacy-item.ts");
152
+ fs.mkdirSync(path.dirname(file), { recursive: true });
153
+ fs.writeFileSync(file, model("legacy-item", [
154
+ " title: z.string().startsWith('t').min(2),",
155
+ " score: z.number().int().positive(),",
156
+ " publishedAt: z.string().datetime().optional(),",
157
+ " kind: z.enum(['a', 'b']),",
158
+ " nested: z.object({ x: z.string().max(3) }),",
159
+ ].join("\n")).replace("from 'zod/v4'", "from 'zod'"));
160
+
161
+ const info = await parseModelFile(file);
162
+ const field = (name: string) => info.fields.find((candidate) => candidate.name === name)!;
163
+ expect(info.parserMode).toBe("dynamic");
164
+ expect(field("title").constraints).toEqual({ minLength: 2 });
165
+ expect(field("score")).toMatchObject({ isInteger: true, constraints: { minimum: 0, exclusiveMinimum: true } });
166
+ expect(field("publishedAt").format).toBe("date-time");
167
+ expect(field("kind").enumValues).toEqual(["a", "b"]);
168
+ expect(field("nested").objectFields).toEqual([expect.objectContaining({ name: "x", constraints: { maxLength: 3 } })]);
169
+ });
170
+ });
171
+
172
+ describe("Python schema assets", () => {
173
+ let root: string;
174
+ const originalCwd = process.cwd();
175
+ beforeEach(() => {
176
+ root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-python-schema-"));
177
+ });
178
+ afterEach(() => {
179
+ process.chdir(originalCwd);
180
+ fs.rmSync(root, { recursive: true, force: true });
181
+ });
182
+
183
+ it("keeps declared types and constraints instead of guessing from names", async () => {
184
+ const file = path.join(root, "shared", "models", "probe-item.ts");
185
+ fs.mkdirSync(path.dirname(file), { recursive: true });
186
+ fs.writeFileSync(file, model("probe-item", [
187
+ " title: z.string().min(1),",
188
+ " code: z.string().regex(/^a\\d$/i).optional(),",
189
+ " tags: z.array(z.string().min(2)).max(3),",
190
+ " score: z.number().min(0).max(1),",
191
+ " count: z.number().int(),",
192
+ " publishedAt: z.iso.datetime().optional(),",
193
+ " createdAt: z.string().optional(),",
194
+ " nested: z.object({ x: z.string() }).optional(),",
195
+ ].join("\n")));
196
+ const info = await parseModelFile(file);
197
+ process.chdir(root);
198
+ await generateLanguageSchemaArtifacts([info], info, "functions", "python", new FileOperationSession("commit", root));
199
+ const modelsDir = path.join(root, "functions", "generated", "python-models", "backend_models", "models");
200
+ const source = fs.readFileSync(path.join(modelsDir, "probe_item.py"), "utf8");
201
+
202
+ expect(source).toContain("created_at: Optional[StrictStr] = Field(default=None, alias=\"createdAt\")");
203
+ expect(source).toContain("published_at: Optional[datetime] = Field(default=None, alias=\"publishedAt\")");
204
+ expect(source).toContain("title: StrictStr = Field(min_length=1)");
205
+ expect(source).toContain("tags: List[Annotated[StrictStr, Field(min_length=2)]] = Field(max_length=3)");
206
+ expect(source).toContain("score: Union[StrictFloat, StrictInt] = Field(ge=0, le=1)");
207
+ expect(source).toContain("count: StrictInt");
208
+ expect(source).toContain("nested: Optional[ProbeItemNested] = Field(default=None)");
209
+ expect(source).toContain('re.search("^a\\\\d$", item, re.IGNORECASE)');
210
+ expect(source).toContain("return self.model_dump_json(by_alias=True, exclude_none=True)");
211
+ expect(fs.readFileSync(path.join(modelsDir, "probe_item_nested.py"), "utf8")).toContain("class ProbeItemNested(BaseModel):");
212
+ });
213
+ });
214
+
215
+ describe("Python regex translation", () => {
216
+ it("translates JavaScript named groups and back-references", () => {
217
+ expect(toPythonRegex("^(?<code>[A-Z]+)-\\k<code>$")).toEqual({ pattern: "^(?P<code>[A-Z]+)-(?P=code)$" });
218
+ expect(toPythonRegex("^a\\d$", "i")).toEqual({ pattern: "^a\\d$" });
219
+ });
220
+
221
+ it.each([
222
+ ["\\p{L}+", "", /Unicode property/],
223
+ ["(?<=a)b", "", /lookbehind/],
224
+ ["[^]", "", /empty character classes/],
225
+ ["\\u{1F600}", "u", /flag\(s\) u|code point/],
226
+ ["abc", "y", /flag\(s\) y/],
227
+ ])("reports %s /%s as not enforceable", (pattern, flags, reason) => {
228
+ const result = toPythonRegex(pattern, flags);
229
+ expect("unsupported" in result && result.unsupported).toMatch(reason);
230
+ });
231
+
232
+ it("skips unenforceable patterns with a warning and validates raw strings before conversion", async () => {
233
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-python-regex-"));
234
+ const originalCwd = process.cwd();
235
+ try {
236
+ const file = path.join(root, "shared", "models", "regex-item.ts");
237
+ fs.mkdirSync(path.dirname(file), { recursive: true });
238
+ fs.writeFileSync(file, model("regex-item", [
239
+ " code: z.string().regex(/^(?<code>[A-Z]+)$/),",
240
+ " letters: z.string().regex(/^\\p{L}+$/u),",
241
+ " when: z.string().datetime().regex(/Z$/),",
242
+ ].join("\n")));
243
+ const info = await parseModelFile(file);
244
+ expect(getPythonRegexWarnings(info)).toEqual([
245
+ expect.stringMatching(/^python-regex-not-enforced: RegexItem\.letters uses Unicode property escapes/),
246
+ ]);
247
+ process.chdir(root);
248
+ const session = new FileOperationSession("commit", root);
249
+ await generateLanguageSchemaArtifacts([info], info, "functions", "python", session);
250
+ expect(session.warnings).toEqual(getPythonRegexWarnings(info));
251
+ const source = fs.readFileSync(path.join(root, "functions", "generated", "python-models", "backend_models", "models", "regex_item.py"), "utf8");
252
+ expect(source).toContain('re.search("^(?P<code>[A-Z]+)$", item, 0)');
253
+ expect(source).not.toContain("letters_validate_pattern");
254
+ expect(source).toContain("@field_validator('when', mode='before')");
255
+ expect(source).toContain("if isinstance(item, str) and not re.search");
256
+ } finally {
257
+ process.chdir(originalCwd);
258
+ fs.rmSync(root, { recursive: true, force: true });
259
+ }
260
+ });
261
+ });
262
+
263
+ const pydanticPython = ["python", "python3"].find((command) => spawnSync(command, ["-c", "import pydantic"], { stdio: "ignore" }).status === 0);
264
+ const itWithPydantic = pydanticPython ? it : it.skip;
265
+
266
+ describe("generated Python model runtime", () => {
267
+ itWithPydantic("accepts valid regex and timestamp values and rejects invalid ones", async () => {
268
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-python-runtime-"));
269
+ const originalCwd = process.cwd();
270
+ try {
271
+ const file = path.join(root, "shared", "models", "regex-item.ts");
272
+ fs.mkdirSync(path.dirname(file), { recursive: true });
273
+ fs.writeFileSync(file, model("regex-item", [
274
+ " code: z.string().regex(/^(?<code>[A-Z]+)$/),",
275
+ " when: z.iso.datetime().regex(/Z$/),",
276
+ " tags: z.array(z.string().regex(/^t/)),",
277
+ ].join("\n")));
278
+ const info = await parseModelFile(file);
279
+ process.chdir(root);
280
+ await generateLanguageSchemaArtifacts([info], info, "functions", "python", new FileOperationSession("commit", root));
281
+ process.chdir(originalCwd);
282
+ const script = [
283
+ "import sys",
284
+ "sys.path.insert(0, 'generated/python-models')",
285
+ "from backend_models.models.regex_item import RegexItem",
286
+ "ok = {'id': '1', 'code': 'ABC', 'when': '2024-05-06T07:08:09Z', 'tags': ['t1']}",
287
+ "RegexItem.from_dict(ok)",
288
+ "for bad in ({**ok, 'code': 'abc'}, {**ok, 'when': '2024-05-06T07:08:09+09:00'}, {**ok, 'tags': ['x']}):",
289
+ " try:",
290
+ " RegexItem.from_dict(bad)",
291
+ " except ValueError:",
292
+ " continue",
293
+ " raise SystemExit('accepted ' + repr(bad))",
294
+ "print('ok')",
295
+ ].join("\n");
296
+ const run = spawnSync(pydanticPython!, ["-c", script], { cwd: path.join(root, "functions"), encoding: "utf8" });
297
+ if (run.status !== 0) throw new Error(`${run.stdout}\n${run.stderr}`);
298
+ expect(run.stdout.trim()).toBe("ok");
299
+ } finally {
300
+ process.chdir(originalCwd);
301
+ fs.rmSync(root, { recursive: true, force: true });
302
+ }
303
+ }, 60000);
304
+ });
305
+
306
+ describe("user-scoped data warning", () => {
307
+ const info = (partitionKey: string, fields: string[]) => ({
308
+ name: "ProbeItem",
309
+ partitionKey,
310
+ fields: fields.map((name) => ({ name, type: "string", isOptional: false, isArray: false })),
311
+ }) as unknown as Parameters<typeof buildUserScopedDataWarning>[0];
312
+
313
+ it("warns when the partition key is a per-user id and auth is not configured", () => {
314
+ expect(buildUserScopedDataWarning(info("/userId", ["id", "userId"]), undefined))
315
+ .toMatch(/^user-scoped-data-without-auth: ProbeItem\.userId is the partition key and looks like a per-user owner id/);
316
+ expect(buildUserScopedDataWarning(info("/userId", ["id", "userId"]), { provider: "none" })).toBeDefined();
317
+ });
318
+
319
+ it("warns for owner-like fields that are not the partition key", () => {
320
+ expect(buildUserScopedDataWarning(info("/id", ["id", "learnerId"]), undefined))
321
+ .toMatch(/ProbeItem\.learnerId looks like a per-user owner id/);
322
+ });
323
+
324
+ it("stays quiet when authentication is configured or nothing looks user-scoped", () => {
325
+ expect(buildUserScopedDataWarning(info("/userId", ["id", "userId"]), { provider: "swa" })).toBeUndefined();
326
+ expect(buildUserScopedDataWarning(info("/userId", ["id", "userId"]), { schemes: { admin: { provider: "swa" } } } as never)).toBeUndefined();
327
+ expect(buildUserScopedDataWarning(info("/tenantId", ["id", "tenantId", "categoryId"]), undefined)).toBeUndefined();
328
+ });
329
+ });