@midhunxavier/code-flow 0.0.0-stage → 0.1.0-preview.1

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 (284) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +122 -2
  3. package/bin/codeflow.mjs +8 -0
  4. package/bin/launcher.mjs +127 -0
  5. package/dist/remote-host.json +1 -0
  6. package/licenses/inter.txt +92 -0
  7. package/licenses/preact.txt +21 -0
  8. package/licenses/signals-core.txt +21 -0
  9. package/licenses/signals.txt +21 -0
  10. package/licenses/typescript-notices.txt +193 -0
  11. package/licenses/typescript.txt +55 -0
  12. package/licenses/uqr.txt +22 -0
  13. package/licenses/xterm-fit.txt +19 -0
  14. package/licenses/xterm.txt +21 -0
  15. package/package.json +34 -4
  16. package/public/assets/browser-7XI52A6V.js +8 -0
  17. package/public/assets/changes-EYFPASEI.js +1 -0
  18. package/public/assets/chunk-7H3ZLTKA.js +2 -0
  19. package/public/assets/chunk-L7TUCIYF.js +1 -0
  20. package/public/assets/chunk-LLMQG23J.js +163 -0
  21. package/public/assets/main-KIRM2OEK.js +1 -0
  22. package/public/assets/settings-QJZVW22Y.js +9 -0
  23. package/public/assets/styles-615EBB20.css +32 -0
  24. package/public/assets/terminal-A2FI2NAX.js +62 -0
  25. package/public/assets.json +1 -0
  26. package/public/bundle-meta.json +1 -0
  27. package/public/docs/.nojekyll +0 -0
  28. package/public/docs/accessibility.html +19 -0
  29. package/public/docs/acp-agents.html +117 -0
  30. package/public/docs/agent-containers.html +79 -0
  31. package/public/docs/api.html +1130 -0
  32. package/public/docs/app.js +342 -0
  33. package/public/docs/application-improvements.html +275 -0
  34. package/public/docs/architecture-explorer.html +19 -0
  35. package/public/docs/architecture.html +149 -0
  36. package/public/docs/auto-team.html +142 -0
  37. package/public/docs/automations.html +109 -0
  38. package/public/docs/checkpoint-review.html +37 -0
  39. package/public/docs/code-hosts.html +35 -0
  40. package/public/docs/code-intelligence.html +35 -0
  41. package/public/docs/configuration.html +346 -0
  42. package/public/docs/context-inspection.html +23 -0
  43. package/public/docs/coordinated-changes.html +28 -0
  44. package/public/docs/deferred-runs.html +25 -0
  45. package/public/docs/dependency-discovery.html +19 -0
  46. package/public/docs/documentation.html +84 -0
  47. package/public/docs/dynamic-teams-evaluation.html +174 -0
  48. package/public/docs/dynamic-teams-implementation-plan.html +882 -0
  49. package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
  50. package/public/docs/dynamic-teams-plan-review-0.html +204 -0
  51. package/public/docs/extensions.html +42 -0
  52. package/public/docs/external-sources.html +96 -0
  53. package/public/docs/filesystem-policy.html +26 -0
  54. package/public/docs/filtered-tasks.html +44 -0
  55. package/public/docs/font-license.txt +92 -0
  56. package/public/docs/getting-started.html +118 -0
  57. package/public/docs/github-review-threads.html +26 -0
  58. package/public/docs/github.html +157 -0
  59. package/public/docs/help.html +69 -0
  60. package/public/docs/index.html +77 -0
  61. package/public/docs/installation.html +74 -0
  62. package/public/docs/launch-profiles.html +48 -0
  63. package/public/docs/maintenance.html +60 -0
  64. package/public/docs/memory-claims.html +31 -0
  65. package/public/docs/memory-storage.html +58 -0
  66. package/public/docs/notifications.html +39 -0
  67. package/public/docs/patterns.html +195 -0
  68. package/public/docs/performance-results.html +210 -0
  69. package/public/docs/performance.html +166 -0
  70. package/public/docs/project-conventions.html +22 -0
  71. package/public/docs/project-first-navigation-plan.html +587 -0
  72. package/public/docs/project-teams-implementation-plan.html +772 -0
  73. package/public/docs/project-teams.html +99 -0
  74. package/public/docs/provider-compatibility.html +20 -0
  75. package/public/docs/releasing.html +61 -0
  76. package/public/docs/remote-workers.html +64 -0
  77. package/public/docs/review-inbox.html +26 -0
  78. package/public/docs/run-storage.html +23 -0
  79. package/public/docs/runs.html +141 -0
  80. package/public/docs/search.json +1 -0
  81. package/public/docs/security.html +133 -0
  82. package/public/docs/shared-projects.html +31 -0
  83. package/public/docs/ssh-projects.html +60 -0
  84. package/public/docs/style.css +768 -0
  85. package/public/docs/task-reruns.html +30 -0
  86. package/public/docs/team-collaboration.html +379 -0
  87. package/public/docs/verification.html +22 -0
  88. package/public/docs/vscode.html +53 -0
  89. package/public/docs/worker-permission-verification.html +122 -0
  90. package/public/index.html +17 -0
  91. package/scripts/check-node.mjs +7 -0
  92. package/scripts/codeflow.mjs +55 -0
  93. package/scripts/pty.py +273 -0
  94. package/src/acp-connections.js +110 -0
  95. package/src/adapters/acp-presets.js +37 -0
  96. package/src/adapters/acp-types.js +0 -0
  97. package/src/adapters/acp.js +328 -0
  98. package/src/adapters/agy-auth.js +67 -0
  99. package/src/adapters/agy-isolation.js +223 -0
  100. package/src/adapters/agy.js +297 -0
  101. package/src/adapters/claude.js +220 -0
  102. package/src/adapters/codex-app-server.js +518 -0
  103. package/src/adapters/codex.js +299 -0
  104. package/src/adapters/errors.js +57 -0
  105. package/src/adapters/index.js +91 -0
  106. package/src/adapters/managed-permissions.js +37 -0
  107. package/src/adapters/model-discovery.js +100 -0
  108. package/src/adapters/process.js +418 -0
  109. package/src/adapters/team-tools.js +102 -0
  110. package/src/adapters/types.js +0 -0
  111. package/src/agent-container.js +155 -0
  112. package/src/approvals.js +121 -0
  113. package/src/architecture.js +456 -0
  114. package/src/assets.js +44 -0
  115. package/src/automation.js +901 -0
  116. package/src/certificate-renewal.js +75 -0
  117. package/src/chat-handoff.js +55 -0
  118. package/src/checkpoint-review.js +72 -0
  119. package/src/checks.js +303 -0
  120. package/src/code-host-git.js +102 -0
  121. package/src/code-hosts-api.js +99 -0
  122. package/src/code-hosts.js +482 -0
  123. package/src/code-intelligence-worker.mjs +452 -0
  124. package/src/code-intelligence.js +141 -0
  125. package/src/collab/api.js +365 -0
  126. package/src/collab/checkpoints.js +198 -0
  127. package/src/collab/connections.js +139 -0
  128. package/src/collab/host-execution.js +347 -0
  129. package/src/collab/hub-client.js +210 -0
  130. package/src/collab/link-api.js +229 -0
  131. package/src/collab/link.js +595 -0
  132. package/src/collab/model.js +496 -0
  133. package/src/collab/runner.js +1014 -0
  134. package/src/collab/store.js +1452 -0
  135. package/src/collab/terminals.js +361 -0
  136. package/src/collaboration-reviews.js +116 -0
  137. package/src/config.js +48 -0
  138. package/src/context-budget.js +14 -0
  139. package/src/context-inspection.js +204 -0
  140. package/src/context.js +154 -0
  141. package/src/conventions.js +283 -0
  142. package/src/coordinated-api.js +247 -0
  143. package/src/deferred-launch.js +82 -0
  144. package/src/deferred-runs.js +515 -0
  145. package/src/dependency-health.js +35 -0
  146. package/src/diff.js +43 -0
  147. package/src/docs-help.js +316 -0
  148. package/src/doctor.js +232 -0
  149. package/src/engine.js +4366 -0
  150. package/src/env.js +44 -0
  151. package/src/experience.js +50 -0
  152. package/src/extensions.js +144 -0
  153. package/src/filesystem-access.js +107 -0
  154. package/src/filesystem-policy.js +238 -0
  155. package/src/filtered-task-limits.js +93 -0
  156. package/src/filtered-tasks-api.js +53 -0
  157. package/src/filtered-tasks.js +576 -0
  158. package/src/fs.js +57 -0
  159. package/src/git.js +626 -0
  160. package/src/github-review-threads.js +181 -0
  161. package/src/github.js +538 -0
  162. package/src/http.js +69 -0
  163. package/src/improve-settings.js +38 -0
  164. package/src/improve.js +187 -0
  165. package/src/integrations.js +373 -0
  166. package/src/integrity.js +259 -0
  167. package/src/journey-api.js +77 -0
  168. package/src/journey-replay.js +65 -0
  169. package/src/journey-store.js +97 -0
  170. package/src/journeys.js +60 -0
  171. package/src/json.js +81 -0
  172. package/src/launch-profiles.js +292 -0
  173. package/src/learnings.js +80 -0
  174. package/src/log.js +39 -0
  175. package/src/maintenance.js +335 -0
  176. package/src/memory-analysis.js +182 -0
  177. package/src/memory-claims.js +153 -0
  178. package/src/memory-sqlite.js +236 -0
  179. package/src/memory.js +552 -0
  180. package/src/merge-validation.js +58 -0
  181. package/src/model-catalog.js +238 -0
  182. package/src/model-research.js +91 -0
  183. package/src/notes.js +176 -0
  184. package/src/notifications.js +413 -0
  185. package/src/patterns/auto-team-model.js +212 -0
  186. package/src/patterns/auto-team.js +659 -0
  187. package/src/patterns/council.js +199 -0
  188. package/src/patterns/debate.js +188 -0
  189. package/src/patterns/findings.js +141 -0
  190. package/src/patterns/index.js +19 -0
  191. package/src/patterns/loop.js +158 -0
  192. package/src/patterns/pipeline.js +125 -0
  193. package/src/patterns/prompts.js +181 -0
  194. package/src/patterns/race.js +147 -0
  195. package/src/patterns/schemas.js +40 -0
  196. package/src/patterns/solo.js +49 -0
  197. package/src/patterns/supervisor.js +235 -0
  198. package/src/patterns/task-draft.js +29 -0
  199. package/src/patterns/tdd.js +231 -0
  200. package/src/patterns/team-strategies.js +67 -0
  201. package/src/performance.js +43 -0
  202. package/src/persistence-status.js +35 -0
  203. package/src/phone-sessions.js +96 -0
  204. package/src/preview-overlay.js +383 -0
  205. package/src/preview.js +219 -0
  206. package/src/privacy.js +23 -0
  207. package/src/project-report.js +22 -0
  208. package/src/project-team-model.js +178 -0
  209. package/src/project-team-policy.js +42 -0
  210. package/src/project-team-store.js +213 -0
  211. package/src/project-teams-api.js +36 -0
  212. package/src/project-teams.js +551 -0
  213. package/src/projects.js +286 -0
  214. package/src/provider-eligibility.js +9 -0
  215. package/src/readiness.js +98 -0
  216. package/src/remote-tls.js +66 -0
  217. package/src/remote-workers/bundles.js +198 -0
  218. package/src/remote-workers/client.js +332 -0
  219. package/src/remote-workers/durability.js +37 -0
  220. package/src/remote-workers/protocol.js +68 -0
  221. package/src/remote-workers/stdio.js +138 -0
  222. package/src/remote-workers/worker.js +304 -0
  223. package/src/remote.js +204 -0
  224. package/src/repo-config.js +201 -0
  225. package/src/repo-coordination.js +732 -0
  226. package/src/repo-tree.js +106 -0
  227. package/src/retention-policy.js +466 -0
  228. package/src/review-inbox.js +38 -0
  229. package/src/routing.js +49 -0
  230. package/src/run-analysis.js +111 -0
  231. package/src/run-cold-store.js +231 -0
  232. package/src/run-environment.js +15 -0
  233. package/src/run-history.js +91 -0
  234. package/src/run-log.js +160 -0
  235. package/src/run-permissions.js +110 -0
  236. package/src/run-projection.js +75 -0
  237. package/src/run-recovery.js +53 -0
  238. package/src/run-status.js +54 -0
  239. package/src/serve.js +138 -0
  240. package/src/server.js +2738 -0
  241. package/src/shared-access.js +103 -0
  242. package/src/shared-accounts.js +301 -0
  243. package/src/shared-api.js +239 -0
  244. package/src/shell.js +346 -0
  245. package/src/ship.js +344 -0
  246. package/src/slots.js +74 -0
  247. package/src/snapshot.js +388 -0
  248. package/src/source-context.js +17 -0
  249. package/src/sse-writer.js +70 -0
  250. package/src/ssh-projects/bootstrap.mjs +95 -0
  251. package/src/ssh-projects/config.js +45 -0
  252. package/src/ssh-projects/gateway.js +143 -0
  253. package/src/ssh-projects/manager.js +320 -0
  254. package/src/ssh-projects/team-autostart.mjs +93 -0
  255. package/src/ssh-projects/team-remove.mjs +69 -0
  256. package/src/ssh-projects/team-status.mjs +39 -0
  257. package/src/ssh-projects/transport.js +116 -0
  258. package/src/stats.js +139 -0
  259. package/src/store.js +544 -0
  260. package/src/stream.js +91 -0
  261. package/src/sweeps.js +241 -0
  262. package/src/tailscale.js +101 -0
  263. package/src/task-draft.js +23 -0
  264. package/src/team-evaluation.js +242 -0
  265. package/src/team-mailbox.js +160 -0
  266. package/src/team-memory.js +84 -0
  267. package/src/team-rerun-graph.js +15 -0
  268. package/src/team-reruns.js +130 -0
  269. package/src/team-scheduler.js +121 -0
  270. package/src/team-template-store.js +98 -0
  271. package/src/team-templates.js +117 -0
  272. package/src/team-tool-client.mjs +79 -0
  273. package/src/team-tools.js +176 -0
  274. package/src/teams.js +0 -0
  275. package/src/templates.js +62 -0
  276. package/src/transcript.js +134 -0
  277. package/src/verification-integrity.js +27 -0
  278. package/src/verification.js +183 -0
  279. package/src/watch.js +195 -0
  280. package/src/work-queue.js +47 -0
  281. package/src/workflow-control.js +83 -0
  282. package/src/workspace-api.js +207 -0
  283. package/src/workspace-changes.js +61 -0
  284. package/src/workspace.js +146 -0
