oh-my-opencode 4.18.0 → 4.18.2

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 (226) hide show
  1. package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3677 -0
  2. package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3154 -0
  3. package/.agents/skills/work-with-pr/SKILL.md +16 -37
  4. package/.agents/skills/work-with-pr-workspace/evals/evals.json +3 -3
  5. package/.opencode/skills/work-with-pr/SKILL.md +16 -37
  6. package/.opencode/skills/work-with-pr-workspace/evals/evals.json +3 -3
  7. package/bin/AGENTS.md +33 -0
  8. package/dist/cli/index.js +500 -158
  9. package/dist/cli-node/index.js +500 -158
  10. package/dist/hooks/anthropic-context-window-limit-recovery/empty-content-recovery-sdk.d.ts +6 -0
  11. package/dist/hooks/category-skill-reminder/hook.d.ts +9 -1
  12. package/dist/hooks/comment-checker/hook.d.ts +8 -1
  13. package/dist/hooks/todo-continuation-enforcer/types.d.ts +3 -0
  14. package/dist/index.js +956 -702
  15. package/dist/plugin/messages-transform.d.ts +1 -0
  16. package/dist/plugin-handlers/prometheus-agent-config-builder.d.ts +2 -0
  17. package/dist/tui.js +43 -4
  18. package/package.json +16 -16
  19. package/packages/git-bash-mcp/dist/cli.js +81 -19
  20. package/packages/lsp-core/package.json +4 -0
  21. package/packages/lsp-core/src/index.ts +1 -0
  22. package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
  23. package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
  24. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
  25. package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
  26. package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
  27. package/packages/lsp-core/src/lsp/client.ts +262 -80
  28. package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
  29. package/packages/lsp-core/src/lsp/connection.ts +12 -6
  30. package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +221 -0
  31. package/packages/lsp-core/src/lsp/directory-diagnostics.ts +61 -28
  32. package/packages/lsp-core/src/lsp/errors.ts +11 -0
  33. package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
  34. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
  35. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
  36. package/packages/lsp-core/src/lsp/formatters.ts +3 -0
  37. package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
  38. package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
  39. package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
  40. package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
  41. package/packages/lsp-core/src/lsp/transport.ts +96 -70
  42. package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
  43. package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
  44. package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
  45. package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
  46. package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
  47. package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
  48. package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
  49. package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
  50. package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
  51. package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
  52. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
  53. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
  54. package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
  55. package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
  56. package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
  57. package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
  58. package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
  59. package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
  60. package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
  61. package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
  62. package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
  63. package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
  64. package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
  65. package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
  66. package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
  67. package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
  68. package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
  69. package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
  70. package/packages/lsp-core/src/mcp.ts +18 -7
  71. package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
  72. package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
  73. package/packages/lsp-core/src/post-edit/index.ts +1 -0
  74. package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
  75. package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
  76. package/packages/lsp-core/src/request-context.test.ts +171 -0
  77. package/packages/lsp-core/src/request-context.ts +222 -9
  78. package/packages/lsp-core/src/tool-surface.test.ts +4 -1
  79. package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
  80. package/packages/lsp-core/src/tools/navigation.ts +12 -12
  81. package/packages/lsp-core/src/tools/rename.ts +10 -15
  82. package/packages/lsp-core/src/tools/symbols.ts +11 -11
  83. package/packages/lsp-core/src/tools/types.ts +2 -1
  84. package/packages/lsp-daemon/dist/cli.js +3330 -764
  85. package/packages/lsp-daemon/dist/client.d.ts +105 -0
  86. package/packages/lsp-daemon/dist/client.js +5995 -0
  87. package/packages/lsp-daemon/dist/daemon-client.d.ts +12 -7
  88. package/packages/lsp-daemon/dist/daemon-client.js +139 -32
  89. package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
  90. package/packages/lsp-daemon/dist/daemon-server.js +40 -15
  91. package/packages/lsp-daemon/dist/ensure-daemon.d.ts +10 -8
  92. package/packages/lsp-daemon/dist/ensure-daemon.js +135 -51
  93. package/packages/lsp-daemon/dist/index.d.ts +2 -2
  94. package/packages/lsp-daemon/dist/index.js +3093 -786
  95. package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
  96. package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
  97. package/packages/lsp-daemon/dist/lock.js +14 -4
  98. package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
  99. package/packages/lsp-daemon/dist/ownership.js +168 -0
  100. package/packages/lsp-daemon/dist/paths.d.ts +33 -9
  101. package/packages/lsp-daemon/dist/paths.js +72 -33
  102. package/packages/lsp-daemon/dist/proxy.d.ts +5 -0
  103. package/packages/lsp-daemon/dist/proxy.js +123 -16
  104. package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
  105. package/packages/lsp-daemon/dist/request-routing.js +71 -22
  106. package/packages/lsp-daemon/dist/run-daemon.js +9 -2
  107. package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
  108. package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
  109. package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
  110. package/packages/lsp-daemon/package.json +12 -3
  111. package/packages/lsp-tools-mcp/dist/cli.js +2189 -454
  112. package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
  113. package/packages/lsp-tools-mcp/dist/mcp.js +2206 -471
  114. package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
  115. package/packages/lsp-tools-mcp/dist/tools.js +2119 -447
  116. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  117. package/packages/omo-codex/plugin/.mcp.json +2 -1
  118. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  119. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  120. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +100 -28
  121. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +100 -28
  122. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  123. package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +21 -9
  124. package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +35 -0
  125. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge-lifecycle.test.ts +69 -0
  126. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +57 -1
  127. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  128. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  129. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  130. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  131. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  132. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  133. package/packages/omo-codex/plugin/components/lsp/.mcp.json +2 -1
  134. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
  135. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +3033 -936
  136. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
  137. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
  138. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
  139. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
  140. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
  141. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
  142. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
  143. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  144. package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
  145. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
  146. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
  147. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
  148. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
  149. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
  150. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
  151. package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
  152. package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
  153. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
  154. package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
  155. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +20 -5
  156. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
  157. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +5 -3
  158. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  159. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  160. package/packages/omo-codex/plugin/components/start-work-continuation/README.md +2 -2
  161. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +3 -3
  162. package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +2 -2
  163. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  164. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  165. package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +1 -1
  166. package/packages/omo-codex/plugin/components/start-work-continuation/src/codex-hook.ts +1 -1
  167. package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +15 -1
  168. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +20 -0
  169. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  170. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  171. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  172. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  173. package/packages/omo-codex/plugin/components/ultrawork/directive.md +50 -33
  174. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  175. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  176. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +50 -33
  177. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +50 -33
  178. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +3 -3
  179. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +5 -6
  180. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  181. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  182. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
  183. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +6 -5
  184. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +5 -6
  185. package/packages/omo-codex/plugin/components/ulw-loop/test/stop-resume-hook.test.ts +2 -2
  186. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  187. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  188. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  189. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  190. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  191. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  192. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  193. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  194. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  195. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  196. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  197. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  198. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  199. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  200. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  201. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  202. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  203. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  204. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  205. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  206. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  207. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  208. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  209. package/packages/omo-codex/plugin/package-lock.json +26 -14
  210. package/packages/omo-codex/plugin/package.json +1 -1
  211. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
  212. package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
  213. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +9 -1
  214. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +7 -0
  215. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +2 -1
  216. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +50 -33
  217. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
  218. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +6 -5
  219. package/packages/omo-codex/plugin/test/aggregate-build.test.mjs +8 -0
  220. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +128 -15
  221. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +10 -0
  222. package/packages/omo-codex/plugin/test/lsp-prebuild-layouts.test.mjs +2 -0
  223. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +1 -0
  224. package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +7 -0
  225. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +34 -3
  226. package/packages/omo-codex/scripts/install-dist/install-local.mjs +328 -63
