candor-ts 0.8.3 → 0.8.4
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.
- package/lsp.mjs +48 -1
- package/package.json +1 -1
package/lsp.mjs
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* how many functions transitively call it (who is affected if it changes).
|
|
8
8
|
* • Diagnostics: the repo's architecture-policy verdict (the §6.2 gate, resolved from CANDOR_POLICY
|
|
9
9
|
* or the checked-in .candor/config — spec §3.4) as squiggles at each violating function's line.
|
|
10
|
+
* • Hover: effect PROVENANCE — for each inherited effect, the `path` hop chain to the function that
|
|
11
|
+
* performs it directly ("Net via mid → leaf (source)"), plus unknownWhy when the fn discloses opacity.
|
|
10
12
|
*
|
|
11
13
|
* The server is a pure CONSUMER of the spec report envelope + callgraph sidecar (any engine — JVM /
|
|
12
14
|
* Rust / TS / Swift / agents; the same read layer as candor-mcp), and it never scans (the analyzer
|
|
@@ -29,7 +31,10 @@ import { fileURLToPath } from "node:url";
|
|
|
29
31
|
import * as Q from "./query-core.mjs";
|
|
30
32
|
import { discoverConfigPolicy, evaluatePolicy, parsePolicy } from "./policy.mjs";
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
// Version: from the sibling package.json when running inside the npm package; a single-file BUNDLE of
|
|
35
|
+
// this server (the IDE-plugin embedding) has no sibling package.json — fall back rather than crash.
|
|
36
|
+
let VERSION = "bundled";
|
|
37
|
+
try { VERSION = createRequire(import.meta.url)("./package.json").version; } catch { /* bundled */ }
|
|
33
38
|
|
|
34
39
|
// ---- state (set at initialize) ---------------------------------------------------------------------
|
|
35
40
|
let rootPath = null;
|
|
@@ -96,6 +101,43 @@ function codeLenses(docPath) {
|
|
|
96
101
|
});
|
|
97
102
|
}
|
|
98
103
|
|
|
104
|
+
// ---- Hover: effect provenance at the cursor ----------------------------------------------------------
|
|
105
|
+
// The entry ENCLOSING the hovered line: the report pins each fn at its declaration line, so the match is
|
|
106
|
+
// the greatest entry line ≤ the cursor (functions are sequential in a file — a sound approximation that
|
|
107
|
+
// needs no parser). For each inferred effect: direct → "performed here"; inherited → the §3.1 `path`
|
|
108
|
+
// chain to the direct source. unknownWhy rides along when the fn introduces opacity.
|
|
109
|
+
function hoverAt(docPath, line) {
|
|
110
|
+
const found = entriesInDoc(docPath);
|
|
111
|
+
if (!found || !found.length) return null;
|
|
112
|
+
const at = found.filter((x) => x.line <= line).sort((a, b) => b.line - a.line)[0];
|
|
113
|
+
if (!at) return null;
|
|
114
|
+
const { entry } = at;
|
|
115
|
+
const fns = Q.loadReport(reportPrefix);
|
|
116
|
+
const cg = Q.loadCallgraph(reportPrefix);
|
|
117
|
+
const lines = [`**${entry.fn}** — ⚡ { ${(entry.inferred || []).join(", ") || "pure"} }`];
|
|
118
|
+
for (const eff of entry.inferred || []) {
|
|
119
|
+
if (eff === "Unknown") continue; // covered by unknownWhy below
|
|
120
|
+
if ((entry.direct || []).includes(eff)) {
|
|
121
|
+
lines.push(`- **${eff}** — performed directly here`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const hops = (Q.path(fns, cg, entry.fn, eff)?.path || []).map((h) => h.fn.split(/[.:]+/).pop() + (h.source ? " (source)" : ""));
|
|
126
|
+
lines.push(hops.length > 1 ? `- **${eff}** — via ${hops.slice(1).join(" → ")}` : `- **${eff}** — inherited (source is cross-boundary or framework-synthesised)`);
|
|
127
|
+
} catch { lines.push(`- **${eff}** — inherited`); }
|
|
128
|
+
}
|
|
129
|
+
if (entry.unknownWhy?.length) lines.push(`- **Unknown** — ${entry.unknownWhy.join(", ")}`);
|
|
130
|
+
if (entry.invisible?.length) lines.push(`- _invisible_: ${entry.invisible.join(", ")} (unmodeled — the effect set is a lower bound)`);
|
|
131
|
+
try {
|
|
132
|
+
const c = Q.callers(cg, entry.fn);
|
|
133
|
+
lines.push(`\nBlast radius: **${(c?.transitive || []).length}** transitive caller(s)`);
|
|
134
|
+
} catch { /* no callgraph */ }
|
|
135
|
+
return {
|
|
136
|
+
contents: { kind: "markdown", value: lines.join("\n") },
|
|
137
|
+
range: { start: { line: at.line, character: 0 }, end: { line: at.line, character: 200 } },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
99
141
|
// ---- Diagnostics (the live gate) ---------------------------------------------------------------------
|
|
100
142
|
function activePolicy() {
|
|
101
143
|
const env = process.env.CANDOR_POLICY;
|
|
@@ -151,6 +193,7 @@ function handle(msg) {
|
|
|
151
193
|
capabilities: {
|
|
152
194
|
textDocumentSync: { openClose: true, save: true, change: 0 }, // report-backed: buffer edits don't move the map
|
|
153
195
|
codeLensProvider: { resolveProvider: false },
|
|
196
|
+
hoverProvider: true,
|
|
154
197
|
},
|
|
155
198
|
serverInfo: { name: "candor-lsp", version: VERSION },
|
|
156
199
|
});
|
|
@@ -161,6 +204,10 @@ function handle(msg) {
|
|
|
161
204
|
if (method === "textDocument/didChange") return; // see textDocumentSync: report-backed
|
|
162
205
|
if (method === "textDocument/didClose")
|
|
163
206
|
return send({ jsonrpc: "2.0", method: "textDocument/publishDiagnostics", params: { uri: params.textDocument.uri, diagnostics: [] } });
|
|
207
|
+
if (method === "textDocument/hover") {
|
|
208
|
+
try { return result(id, hoverAt(fileURLToPath(params.textDocument.uri), params.position?.line ?? 0)); }
|
|
209
|
+
catch { return result(id, null); } // hover is best-effort — null, never a crash
|
|
210
|
+
}
|
|
164
211
|
if (method === "textDocument/codeLens") {
|
|
165
212
|
try { return result(id, codeLenses(fileURLToPath(params.textDocument.uri))); }
|
|
166
213
|
catch { return result(id, []); } // a non-file URI / unreadable report → no lenses, never a crash
|