candor-ts 0.8.10 → 0.8.12
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 +3 -0
- package/mcp.mjs +5 -1
- package/package.json +1 -1
- package/query-core.mjs +34 -7
- package/query.mjs +8 -2
package/lsp.mjs
CHANGED
|
@@ -352,6 +352,9 @@ function runFix(a) {
|
|
|
352
352
|
if (r.cleanHoist) {
|
|
353
353
|
lines.push(`hoist ${a.effect} to: ${r.hoistTo.join(", ")}`);
|
|
354
354
|
lines.push(`then pure (thread the value): ${r.deniedSpan.join(", ")}`);
|
|
355
|
+
if (r.hoistHigher?.length) {
|
|
356
|
+
lines.push(`or hoist higher (up to ${r.hoistHigher.slice(0, 4).join(", ")}): keeps the frontier pure too, threads through more signatures`);
|
|
357
|
+
}
|
|
355
358
|
} else {
|
|
356
359
|
lines.push("no clean hoist — introduce a port (inject the effect from an allowed layer), or relax the boundary");
|
|
357
360
|
}
|
package/mcp.mjs
CHANGED
|
@@ -191,7 +191,11 @@ const TOOLS = {
|
|
|
191
191
|
if (!cfg) throw new Error("no policy: pass `policy`, or check one into the repo's .candor/config (spec §3.4) — the fix is defined relative to the boundary it crosses");
|
|
192
192
|
text = confinedPolicyRead(cfg.policyPath, p, cfg.repoRoot);
|
|
193
193
|
}
|
|
194
|
-
const
|
|
194
|
+
const cg = Q.loadCallgraph(p);
|
|
195
|
+
// The sidecar is the only graph a candor-ts report carries — fail loud (tool error) when it's absent,
|
|
196
|
+
// never a degenerate empty-graph remedy. (/code-review.)
|
|
197
|
+
if (!cg || Object.keys(cg).length === 0) throw new Error(`no call-graph sidecar for the report — fix needs it (re-scan with --out)`);
|
|
198
|
+
const r = Q.fix(cg, Q.loadReport(p), a.fn, a.effect, parsePolicy(text), scopeMatches);
|
|
195
199
|
if (r === null) throw new Error(`no function matching \`${clip(a.fn)}\` in the call graph`);
|
|
196
200
|
return r;
|
|
197
201
|
},
|
package/package.json
CHANGED
package/query-core.mjs
CHANGED
|
@@ -527,7 +527,9 @@ function computeRemedy(start, eff, layer, cg, rev, byName, policyParsed, scopeMa
|
|
|
527
527
|
const cur = up.shift();
|
|
528
528
|
for (const caller of rev.get(cur) ?? []) {
|
|
529
529
|
const ce = byName.get(caller);
|
|
530
|
-
|
|
530
|
+
// skip a caller that doesn't route the effect — INCLUDING one absent from the report (a pure
|
|
531
|
+
// callgraph-only node never carries the effect). Matches candor-swift. (/code-review — was `ce && !…`.)
|
|
532
|
+
if (!ce || !(ce.inferred ?? []).includes(eff)) continue;
|
|
531
533
|
if (deniedLayer(caller, eff, policyParsed, scopeMatches) !== null) {
|
|
532
534
|
if (!deniedSpan.has(caller)) { deniedSpan.add(caller); up.push(caller); }
|
|
533
535
|
} else {
|
|
@@ -535,12 +537,30 @@ function computeRemedy(start, eff, layer, cg, rev, byName, policyParsed, scopeMa
|
|
|
535
537
|
}
|
|
536
538
|
}
|
|
537
539
|
}
|
|
540
|
+
// higher hoist options: allowed-layer transitive callers of the minimal frontier that also route the
|
|
541
|
+
// effect — hoisting higher keeps the frontier pure too, at the cost of threading through more signatures
|
|
542
|
+
// (FIX-SPEC: the trade-off, disclosed not hidden).
|
|
543
|
+
const hoistHigher = new Set();
|
|
544
|
+
const hseen = new Set(hoistTo);
|
|
545
|
+
const hq = [...hoistTo];
|
|
546
|
+
while (hq.length) {
|
|
547
|
+
const cur = hq.shift();
|
|
548
|
+
for (const caller of rev.get(cur) ?? []) {
|
|
549
|
+
const ce = byName.get(caller);
|
|
550
|
+
if (ce && (ce.inferred ?? []).includes(eff) && deniedLayer(caller, eff, policyParsed, scopeMatches) === null && !hseen.has(caller)) {
|
|
551
|
+
hseen.add(caller);
|
|
552
|
+
hoistHigher.add(caller);
|
|
553
|
+
hq.push(caller);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
538
557
|
return {
|
|
539
558
|
fn: start, effect: eff, layer,
|
|
540
559
|
cleanHoist: hoistTo.size > 0,
|
|
541
560
|
site: [...sites].sort(),
|
|
542
561
|
deniedSpan: [...deniedSpan].sort(),
|
|
543
562
|
hoistTo: [...hoistTo].sort(),
|
|
563
|
+
hoistHigher: [...hoistHigher].sort(),
|
|
544
564
|
policyAlternative: layer ? `allow ${eff} ${layer}` : `allow ${eff}`,
|
|
545
565
|
};
|
|
546
566
|
}
|
|
@@ -548,10 +568,14 @@ function computeRemedy(start, eff, layer, cg, rev, byName, policyParsed, scopeMa
|
|
|
548
568
|
// fix: the boundary remedy for ONE function (the remedial inverse of whatif). Returns null if the function
|
|
549
569
|
// isn't in the graph; `{ crossing:false, reason }` if it performs the effect but no policy forbids it there
|
|
550
570
|
// (or it doesn't perform it) — a no-op the caller reports plainly; else the full remedy (`crossing:true`).
|
|
571
|
+
// NOTE: `cg` (the callgraph sidecar) is REQUIRED — unlike candor-query/java/swift, a candor-ts report does not
|
|
572
|
+
// embed inline `calls`, so the sidecar is the only graph; the CLI/MCP callers fail loud when it's absent
|
|
573
|
+
// rather than compute a degenerate empty-graph remedy. (/code-review.)
|
|
551
574
|
export function fix(cg, fns, target, eff, policyParsed, scopeMatches) {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
575
|
+
// Resolve against REPORT function names only (not callgraph nodes, which include pure fns absent from the
|
|
576
|
+
// report) — so `fix <pure-fn>` is a uniform "no such fn" across engines, not a TS-only crossing:false.
|
|
577
|
+
// (/code-review — candor-query/java/swift all match report fns only.)
|
|
578
|
+
const m = matches(fns.map((e) => e.fn), target);
|
|
555
579
|
if (m.length === 0) return null;
|
|
556
580
|
const byName = indexFns(fns);
|
|
557
581
|
// prefer a match that actually performs the effect, so a bare leaf resolves to the violating function
|
|
@@ -573,8 +597,10 @@ export function fixGate(cg, fns, policyParsed, scopeMatches) {
|
|
|
573
597
|
const byName = indexFns(fns);
|
|
574
598
|
const rev = reverseGraph(cg);
|
|
575
599
|
const plans = new Map();
|
|
576
|
-
|
|
577
|
-
|
|
600
|
+
// Iterate functions in sorted-name order so the first-writer-wins `fn` representative of a collapsed
|
|
601
|
+
// remedy is deterministic across engines (candor-query/java/swift all iterate a sorted key set).
|
|
602
|
+
for (const e of [...fns].sort((a, b) => (a.fn < b.fn ? -1 : a.fn > b.fn ? 1 : 0))) {
|
|
603
|
+
for (const eff of [...(e.inferred ?? [])].sort()) {
|
|
578
604
|
const layer = deniedLayer(e.fn, eff, policyParsed, scopeMatches);
|
|
579
605
|
if (layer !== null) {
|
|
580
606
|
const p = computeRemedy(e.fn, eff, layer, cg, rev, byName, policyParsed, scopeMatches);
|
|
@@ -583,6 +609,7 @@ export function fixGate(cg, fns, policyParsed, scopeMatches) {
|
|
|
583
609
|
}
|
|
584
610
|
}
|
|
585
611
|
}
|
|
586
|
-
|
|
612
|
+
// Emit remedies in dedup-key order (candor-query BTreeMap / java TreeMap / swift sorted-keys all do).
|
|
613
|
+
const remedies = [...plans.keys()].sort().map((k) => plans.get(k));
|
|
587
614
|
return { ok: remedies.length === 0, remedies };
|
|
588
615
|
}
|
package/query.mjs
CHANGED
|
@@ -263,7 +263,11 @@ switch (cmd) {
|
|
|
263
263
|
let ptext;
|
|
264
264
|
try { ptext = fs.readFileSync(policyFile, "utf8"); }
|
|
265
265
|
catch { console.error(`candor: policy ${policyFile} could not be read — no fix computed`); process.exit(2); }
|
|
266
|
-
const
|
|
266
|
+
const cg = loadCallgraph(prefix);
|
|
267
|
+
// The sidecar is the ONLY graph a candor-ts report carries (it embeds no inline `calls`). Fail LOUD when
|
|
268
|
+
// it's absent — never compute a degenerate empty-graph remedy that reads as a false "no clean hoist".
|
|
269
|
+
if (!cg || Object.keys(cg).length === 0) { console.error(`candor: no call-graph sidecar for '${prefix}' — fix needs it (re-run: candor-ts <src> --out ${prefix})`); process.exit(2); }
|
|
270
|
+
const r = coreFix(cg, loadReport(prefix), target, eff, parsePolicy(ptext), scopeMatches);
|
|
267
271
|
if (r === null) { console.error(`candor: no function matching \`${target}\` in the call graph`); process.exit(2); }
|
|
268
272
|
emit(r);
|
|
269
273
|
break;
|
|
@@ -275,7 +279,9 @@ switch (cmd) {
|
|
|
275
279
|
let ptext;
|
|
276
280
|
try { ptext = fs.readFileSync(policyFile, "utf8"); }
|
|
277
281
|
catch { console.error(`candor: policy ${policyFile} could not be read — no fix computed`); process.exit(2); }
|
|
278
|
-
|
|
282
|
+
const cg = loadCallgraph(prefix);
|
|
283
|
+
if (!cg || Object.keys(cg).length === 0) { console.error(`candor: no call-graph sidecar for '${prefix}' — fix-gate needs it (re-run: candor-ts <src> --out ${prefix})`); process.exit(2); }
|
|
284
|
+
emit(coreFixGate(cg, loadReport(prefix), parsePolicy(ptext), scopeMatches));
|
|
279
285
|
break;
|
|
280
286
|
}
|
|
281
287
|
default:
|