@@ -0,0 +1,221 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { describe, expect, it } from "bun:test";
6
+
7
+ import { aggregateDiagnosticsForDirectory } from "./directory-diagnostics.js";
8
+ import { LspClient } from "./client.js";
9
+ import { LspManager } from "./manager.js";
10
+ import type { Diagnostic, ResolvedServer } from "./types.js";
11
+
12
+ describe("directory diagnostics", () => {
13
+ it("#given multiple files and one per-file failure #when directory diagnostics run #then work stays bounded and file failures stay structured", async () => {
14
+ const workspace = mkdtempSync(join(tmpdir(), "lsp-directory-diag-"));
15
+ const active: string[] = [];
16
+ let maxActive = 0;
17
+ const diagnosticsByFile = new Map<string, Diagnostic[] | Error>([
18
+ [join(workspace, "a.ts"), [diagnostic("a")]],
19
+ [join(workspace, "b.ts"), new Error("b failed")],
20
+ [join(workspace, "c.ts"), [diagnostic("c")]],
21
+ [join(workspace, "d.ts"), [diagnostic("d")]],
22
+ [join(workspace, "e.ts"), [diagnostic("e")]],
23
+ ]);
24
+ const client = {
25
+ async diagnostics(filePath: string) {
26
+ active.push(filePath);
27
+ maxActive = Math.max(maxActive, active.length);
28
+ await Promise.resolve();
29
+ active.splice(active.indexOf(filePath), 1);
30
+ const entry = diagnosticsByFile.get(filePath);
31
+ if (entry instanceof Error) throw entry;
32
+ return { items: entry ?? [] };
33
+ },
34
+ } as Pick<LspClient, "diagnostics"> as LspClient;
35
+ const server: ResolvedServer = { id: "typescript", command: ["typescript-language-server"], extensions: [".ts"], priority: 1 };
36
+ const manager = {
37
+ async getClient() {
38
+ return client;
39
+ },
40
+ releaseClient() {},
41
+ };
42
+
43
+ try {
44
+ const result = await aggregateDiagnosticsForDirectory(workspace, ".ts", "all", 4, {
45
+ manager: manager as never,
46
+ listFiles: () => [...diagnosticsByFile.keys()],
47
+ workspaceRoot: workspace,
48
+ server,
49
+ maxConcurrency: 2,
50
+ });
51
+
52
+ expect(maxActive).toBeLessThanOrEqual(2);
53
+ expect(result.fileFailures).toEqual([{ file: join(workspace, "b.ts"), error: "b failed" }]);
54
+ expect(result.totalDiagnostics).toBe(3);
55
+ expect(result.output).toContain("File processing errors:");
56
+ } finally {
57
+ rmSync(workspace, { recursive: true, force: true });
58
+ }
59
+ });
60
+
61
+ it("#given directory diagnostics are aborted after one file #when the worker loops #then no later file is scheduled", async () => {
62
+ const workspace = mkdtempSync(join(tmpdir(), "lsp-directory-diag-cancel-"));
63
+ const controller = new AbortController();
64
+ const visited: string[] = [];
65
+ const firstFile = join(workspace, "a.ts");
66
+ const files = [firstFile, join(workspace, "b.ts"), join(workspace, "c.ts")];
67
+ const client = {
68
+ async diagnostics(filePath: string) {
69
+ visited.push(filePath);
70
+ controller.abort();
71
+ return { items: [] };
72
+ },
73
+ } as Pick<LspClient, "diagnostics"> as LspClient;
74
+ const server: ResolvedServer = { id: "typescript", command: ["typescript-language-server"], extensions: [".ts"], priority: 1 };
75
+ const manager = {
76
+ async getClient() {
77
+ return client;
78
+ },
79
+ releaseClient() {},
80
+ };
81
+
82
+ try {
83
+ const result = await aggregateDiagnosticsForDirectory(workspace, ".ts", "all", 3, {
84
+ manager: manager as never,
85
+ listFiles: () => files,
86
+ workspaceRoot: workspace,
87
+ server,
88
+ maxConcurrency: 1,
89
+ signal: controller.signal,
90
+ });
91
+
92
+ expect(result.totalDiagnostics).toBe(0);
93
+ expect(visited).toEqual([firstFile]);
94
+ } finally {
95
+ rmSync(workspace, { recursive: true, force: true });
96
+ }
97
+ });
98
+
99
+ it("#given cold client acquisition is pending #when directory diagnostics are aborted #then acquisition settles and cleans up before startup releases", async () => {
100
+ const workspace = mkdtempSync(join(tmpdir(), "lsp-directory-diag-cold-cancel-"));
101
+ const controller = new AbortController();
102
+ const startupStarted = deferred();
103
+ const startupRelease = deferred();
104
+ const server: ResolvedServer = {
105
+ id: "typescript",
106
+ command: ["typescript-language-server"],
107
+ extensions: [".ts"],
108
+ priority: 1,
109
+ };
110
+ const client = new PendingStartLspClient({
111
+ root: workspace,
112
+ server,
113
+ startupStarted,
114
+ startupRelease,
115
+ });
116
+ const manager = new LspManager({
117
+ clientFactory: () => client,
118
+ reaperIntervalMs: 60_000,
119
+ });
120
+ const aggregation = aggregateDiagnosticsForDirectory(workspace, ".ts", "all", 1, {
121
+ manager,
122
+ listFiles: () => [join(workspace, "a.ts")],
123
+ workspaceRoot: workspace,
124
+ server,
125
+ signal: controller.signal,
126
+ });
127
+
128
+ try {
129
+ await startupStarted.promise;
130
+ controller.abort();
131
+
132
+ const settledBeforeStartupRelease = await settlesWithin(aggregation, 100);
133
+
134
+ expect(settledBeforeStartupRelease).toBe(true);
135
+ await expect(aggregation).rejects.toHaveProperty("name", "AbortError");
136
+ expect(manager.clientCount()).toBe(0);
137
+ expect(client.stopCallCount).toBe(1);
138
+ } finally {
139
+ startupRelease.resolve();
140
+ await aggregation.catch(() => undefined);
141
+ await manager.stopAll();
142
+ rmSync(workspace, { recursive: true, force: true });
143
+ }
144
+ });
145
+ });
146
+
147
+ class PendingStartLspClient extends LspClient {
148
+ stopCallCount = 0;
149
+ private readonly startupStarted: Deferred;
150
+ private readonly startupRelease: Deferred;
151
+
152
+ constructor(options: PendingStartLspClientOptions) {
153
+ super(options.root, options.server);
154
+ this.startupStarted = options.startupStarted;
155
+ this.startupRelease = options.startupRelease;
156
+ }
157
+
158
+ override async start(): Promise<void> {
159
+ this.startupStarted.resolve();
160
+ await this.startupRelease.promise;
161
+ }
162
+
163
+ override async initialize(): Promise<void> {}
164
+
165
+ override async stop(): Promise<void> {
166
+ this.stopCallCount += 1;
167
+ }
168
+
169
+ override isAlive(): boolean {
170
+ return true;
171
+ }
172
+
173
+ override async diagnostics(): Promise<{ readonly items: Diagnostic[] }> {
174
+ return { items: [] };
175
+ }
176
+ }
177
+
178
+ interface PendingStartLspClientOptions {
179
+ readonly root: string;
180
+ readonly server: ResolvedServer;
181
+ readonly startupStarted: Deferred;
182
+ readonly startupRelease: Deferred;
183
+ }
184
+
185
+ interface Deferred {
186
+ readonly promise: Promise<void>;
187
+ resolve(): void;
188
+ }
189
+
190
+ function deferred(): Deferred {
191
+ let resolvePromise: (() => void) | undefined;
192
+ const promise = new Promise<void>((resolve) => {
193
+ resolvePromise = resolve;
194
+ });
195
+ return { promise, resolve: () => resolvePromise?.() };
196
+ }
197
+
198
+ async function settlesWithin(promise: Promise<unknown>, timeoutMs: number): Promise<boolean> {
199
+ let timer: ReturnType<typeof setTimeout> | undefined;
200
+ try {
201
+ return await Promise.race([
202
+ promise.then(
203
+ () => true,
204
+ () => true,
205
+ ),
206
+ new Promise<boolean>((resolve) => {
207
+ timer = setTimeout(() => resolve(false), timeoutMs);
208
+ timer.unref();
209
+ }),
210
+ ]);
211
+ } finally {
212
+ if (timer !== undefined) clearTimeout(timer);
213
+ }
214
+ }
215
+
216
+ function diagnostic(message: string): Diagnostic {
217
+ return {
218
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
219
+ message,
220
+ };
221
+ }
@@ -6,17 +6,38 @@ import { DEFAULT_MAX_DIAGNOSTICS, DEFAULT_MAX_DIRECTORY_FILES } from "./constant
6
6
  import { effectiveExtension } from "./effective-extension.js";
