@mrclrchtr/supi-code-intelligence 5.0.0 → 6.0.0

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 (139) hide show
  1. package/README.md +2 -2
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  4. package/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  5. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -3
  6. package/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  8. package/node_modules/@mrclrchtr/supi-lsp/README.md +34 -3
  9. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  11. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  12. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -3
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  15. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  16. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +12 -1
  17. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +67 -28
  18. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +63 -6
  19. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +46 -5
  20. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +1 -0
  21. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +214 -0
  22. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +278 -79
  23. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +29 -8
  24. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +101 -19
  25. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +19 -1
  26. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +49 -3
  27. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +73 -15
  28. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +1 -1
  29. package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +2 -2
  30. package/node_modules/@mrclrchtr/supi-lsp/src/config/config.ts +5 -5
  31. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +3 -0
  32. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-extends.ts +68 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-path.ts +8 -0
  34. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +176 -24
  35. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +4 -0
  36. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +46 -10
  37. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +4 -3
  38. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +65 -17
  39. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +143 -21
  40. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +2 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +43 -1
  42. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +31 -0
  43. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  44. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +2 -2
  45. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
  46. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -3
  47. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
  48. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
  49. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
  50. package/package.json +5 -5
  51. package/src/analysis/health/diagnostics.ts +58 -126
  52. package/src/analysis/health/file-scope.ts +184 -0
  53. package/src/analysis/health/recovery.ts +8 -1
  54. package/src/analysis/readiness.ts +4 -0
  55. package/src/analysis/search/ast-scan.ts +4 -1
  56. package/src/analysis/search/pattern-analysis.ts +10 -3
  57. package/src/analysis/search/pattern.ts +6 -1
  58. package/src/config.ts +35 -1
  59. package/src/extension.ts +57 -14
  60. package/src/overview/overview-data.ts +9 -2
  61. package/src/overview/overview.ts +49 -30
  62. package/src/overview/types.ts +2 -0
  63. package/src/session/find-types.ts +2 -2
  64. package/src/session/find-workflow.ts +3 -2
  65. package/src/session/health-refresh.ts +76 -49
  66. package/src/session/health-types.ts +7 -1
  67. package/src/session/health-workflow.ts +35 -18
  68. package/src/session/input/workflows.ts +2 -2
  69. package/src/session/inspect-workflow.ts +2 -1
  70. package/src/session/orientation/collect.ts +2 -2
  71. package/src/session/orientation-types.ts +2 -0
  72. package/src/session/orientation-workflow.ts +8 -2
  73. package/src/session/refactor-workflow.ts +2 -1
  74. package/src/session/session.ts +25 -1
  75. package/src/substrate/lsp/maintenance.ts +105 -23
  76. package/src/substrate/lsp/recovery.ts +11 -5
  77. package/src/tool/code_find/execute.ts +23 -0
  78. package/src/tool/code_find/guidance.ts +8 -0
  79. package/src/tool/{find → code_find}/render.ts +1 -1
  80. package/src/tool/{result/find.ts → code_find/result.ts} +36 -11
  81. package/src/tool/code_find/spec.ts +37 -0
  82. package/src/tool/code_graph/execute.ts +24 -0
  83. package/src/tool/code_graph/guidance.ts +6 -0
  84. package/src/tool/{graph → code_graph}/markdown.ts +1 -1
  85. package/src/tool/{result/graph.ts → code_graph/result.ts} +69 -4
  86. package/src/tool/code_graph/spec.ts +39 -0
  87. package/src/tool/{health → code_health}/execute.ts +2 -18
  88. package/src/tool/code_health/guidance.ts +8 -0
  89. package/src/tool/{health → code_health}/markdown.ts +68 -18
  90. package/src/tool/{result/health.ts → code_health/result.ts} +29 -4
  91. package/src/tool/code_health/spec.ts +41 -0
  92. package/src/tool/code_inspect/execute.ts +20 -0
  93. package/src/tool/code_inspect/guidance.ts +6 -0
  94. package/src/tool/{inspect → code_inspect}/markdown.ts +1 -1
  95. package/src/tool/{result/inspect.ts → code_inspect/result.ts} +32 -3
  96. package/src/tool/code_inspect/spec.ts +25 -0
  97. package/src/tool/code_orientation/execute.ts +25 -0
  98. package/src/tool/code_orientation/guidance.ts +9 -0
  99. package/src/tool/{orientation → code_orientation}/markdown.ts +1 -1
  100. package/src/tool/{result/orientation.ts → code_orientation/result.ts} +67 -3
  101. package/src/tool/code_orientation/spec.ts +25 -0
  102. package/src/tool/code_refactor_apply/execute.ts +17 -0
  103. package/src/tool/code_refactor_apply/guidance.ts +5 -0
  104. package/src/tool/{refactor-apply/execute.ts → code_refactor_apply/result.ts} +5 -11
  105. package/src/tool/code_refactor_apply/spec.ts +26 -0
  106. package/src/tool/code_refactor_plan/execute.ts +27 -0
  107. package/src/tool/code_refactor_plan/guidance.ts +8 -0
  108. package/src/tool/{refactor-plan/execute.ts → code_refactor_plan/result.ts} +9 -22
  109. package/src/tool/code_refactor_plan/spec.ts +25 -0
  110. package/src/tool/code_resolve/execute.ts +19 -0
  111. package/src/tool/code_resolve/guidance.ts +8 -0
  112. package/src/tool/{resolve → code_resolve}/markdown.ts +1 -1
  113. package/src/tool/{result/resolve.ts → code_resolve/result.ts} +37 -24
  114. package/src/tool/code_resolve/spec.ts +25 -0
  115. package/src/tool/guidance.ts +71 -52
  116. package/src/tool/infra/truncate.ts +40 -0
  117. package/src/tool/{refactor-plan/markdown.ts → refactor-markdown.ts} +4 -7
  118. package/src/tool/register.ts +9 -97
  119. package/src/tool/result/refactor.ts +1 -4
  120. package/src/tool/schemas.ts +26 -157
  121. package/src/tool/specs.ts +40 -126
  122. package/src/tool/find/execute.ts +0 -43
  123. package/src/tool/graph/execute.ts +0 -82
  124. package/src/tool/inspect/execute.ts +0 -40
  125. package/src/tool/orientation/execute.ts +0 -80
  126. package/src/tool/resolve/execute.ts +0 -53
  127. /package/src/tool/{find → code_find}/ast-kinds.ts +0 -0
  128. /package/src/tool/{find → code_find}/markdown.ts +0 -0
  129. /package/src/tool/{find → code_find}/modes.ts +0 -0
  130. /package/src/tool/{find → code_find}/tui.ts +0 -0
  131. /package/src/tool/{graph → code_graph}/tui.ts +0 -0
  132. /package/src/tool/{health → code_health}/refresh-status.ts +0 -0
  133. /package/src/tool/{health → code_health}/semantic-state.ts +0 -0
  134. /package/src/tool/{health → code_health}/tui.ts +0 -0
  135. /package/src/tool/{inspect → code_inspect}/tui.ts +0 -0
  136. /package/src/tool/{orientation → code_orientation}/tui.ts +0 -0
  137. /package/src/tool/{refactor-apply → code_refactor_apply}/tui.ts +0 -0
  138. /package/src/tool/{refactor-plan → code_refactor_plan}/tui.ts +0 -0
  139. /package/src/tool/{resolve → code_resolve}/tui.ts +0 -0