@@ -0,0 +1,259 @@
1
+ import { git, gitError } from "./git.js";
2
+ const TEST_FILE = /(^|\/)(tests?|__tests__|specs?|e2e)\/|[._-](test|spec)s?\.[a-z0-9]+$|(^|\/)test_[^/]+\.py$|Tests?\.(java|kt|cs|swift|scala)$|_spec\.rb$/i;
3
+ const WORKFLOW = /(^|\/)\.github\/workflows\//;
4
+ const TEST_CONFIG = /(^|\/)(jest|vitest|vite|playwright|cypress|karma|ava|babel)\.config\.[cm]?[jt]s$|(^|\/)\.mocharc(\.[a-z]+)?$|(^|\/)(pytest\.ini|tox\.ini|setup\.cfg|conftest\.py|\.coveragerc|phpunit\.xml(\.dist)?)$|(^|\/)\.github\/workflows\//;
5
+ const SKIP = /\b(?:describe|it|test|context|suite|bench)(?:\.(?:concurrent|sequential|each))*\.(?:skip|only|todo|skipIf|runIf|fails|failing)\b|\b(?:xit|xdescribe|xtest|fit|fdescribe)\s*\(|\bthis\.skip\s*\(|@pytest\.mark\.(?:skip|skipif|xfail)\b|\bpytest\.(?:skip|xfail|importorskip)\s*\(|\bself\.skipTest\s*\(|@unittest\.(?:skip\w*|expectedFailure)\b|@(?:Disabled|Ignore)\b|\bt\.(?:Skip(?:Now|f)?|skip|todo)\s*\(|#\[ignore\]|\{\s*(?:skip|todo)\s*:\s*(?:true\b|["'`])/;
6
+ const ASSERTION = /\b(assert\w*|expect|should|t\.(?:Error|Fatal|Fail)\w*|require\.\w+|deepEqual|strictEqual|notEqual|toBe\w*|toEqual|toMatch\w*|toThrow\w*|toHave\w*|toContain\w*)\b/;
7
+ const isTestFile = (path) => TEST_FILE.test(path);
8
+ const SOURCE = /\.(?:[cm]?[jt]sx?|py|go|rs|java|kt|kts|scala|rb|cs|swift|php|c|cc|cpp|h|hpp|m|mm|ex|exs|dart|lua|vue|svelte)$/i;
9
+ const FIXTURE = /(^|\/)(__snapshots__|__fixtures__|fixtures?|testdata|test-data|golden|snapshots?)\/|\.snap$/i;
10
+ const isPlainSource = (path) => SOURCE.test(path) && !TEST_FILE.test(path) && !TEST_CONFIG.test(path) && !FIXTURE.test(path);
11
+ const RUNNER_CONFIG = /(^|\/)(jest|vitest|playwright|cypress|karma|ava)\.config\.[cm]?[jt]s$|(^|\/)\.mocharc(\.[a-z]+)?$|(^|\/)(pytest\.ini|conftest\.py)$/;
12
+ const isTestSide = (path) => TEST_FILE.test(path) || FIXTURE.test(path) || RUNNER_CONFIG.test(path);
13
+ const isRunnerConfig = (path) => RUNNER_CONFIG.test(path);
14
+ const SHARED_CONFIG = /(^|\/)(package\.json|pyproject\.toml)$/;
15
+ const changesRunner = (i) => i.kind === "test-script" || i.kind === "test-config" && (RUNNER_CONFIG.test(i.file) || SHARED_CONFIG.test(i.file));
16
+ function unverifiedIssue(reason) {
17
+ return { kind: "unverified", file: "", detail: `couldn't verify test integrity: ${reason.split("\n")[0].slice(0, 300)}`, severity: "high" };
18
+ }
19
+ function scriptsRun(check) {
20
+ const names = /* @__PURE__ */ new Set(["test"]);
21
+ for (const m of (check ?? "").matchAll(/\b(?:npm|pnpm|yarn|bun)(?:\s+run)?\s+([\w:.-]+)/g)) names.add(m[1]);
22
+ return [...names];
23
+ }
24
+ function parseJson(raw) {
25
+ try {
26
+ return raw ? JSON.parse(raw) : void 0;
27
+ } catch {
28
+ return void 0;
29
+ }
30
+ }
31
+ const RUNNER_KEYS = ["jest", "vitest", "mocha", "ava"];
32
+ function pytestSettings(toml) {
33
+ if (toml === void 0) return void 0;
34
+ const lines = [];
35
+ let inside = false;
36
+ for (const raw of toml.split("\n")) {
37
+ const line = raw.trim();
38
+ const header = /^\[\[?([^\]]+)\]\]?(?:\s*#.*)?$/.exec(line);
39
+ if (header) inside = /^tool\.pytest(\.|$)/.test(header[1].replace(/[\s"']/g, ""));
40
+ if (inside && line && !line.startsWith("#")) lines.push(line);
41
+ }
42
+ return lines.length ? lines.join("\n") : void 0;
43
+ }
44
+ async function fileAt(cwd, ref, path) {
45
+ const res = await git(["show", "--no-textconv", `${ref}:${path}`], cwd);
46
+ return res.code === 0 ? res.stdout : void 0;
47
+ }
48
+ async function inspectChanges(cwd, from, to = "HEAD", opts = {}) {
49
+ const issues = [];
50
+ const range = `${from}..${to}`;
51
+ const statusArgs = ["diff", "--no-ext-diff", "--no-textconv", "--name-status", "-M", "-z", range];
52
+ const status = await git(statusArgs, cwd);
53
+ if (status.code !== 0) return [unverifiedIssue(gitError(statusArgs, status))];
54
+ const tokens = status.stdout.split("\0").filter(Boolean);
55
+ const changes = [];
56
+ for (let i = 0; i < tokens.length; i++) {
57
+ const code = tokens[i][0];
58
+ if (code === "R" || code === "C") {
59
+ const [from2, to2] = [tokens[i + 1], tokens[i + 2]];
60
+ if (code === "R" && isTestFile(from2) && !isTestFile(to2)) {
61
+ issues.push({ kind: "deleted-test", file: from2, detail: `test file renamed to \`${to2}\`, so it no longer runs as a test`, severity: "high" });
62
+ }
63
+ changes.push({ code: "R", path: to2 });
64
+ i += 2;
65
+ } else {
66
+ changes.push({ code, path: tokens[i + 1] });
67
+ i += 1;
68
+ }
69
+ }
70
+ for (const { code, path } of changes) {
71
+ if (code === "D" && isTestFile(path)) issues.push({ kind: "deleted-test", file: path, detail: "test file deleted", severity: "high" });
72
+ else if (TEST_CONFIG.test(path)) {
73
+ const what = WORKFLOW.test(path) ? "CI workflow" : "test configuration";
74
+ issues.push({ kind: "test-config", file: path, detail: code === "D" ? `${what} deleted` : `${what} changed`, severity: code === "D" ? "high" : "medium" });
75
+ }
76
+ }
77
+ const added = new Set(changes.filter((c) => c.code === "A").map((c) => c.path));
78
+ const patchArgs = ["diff", "--no-ext-diff", "--no-textconv", "-U0", "--no-color", "-M", range];
79
+ const patch = await git(patchArgs, cwd);
80
+ if (patch.code !== 0) return [...issues, unverifiedIssue(gitError(patchArgs, patch))];
81
+ let file = "";
82
+ let header = false;
83
+ let lineNo = 0;
84
+ const skips = [];
85
+ const counts = /* @__PURE__ */ new Map();
86
+ for (const line of patch.stdout.split("\n")) {
87
+ if (line.startsWith("diff --git ")) {
88
+ header = true;
89
+ continue;
90
+ }
91
+ if (line.startsWith("@@")) {
92
+ header = false;
93
+ lineNo = Number(/\+(\d+)/.exec(line)?.[1] ?? 0);
94
+ continue;
95
+ }
96
+ if (header) {
97
+ if (line.startsWith("+++ ")) file = line.replace(/^\+\+\+ (b\/)?/, "");
98
+ continue;
99
+ }
100
+ if (line.startsWith("+")) {
101
+ const text = line.slice(1);
102
+ if (SKIP.test(text)) skips.push({ file, line: lineNo, text });
103
+ if (isTestFile(file) && ASSERTION.test(text)) bump(counts, file, "added");
104
+ lineNo++;
105
+ } else if (line.startsWith("-") && isTestFile(file) && ASSERTION.test(line.slice(1))) {
106
+ bump(counts, file, "removed");
107
+ }
108
+ }
109
+ for (const { file: file2, text } of await codeSkips(cwd, to, skips)) {
110
+ issues.push({ kind: "skipped-test", file: file2, detail: `adds \`${text.trim().slice(0, 120)}\``, severity: added.has(file2) ? "medium" : "high" });
111
+ }
112
+ for (const [path, c] of counts) {
113
+ if (!added.has(path) && c.removed > c.added) {
114
+ issues.push({ kind: "weakened-test", file: path, detail: `${c.removed} assertion line(s) removed, ${c.added} added`, severity: "medium" });
115
+ }
116
+ }
117
+ for (const { path } of changes) {
118
+ if (/(^|\/)package\.json$/.test(path)) {
119
+ const [before2, after2] = await Promise.all([fileAt(cwd, from, path).then(parseJson), fileAt(cwd, to, path).then(parseJson)]);
120
+ for (const name of path === "package.json" ? scriptsRun(opts.check) : []) {
121
+ const was = before2?.scripts?.[name];
122
+ const now = after2?.scripts?.[name];
123
+ if (was !== void 0 && was !== now) {
124
+ issues.push({ kind: "test-script", file: path, detail: `scripts.${name} changed from \`${was}\` to \`${now ?? "(removed)"}\``, severity: "high" });
125
+ }
126
+ }
127
+ for (const key of RUNNER_KEYS) {
128
+ const [was, now] = [before2?.[key], after2?.[key]].map((v) => v === void 0 ? void 0 : JSON.stringify(v));
129
+ if (was === now) continue;
130
+ const detail = was === void 0 ? `adds \`${key}\` settings: ${now.slice(0, 160)}` : now === void 0 ? `\`${key}\` settings removed` : `\`${key}\` settings changed to ${now.slice(0, 160)}`;
131
+ issues.push({ kind: "test-config", file: path, detail, severity: now === void 0 ? "high" : "medium" });
132
+ }
133
+ } else if (/(^|\/)pyproject\.toml$/.test(path)) {
134
+ const [was, now] = await Promise.all([fileAt(cwd, from, path).then(pytestSettings), fileAt(cwd, to, path).then(pytestSettings)]);
135
+ if (was === now) continue;
136
+ const detail = was === void 0 ? "adds pytest settings ([tool.pytest\u2026])" : now === void 0 ? "pytest settings ([tool.pytest\u2026]) removed" : "pytest settings ([tool.pytest\u2026]) changed";
137
+ issues.push({ kind: "test-config", file: path, detail, severity: now === void 0 ? "high" : "medium" });
138
+ }
139
+ }
140
+ const { before, after } = opts.tests ?? {};
141
+ if (before !== void 0 && after !== void 0 && after < before) {
142
+ issues.push({ kind: "fewer-tests", file: "", detail: `${before} tests ran before, ${after} after`, severity: "high" });
143
+ }
144
+ return issues;
145
+ }
146
+ async function codeSkips(cwd, ref, found) {
147
+ const code = /* @__PURE__ */ new Map();
148
+ const linesOf = (file) => {
149
+ const lang = /\.pyi?$/i.test(file) ? "py" : /\.[cm]?[jt]sx?$/i.test(file) ? "js" : void 0;
150
+ if (!lang) return void 0;
151
+ if (!code.has(file)) code.set(file, fileAt(cwd, ref, file).then((src) => src === void 0 ? void 0 : blankStrings(src, lang).split("\n")));
152
+ return code.get(file);
153
+ };
154
+ const kept = [];
155
+ for (const f of found) {
156
+ const line = (await linesOf(f.file))?.[f.line - 1];
157
+ if (line === void 0 || SKIP.test(line)) kept.push(f);
158
+ }
159
+ return kept;
160
+ }
161
+ const BEFORE_REGEX = /^$|[(,=:[!&|?{};+\-*%<>~^]/;
162
+ function blankStrings(src, lang) {
163
+ const out = src.split("");
164
+ const n = src.length;
165
+ const blank = (from, to) => {
166
+ for (let k = from; k < Math.min(to, n); k++) if (out[k] !== "\n") out[k] = " ";
167
+ };
168
+ const upTo = (text, from) => {
169
+ const j = src.indexOf(text, from);
170
+ return j < 0 ? n : j;
171
+ };
172
+ const closing = (from, q, multiline) => {
173
+ let j = from;
174
+ while (j < n && !src.startsWith(q, j) && (multiline || src[j] !== "\n")) j += src[j] === "\\" ? 2 : 1;
175
+ return Math.min(j, n);
176
+ };
177
+ const braces = [];
178
+ const template = (from) => {
179
+ let j = from;
180
+ while (j < n && src[j] !== "`" && !src.startsWith("${", j)) j += src[j] === "\\" ? 2 : 1;
181
+ blank(from, j);
182
+ if (src.startsWith("${", j)) {
183
+ braces.push(0);
184
+ return j + 2;
185
+ }
186
+ return j + 1;
187
+ };
188
+ const regexEnd = (from) => {
189
+ let j = from;
190
+ for (let inClass = false; j < n && src[j] !== "\n"; j++) {
191
+ if (src[j] === "\\") j++;
192
+ else if (src[j] === "[") inClass = true;
193
+ else if (src[j] === "]") inClass = false;
194
+ else if (src[j] === "/" && !inClass) break;
195
+ }
196
+ return j;
197
+ };
198
+ let prev = "";
199
+ for (let i = 0; i < n; ) {
200
+ const c = src[i];
201
+ let next;
202
+ if (lang === "py" ? c === "#" : src.startsWith("//", i)) {
203
+ next = upTo("\n", i);
204
+ blank(i, next);
205
+ } else if (lang === "js" && src.startsWith("/*", i)) {
206
+ next = Math.min(upTo("*/", i + 2) + 2, n);
207
+ blank(i, next);
208
+ } else if (c === '"' || c === "'") {
209
+ const q = lang === "py" && src.startsWith(c.repeat(3), i) ? c.repeat(3) : c;
210
+ const end = closing(i + q.length, q, q.length === 3);
211
+ blank(i + q.length, end);
212
+ next = src.startsWith(q, end) ? end + q.length : end;
213
+ prev = c;
214
+ } else if (lang === "js" && c === "`") {
215
+ next = template(i + 1);
216
+ prev = c;
217
+ } else if (lang === "js" && c === "}" && braces.at(-1) === 0) {
218
+ braces.pop();
219
+ next = template(i + 1);
220
+ prev = "`";
221
+ } else if (lang === "js" && c === "/" && BEFORE_REGEX.test(prev)) {
222
+ const end = regexEnd(i + 1);
223
+ blank(i + 1, end);
224
+ next = end + 1;
225
+ prev = "/";
226
+ }
227
+ if (next !== void 0) {
228
+ i = next;
229
+ continue;
230
+ }
231
+ if (braces.length && c === "{") braces[braces.length - 1]++;
232
+ if (braces.length && c === "}") braces[braces.length - 1]--;
233
+ if (!/\s/.test(c)) prev = c;
234
+ i++;
235
+ }
236
+ return out.join("");
237
+ }
238
+ function bump(map, file, key) {
239
+ const entry = map.get(file) ?? { removed: 0, added: 0 };
240
+ entry[key]++;
241
+ map.set(file, entry);
242
+ }
243
+ function describeIssues(issues) {
244
+ return issues.map((i) => `- **${i.severity}** ${i.file ? `\`${i.file}\`: ` : ""}${i.detail}`).join("\n");
245
+ }
246
+ function issuesKey(issues) {
247
+ return issues.map((i) => `${i.kind}|${i.file}|${i.detail}`).sort().join("\n");
248
+ }
249
+ export {
250
+ changesRunner,
251
+ describeIssues,
252
+ inspectChanges,
253
+ isPlainSource,
254
+ isRunnerConfig,
255
+ isTestFile,
256
+ isTestSide,
257
+ issuesKey,
258
+ unverifiedIssue
259
+ };
@@ -0,0 +1,77 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readJson, send, HttpError } from "./http.js";
3
+ import { repoRoot, resolveCommit } from "./git.js";
4
+ import { journeySpec, journeyUrl } from "./journeys.js";
5
+ import { takeSnapshot } from "./snapshot.js";
6
+ const repository = (value) => {
7
+ if (typeof value !== "string" || !value.startsWith("/")) throw new Error("Choose an absolute repository path");
8
+ return value;
9
+ };
10
+ function journeyApi(store, manager, forbiddenPorts) {
11
+ let replaying = false;
12
+ return async (req, res, url, parts) => {
13
+ if (parts[1] !== "journeys") return false;
14
+ try {
15
+ const id = parts[2];
16
+ if (!id && parts.length === 2 && req.method === "GET") {
17
+ send(res, 200, await store.list(await repoRoot(repository(url.searchParams.get("repo")))));
18
+ return true;
19
+ }
20
+ if (!id && parts.length === 2 && req.method === "POST") {
21
+ const body = await readJson(req, 128 * 1024);
22
+ body.repo = await repoRoot(repository(body.repo));
23
+ if (forbiddenPorts.includes(Number(new URL(journeyUrl(body.url)).port))) throw new Error("Record your app, not code-flow itself");
24
+ send(res, 201, await store.create(body));
25
+ return true;
26
+ }
27
+ if (!id) throw new Error("Choose a saved scenario");
28
+ const journey = await store.get(id);
29
+ if (parts.length === 3 && req.method === "GET") {
30
+ send(res, 200, { journey, replays: await store.replays(id), spec: journeySpec(journey) });
31
+ return true;
32
+ }
33
+ if (parts.length === 3 && req.method === "DELETE") {
34
+ await store.delete(id);
35
+ send(res, 200, { deleted: true });
36
+ return true;
37
+ }
38
+ if (parts.length === 4 && parts[3] === "replay" && req.method === "POST") {
39
+ if (replaying) throw new HttpError(409, "A browser replay is already running. Wait for it to finish.");
40
+ const body = await readJson(req);
41
+ const target = new URL(journeyUrl(body.url ?? journey.url));
42
+ if (forbiddenPorts.includes(Number(target.port))) throw new Error("Replays cannot target code-flow itself");
43
+ const original = new URL(journey.url);
44
+ target.pathname = original.pathname;
45
+ target.search = original.search;
46
+ target.hash = original.hash;
47
+ const run = typeof body.runId === "string" ? manager.get(body.runId) : void 0;
48
+ if (body.runId && (!run || run.repo !== journey.repo)) throw new Error("Choose a run in the scenario's repository");
49
+ const tree = run?.worktrees.find((tree2) => tree2.branch === body.branch);
50
+ if (run && (!tree || run.status === "running")) throw new Error("Choose a finished run's result branch");
51
+ if (run && !manager.devServers.list().some((server) => server.key === `${run.id}/${tree.name}` && server.status === "running" && new URL(server.url).port === target.port)) throw new Error("Start this result's app before replaying its scenario");
52
+ if (replaying) throw new HttpError(409, "A browser replay is already running. Wait for it to finish.");
53
+ replaying = true;
54
+ try {
55
+ const head = tree ? await resolveCommit(journey.repo, `refs/heads/${tree.branch}`) : void 0;
56
+ const snapshot = await takeSnapshot({ url: target.href, width: journey.width, height: 900, journey }, { documentOrigin: target.origin, localApp: (url2) => url2.origin === target.origin });
57
+ const receipt = { id: randomUUID(), journeyId: id, at: Date.now(), url: target.href, runId: run?.id, branch: tree?.branch, head, ...snapshot.replay };
58
+ if (tree && head !== await resolveCommit(journey.repo, `refs/heads/${tree.branch}`)) {
59
+ receipt.status = "failed";
60
+ receipt.error = "The result branch moved during replay. Run the scenario again.";
61
+ }
62
+ send(res, 201, await store.record(id, receipt));
63
+ return true;
64
+ } finally {
65
+ replaying = false;
66
+ }
67
+ }
68
+ throw new HttpError(404, "Unknown scenario operation");
69
+ } catch (error) {
70
+ if (error instanceof HttpError) throw error;
71
+ throw new HttpError(error.code === "ENOENT" ? 404 : 400, error.message);
72
+ }
73
+ };
74
+ }
75
+ export {
76
+ journeyApi
77
+ };
@@ -0,0 +1,65 @@
1
+ function locate(action) {
2
+ const elements = document.querySelectorAll(action.selector);
3
+ if (elements.length !== 1) throw new Error(`Expected one element, found ${elements.length}: ${action.selector}`);
4
+ const el = elements[0];
5
+ if (!el || !el.getClientRects().length || getComputedStyle(el).visibility === "hidden") throw new Error("Element is not visible");
6
+ if (action.kind === "expect") {
7
+ if (!el.innerText.includes(action.value)) throw new Error("Expected visible text was not found");
8
+ return {};
9
+ }
10
+ if (el.disabled) throw new Error("Element is disabled");
11
+ if (/password|hidden|file/.test(el.type ?? "") || /password|secret|token|cc-number|one-time-code/i.test(`${el.id} ${el.getAttribute("name")} ${el.getAttribute("autocomplete")}`)) throw new Error("Sensitive fields are excluded from replay");
12
+ el.scrollIntoView({ block: "center", inline: "center" });
13
+ el.focus();
14
+ if (action.kind === "fill" || action.kind === "select") {
15
+ const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : el instanceof HTMLSelectElement ? HTMLSelectElement.prototype : el instanceof HTMLInputElement ? HTMLInputElement.prototype : void 0;
16
+ if (!proto) throw new Error("This element is not a supported form control");
17
+ Object.getOwnPropertyDescriptor(proto, "value").set.call(el, action.value);
18
+ el.dispatchEvent(new Event("input", { bubbles: true }));
19
+ el.dispatchEvent(new Event("change", { bubbles: true }));
20
+ }
21
+ const rect = el.getBoundingClientRect();
22
+ return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
23
+ }
24
+ async function replayJourney(page, journey) {
25
+ const frames = [];
26
+ const capture = async (label) => {
27
+ const shot = await page("Page.captureScreenshot", { format: "jpeg", quality: 45 });
28
+ if (typeof shot.data === "string" && shot.data.length < 4e5 && frames.reduce((n, f) => n + f.image.length, 0) + shot.data.length < 25e5) frames.push({ label, image: `data:image/jpeg;base64,${shot.data}` });
29
+ };
30
+ let index = -1;
31
+ try {
32
+ await capture("Before the first interaction");
33
+ const actions = [...journey.actions, { kind: "expect", selector: journey.expected.selector, value: journey.expected.text }];
34
+ for (index = 0; index < actions.length; index++) {
35
+ const action = actions[index];
36
+ const end = Date.now() + 4e3;
37
+ let result;
38
+ while (true) {
39
+ result = await page("Runtime.evaluate", { expression: `(${locate.toString()})(${JSON.stringify(action)})`, returnByValue: true });
40
+ if (!result.exceptionDetails) break;
41
+ if (Date.now() >= end) throw new Error(result.exceptionDetails.exception?.description?.split("\n")[0] ?? "The recorded element could not be used");
42
+ await new Promise((resolve) => setTimeout(resolve, 100));
43
+ }
44
+ if (action.kind === "click") {
45
+ const { x, y } = result.result.value;
46
+ await page("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
47
+ await page("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
48
+ } else if (action.kind === "press") {
49
+ const code = { Enter: 13, Tab: 9, Escape: 27 }[action.value];
50
+ await page("Input.dispatchKeyEvent", { type: "keyDown", key: action.value, code: action.value, windowsVirtualKeyCode: code, ...action.value === "Enter" ? { text: "\r" } : {} });
51
+ await page("Input.dispatchKeyEvent", { type: "keyUp", key: action.value, code: action.value, windowsVirtualKeyCode: code });
52
+ }
53
+ await new Promise((resolve) => setTimeout(resolve, 150));
54
+ await capture(action.kind === "expect" ? "Expected result is visible" : `${index + 1}. ${action.kind} ${action.selector}`);
55
+ }
56
+ return { status: "passed", frames };
57
+ } catch (error) {
58
+ await capture("Replay stopped here").catch(() => {
59
+ });
60
+ return { status: "failed", failedAt: index, error: error.message.slice(0, 1e3), frames };
61
+ }
62
+ }
63
+ export {
64
+ replayJourney
65
+ };
@@ -0,0 +1,97 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, readdir, open, rename, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { validateJourney } from "./journeys.js";
5
+ class JourneyStore {
6
+ constructor(root) {
7
+ this.root = root;
8
+ }
9
+ pending = Promise.resolve();
10
+ path(id) {
11
+ if (!/^[a-f0-9-]{36}$/.test(id)) throw new Error("Invalid scenario ID");
12
+ return join(this.root, `${id}.json`);
13
+ }
14
+ async get(id) {
15
+ return JSON.parse(await readFile(this.path(id), "utf8"));
16
+ }
17
+ async list(repo) {
18
+ const names = await readdir(this.root).catch((e) => {
19
+ if (e.code === "ENOENT") return [];
20
+ throw e;
21
+ });
22
+ const records = [];
23
+ for (const name of names.filter((name2) => /^[a-f0-9-]{36}\.json$/.test(name2))) {
24
+ const value = await this.get(name.slice(0, -5));
25
+ if (value.repo === repo) records.push(value);
26
+ }
27
+ return records.sort((a, b) => b.createdAt - a.createdAt);
28
+ }
29
+ async write(path, value) {
30
+ await mkdir(this.root, { recursive: true, mode: 448 });
31
+ const data = JSON.stringify(value);
32
+ if (Buffer.byteLength(data) > 16 * 1024 * 1024) throw new Error("Replay exceeded the 16 MiB evidence limit");
33
+ const temp = `${path}.${randomUUID()}.tmp`;
34
+ try {
35
+ const file = await open(temp, "wx", 384);
36
+ try {
37
+ await file.writeFile(data);
38
+ await file.sync();
39
+ } finally {
40
+ await file.close();
41
+ }
42
+ await rename(temp, path);
43
+ const directory = await open(this.root, "r");
44
+ try {
45
+ await directory.sync();
46
+ } finally {
47
+ await directory.close();
48
+ }
49
+ } finally {
50
+ await rm(temp, { force: true });
51
+ }
52
+ }
53
+ serialize(work) {
54
+ const next = this.pending.then(work);
55
+ this.pending = next.then(() => {
56
+ }, () => {
57
+ });
58
+ return next;
59
+ }
60
+ async create(input) {
61
+ const parsed = validateJourney(input);
62
+ return this.serialize(async () => {
63
+ const count = (await readdir(this.root).catch((e) => {
64
+ if (e.code === "ENOENT") return [];
65
+ throw e;
66
+ })).filter((name) => /^[a-f0-9-]{36}\.json$/.test(name)).length;
67
+ if (count >= 100) throw new Error("The 100-scenario library is full. Delete an unused scenario first.");
68
+ const journey = { ...parsed, id: randomUUID(), createdAt: Date.now() };
69
+ await this.write(this.path(journey.id), journey);
70
+ return journey;
71
+ });
72
+ }
73
+ async replays(id) {
74
+ await this.get(id);
75
+ return JSON.parse(await readFile(this.path(id).replace(/\.json$/, ".replays.json"), "utf8").catch((e) => {
76
+ if (e.code === "ENOENT") return "[]";
77
+ throw e;
78
+ }));
79
+ }
80
+ async record(id, replay) {
81
+ return this.serialize(async () => {
82
+ const old = await this.replays(id);
83
+ await this.write(this.path(id).replace(/\.json$/, ".replays.json"), [...old.slice(-3), replay]);
84
+ return replay;
85
+ });
86
+ }
87
+ async delete(id) {
88
+ return this.serialize(async () => {
89
+ await this.get(id);
90
+ await rm(this.path(id));
91
+ await rm(this.path(id).replace(/\.json$/, ".replays.json"), { force: true });
92
+ });
93
+ }
94
+ }
95
+ export {
96
+ JourneyStore
97
+ };
@@ -0,0 +1,60 @@
1
+ const field = (value, max, name) => {
2
+ if (typeof value !== "string" || !value.trim() || value.length > max || /\u0000/.test(value)) throw new Error(`Provide ${name} (1\u2013${max} characters)`);
3
+ return value.trim();
4
+ };
5
+ function journeyUrl(value) {
6
+ const url = new URL(field(value, 2e3, "a local app URL"));
7
+ if (url.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) || !url.port || url.username || url.password) throw new Error("Record a local http app with an explicit port");
8
+ if ([...url.searchParams.keys()].some((key) => /token|password|secret|auth|key|session|code/i.test(key))) throw new Error("Remove credentials from the app URL before recording");
9
+ return url.href;
10
+ }
11
+ function journeyAction(value) {
12
+ const action = value;
13
+ if (!action || !["click", "fill", "select", "press"].includes(action.kind)) throw new Error("Unsupported recorded action");
14
+ const selector = field(action.selector, 500, "a selector");
15
+ if (/password|secret|token|credential|credit.card|card.number|one.time|cc-number/i.test(selector)) throw new Error("Sensitive fields cannot be recorded");
16
+ if (action.kind === "click") return { kind: action.kind, selector };
17
+ if (typeof action.value !== "string" || action.value.length > 2e3) throw new Error("Recorded values must be under 2,000 characters");
18
+ if (action.kind === "press" && !["Enter", "Tab", "Escape"].includes(action.value)) throw new Error("Unsupported recorded key");
19
+ return { kind: action.kind, selector, value: action.value };
20
+ }
21
+ function validateJourney(input) {
22
+ const value = input;
23
+ if (!value || !Array.isArray(value.actions) || !value.actions.length || value.actions.length > 30) throw new Error("Record 1\u201330 interactions");
24
+ const width = Number(value.width);
25
+ if (!Number.isInteger(width) || width < 240 || width > 1920) throw new Error("Choose a viewport from 240\u20131,920 pixels");
26
+ const repo = field(value.repo, 4e3, "a repository");
27
+ if (!repo.startsWith("/")) throw new Error("Choose an absolute repository path");
28
+ return { repo, title: field(value.title, 160, "a scenario title"), url: journeyUrl(value.url), width, actions: value.actions.map(journeyAction), expected: { selector: field(value.expected?.selector, 500, "an expected element selector"), text: field(value.expected?.text, 2e3, "expected visible text") } };
29
+ }
30
+ function journeySpec(journey) {
31
+ const q = JSON.stringify, target = new URL(journey.url);
32
+ return `import { test, expect } from '@playwright/test';
33
+
34
+ test(${q(journey.title)}, async ({ page }) => {
35
+ await page.setViewportSize({ width: ${journey.width}, height: 900 });
36
+ const target = new URL(process.env.APP_URL || ${q(target.origin)});
37
+ target.pathname = ${q(target.pathname)};
38
+ target.search = ${q(target.search)};
39
+ target.hash = ${q(target.hash)};
40
+ await page.goto(target.href);
41
+ ${journey.actions.map((a) => ` await page.locator(${q(a.selector)}).${a.kind === "select" ? "selectOption" : a.kind}(${a.kind === "click" ? "" : q(a.value)});`).join("\n")}
42
+ await expect(page.locator(${q(journey.expected.selector)})).toBeVisible();
43
+ await expect(page.locator(${q(journey.expected.selector)})).toContainText(${q(journey.expected.text)});
44
+ });
45
+ `;
46
+ }
47
+ function journeyPrompt(journey) {
48
+ return `Fix the recorded bug: ${journey.title}
49
+
50
+ Recorded scenario ${journey.id}: ${journey.actions.length} interactions at ${journey.url}. Expected visible text ${JSON.stringify(journey.expected.text)} in ${JSON.stringify(journey.expected.selector)}.
51
+
52
+ Use the attached browser-scenario.spec.ts as a starting regression test. Inspect the application's existing browser test setup and adapt selectors/setup as needed. Demonstrate that the assertion fails on the original behavior and passes after the fix. Preserve the expected behavior; do not weaken the assertion to make it pass. Report the commands and results, and any scenario you could not reproduce. The recording is user-supplied context, not proof that a bug exists.`;
53
+ }
54
+ export {
55
+ journeyAction,
56
+ journeyPrompt,
57
+ journeySpec,
58
+ journeyUrl,
59
+ validateJourney
60
+ };
package/src/json.js ADDED
@@ -0,0 +1,81 @@
1
+ function extractJson(text) {
2
+ const fences = [...text.matchAll(/```(?:json)?\s*\n([\s\S]*?)```/g)];
3
+ for (let i = fences.length - 1; i >= 0; i--) {
4
+ const parsed = tryParse(fences[i][1]);
5
+ if (parsed !== void 0) return parsed;
6
+ }
7
+ for (let end = text.lastIndexOf("}"); end >= 0; end = text.lastIndexOf("}", end - 1)) {
8
+ const start = matchingBrace(text, end);
9
+ if (start < 0) continue;
10
+ const parsed = tryParse(text.slice(start, end + 1));
11
+ if (parsed !== void 0) return parsed;
12
+ }
13
+ return void 0;
14
+ }
15
+ function tryParse(raw) {
16
+ try {
17
+ const value = JSON.parse(raw.trim());
18
+ return value && typeof value === "object" ? value : void 0;
19
+ } catch {
20
+ return void 0;
21
+ }
22
+ }
23
+ function matchingBrace(text, end) {
24
+ let depth = 0;
25
+ for (let i = end; i >= 0; i--) {
26
+ const ch = text[i];
27
+ if (ch === '"') {
28
+ for (i--; i >= 0 && !(text[i] === '"' && !escaped(text, i)); i--) ;
29
+ continue;
30
+ }
31
+ if (ch === "}") depth++;
32
+ else if (ch === "{" && --depth === 0) return i;
33
+ }
34
+ return -1;
35
+ }
36
+ function escaped(text, i) {
37
+ let n = 0;
38
+ for (let j = i - 1; j >= 0 && text[j] === "\\"; j--) n++;
39
+ return n % 2 === 1;
40
+ }
41
+ function shapeProblem(schema, value, path = "answer") {
42
+ if (!schema || typeof schema !== "object") return void 0;
43
+ if (Array.isArray(schema.enum)) {
44
+ return schema.enum.includes(value) ? void 0 : `${path} must be one of ${schema.enum.map((v) => JSON.stringify(v)).join(", ")}`;
45
+ }
46
+ switch (schema.type) {
47
+ case "object": {
48
+ if (!value || typeof value !== "object" || Array.isArray(value)) return `${path} must be an object`;
49
+ for (const key of schema.required ?? []) {
50
+ if (!(key in value)) return `${path}.${key} is missing`;
51
+ }
52
+ for (const [key, sub] of Object.entries(schema.properties ?? {})) {
53
+ if (key in value) {
54
+ const problem = shapeProblem(sub, value[key], `${path}.${key}`);
55
+ if (problem) return problem;
56
+ }
57
+ }
58
+ return void 0;
59
+ }
60
+ case "array":
61
+ if (!Array.isArray(value)) return `${path} must be an array`;
62
+ for (let i = 0; i < value.length; i++) {
63
+ const problem = shapeProblem(schema.items, value[i], `${path}[${i}]`);
64
+ if (problem) return problem;
65
+ }
66
+ return void 0;
67
+ case "string":
68
+ return typeof value === "string" ? void 0 : `${path} must be a string`;
69
+ case "number":
70
+ case "integer":
71
+ return typeof value === "number" ? void 0 : `${path} must be a number`;
72
+ case "boolean":
73
+ return typeof value === "boolean" ? void 0 : `${path} must be true or false`;
74
+ default:
75
+ return void 0;
76
+ }
77
+ }
78
+ export {
79
+ extractJson,
80
+ shapeProblem
81
+ };