7
7
  import { LspInvalidPathError, LspServerLookupError } from "./errors.js";
8
8
  import { filterDiagnosticsBySeverity, formatDiagnostic } from "./formatters.js";
9
- import { getLspManager } from "./manager.js";
9
+ import { getLspManager, type LspManager } from "./manager.js";
10
10
  import { findServerForExtension } from "./server-resolution.js";
11
- import type { Diagnostic, SeverityFilter } from "./types.js";
11
+ import type { Diagnostic, ResolvedServer, SeverityFilter } from "./types.js";
12
12
 
13
13
  const SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
14
+ const DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY = 4;
14
15
 
15
16
  interface FileDiagnostic {
16
17
  filePath: string;
17
18
  diagnostic: Diagnostic;
18
19
  }
19
20
 
21
+ export interface DirectoryDiagnosticsFileFailure {
22
+ readonly file: string;
23
+ readonly error: string;
24
+ }
25
+
26
+ export interface DirectoryDiagnosticsResult {
27
+ readonly output: string;
28
+ readonly totalDiagnostics: number;
29
+ readonly fileFailures: readonly DirectoryDiagnosticsFileFailure[];
30
+ }
31
+
32
+ export interface DirectoryDiagnosticsOptions {
33
+ readonly listFiles?: (directory: string, extension: string, maxFiles: number) => string[];
34
+ readonly manager?: LspManager;
35
+ readonly maxConcurrency?: number;
36
+ readonly workspaceRoot?: string;
37
+ readonly server?: ResolvedServer;
38
+ readonly signal?: AbortSignal;
39
+ }
40
+
20
41
  export function collectFilesWithExtension(dir: string, extension: string, maxFiles: number): string[] {
21
42
  const files: string[] = [];
22
43
 
@@ -63,61 +84,73 @@ export async function aggregateDiagnosticsForDirectory(
63
84
  extension: string,
64
85
  severity?: SeverityFilter,
65
86
  maxFiles: number = DEFAULT_MAX_DIRECTORY_FILES,
66
- ): Promise<string> {
87
+ options: DirectoryDiagnosticsOptions = {},
88
+ ): Promise<DirectoryDiagnosticsResult> {
67
89
  if (!extension.startsWith(".")) {
68
90
  throw new LspInvalidPathError(
69
91
  `Extension must start with a dot (e.g., ".ts", not "${extension}"). Use ".${extension}" instead.`,
70
92
  );
71
93
  }
72
94
 
73
- const absDir = resolve(contextCwd(), directory);
95
+ const absDir = resolve(options.workspaceRoot ?? contextCwd(), directory);
74
96
  if (!existsSync(absDir)) {
75
97
  throw new LspInvalidPathError(`Directory does not exist: ${absDir}`);
76
98
  }
77
99
 
78
- const serverResult = findServerForExtension(extension);
100
+ const serverResult = options.server === undefined ? findServerForExtension(extension) : { status: "found" as const, server: options.server };
79
101
  if (serverResult.status !== "found") {
80
102
  throw new LspServerLookupError(formatServerLookupError(serverResult));
81
103
  }
82
104
 
83
105
  const server = serverResult.server;
84
- const allFiles = collectFilesWithExtension(absDir, extension, maxFiles + 1);
106
+ const allFiles = (options.listFiles ?? collectFilesWithExtension)(absDir, extension, maxFiles + 1);
85
107
  const wasCapped = allFiles.length > maxFiles;
86
108
  const filesToProcess = allFiles.slice(0, maxFiles);
87
109
 
88
110
  if (filesToProcess.length === 0) {
89
- return [
111
+ const output = [
90
112
  `Directory: ${absDir}`,
91
113
  `Extension: ${extension}`,
92
114
  "Files scanned: 0",
93
115
  `No files found with extension "${extension}".`,
94
116
  ].join("\n");
117
+ return { output, totalDiagnostics: 0, fileFailures: [] };
95
118
  }
96
119
 
97
- const root = findWorkspaceRoot(absDir);
98
- const manager = getLspManager();
120
+ const root = options.workspaceRoot ?? findWorkspaceRoot(absDir);
121
+ const manager = options.manager ?? getLspManager();
99
122
  const allDiagnostics: FileDiagnostic[] = [];
100
- const fileErrors: { file: string; error: string }[] = [];
123
+ const fileErrors: DirectoryDiagnosticsFileFailure[] = [];
124
+ const maxConcurrency = Math.max(1, options.maxConcurrency ?? DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY);
101
125
 
102
- const client = await manager.getClient(root, server);
126
+ options.signal?.throwIfAborted();
127
+ const client = await manager.getClient(root, server, options.signal);
103
128
  try {
104
- for (const file of filesToProcess) {
105
- try {
106
- const result = await client.diagnostics(file);
107
- const filtered = filterDiagnosticsBySeverity(result.items, severity);
108
- allDiagnostics.push(
109
- ...filtered.map((diagnostic) => ({
110
- filePath: file,
111
- diagnostic,
112
- })),
113
- );
114
- } catch (e) {
115
- fileErrors.push({
116
- file,
117
- error: e instanceof Error ? e.message : String(e),
118
- });
129
+ let nextIndex = 0;
130
+ const workers = Array.from({ length: Math.min(maxConcurrency, filesToProcess.length) }, async () => {
131
+ for (;;) {
132
+ if (options.signal?.aborted) return;
133
+ const file = filesToProcess[nextIndex];
134
+ nextIndex += 1;
135
+ if (file === undefined) return;
136
+ try {
137
+ const result = await client.diagnostics(file, options.signal);
138
+ const filtered = filterDiagnosticsBySeverity(result.items, severity);
139
+ allDiagnostics.push(
140
+ ...filtered.map((diagnostic) => ({
141
+ filePath: file,
142
+ diagnostic,
143
+ })),
144
+ );
145
+ } catch (e) {
146
+ fileErrors.push({
147
+ file,
148
+ error: e instanceof Error ? e.message : String(e),
149
+ });
150
+ }
119
151
  }
120
- }
152
+ });
153
+ await Promise.all(workers);
121
154
  } finally {
122
155
  manager.releaseClient(root, server.id);
123
156
  }
@@ -150,5 +183,5 @@ export async function aggregateDiagnosticsForDirectory(
150
183
  }
151
184
  }
152
185
 
153
- return lines.join("\n");
186
+ return { output: lines.join("\n"), totalDiagnostics: allDiagnostics.length, fileFailures: fileErrors };
154
187
  }
@@ -1,3 +1,7 @@
1
+ import type { ServerLookupResult } from "./types.js";
2
+
3
+ export type FailedServerLookupResult = Exclude<ServerLookupResult, { status: "found" }>;
4
+
1
5
  export class LspConnectionClosedError extends Error {
2
6
  override readonly name = "LspConnectionClosedError";
3
7
 
@@ -42,6 +46,13 @@ export class LspInvalidPathError extends Error {
42
46
 
43
47
  export class LspServerLookupError extends Error {
44
48
  override readonly name = "LspServerLookupError";
49
+
50
+ constructor(
51
+ message: string,
52
+ readonly lookup?: FailedServerLookupResult,
53
+ ) {
54
+ super(message);
55
+ }
45
56
  }
46
57
 
47
58
  export class LspServerInitializingError extends Error {
@@ -0,0 +1,283 @@
1
+ import { writeFileSync } from "node:fs";
2
+
3
+ import {
4
+ createWorkspaceEditTestHarness,
5
+ diagnostic,
6
+ readEvents,
7
+ waitForEventCount,
8
+ } from "../workspace-apply-edit-test-support.js";
9
+
10
+ class DiagnosticsFreshnessContractProbeError extends Error {
11
+ override readonly name = "DiagnosticsFreshnessContractProbeError";
12
+ }
13
+
14
+ function requireProbe(condition: boolean, message: string): asserts condition {
15
+ if (!condition) throw new DiagnosticsFreshnessContractProbeError(message);
16
+ }
17
+
18
+ const outputPath = process.argv[2];
19
+ if (!outputPath) throw new DiagnosticsFreshnessContractProbeError("output path is required");
20
+
21
+ const harness = createWorkspaceEditTestHarness();
22
+
23
+ try {
24
+ const exactCurrent = await harness.makeClient(
25
+ {
26
+ publishDiagnostics: [
27
+ {
28
+ trigger: "didOpen",
29
+ version: 1,
30
+ diagnostics: [diagnostic("exact-current")],
31
+ },
32
+ ],
33
+ },
34
+ { diagnosticsFreshnessTimeoutMs: 50, versionlessPublishQuiescenceMs: 5 },
35
+ );
36
+ const [exactFirst, exactSecond] = await Promise.all([
37
+ exactCurrent.client.diagnostics(exactCurrent.source),
38
+ exactCurrent.client.diagnostics(exactCurrent.source),
39
+ ]);
40
+ const exactDidOpenCount = readEvents(exactCurrent.events).filter(
41
+ (event) => event.type === "clientNotification" && event.method === "textDocument/didOpen",
42
+ ).length;
43
+ const exactCurrentEvidence = {
44
+ outcome: "exact-current",
45
+ items: exactFirst.items.map((entry) => entry.message),
46
+ concurrentItems: exactSecond.items.map((entry) => entry.message),
47
+ didOpenCount: exactDidOpenCount,
48
+ ok:
49
+ exactDidOpenCount === 1
50
+ && JSON.stringify(exactFirst.items) === JSON.stringify([diagnostic("exact-current")])
51
+ && JSON.stringify(exactSecond.items) === JSON.stringify([diagnostic("exact-current")]),
52
+ };
53
+ requireProbe(exactCurrentEvidence.ok, "exact-current diagnostics did not resolve immediately with a single didOpen");
54
+
55
+ const postGenerationVersionless = await harness.makeClient(
56
+ {
57
+ publishDiagnostics: [
58
+ {
59
+ trigger: "didChange",
60
+ diagnostics: [diagnostic("post-generation-versionless")],
61
+ },
62
+ ],
63
+ },
64
+ { diagnosticsFreshnessTimeoutMs: 80, versionlessPublishQuiescenceMs: 20 },
65
+ );
66
+ await postGenerationVersionless.client.openFile(postGenerationVersionless.source);
67
+ await waitForEventCount(
68
+ postGenerationVersionless.events,
69
+ (event) => event.type === "serverNotification" && event.method === "textDocument/publishDiagnostics",
70
+ 1,
71
+ );
72
+ writeFileSync(postGenerationVersionless.source, "const post_generation = 1;\n", "utf-8");
73
+ await postGenerationVersionless.client.openFile(postGenerationVersionless.source);
74
+ const versionlessStartedAt = Date.now();
75
+ const versionlessResult = await postGenerationVersionless.client.diagnostics(postGenerationVersionless.source);
76
+ const versionlessElapsedMs = Date.now() - versionlessStartedAt;
77
+ const postGenerationVersionlessEvidence = {
78
+ outcome: "post-generation-versionless",
79
+ items: versionlessResult.items.map((entry) => entry.message),
80
+ elapsedMs: versionlessElapsedMs,
81
+ ok:
82
+ JSON.stringify(versionlessResult.items) === JSON.stringify([diagnostic("post-generation-versionless")])
83
+ && versionlessElapsedMs >= 15,
84
+ };
85
+ requireProbe(
86
+ postGenerationVersionlessEvidence.ok,
87
+ "post-generation versionless diagnostics did not wait for quiescence before resolving",
88
+ );
89
+
90
+ const stale = await harness.makeClient(
91
+ {
92
+ publishDiagnostics: [
93
+ {
94
+ trigger: "didChange",
95
+ version: 1,
96
+ diagnostics: [diagnostic("stale")],
97
+ },
98
+ ],
99
+ },
100
+ { diagnosticsFreshnessTimeoutMs: 30, versionlessPublishQuiescenceMs: 5 },
101
+ );
102
+ await stale.client.openFile(stale.source);
103
+ writeFileSync(stale.source, "const stale = 1;\n", "utf-8");
104
+ await stale.client.openFile(stale.source);
105
+ const staleResult = await stale.client.diagnostics(stale.source);
106
+ const staleEvidence = {
107
+ outcome: "stale",
108
+ items: staleResult.items.map((entry) => entry.message),
109
+ transientError: staleResult.transientError ?? null,
110
+ ok: staleResult.items.length === 0 && staleResult.transientError?.kind === "freshness_timeout",
111
+ };
112
+ requireProbe(staleEvidence.ok, "stale diagnostics unexpectedly satisfied the current request");
113
+
114
+ const future = await harness.makeClient(
115
+ {
116
+ publishDiagnostics: [
117
+ {
118
+ trigger: "didChange",
119
+ version: 3,
120
+ diagnostics: [diagnostic("future")],
121
+ },
122
+ ],
123
+ },
124
+ { diagnosticsFreshnessTimeoutMs: 30, versionlessPublishQuiescenceMs: 5 },
125
+ );
126
+ await future.client.openFile(future.source);
127
+ writeFileSync(future.source, "const future = 1;\n", "utf-8");
128
+ await future.client.openFile(future.source);
129
+ const futureResult = await future.client.diagnostics(future.source);
130
+ const futureEvidence = {
131
+ outcome: "future",
132
+ items: futureResult.items.map((entry) => entry.message),
133
+ transientError: futureResult.transientError ?? null,
134
+ ok: futureResult.items.length === 0 && futureResult.transientError?.kind === "freshness_timeout",
135
+ };
136
+ requireProbe(futureEvidence.ok, "future diagnostics unexpectedly satisfied the current request");
137
+
138
+ const pullOvertaken = await harness.makeClient(
139
+ {
140
+ capabilities: { diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false } },
141
+ diagnosticResponses: [
142
+ {
143
+ delayMs: 20,
144
+ report: { kind: "full", resultId: "v1", items: [diagnostic("pull-stale")] },
145
+ },
146
+ {
147
+ report: { kind: "full", resultId: "v2", items: [diagnostic("pull-fresh")] },
148
+ },
149
+ ],
150
+ },
151
+ { diagnosticsFreshnessTimeoutMs: 80, versionlessPublishQuiescenceMs: 5 },
152
+ );
153
+ await pullOvertaken.client.openFile(pullOvertaken.source);
154
+ const pendingPull = pullOvertaken.client.diagnostics(pullOvertaken.source);
155
+ await waitForEventCount(
156
+ pullOvertaken.events,
157
+ (event) => event.type === "clientRequest" && event.method === "textDocument/diagnostic",
158
+ 1,
159
+ );
160
+ writeFileSync(pullOvertaken.source, "const pull_fresh = 1;\n", "utf-8");
161
+ await pullOvertaken.client.openFile(pullOvertaken.source);
162
+ const pullOvertakenResult = await pendingPull;
163
+ const pullRequestCount = readEvents(pullOvertaken.events).filter(
164
+ (event) => event.type === "clientRequest" && event.method === "textDocument/diagnostic",
165
+ ).length;
166
+ const pullOvertakenEvidence = {
167
+ outcome: "pull-overtaken",
168
+ items: pullOvertakenResult.items.map((entry) => entry.message),
169
+ requestCount: pullRequestCount,
170
+ ok:
171
+ JSON.stringify(pullOvertakenResult.items) === JSON.stringify([diagnostic("pull-fresh")])
172
+ && pullRequestCount === 2,
173
+ };
174
+ requireProbe(
175
+ pullOvertakenEvidence.ok,
176
+ "pull diagnostics were not invalidated and retried after a local change overtook the in-flight request",
177
+ );
178
+
179
+ const silent = await harness.makeClient({}, { diagnosticsFreshnessTimeoutMs: 35, versionlessPublishQuiescenceMs: 5 });
180
+ const silentResult = await silent.client.diagnostics(silent.source);
181
+ const silentEvidence = {
182
+ outcome: "silent",
183
+ items: silentResult.items.map((entry) => entry.message),
184
+ transientError: silentResult.transientError ?? null,
185
+ ok: silentResult.items.length === 0 && silentResult.transientError?.kind === "freshness_timeout",
186
+ };
187
+ requireProbe(silentEvidence.ok, "silent diagnostics were labeled clean instead of timing out transiently");
188
+
189
+ const closedServer = await harness.makeClient(
190
+ {
191
+ capabilities: { diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false } },
192
+ diagnosticResponses: [{ action: "close" }],
193
+ },
194
+ { diagnosticsFreshnessTimeoutMs: 50, versionlessPublishQuiescenceMs: 5 },
195
+ );
196
+ await closedServer.client.openFile(closedServer.source);
197
+ let closedServerError = "";
198
+ try {
199
+ await closedServer.client.diagnostics(closedServer.source);
200
+ } catch (error) {
201
+ closedServerError = error instanceof Error ? error.message : String(error);
202
+ }
203
+ const closedServerEvidence = {
204
+ outcome: "closed-server",
205
+ error: closedServerError,
206
+ ok: /exited|closed/i.test(closedServerError),
207
+ };
208
+ requireProbe(closedServerEvidence.ok, "closed-server diagnostics failure was not surfaced as a transport error");
209
+
210
+ const unsupportedPull = await harness.makeClient(
211
+ {
212
+ capabilities: { diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false } },
213
+ publishDiagnostics: [
214
+ {
215
+ trigger: "didOpen",
216
+ version: 1,
217
+ diagnostics: [diagnostic("push-fallback")],
218
+ },
219
+ ],
220
+ diagnosticResponses: [{ error: { code: -32601, message: "Method not found" } }],
221
+ },
222
+ { diagnosticsFreshnessTimeoutMs: 50, versionlessPublishQuiescenceMs: 5 },
223
+ );
224
+ const unsupportedPullResult = await unsupportedPull.client.diagnostics(unsupportedPull.source);
225
+ const unsupportedPullEvidence = {
226
+ outcome: "unsupported-pull",
227
+ items: unsupportedPullResult.items.map((entry) => entry.message),
228
+ ok: JSON.stringify(unsupportedPullResult.items) === JSON.stringify([diagnostic("push-fallback")]),
229
+ };
230
+ requireProbe(unsupportedPullEvidence.ok, "explicitly unsupported pull did not fall back to push diagnostics");
231
+
232
+ const unchanged = await harness.makeClient(
233
+ {
234
+ capabilities: { diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false } },
235
+ diagnosticResponses: [
236
+ {
237
+ report: { kind: "full", resultId: "same-version", items: [diagnostic("cached-full")] },
238
+ },
239
+ {
240
+ report: { kind: "unchanged", resultId: "same-version" },
241
+ },
242
+ ],
243
+ },
244
+ { diagnosticsFreshnessTimeoutMs: 50, versionlessPublishQuiescenceMs: 5 },
245
+ );
246
+ await unchanged.client.openFile(unchanged.source);
247
+ const unchangedFirst = await unchanged.client.diagnostics(unchanged.source);
248
+ const unchangedSecond = await unchanged.client.diagnostics(unchanged.source);
249
+ const unchangedEvidence = {
250
+ outcome: "same-version-unchanged",
251
+ firstItems: unchangedFirst.items.map((entry) => entry.message),
252
+ secondItems: unchangedSecond.items.map((entry) => entry.message),
253
+ ok:
254
+ JSON.stringify(unchangedFirst.items) === JSON.stringify([diagnostic("cached-full")])
255
+ && JSON.stringify(unchangedSecond.items) === JSON.stringify([diagnostic("cached-full")]),
256
+ };
257
+ requireProbe(unchangedEvidence.ok, "unchanged pull did not reuse the cached same-version result");
258
+
259
+ writeFileSync(
260
+ outputPath,
261
+ `${JSON.stringify(
262
+ {
263
+ schemaVersion: 1,
264
+ outcomes: {
265
+ exactCurrent: exactCurrentEvidence,
266
+ postGenerationVersionless: postGenerationVersionlessEvidence,
267
+ stale: staleEvidence,
268
+ future: futureEvidence,
269
+ pullOvertaken: pullOvertakenEvidence,
270
+ silent: silentEvidence,
271
+ closedServer: closedServerEvidence,
272
+ unsupportedPull: unsupportedPullEvidence,
273
+ sameVersionUnchanged: unchangedEvidence,
274
+ },
275
+ },
276
+ null,
277
+ 2,
278
+ )}\n`,
279
+ "utf-8",
280
+ );
281
+ } finally {
282
+ await harness.cleanup();
283
+ }