package/README.md CHANGED
@@ -30,7 +30,7 @@ After installation, keep asking Pi normal coding questions. The agent can:
30
30
  - **Refactor safely** — preview language-aware renames and extractions before applying them. Plans are rejected if the files changed in the meantime.
31
31
  - **See uncertainty clearly** — results distinguish “nothing found” from incomplete or unavailable analysis and disclose omitted matches.
32
32
 
33
- When Pi recognizes a workspace, the agent also receives an architecture overview near the start of the session, so it can orient before spending turns opening files.
33
+ When Pi recognizes a workspace, the agent also receives a compact architecture overview near the start of the session, so it can orient before spending turns opening files. The overview contains manifest facts (module names, one-line descriptions, declared topology, declared entrypoints, and detected languages), is labeled as untrusted repository evidence, and is controlled by the `code-intelligence.overviewEnabled` setting in `/supi-settings`.
34
34
 
35
35
  ## Example requests
36
36
 
@@ -127,7 +127,7 @@ Open the status view to see detected languages, running or missing servers, and
127
127
  /supi-ci-status
128
128
  ```
129
129
 
130
- Use `/supi-settings` to disable language servers you do not need or change the instruction filenames surfaced during directory orientation. The defaults are `CLAUDE.md` and `AGENTS.md`.
130
+ Use `/supi-settings` to disable language servers you do not need, change the instruction filenames surfaced during directory orientation (defaults `CLAUDE.md` and `AGENTS.md`), or disable the first-turn architecture overview with `overviewEnabled`.
131
131
 
132
132
  [workspace-orientation]: https://raw.githubusercontent.com/mrclrchtr/supi/main/packages/supi-code-intelligence/assets/workspace-orientation.png
133
133
  [symbol-inspection]: https://raw.githubusercontent.com/mrclrchtr/supi/main/packages/supi-code-intelligence/assets/symbol-inspection.png
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-code-runtime",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "Shared workspace context and capability contracts for code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "Shared settings, configuration, reporting, and session infrastructure",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -53,7 +53,7 @@
53
53
  "./api": "./src/api.ts",
54
54
  "./config": "./src/config.ts",
55
55
  "./context": "./src/context.ts",
56
- "./debug": "./src/debug-registry.ts",
56
+ "./debug": "./src/debug.ts",
57
57
  "./evidence-badge": "./src/evidence-badge.ts",
58
58
  "./footer-registry": "./src/footer-registry.ts",
59
59
  "./llm": "./src/llm.ts",
@@ -11,7 +11,7 @@ export * from "./config.ts";
11
11
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
12
12
  export * from "./context.ts";
13
13
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
14
- export * from "./debug-registry.ts";
14
+ export * from "./debug.ts";
15
15
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
16
16
  export * from "./evidence-badge.ts";
17
17
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
@@ -4,9 +4,6 @@
4
4
  // supi-debug extension owns policy/configuration and exposes events through a
5
5
  // command/tool while this module stays dependency-free for producers.
6
6
 
7
- // biome-ignore lint/performance/noReExportAll: preserve the stable debug domain entry point
8
- export * from "./debug-timing.ts";
9
-
10
7
  export type DebugLevel = "debug" | "info" | "warning" | "error";
11
8
  export type DebugAgentAccess = "off" | "sanitized" | "raw";
12
9
  export interface DebugRegistryConfig {
@@ -0,0 +1,9 @@
1
+ // Debug domain entry for `@mrclrchtr/supi-core/debug`.
2
+ //
3
+ // Kept separate from debug-registry.ts so debug-timing.ts can import the
4
+ // registry without creating an import cycle through the barrel re-export.
5
+
6
+ // biome-ignore lint/performance/noReExportAll: preserve the stable debug domain entry point
7
+ export * from "./debug-registry.ts";
8
+ // biome-ignore lint/performance/noReExportAll: preserve the stable debug domain entry point
9
+ export * from "./debug-timing.ts";
@@ -11,7 +11,7 @@ export * from "./config.ts";
11
11
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
12
12
  export * from "./context.ts";
13
13
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
14
- export * from "./debug-registry.ts";
14
+ export * from "./debug.ts";
15
15
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
16
16
  export * from "./footer-registry.ts";
17
17
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
@@ -37,17 +37,26 @@ The table records an initialize-handshake audit performed on 2026-08-21 against
37
37
  | SQL | `sql-language-server` 1.7.1 | No (confirmed) | Push | No `diagnosticProvider` in the initialize result. |
38
38
  | R | `R` 4.6.1 (languageserver) | No (confirmed) | Push | No `diagnosticProvider` in the initialize result. |
39
39
 
40
- SuPi advertises static and dynamic pull support (`textDocument.diagnostic.dynamicRegistration: true`). A server that declares a valid `diagnosticProvider` in its initialize result gets pull diagnostics; a server that registers `textDocument/diagnostic` after initialization gets pull diagnostics for as long as its registration stays active. Registration parameters are validated and malformed values reject the request without enabling pull. Other registration methods are ignored. Protocol support is separate from the configured mode: a server may support pull yet stay in SuPi's push mode because the built-in configuration does not enable it.
40
+ SuPi advertises static and dynamic pull support. It advertises server-requested refresh support too:
41
+
42
+ - `textDocument.diagnostic.dynamicRegistration: true`
43
+ - `workspace.diagnostics.refreshSupport: true`
44
+
45
+ A server gets pull diagnostics when it declares a valid `diagnosticProvider` during initialization. A server also gets pull diagnostics after it registers `textDocument/diagnostic`. The pull support stays active until the server removes the registration. SuPi validates registration parameters. Invalid parameters do not enable pull support. SuPi ignores other registration methods.
46
+
47
+ When a server sends `workspace/diagnostic/refresh`, SuPi returns `null` immediately. It then refreshes the owning client's tracked documents in the background. The refresh covers open, cached, and failed tracked documents. SuPi does not add workspace-wide `workspace/diagnostic` pulls.
48
+
49
+ Protocol support is separate from the configured mode. A server may support pull diagnostics and still use SuPi's push mode because the built-in configuration does not enable pull mode.
41
50
 
42
51
  The LSP 3.18 specification adds `Diagnostic.message` markup content, guarded by the client capability `textDocument.diagnostic.markupMessageSupport`; SuPi's validator already accepts plaintext and markdown messages but does not advertise the capability. Other 3.18 features (snippet text edits, inline completion, folding-range refresh, multi-range formatting) are outside the diagnostic surface and are not implemented.
43
52
 
44
53
  Pull diagnostics use `textDocument/diagnostic`, so the client can tie a report to the current request. Push diagnostics are asynchronous and can omit a document version. After a workspace change, SuPi may report push-only diagnostics as partial or unavailable when it cannot prove that the result matches the current document. It does not treat missing fresh evidence as a clean file.
45
54
 
46
- Unversioned pushes are accepted for an open document when they arrive after the document's sync moment (the client-side instant the `didChange` or `didOpen` that produced them was sent) and are re-stamped with the current synchronization; unversioned pushes for closed or untracked URIs, and pushes that arrive before a sync moment, stay fail-closed. On push-only routes, a document still unconfirmed after the settle window is closed and reopened over the protocol so the server publishes on `didOpen` (clean files get no push on `didChange` at all). The reopen keeps the cache entry and version history intact and does not restart the server.
55
+ Unversioned pushes are accepted for an open document when they arrive after the document's sync moment (the client-side instant the `didChange` or `didOpen` that produced them was sent) and are re-stamped with the current synchronization; unversioned pushes for closed or untracked URIs, and pushes that arrive before a sync moment, stay fail-closed. On push-only routes, the first valid publication for a synchronization is tentative. A later valid publication for the same synchronization confirms it, and every publication restarts the quiet period. If no publication arrives, the existing bounded reopen path may ask the server to publish on `didOpen`; a tentative timeout does not reopen that document. In a mixed batch, silent documents can still use the reopen path. Non-empty tentative diagnostics are visible as partial evidence, but they do not enter the confirmed path. An empty tentative publication cannot establish a clean file and stays unavailable until a diagnostic republish arrives. Repeated unchanged queries share the tentative publication's wait age instead of starting a new full wait. A late republish promotes the cache without a new refresh.
47
56
 
48
57
  Server readiness follows LSP work-done progress: a created progress token is pending and never blocks readiness; an observed `begin` marks active work and makes the client not ready until its `end` or the bounded per-token timeout.
49
58
 
50
- A workspace diagnostic refresh returns exact coverage counts for requested, confirmed, unconfirmed, failed, and removed tracked documents. `code_health` marks tracked-file diagnostics as complete only when every document in the requested scope has confirmed evidence. It keeps cached diagnostics as partial evidence and shows the same coverage counts in summary and detailed views; a refresh attempt does not prove fresh evidence by itself.
59
+ A workspace diagnostic refresh returns exact coverage counts for requested, confirmed, unconfirmed, failed, and removed tracked documents. `code_health` marks tracked-file diagnostics as complete when each requested document is confirmed or known to be removed. It shows non-empty tentative diagnostics as partial entries, reports the same coverage counts in summary and detailed views, and explains when a diagnostic republish is needed. A refresh attempt does not prove fresh evidence by itself. A removed file is reported by the refresh that finds it and is not retained in later tracked-file snapshots.
51
60
 
52
61
  An explicit recovery pass restarts a push-only client only on a protocol-stall signal (a readiness stall, or repeated JSON-RPC request failures) — never on unconfirmed evidence alone, because the reopen-resync fallback recovers unconfirmed documents without discarding warm server state. It never restarts a pull-capable client because push evidence is absent, and it never restarts a client during passive health display. Each client route restarts at most once per workspace invalidation generation. The replacement process has a fixed startup bound of 5 seconds; exceeding the bound fails closed as start-failed without retry. Recovery telemetry records the outcome, elapsed time, attempted clients, restart count, the bounded server names involved, and the stall signal that triggered a restart, without changing the evidence semantics of the result.
53
62
 
@@ -71,6 +80,28 @@ Configuration overrides merge with the built-in server definitions. Use `.pi/sup
71
80
 
72
81
  Gopls pull diagnostics stay opt-in while golang/go#70199 is open; without the option the built-in Go configuration stays in push mode. Kotlin's `--stdio` argument is already part of the built-in configuration and needs no override.
73
82
 
83
+ ### Custom server configuration
84
+
85
+ A custom server needs a command and at least one file type:
86
+
87
+ ```json
88
+ {
89
+ "lsp": {
90
+ "servers": {
91
+ "custom": {
92
+ "command": "custom-lsp",
93
+ "args": ["--stdio"],
94
+ "fileTypes": ["custom"],
95
+ "env": { "CUSTOM_LSP_LOG": "debug" },
96
+ "initializationOptions": { "mode": "project" }
97
+ }
98
+ }
99
+ }
100
+ }
101
+ ```
102
+
103
+ File types do not include a leading dot. If `rootMarkers` is omitted, the server uses the session root.
104
+
74
105
  The `lsp.enabled` and `lsp.active` settings are deprecated and ignored. Disable one language with `lsp.servers.<language>.enabled: false`. Use `/supi-ci-status` from `@mrclrchtr/supi-code-intelligence` to see detected, running, and missing servers.
75
106
 
76
107
  ## What it provides
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-code-runtime",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "Shared workspace context and capability contracts for code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "Shared settings, configuration, reporting, and session infrastructure",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -53,7 +53,7 @@
53
53
  "./api": "./src/api.ts",
54
54
  "./config": "./src/config.ts",
55
55
  "./context": "./src/context.ts",
56
- "./debug": "./src/debug-registry.ts",
56
+ "./debug": "./src/debug.ts",
57
57
  "./evidence-badge": "./src/evidence-badge.ts",
58
58
  "./footer-registry": "./src/footer-registry.ts",
59
59
  "./llm": "./src/llm.ts",
@@ -11,7 +11,7 @@ export * from "./config.ts";
11
11
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
12
12
  export * from "./context.ts";
13
13
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
14
- export * from "./debug-registry.ts";
14
+ export * from "./debug.ts";
15
15
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
16
16
  export * from "./evidence-badge.ts";
17
17
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
@@ -4,9 +4,6 @@
4
4
  // supi-debug extension owns policy/configuration and exposes events through a
5
5
  // command/tool while this module stays dependency-free for producers.
6
6
 
7
- // biome-ignore lint/performance/noReExportAll: preserve the stable debug domain entry point
8
- export * from "./debug-timing.ts";
9
-
10
7
  export type DebugLevel = "debug" | "info" | "warning" | "error";
11
8
  export type DebugAgentAccess = "off" | "sanitized" | "raw";
12
9
  export interface DebugRegistryConfig {
@@ -0,0 +1,9 @@
1
+ // Debug domain entry for `@mrclrchtr/supi-core/debug`.
2
+ //
3
+ // Kept separate from debug-registry.ts so debug-timing.ts can import the
4
+ // registry without creating an import cycle through the barrel re-export.
5
+
6
+ // biome-ignore lint/performance/noReExportAll: preserve the stable debug domain entry point
7
+ export * from "./debug-registry.ts";
8
+ // biome-ignore lint/performance/noReExportAll: preserve the stable debug domain entry point
9
+ export * from "./debug-timing.ts";
@@ -11,7 +11,7 @@ export * from "./config.ts";
11
11
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
12
12
  export * from "./context.ts";
13
13
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
14
- export * from "./debug-registry.ts";
14
+ export * from "./debug.ts";
15
15
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
16
16
  export * from "./footer-registry.ts";
17
17
  // biome-ignore lint/performance/noReExportAll: intentional convenience barrel
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-lsp",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "Language Server Protocol runtime for SuPi code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -37,8 +37,8 @@
37
37
  "vscode-jsonrpc": "^9.0.0",
38
38
  "vscode-languageserver-protocol": "^3.18.0",
39
39
  "vscode-languageserver-types": "^3.18.0",
40
- "@mrclrchtr/supi-code-runtime": "5.0.0",
41
- "@mrclrchtr/supi-core": "5.0.0"
40
+ "@mrclrchtr/supi-code-runtime": "6.0.0",
41
+ "@mrclrchtr/supi-core": "6.0.0"
42
42
  },
43
43
  "bundledDependencies": [
44
44
  "@mrclrchtr/supi-code-runtime",
@@ -8,7 +8,16 @@ export {
8
8
  hasDeprecatedLspKeys,
9
9
  loadLspSettings,
10
10
  } from "./config/lsp-settings.ts";
11
- export { clearTsconfigCache } from "./config/tsconfig-scope.ts";
11
+ export {
12
+ clearTsconfigCache,
13
+ type FileScopeDecision,
14
+ type FileScopeStatus,
15
+ getFileScopeDecision,
16
+ invalidateTsconfigCacheForConfig,
17
+ invalidateTsconfigCacheForConfigDir,
18
+ isProjectConfigFileName,
19
+ type ScopeDecisionBasis,
20
+ } from "./config/tsconfig-scope.ts";
12
21
  export type {
13
22
  CodeAction,
14
23
  Diagnostic,
@@ -28,6 +37,7 @@ export type {
28
37
  } from "./config/types.ts";
29
38
  export { FileChangeType } from "./config/types.ts";
30
39
  export { toLspPosition, toOneBasedPosition } from "./coordinates.ts";
40
+ export { TENTATIVE_PUSH_UNAVAILABLE_REASON } from "./diagnostics/evidence.ts";
31
41
  export { isLikelyStaleDiagnostic } from "./diagnostics/stale-diagnostics.ts";
32
42
  export {
33
43
  scanWorkspaceSentinels,
@@ -50,6 +60,7 @@ export type {
50
60
  RecoverDiagnosticsResult,
51
61
  RoutedMutationResponse,
52
62
  SemanticReadinessResult,
63
+ WorkspaceDiagnosticReport,
53
64
  WorkspaceDiagnosticSnapshot,
54
65
  WorkspaceDiagnosticSummaryEntry,
55
66
  WorkspaceLspDiagnosticSurface,
@@ -2,6 +2,7 @@ import type { PublishDiagnosticsParams } from "../config/types.ts";
2
2
  import { uriToFile } from "../utils.ts";
3
3
  import {
4
4
  type DiagnosticCacheEntry,
5
+ isTentativePushEntry,
5
6
  isValidPublishDiagnosticsParams,
6
7
  } from "./client-diagnostic-evidence.ts";
7
8
  import type { ClientDiagnosticSnapshot, OpenDocumentState } from "./client-document-state.ts";
@@ -20,27 +21,44 @@ interface ApplyPushOptions {
20
21
  }
21
22
 
22
23
  /**
23
- * Apply one valid push publication and return whether it can release current waiters.
24
+ * Apply one valid push publication.
24
25
  *
25
- * Fail-closed policy (ADR 0020): unversioned pushes are rejected for closed
26
- * and untracked URIs and for arrivals before the URI's sync moment; versioned
27
- * pushes for a URI closed by a lifecycle operation are rejected because their
28
- * version cannot be verified. An unversioned push that arrives after the sync
29
- * moment of an open document is accepted and re-stamped with that document's
30
- * current synchronization state.
26
+ * Returns whether the publication was accepted and whether it promoted a
27
+ * tentative entry to confirmed. Fail-closed policy (ADR 0020): unversioned
28
+ * pushes are rejected for closed and untracked URIs and for arrivals before
29
+ * the URI's sync moment; versioned pushes for a URI closed by a lifecycle
30
+ * operation are rejected because their version cannot be verified. An
31
+ * unversioned push that arrives after the sync moment of an open document
32
+ * is accepted and re-stamped with that document's current synchronization
33
+ * state. The first valid publication for a synchronization is tentative; a
34
+ * later valid publication for the same synchronization ID and evidence
35
+ * revision promotes the cache to confirmed (ADR 0021).
31
36
  */
32
- export function applyPushDiagnostics(options: ApplyPushOptions): boolean {
33
- if (!isValidPublishDiagnosticsParams(options.params)) return false;
37
+ export function applyPushDiagnostics(options: ApplyPushOptions): {
38
+ accepted: boolean;
39
+ promoted: boolean;
40
+ } {
41
+ if (!isValidPublishDiagnosticsParams(options.params)) return { accepted: false, promoted: false };
34
42
  const openDocument = options.openDocuments.get(options.params.uri);
35
43
  if (options.params.version !== undefined) {
36
- if (!Number.isInteger(options.params.version)) return false;
37
- if (openDocument && options.params.version !== openDocument.version) return false;
38
- if (!openDocument && options.closedVersionedBarrier) return false;
44
+ if (!Number.isInteger(options.params.version)) return { accepted: false, promoted: false };
45
+ if (openDocument && options.params.version !== openDocument.version) {
46
+ return { accepted: false, promoted: false };
47
+ }
48
+ if (!openDocument && options.closedVersionedBarrier)
49
+ return { accepted: false, promoted: false };
39
50
  }
40
- if (!Array.isArray(options.params.diagnostics)) return false;
41
- if (!acceptUnversionedPush(options, openDocument)) return false;
42
- options.store.set(options.params.uri, buildPushCacheEntry(options, openDocument));
43
- return true;
51
+ if (!Array.isArray(options.params.diagnostics)) return { accepted: false, promoted: false };
52
+ if (!acceptUnversionedPush(options, openDocument)) return { accepted: false, promoted: false };
53
+ const entry = buildPushCacheEntry(options, openDocument);
54
+ const previous = options.store.get(options.params.uri);
55
+ const promoted =
56
+ entry.source === "push" &&
57
+ entry.publications !== undefined &&
58
+ entry.publications >= 2 &&
59
+ isTentativePushEntry(previous);
60
+ options.store.set(options.params.uri, entry);
61
+ return { accepted: true, promoted };
44
62
  }
45
63
 
46
64
  /** Gate one unversioned push publication against the sync-moment policy. */
@@ -69,13 +87,27 @@ function buildPushCacheEntry(
69
87
  ): DiagnosticCacheEntry {
70
88
  const currentRevision = openDocument?.evidenceRevision === options.evidenceRevision;
71
89
  const unversioned = options.params.version === undefined && openDocument !== undefined;
90
+ const synchronizationId =
91
+ unversioned || currentRevision ? openDocument?.synchronizationId : undefined;
92
+ const evidenceRevision = openDocument?.evidenceRevision;
93
+ const previous = options.store.get(options.params.uri);
94
+ // A later valid publication for the same synchronization ID and evidence
95
+ // revision continues the entry's publication count: a confirmed pull
96
+ // entry keeps the synchronization confirmed, and a tentative push entry
97
+ // is promoted. Any other publication starts a fresh count.
98
+ const sameSynchronization =
99
+ synchronizationId !== undefined &&
100
+ previous?.synchronizationId === synchronizationId &&
101
+ previous.evidenceRevision === evidenceRevision;
102
+ const publications = sameSynchronization ? Math.min((previous.publications ?? 1) + 1, 2) : 1;
72
103
  return {
73
104
  diagnostics: options.params.diagnostics,
74
105
  receivedAt: Date.now(),
75
106
  source: "push",
76
- synchronizationId: unversioned || currentRevision ? openDocument?.synchronizationId : undefined,
77
- evidenceRevision: openDocument?.evidenceRevision,
107
+ synchronizationId,
108
+ evidenceRevision,
78
109
  version: options.params.version,
110
+ publications,
79
111
  };
80
112
  }
81
113
 
@@ -119,17 +151,18 @@ function collectOpenDocumentFreshness(options: {
119
151
  const uris = new Set<string>();
120
152
  for (const [uri, document] of options.openDocuments) {
121
153
  uris.add(uri);
154
+ const entry = options.store.get(uri);
122
155
  const fileState = getDiagnosticFileState(uriToFile(uri));
123
- const current = hasCurrentDiagnosticEvidence(
124
- document,
125
- options.store.get(uri),
126
- options.evidenceRevision,
127
- );
156
+ const current = hasCurrentDiagnosticEvidence(document, entry, options.evidenceRevision);
128
157
  const failed = options.failedDocuments.has(uri);
158
+ // A current tentative push maps to unconfirmed, not to a new public
159
+ // status: it matches the synchronization but cannot confirm it yet, so
160
+ // it must not claim current snapshot or document state (issue #351).
161
+ const tentative = current && isTentativePushEntry(entry);
129
162
  documents.push({
130
163
  uri,
131
- current: fileState === "present" && current && !failed,
132
- status: statusForDocument(fileState, current, failed),
164
+ current: fileState === "present" && current && !failed && !tentative,
165
+ status: statusForDocument(fileState, current, failed, tentative),
133
166
  });
134
167
  }
135
168
  return { documents, uris };
@@ -174,10 +207,12 @@ function statusForDocument(
174
207
  fileState: ReturnType<typeof getDiagnosticFileState>,
175
208
  current: boolean,
176
209
  failed: boolean,
210
+ tentative: boolean,
177
211
  ): "confirmed" | "unconfirmed" | "failed" | "removed" {
178
212
  if (fileState === "removed") return "removed";
179
213
  if (fileState === "unreadable" || failed) return "failed";
180
- return current ? "confirmed" : "unconfirmed";
214
+ if (tentative || !current) return "unconfirmed";
215
+ return "confirmed";
181
216
  }
182
217
 
183
218
  function collectCachedDiagnostics(options: {
@@ -197,9 +232,13 @@ function collectCachedDiagnostics(options: {
197
232
  entry,
198
233
  options.evidenceRevision,
199
234
  );
200
- current &&= entryCurrent;
235
+ // A current tentative error is useful partial evidence. Keep its entry
236
+ // non-current so it cannot establish a clean or settled result (ADR 0021).
237
+ const tentative = entryCurrent && isTentativePushEntry(entry);
238
+ const confirmed = entryCurrent && !tentative;
239
+ current &&= confirmed;
201
240
  if (entry.diagnostics.length > 0) {
202
- entries.push({ uri, diagnostics: entry.diagnostics, current: entryCurrent });
241
+ entries.push({ uri, diagnostics: entry.diagnostics, current: confirmed });
203
242
  }
204
243
  }
205
244
  return { entries, current };
@@ -3,10 +3,12 @@ import {
3
3
  type CodeRequestControl,
4
4
  completedCodeQuery,
5
5
  isCodeRequestInterruption,
6
+ partialCodeQuery,
6
7
  throwIfCodeRequestInterrupted,
7
8
  unavailableCodeQuery,
8
9
  } from "@mrclrchtr/supi-code-runtime/api";
9
10
  import type { Diagnostic } from "../config/types.ts";
11
+ import { TENTATIVE_PUSH_UNAVAILABLE_REASON } from "../diagnostics/evidence.ts";
10
12
  import {
11
13
  type DiagnosticSynchronization,
12
14
  incompleteDiagnosticResult,
@@ -25,6 +27,8 @@ interface FileDiagnosticCollectionOptions {
25
27
  readonly waiters: DiagnosticWaitRegistry;
26
28
  readonly current: () => boolean;
27
29
  readonly freshPush: () => boolean;
30
+ /** Receive time of a current tentative push that is already cached. */
31
+ readonly currentPushReceivedAt: () => number | undefined;
28
32
  readonly diagnostics: () => Diagnostic[];
29
33
  readonly pullDiagnostics: (timeoutMs: number, signal: AbortSignal) => Promise<boolean>;
30
34
  /** Observe the push wait outcome without finishing the observer; the caller finishes once. */
@@ -58,22 +62,75 @@ export async function collectSynchronizedFileDiagnostics(
58
62
  return completedCodeQuery(options.diagnostics());
59
63
  }
60
64
 
61
- const push = await options.waiters.waitForPush(
62
- options.request.uri,
63
- Math.max(0, options.maxWaitMs - (Date.now() - options.syncStart)),
64
- control,
65
- );
65
+ const push = await waitForConfirmedPush(options, control);
66
66
  if (options.onPushWait) options.onPushWait(push);
67
67
  else options.observer.pushWaitCompleted(1, push);
68
- if (push === "published" && options.freshPush()) {
68
+ if (push === "published") {
69
69
  return completedCodeQuery(options.diagnostics());
70
70
  }
71
+ if (push === "tentative") {
72
+ // A current tentative error is useful partial evidence, but an empty
73
+ // publication cannot establish that the document is clean (ADR 0021).
74
+ const diagnostics = options.diagnostics();
75
+ return diagnostics.length > 0
76
+ ? partialCodeQuery(diagnostics, TENTATIVE_PUSH_UNAVAILABLE_REASON)
77
+ : unavailableCodeQuery(TENTATIVE_PUSH_UNAVAILABLE_REASON);
78
+ }
71
79
  return incompleteDiagnosticResult(
72
80
  options.cachedDiagnostics,
73
81
  push === "released" ? "released" : "timed-out",
74
82
  );
75
83
  }
76
84
 
85
+ /**
86
+ * Wait until a confirmed push publication settles the synchronization.
87
+ *
88
+ * Every accepted publication releases the waiter. A confirmed push ends the
89
+ * wait as "published". A publication that stays tentative (no republish
90
+ * arrived for the same synchronization) ends as "tentative" when the budget
91
+ * expires. A wait with no publication at all keeps the waiter's
92
+ * "timed-out", and a lifecycle release stays "released".
93
+ */
94
+ async function waitForConfirmedPush(
95
+ options: FileDiagnosticCollectionOptions,
96
+ control?: CodeRequestControl,
97
+ ): Promise<DiagnosticPushWaitOutcome> {
98
+ // A tentative publication may already sit in the cache when the wait
99
+ // starts (ADR 0021). Do not grant repeated callers a new full wait window:
100
+ // bound them by the first operation window or the cached publication age.
101
+ const initialPushReceivedAt = options.currentPushReceivedAt();
102
+ const waitDeadline = Math.min(
103
+ options.syncStart + options.maxWaitMs,
104
+ initialPushReceivedAt === undefined
105
+ ? Number.POSITIVE_INFINITY
106
+ : initialPushReceivedAt + options.maxWaitMs,
107
+ );
108
+ let observedPublication = initialPushReceivedAt !== undefined;
109
+ for (;;) {
110
+ throwIfCodeRequestInterrupted(control);
111
+ // A republish can arrive after the previous wait released but before the
112
+ // next waiter registers; re-check the store on every loop pass.
113
+ if (options.freshPush()) return "published";
114
+ const push = await options.waiters.waitForPush(
115
+ options.request.uri,
116
+ Math.max(0, waitDeadline - Date.now()),
117
+ control,
118
+ );
119
+ // A lifecycle release ends the wait definitively: a tentative
120
+ // publication observed earlier must not reclassify the release.
121
+ if (push === "released") return "released";
122
+ // A promotion can land while the timed-out waiter tears down; the cache
123
+ // must complete the wait before the timeout classifies the outcome.
124
+ if (options.freshPush()) return "published";
125
+ if (push === "timed-out") {
126
+ return observedPublication || options.currentPushReceivedAt() !== undefined
127
+ ? "tentative"
128
+ : "timed-out";
129
+ }
130
+ observedPublication = true;
131
+ }
132
+ }
133
+
77
134
  async function collectPullEvidence(
78
135
  options: FileDiagnosticCollectionOptions,
79
136
  control?: CodeRequestControl,
@@ -163,6 +163,25 @@ export interface DiagnosticCacheEntry {
163
163
  evidenceRevision?: number;
164
164
  version?: number;
165
165
  resultId?: string;
166
+ /**
167
+ * Valid publications observed for the entry's synchronization. The first
168
+ * valid publication for a synchronization is tentative; a later valid
169
+ * publication promotes the entry to confirmed (ADR 0021). A confirmed
170
+ * pull entry counts as the first publication when a push continues the
171
+ * same synchronization, so that push keeps the confirmation. Pull
172
+ * entries as such carry no count and are confirmed on arrival.
173
+ */
174
+ publications?: number;
175
+ }
176
+
177
+ /**
178
+ * Test whether one push entry is still tentative for its synchronization.
179
+ *
180
+ * A pull entry confirms on arrival; a push entry confirms only after a
181
+ * later valid publication for the same synchronization (ADR 0021).
182
+ */
183
+ export function isTentativePushEntry(entry: DiagnosticCacheEntry | undefined): boolean {
184
+ return Boolean(entry && entry.source === "push" && (entry.publications ?? 1) < 2);
166
185
  }
167
186
 
168
187
  /** One document synchronization that needs current diagnostic evidence. */
@@ -204,6 +223,7 @@ export function hasFreshPush(
204
223
  const entry = store.get(synchronization.uri);
205
224
  return Boolean(
206
225
  entry?.source === "push" &&
226
+ !isTentativePushEntry(entry) &&
207
227
  entry.synchronizationId === synchronization.synchronizationId &&
208
228
  (synchronization.evidenceRevision === undefined ||
209
229
  entry.evidenceRevision === synchronization.evidenceRevision) &&
@@ -211,8 +231,11 @@ export function hasFreshPush(
211
231
  );
212
232
  }
213
233
 
214
- /** Test whether pull or push evidence confirms the supplied synchronization. */
215
- export function hasFreshEvidence(
234
+ /**
235
+ * Test whether stored evidence matches a synchronization without a
236
+ * confirmation gate. A tentative push matches here but cannot confirm.
237
+ */
238
+ export function hasCurrentEvidence(
216
239
  store: ReadonlyMap<string, DiagnosticCacheEntry>,
217
240
  synchronization: DiagnosticSynchronization,
218
241
  currentEvidenceRevision?: number,
@@ -227,8 +250,26 @@ export function hasFreshEvidence(
227
250
  );
228
251
  }
229
252
 
230
- /** Return the latest cache update that confirms one of the synchronizations. */
231
- export function latestFreshEvidenceReceivedAt(
253
+ /** Test whether pull or push evidence confirms the supplied synchronization. */
254
+ export function hasFreshEvidence(
255
+ store: ReadonlyMap<string, DiagnosticCacheEntry>,
256
+ synchronization: DiagnosticSynchronization,
257
+ currentEvidenceRevision?: number,
258
+ ): boolean {
259
+ return (
260
+ hasCurrentEvidence(store, synchronization, currentEvidenceRevision) &&
261
+ !isTentativePushEntry(store.get(synchronization.uri))
262
+ );
263
+ }
264
+
265
+ /**
266
+ * Return the latest cache update that matches one of the synchronizations.
267
+ *
268
+ * Every accepted publication restarts the settle quiet period, tentative
269
+ * publications included, so this helper does not apply the confirmation
270
+ * gate (ADR 0021).
271
+ */
272
+ export function latestCurrentEvidenceReceivedAt(
232
273
  store: ReadonlyMap<string, DiagnosticCacheEntry>,
233
274
  synchronizations: DiagnosticSynchronization[],
234
275
  currentEvidenceRevision?: number,
@@ -236,7 +277,7 @@ export function latestFreshEvidenceReceivedAt(
236
277
  let latest = 0;
237
278
  for (const synchronization of synchronizations) {
238
279
  const entry = store.get(synchronization.uri);
239
- if (hasFreshEvidence(store, synchronization, currentEvidenceRevision)) {
280
+ if (hasCurrentEvidence(store, synchronization, currentEvidenceRevision)) {
240
281
  latest = Math.max(latest, entry?.receivedAt ?? 0);
241
282
  }
242
283
  }
@@ -9,6 +9,7 @@ export interface ClientDiagnosticsHost {
9
9
  cwd?: string;
10
10
  isOperational(): boolean;
11
11
  supportsPullDiagnostics(): boolean;
12
+ usesIncrementalDocumentSync(): boolean;
12
13
  sendNotification(method: string, params: unknown): void;
13
14
  pullDocumentDiagnostics(request: DiagnosticPullRequest): Promise<DocumentDiagnosticReport | null>;
14
15
  }