Honest priority order within each section. Sources: CRITIQUE.md, EVAL.md, hands-on findings.
The entry below says tokio's residual "falls through" R128's silent path — that a use bound to
crate::blocking::spawn_blocking names nothing. MEASURED FALSE. With R128 fixed, tokio's fs rows
still vanished under R123, and instrumenting the resolver showed why: crate::blocking::spawn_blocking
resolves fine, resolved_local=true, to runtime::blocking::pool::spawn_blocking via the re-export
alias index. R128's branch never fires there.
What was actually happening is visible in the A/B's own pre-image. The removed fs::asyncify row
carries calls: ["fs::mocks::spawn_blocking"]. Every one of those 35 tokio rows got its
["Log","Unknown"] from #[cfg(test)] mod mocks — i.e. the disclosure R123 was held for was
MANUFACTURED BY THE SIN R123 FIXES. Holding R123 to preserve it meant keeping a published cardinal
sin in order to keep an answer that was right for the wrong reason.
tokio::fs::symlink carried no Fs of its own because its whole body is
asyncify(move || std::os::unix::fs::symlink(..)) — and std::os::unix::fs::symlink classified as
PURE, along with chown, lchown and chroot: std::fs:: was the entire filesystem prefix rule and
the platform half of std's filesystem API was not under it. That is its own cardinal sin (no row number
— assign one), ground-truthed by a cargo run that created a real symlink and stat'd it back, and it
is fixed in classify: std::os::{unix,windows,wasi}::fs is filesystem I/O. With it fixed,
tokio::fs::symlink reads ["Fs"] and pure fs::symlink::symlink stays exit 1 across R123.
The lesson is the family's own: a BACKLOG entry is a snapshot, and a diagnosis inside one is the part that goes stale worst. The entry below stated a mechanism it had not instrumented, and the fix order it derived was right by accident — R128 needed doing, just not for the reason given.
- The body-local
usehalf of R123.LocalUseCollector(decls.rs) andCallCollector::visit_item_use(collector.rs) still collect auseinside a fn body unfiltered and order-decided. 20 sites in 12 of the 1489 crates. Pinned byr123_body_local_cfg_test_import_is_unfiltered_too, which ASSERTS the defect. - R128's stated under-reports, each pinned by a test: the associated-fn spelling
(
crate::<mod>::<T>::<assoc>), and an explicitly-writtencrate::m::f()(expandstrips the prefix, making it byte-identical to a bare external call). - R128's residual over-charge: ~34 of its 325 corpus hits construct a macro-declared TYPE, which is pure. Needs an index of ALL local type names, not only those carrying an impl.
- An AMBIGUOUS local call target silently loses its edge when the path is qualified. The existing
ambiguous:same-name local defsdisclosure covers only the BARE-leaf spelling. Found while guarding R128 (redis'scrate::types::from_redis_value, 67 call sites); not fixed, not a claim about size. - tokio's
fsreach into the runtime is still lost, independently of all three fixes:pool::spawn_blocking->Handle::spawn_blockingresolves, and that body's own chain reads pure.fs::asyncifyis now ABSENT for that reason rather than because of a mock. Not measured further.
R122 was fixed first exactly as planned, and the plan's premise held: the curve25519-dalek
Scalar::random loss that sank the previous R123 prototype was caused by R122 and is gone. Same
prototype, re-measured over the same 1489 crates on top of the R122 fix: ADDED 0, REMOVED 10,
CHANGED 39 in 6 crates, against ADDED 0, REMOVED 11, CHANGED 39 in 7 crates before. One removal
and one crate — curve25519-dalek — and that was the one the audit had flagged.
[P0] R123 — STILL OPEN, AND THE ONE-LINE FIX IS STILL UNSOUND, FOR A NEW AND FULLY IDENTIFIED REASON
The sin is unchanged and reproduces on HEAD: two use lines differing only in ORDER decide whether a
production call resolves through a TEST MOCK. Both arms compile and RUN (ran=true, a real spawned
process). Now PINNED as executable tests —
r123_a_module_level_cfg_test_import_still_decides_by_source_order and
r123_body_local_cfg_test_import_is_unfiltered_too assert the defect, so it cannot go unmeasured
and an accidental fix cannot go unnoticed. Both carry the standing instruction to delete them and
re-run the A/B if they start failing.
THE BODY-LOCAL SITE IS NOW MEASURED (it never had been). LocalUseCollector (decls.rs) and
CallCollector::visit_item_use (collector.rs) both collect a use written inside a fn body, neither
has an include_tests to filter on, and the same swap one scope lower is decided by order too —
confirmed by fixture, with the opposite-order control proving the collector is reached. Prevalence:
20 sites in 12 of the 1489 registry crates carry an indented #[cfg(test)]-gated use
(aws-smithy-http-client, aws-config, bigdecimal, hashbrown ×2, ordered-float ×2, log ×4, scrypt).
Fixing those two needs include_tests threaded through fninfo (5 call sites) and onto CallCollector,
which has no such field at all.
WHY THE FIX IS HELD. The full fix — one use_item_applies / collect_item_uses authority in
lang.rs, applied at all five sites (scan_items, collect_decls and collect_root_reexports were the
three unfiltered ones; collect_root_reexports needed an include_tests parameter it did not have),
plus a cache rev14→rev15 bump because the cached uses/root_reexports would otherwise serve the
removed resolution warm — closes the fixture ORDER-INDEPENDENTLY and passes 330 + 77 tests. It was
revert-tested: with the filter disabled the module-scope test fails left: [] right: ["Exec"].
§E1 reach, counted in the changed branch: 310 use items dropped across 59 of the 1489 crates.
And then it loses ["Log","Unknown"] on 35 rows of tokio 1.53.1's public async fs API, and the
scoped policy forms are the ones that show it:
deny Unknown fs::read::read exit 1 -> 0
deny Unknown fs::symlink::symlink exit 1 -> 0
pure fs::symlink::symlink exit 1 -> 0 <- candor CERTIFIES tokio::fs::symlink pure
deny Unknown fs::asyncify exit 1 -> 0
deny Fs fs::read::read exit 1 -> 1 <- `Fs` is correctly retained throughout
Whole-crate blanket deny Unknown / deny Fs stay exit 1 on all six moved crates, which is why this
is invisible unless you ask the scoped form. fs::asyncify, fs::symlink, fs::symlink_dir,
fs::symlink_file, DirEntry::file_type and DirEntry::metadata disappear from functions[]
entirely.
THE RESIDUAL CAUSE IS ONE THING, AND IT IS PRE-EXISTING AND SEPARATE FROM R123. tokio's
src/fs/mod.rs has the textbook pair, mock last:
#[cfg(not(test))] use crate::blocking::spawn_blocking;
#[cfg(test)] use mocks::spawn_blocking;
Drop the mock and the production import wins — and crate::blocking::spawn_blocking is
pub(crate) use crate::runtime::spawn_blocking; inside a cfg_rt! MACRO BODY in src/blocking.rs.
The scanner does not expand crate-defined macros, so the name resolves to no unit, and a use bound
to a crate-local path that names nothing reads as PURE rather than as a disclosure. That is its own
cardinal sin, R123 merely unmasks it, and it has a four-line repro that needs no cfg at all:
mod hidden { macro_rules! reexp { () => { pub(crate) use crate::real::runner; } } reexp!(); }
pub mod real { pub fn runner<F: FnOnce() -> bool>(f: F) -> bool { f() } }
use crate::hidden::runner;
pub fn go<F: FnOnce() -> bool + Send + 'static>(f: F) -> bool { runner(f) }
real::runner is reported ["Unknown"] with unknownWhy: ["callback:unresolved call"]. go is
ABSENT — it hands a caller-supplied closure to a target candor cannot see and says nothing. Measured
on HEAD; nothing about #[cfg] is involved.
SO THE ORDER IS: fix the absent-use-target sin FIRST, then re-run the R123 A/B, then ship R123.
That is the same shape as "fix R122 before R123", and it was found the same way. Fixing it needs a
measurement this entry does NOT have: how many rows gain Unknown when a call through a use-bound
crate-local path that matches no unit is disclosed instead of dropped. That is the flood question, and
it decides whether the fix is a rule or a narrow one.
Audited in full, the other 4 crates the R123 patch moves are clean: ordered-float ×2 lose two
impl_rkyv::test_* names (test fns, correct), cap-primitives loses invisible: ["ambient_authority"]
on four net::pool::test_* (test fns), similar loses one calls edge on a test_*, and
hickory-resolver GAINS Clock on five rows. Only tokio is the problem, and only through the one
mechanism above.
Both are at origin/main (3cf055d) as well as at HEAD, so neither belongs to the R105/R106/R107/R119
work. Both were found while checking a reviewer's hypothesis rather than by a sweep, so the boundary
below is what I measured, not a claim that the class is exhausted.
scan_items (decls.rs, the for it in items { if let syn::Item::Use(u) = it { collect_use(..) } }
preamble) and collect_decls (same shape, further down) both collect EVERY Item::Use unconditionally.
collect_use inserts into a HashMap, so the LAST spelling of a name wins. The idiomatic mocking pair
is two mutually-exclusive cfgs, and which one candor believes is decided by which was typed second.
TWO ARMS, BYTE-IDENTICAL EXCEPT FOR THE ORDER OF TWO use LINES. Ground truth EXECUTED — each was
compiled with cargo run in a normal (non-test) build and printed ran=true, i.e. really spawned
/usr/bin/true:
#[cfg(not(test))] use std::process::Command as Runner; // ARM "first": mock import FIRST
#[cfg(test)] use crate::mockproc::Runner; // ARM "last" : mock import LAST
pub fn run(p: &str) -> bool { let mut c = Runner::new(p); c.status().map(|s| s.success()).unwrap_or(false) }
arm "first" -> run: ["Exec"]
arm "last" -> run: ABSENT from functions[] <- silent under-report
Measured identically on origin/main and on HEAD. The crate::mockproc::Runner mock is pure by
construction, which is what makes this the worst shape of the class: the answer is confidently pure.
THE ONE-LINE FIX IS UNSOUND — PROTOTYPED, BUILT AND A/B'D, THEN REJECTED. Adding
if !include_tests && is_cfg_test(&u.attrs) { continue; } to both loops does close the fixture (both
arms report ["Exec"], order-independent) and 326+77 tests stay green. But over all 1489 registry
crates against HEAD it is ADDED 0, REMOVED 11, CHANGED 39 (wide, 15 fields) in 7 crates, and the
removals audited in full contain a real loss: curve25519-dalek 4.1.3's scalar::Scalar::random loses
["Rand"] and the row disappears. Cause is SIN 2 below — is_cfg_test fires on
#[cfg(any(test, feature = "rand_core"))], which is a PRODUCTION import whenever that feature is on.
So SIN 2 must be fixed first, or fixing SIN 1 trades one silent under-report for another. The rest
of that A/B: tokio 1.53.1 loses a fabricated Log+Unknown across its whole fs module (35 rows,
Fs correctly retained — this looks like the intended gain), hickory-resolver GAINS Clock on five
rows, and cap-primitives/ordered-float/similar move on test-only functions.
THE SAME QUESTION IS ANSWERED FIVE TIMES AND TWO OF THEM DRIFTED (brief §F1 q3). Sites that DO apply
the filter: collect_module_glob and collect_reexports (both decls.rs). Sites that do NOT:
scan_items, collect_decls, and lang.rs's collect_root_reexports — which has no include_tests
parameter at all, so fixing it needs a signature change. LocalUseCollector (a use inside a fn body)
is unfiltered too and was not measured.
cfg_meta_requires_test returns true for a test found anywhere inside any(...) as well as
all(...). Its own doc says the item "POSITIVELY requires test". For all(test, X) that is right; for
any(test, X) it is exactly backwards — the item compiles when EITHER holds, so it does not require
test at all. A comment asserting the property the code lacks.
Ground truth EXECUTED (cargo run on a default build printed maybe=true — a real spawned process):
[features] default = ["extra"] ; extra = []
#[cfg(any(test, feature = "extra"))]
pub fn maybe_spawn(p: &str) -> bool { std::process::Command::new(p).status().map(|s| s.success()).unwrap_or(false) }
pub fn always_spawn(p: &str) -> bool { /* identical body, no cfg */ } <- the control
maybe_spawn -> ABSENT always_spawn -> ["Exec"] (origin/main AND HEAD, identical)
On an ISOLATED crate whose only function is maybe_spawn, functions[] is [], analyzed.count is 0,
and every policy form passes:
deny Exec · deny Unknown · deny Exec Unknown · deny Exec maybe_spawn · pure maybe_spawn
-> exit 0, exit 0, exit 0, exit 0, exit 0
is_cfg_test gates every item-kind in scan_items (free fn, impl, impl method, mod, trait) plus
declared_item_name, lazy_unit_emitted, collect_module_glob and collect_reexports, so this is not
a use-map issue — the whole item disappears. 102 of the 1489 crate sources in the local registry
contain #[cfg(any(test, …))] (aho-corasick, arrow, aws-config, axum, bstr,
curve25519-dalek, … and candor-scan's own older published versions).
Repro fixtures for both live under this session's scratchpad; each is three files and rebuildable from
the snippets above in under a minute. include_tests: true is unaffected by either.
Found during a guard-deletion sweep of src/lib.rs's callback/thread-local/coroutine-capture
machinery (the same area that produced e43eec0 and 3e9848c). Live-reproduced on unmodified HEAD, no
code changes needed to trigger it:
struct Guard;
impl Drop for Guard {
fn drop(&mut self) { let _ = std::net::TcpStream::connect("10.0.0.2:9"); }
}
fn boxed_dyn_fn_scope_exit() {
let g = Guard;
let _b: Box<dyn Fn()> = Box::new(move || { let _ = &g; });
}
cargo dylint over this produces zero warnings for boxed_dyn_fn_scope_exit — it vanishes from the
report entirely, the identical shape to 3e9848c's closure_scope_exit (a move || {} captured Guard,
dropped unused) except boxed as a trait object first. The bare (unboxed) case IS caught, correctly —
this is specifically the Box<dyn Fn*> path.
Root cause, confirmed with a debug probe (not guessed): mir_spike::local_drop_impls's
TyKind::Dynamic arm resolves the concrete type behind a Box<dyn Trait> by CHA — tcx.trait_impls_of (principal) — and recurses local_drop_impls on each impl's self type. For principal = std::ops::Fn
this returned 13 non-blanket impls on this fixture, none of them the closure's own type: a closure
satisfies Fn/FnMut/FnOnce through compiler-synthesized dispatch, never a registered impl Fn for X item, so ordinary CHA is structurally blind to it — no amount of widening the trait list fixes this,
the closure is simply not in the set CHA enumerates. The arm's own comment ("most trait objects
(Box<dyn Error/Any/Fn…>) have no local impl carrying a Drop, so produce no edge") is wrong for exactly
this case — the CORRECTNESS COMMENT suppressed the measurement that would have falsified it (the review
class from 2026-08-16/29: "when you meet a comment explaining why something is safe, treat it as the
highest-value thing in the file to attack").
Deliberately not fixed in this session — this needs new machinery, not a one-line CHA widening, and a rushed version risks trading this under-report for a fabrication:
- The
Dropterminatordrop_edges()walks only carries the STATIC (erased) type of the dropped place — by the time aBox<dyn Fn()>local reaches itsDropterminator in MIR, the concrete closure type used to construct it is already gone. There is no CHA-style trick that recovers it from the drop site alone. - Candidate fix A (bounded, sound, in-function only): at the construction site — an unsizing
coercion (
CastKind::PointerCoercion(Unsize)in MIR, or the HIR adjustment) of a local closure/ coroutine literal into adyn Fn*trait object — record which concrete closure type was assigned to which local, and thread that through to the laterDropterminator on the SAME local within the SAME function body. No flood: it only ever fires for a closure actually coerced into that exact place, so it can't leak onto an unrelatedBox<dyn Fn()>elsewhere (unlike a blanket "assume every closure in the crate" CHA-widen, which WOULD flood and is not being proposed). - Candidate fix B (charge at construction, rejected): charge the effect directly to the closure's
enclosing function at its construction/coercion site, skipping the drop-site tracking entirely. Sound
within a single function, but wrong across functions:
fn make() -> Box<dyn Fn()> { let g = Guard; Box::new(move || { let _ = &g; }) }constructs the closure inmake, but a caller that never invokes it and simply lets the return value drop is the one that actually RUNS the guard's effect — chargingmakeeither misses the caller's exposure or (if propagated further) double-charges/mis-locates it. Not proposed as the fix; recorded so the next attempt doesn't re-discover it costs a design decision. - A cross-function-sound version of Candidate A (the return-value case above) needs the coercion fact to
travel through the return type / field / whatever carries the
Box<dyn Fn*>onward — genuinely a small interprocedural extension, not a local one.
This is a THIRD instance of the "guard captured by a closure that never runs" class in the same file (the
other two — bare move || {} and the async/async || coroutine forms — are now regression-tested,
see ui-2021/coroutine_drop.rs and tests/integration.sh 9c-iii). Translate the question once B (or A)
lands: swift's deinit and java/kotlin's lambda-capturing-a-Closeable-never-invoked shape should be
asked the identical question (rule F, corpus brief) — untested here, not investigated this session.
is_pure_std_trait (src/lib.rs) exempts dynamic/generic dispatch over
Display/Debug/Error/ToString/Clone/PartialEq/Eq/PartialOrd/Ord/Hash/Default — WHEN the trait's defining
crate is the genuine sysroot core/std/alloc (now enforced by is_real_sysroot_frontier, this
session's fix for the impostor-crate class, above) — from ever reading Unknown, on the stated,
measured rationale that flagging all of them floods reports (ui/trust.rs's format_error pins this
choice deliberately: a &dyn std::error::Error dispatch produces ZERO diagnostic, by design, not by
omission).
Nothing stops a REAL (non-impostor) external crate's Display/Clone/Hash/… impl from doing arbitrary
I/O — unlike Drop::drop, which Rust forbids calling explicitly (E0040, checked and confirmed sound
2026-08-29), there is no language guarantee these traits are pure. Live-reproduced with an ORDINARY
path dependency (no name games at all): a crate effectlib with
impl std::fmt::Display for Sneaky {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let _ = std::fs::write("/tmp/sneaky-display-poc", b"hit");
write!(f, "sneaky")
}
}
called from the CONSUMER crate through &dyn Display (show) and a monomorphized generic
T: Display (show_generic) — both genuinely non-local, non-CHA-resolvable dispatch, the exact
condition is_pure_std_trait's doc comment describes — produced "functions": [] for both callers:
total silence, and deny Fs exited 0 with no warning at all. SneakyClone's effectful Clone impl,
called via .clone(), was silent the same way.
Deliberately not fixed in this session. A blanket removal of the exemption was already tried and
measured too costly (ui/trust.rs's own comment: "found in the wild: dyn Error error-formatting
taints whole call trees") — my repro doesn't invalidate that measurement, it only measures the OTHER
side (the cost of staying silent) that the original decision didn't have data for either. The closest
safe fix — a non-gating disclosure (mirroring invisible's "floored to pure, but flagged" convention,
distinct from Unknown so it doesn't reopen the flood) for dispatch through one of these 11 traits when
the concrete impl is non-local and unverifiable — would add a new wire-format signal, which is a
cross-engine SPEC decision (candor-spec is owned by a different agent this session; java/ts/swift almost
certainly share the identical Display/toString/equals/hashCode purity assumption and the identical
exposure). Filing rather than guessing at a fix candor-spec hasn't ratified.
Reproduction kept as a real two-crate fixture (path dependency + consumer), not minimised further —
built this session under this machine's scratchpad, puretrait-fixture/{effectlib,main}; regenerate from
the effectlib snippet above (a consumer crate calling show/show_generic/clone_it over it) if the
scratch dir is gone. Not covered by any existing conformance row or unit test; ui/trust.rs only pins the
ACCEPTED-exempt case (format_error), never an effectful one, so this class has zero regression coverage
in either direction today.
Both found by generating fixtures against the std API surface rather than reading the tables. Filed rather than patched because each turns on a contract question that should be answered once, not guessed.
-
[P3]A socket METHOD on a typed receiver is not classified by the SYNTACTIC FLOOR. Measured, all 14 probed:UdpSocket::{send_to,recv_from,send,recv,peek_from,peek}andTcpStream::{read,write,write_all, read_to_end,read_to_string,peek,flush,shutdown}on a parameter typed&UdpSocket/&mut TcpStreamreport the enclosing function PURE, while the controlTcpStream::connect(dst)in the same file isNet.deny Netis therefore GREEN overfn f(s: &UdpSocket, …) { s.send_to(b, dst) }; candor-java chargesNeton the identical shape. The same is true ofFile::write_allon a&mut File, so it is not Net-specific — it is the receiver-typing route for std types generally.NOT a cardinal sin, and the reason matters.
candor-scanis the STABLE SYNTACTIC backend and says so on every run: "advisory floor — the syntactic backend under-reports; the nightly engine is the sound gate", and a violating run prints "a clean run is necessary, not sufficient". Under that contract an unresolved receiver is a disclosed limitation, not a false all-clear.THE DEEP ENGINE WAS THEN MEASURED, which is what this entry asked for, and it ANSWERS CORRECTLY (
cargo candor@3f5bb87 on the same fixture):udp_send { Net } ← candor-scan said PURE; `deny Net` CHARGES it here control_connect { Net } tcp_write { Unknown } ← disclosed as unresolved, NOT silently pure (AS-EFF-003 / deny Unknown) file_write { Unknown }So the sound gate is sound, and where it cannot resolve a trait-method receiver it says so rather than certifying. This is therefore a FLOOR-PRECISION item, not a soundness one — P3, not P0. The value in closing it is that
candor-scanis what runs on stock cargo in CI, so the advisory floor is quieter than it needs to be for the most common socket shapes. The classifier is already correct (std::net::UdpSocket::*→ Net, with a pure-accessor denylist), so any fix belongs in receiver typing. -
CLOSED 2026-08-17 — the bind literal is withheld from[P3]A BIND address is published intohosts, a DESTINATION surface.hostsand no hedge is added (an empty surface fails closed on its own, measured four-way); pinned bythe_net_locator_position_and_the_bind_address_rule. Original filing:UdpSocket::bind("0.0.0.0:0")puts0.0.0.0:0inhosts, soallow Net 0.0.0.0reads as "may talk to 0.0.0.0" when the code binds locally and sends anywhere. Measured three-way and there is NO consensus to copy: candor-ts publishes nothing forserver.listen(8080, "127.0.0.1"); candor-java publishes127.0.0.1AND marksincomplete: [Net]. Whether a local bind belongs in a destination surface is a SPEC question (§2hosts), and it should be settled in the spec before any engine moves — changing one engine to match another here would just relocate the divergence.
AUDIT 2026-08-05 — this file had not been touched since 2026-07-09 and four of its claims are contradicted by the code. Checked against the repo, not against the file's own prose, which is the method the umbrella backlog's audit note prescribes after that one found 8 of 13 headings wrong.
·
macro_rules!body effects — SHIPPED, not deferred. Listed below as too risky to attempt; it landed as R48 (94f333c, hardened in8585c42), and the code is atcrates/candor-scan/src/decls.rs:731. It recovers the pervasive local-logging-wrapper idiom. · The "naive fixpoint, a worklist rewrite isn't worth it" note — the worklist SHIPPED.crates/candor-scan/src/propagate.rs:19,50is a worklist, seeded per function, with a documented re-enqueue rule. The 0.23.1 performance sweep did it. · "MCP tool-set divergence (#4), STILL OPEN" — the integration it names NO LONGER EXISTS. There is nointegrations/mcp/in the umbrella; the MCP surface is candor-ts'smcp.mjs, shipped through the editor integrations. The item is not open, it is void. · Envelope divergence (undeclared) — decided, not drifting.crates/candor-scan/src/scan.rs:1743emitsundeclared: Nonedeliberately, becauseundeclared: []would CLAIM the pass ran and found nothing. Absent means not computed. That is the spec's own absence rule, applied on purpose.Entries below still carrying those claims are stale. The rest of the file has not been re-verified line by line — treat an unannotated entry here as a claim, and read the code before acting on it.
Review note (2026-06-21). Landed since this file was last swept (some entries below are now stale — see annotations): the
unknownWhyvocabulary harmonised to the canonical 4 kinds + a conformance check (PART 10); the dispatch-frontier (callers --include-unknown, spec 0.7) across class/protocol engines (PART 9);containment+ the AS-EFF-010 ratchet added to the cross-engine conformance differential (PART 11, Java vs candor-query) and to candor-ts; conformance effect coverage extended to 8 (added Rand/Db/Log). The big cross-engine SOUNDNESS result of the period (candor-java only): the inherited-into-project silent-pure vein class (active-record / repository / modeled-base-subclass) was closed in candor-java and CONFIRMED not shared — candor-ts/scan discloseUnknownfor the same shape (their AST/syntactic models never resolve-to-nothing-then-pure). STILL OPEN here: the κ-treadmill dep-tree scanning (P2), the CI self-guard nightly ICE (a rustc bug), and the MCP tool-set divergence (#4 — VOID 2026-08-05:integrations/mcp/no longer exists).
candor's measured, durable value is narrow and worth protecting from scope creep. Concentrate effort on the two places the evidence is strongest:
- Agent edit-time blast-radius feedback — the diff /
CANDOR_REVIEWself-review / MCP / Claude Code hook. The pre-registered eval's decisive result (full-propagation reporting 7% → 100%) is here. This is the north star (P0); everything that sharpens the delta back to the agent wins. - The effect-regression CI ratchet (AS-EFF-005) and policy boundaries (AS-EFF-006). Low adoption cost (no token threading), real felt need ("fail the PR that makes a parser open a socket"), unambiguous. Under-emphasised relative to its deployability — promote it.
De-prioritise the capability/conformance discipline (token threading, &Fs/cap-std migration):
elegant, but realistically almost no team rewrites a real codebase for it, so it stays the deep,
optional tier — not a place to spend polish.
Honest ceiling to keep in view (don't paper over): correctness is bounded by a curated
classifier, so the failure mode is a silent under-report ("network-free" when it isn't) — the most
dangerous direction. The mitigation (Unknown / coverage-gaps / version-trust) is candor's best
asset; keep widening classifier coverage and never trade the honesty for a cleaner-looking number.
The efficiency claim ("cheaper than reading source") depreciates as models get cheaper/longer-context;
lead with blast-radius, which is a reasoning gap, not a cost gap.
A single aggregate score (incl. the tempting "resolvability = 1 − unresolved%") was considered and rejected. It conflates analyzability with quality (penalises DI / plugins / event-driven — good designs); drifts with the engine version, not just the code (this session's resolution work would move it for unchanged source); is gameable in the harmful direction (de-abstract a seam to satisfy the analyzer); and collapses the actionable signal (the list of unresolved fns) into an ambiguous digit that cross-codebase comparison would read as "team A writes worse code" when it means "team A uses Spring." The right primitives are a ratchet (don't regress) + a map (the descriptive profile) + opt-in discipline counts → 0 (conformance/policy) — candor already has all three. Resolvability is legitimately useful inward, as a metric candor tracks about its own coverage release-over-release — not a developer-facing grade.
This is the point of the rest. The bet: candor's value to a coding agent is verification, not
context. An agent can read source and infer most effects itself — the A/B eval (EVAL.md) showed a
source-only agent matching/beating the report wherever the classifier has a gap, and over-trusting a
blind spot makes it worse. What an agent cannot cheaply compute is the transitive effect delta
of its own edit across the call graph and crate boundaries — exactly the failure mode agents have: a
local edit with a non-local consequence (add a reqwest::get in a helper → twelve callers now
transitively perform Net). candor computes that for free. Lead with the delta, not the dump.
(Rests on the P1 correctness foundation — a feedback signal is only worth acting on if it's right.)
- 1. Agent-facing effect diff —
cargo candor diff. v1 ships: describes the per-function delta vs a baseline (+ worker { +Net }) including the transitive blast radius (a network call added inworkeralso shows+Neton its callermain), flags a newUnknown, and has--jsonfor the agent. v2 ships too: the diff now separates introduced (the new effect is in the function's owndirectset — the source) from inherited (transitive), with a headlineFs: introduced in Cache::get → inherited by 6 caller(s). Remaining polish: the exact call-site location (@ foo.rs:12) —explain(§3) has it; emitting it in the report is the step. - 2. Close the loop in the agent's edit cycle. Opt-in
CANDOR_REVIEW=1: the Stop hook diffs the fresh report vs the baseline and, on a newly-introduced effect, feeds the delta back to the agent (decision:block+additionalContext) as a self-review checkpoint;AGENTS.md§5 tells the agent how to respond. Triple loop-guard: a once-per-effectreview-seenmarker,stop_hook_active, and Claude's 8-block cap; off by default. This is what makes candor change behaviour, not just inform. (Tested: candor-run exit-11 + stop-hook block/no-block.) - 3.
explain <fn>— effect provenance (the P2 item below).cargo candor explain <fn>traces the call path to where each effect originates:main → middle → leafwithleaf via std::net::TcpStream::connect at main.rs:1. For scoping (what flows through here before I edit) and to answer the diff's "why". Engine records effect sites (callee + location) underCANDOR_EXPLAIN; a BFS finds the nearest source. - 4. Speed for a tight loop — done as P0′ §8 (fast
diff,watch, instant queries, incremental re-lint). A full re-lint (~minutes on a big crate) is too slow per edit; need an incremental path, or at least a diff against the cached report that re-lints only the changed crate(s). - 5. Measure it — don't assume (pilot;
EVAL.mdTrial 5). Pre-registered with/without eval on a non-local-effect trap (eval/minicache), blind-judged. Result: treatment 4/4 vs control 1/4 fully identified the transitive blast radius (control 3/4 only gestured "callers/perf"). Honest bounds: candor makes non-local propagation complete & explicit (the axis it targets), but did NOT catch what agents miss entirely — control independently found path-traversal/TTL/error bugs candor doesn't. Pilot caveats: N=4/arm, one task, one capable model. Still to do: multi-task, multi-model study quantifying end-to-end edit-quality gains (not just awareness) before scaling.
The eval reframed the goal. AI agents fail at code in a way candor is positioned for: they don't hold the global architecture in their head, so they put I/O in the wrong layer, break a purity boundary, or give a function an effect it was never meant to have. candor sees the whole effect graph; the agent doesn't. Lean into that asymmetry — not into restating effects the agent already sees locally. (The eval also showed the guard is the dependable value, that candor misses the security/correctness bugs that often matter most, and that its value scales with codebase size.)
-
6. Effect policy / architectural invariants — shipped.
CANDOR_POLICY/cargo candor policyenforces a declarative.candor/policy:deny Net Db Fs domain,pure parse,deny Exec. Each rule checks a function's transitive effect set, so it catches a layer reaching an effect through a helper (AS-EFF-006) — the architectural violation an agent can't see from a local edit. Tested: parser unit test + integration (transitive violation fires, genuinely-pure fn doesn't). Spec'd (AS-EFF-006 in SPEC/SEMANTICS);examples/candor-policy. -
[~] 7. Effects → risk (argument provenance) — v1 shipped (heuristic).
CANDOR_TAINT/cargo candor riskflagsAS-EFF-007: an injection-class effect (Fs/Exec/Db/Net/Env/Ipc) whose argument syntactically derives from a function parameter —fs::read(format!("/x/{key}")),Command::new(name). Catches the path-traversal/command-injection class the eval exposed; a literal-arg effect is not flagged. Honest limits (the~): intraprocedural + syntactic — it misses flow through struct fields and across functions, and over-flags a validated parameter; it's advisory (exit 0), never a gate. Tested (param-derived fires, literal doesn't). The real frontier remains: interprocedural, field-sensitive data flow (a MIR-level pass) for sound taint. -
8. Speed — separate the slow analysis (one compile per change) from instant queries. The principle: the analysis only changes when the code does, so compile once off the critical path and serve queries from the cached report. Done:
cargo candor diffnow reads the kept-fresh.candor/report.*(when its source-hash matches.candor/state, maintained by the Stop hook) instead of recompiling — ~30s → 0.26s in the common case; falls back to a re-lint when stale (content-hash, so never wrong). Also done:cargo candor watch— a background poller that re-lints on a real source change and stamps.candor/stateonly on a successful build, keeping the report fresh off the critical path sodiffis instant even without the Stop hook (the compile runs concurrently with editing). Also done: instant read-only queries served from the fresh report (no recompile) —cargo candor show <fn>(its effect set,*=direct) andcargo candor where <Effect>(functions performing it, split direct-source vs inheritor), both--json. This is the net speed win: the agent answers "what does X do / what touches Net" in one ~0.5s call instead of grepping and tracing source. Also done: a forced re-lint (explain/policy/risk) is now incremental —lint_freshtries an incremental build and only clearstarget/dylinton a pure cache hit (detected via cargo's "Checking"/"Compiling" line), so a re-lint recompiles just the changed crate instead of the whole tree. -
9. Selectivity — surface only the consequential propagation.
cargo candor diffno longer lists every inheritor: it computes, per effect, the top-level gainers (those not called by any other gainer — the entry point / public API where the effect actually surfaces) from the report'scallsgraph, and leads withFs: introduced in Cache::get → reaches main (+5 intermediate). The list shows the source and the top-level endpoints; the in-between plumbing is collapsed to a count.--jsonstill carries everything. Cuts the noise on a wide blast radius to the functions that matter. (Could extend to flag "reaches a policy-forbidden fn" — ties to §6.) Tested (3-hop chain: source shown,maintagged top-level,midcollapsed). -
10. Realize the speed/cost savings — make the agent use the fast queries. §8 made queries instant; this is about the agent reflexively reaching for them instead of grepping/reading. Done:
cargo candor callers <fn>— instant reverse-dependency lookup ("who calls this?", the most common pre-edit grep), served from a new effect-relevantcallsfield in the report. Also done: an MCP server (integrations/mcp/candor-mcp.py, no SDK) exposing the query set (candor_effects/where/callers/diff) as native tools, so an MCP agent calls candor reflexively in one cheap call (CLI is the fallback) — the leverage point converting "candor can answer fast" into "the agent skips reading files". Also done:cargo candor map— a compact module→effects overview (app { … } (80 fns)) to front-load understanding at session start without grepping. Caveat: keep the tool surface small — over-querying adds round-trips.
Not worth doing: more interactive-loop polish (call-site line, prettier output) — the eval says that's the narrow, modest-value axis. Diminishing returns.
-
candor-scan silent-pured an UNCALIBRATED crate reached ONLY via a MACRO — no blind disclosure (FOUND 2026-06-17 by the non-syscall recall corpus, case log_slog; FIXED same day —
visit_macronow records ANY crate-qualified macro path, so a macro reach into a declared-but-unmodeled dep is DISCLOSED blind at attribution (slog::info! → invisible), the same honest Unknown a normal call gets. Verified ZERO over-disclosure: candor-query invisible count identical before/after, 4-way conformance unchanged. recall corpus now 14/14 honest, no tracked exceptions.)slog::info!(logger, "m")reads pure: slog isn't calibrated, AND because the reach is a MACRO,visit_macronever records it (it only records a macro it can classify), so it never reaches the κ-ledger /invisibledisclosure either — whereas the SAME crate reached via a normal call WOULD be disclosed (cf. net_minreq →invisible). So it's silent, not honest-Unknown. DISTINCT from the builder-chain family (that's a recall miss on CALIBRATED crates; this is a DISCLOSURE miss on UNCALIBRATED ones). FIX:visit_macroshould record/disclose a macro call to a DECLARED-but-unmodeled dep as a blind reach (Unknown/invisible), so an uncalibrated macro reads honest-uncertain not pure. -
candor-scan silent-pured
duct::cmd!(...).run()— macro-result receiver typing hole (FOUND by the real-world dynamic oracle 2026-06-17; FIXED same day,scan_builder_entry_effectin candor-scan — over-approximate the duct entrycmd/sh→ Exec for the syntactic engine; shared classifier + deep engine unchanged. The generalization below stays open as a watch-item.) Original finding: the syntactic scanner can't type the result of thecmd!MACRO, so the chained.run()doesn't resolve toduct::Expression::runand its Exec is dropped — the program spawns a process (kernel-confirmed via strace) yet reads pure+certain. The deep engine catches it (typed.run(), lib.rs:3675), so this is scan-only. SAME macro-blindness family as the log-macro bug. FIX is a deliberate cross-engine call: (a) over-approximate the SHARED classifier —duct::cmd/shentry → Exec (safe-direction, candor's never-under-report bias; but degrades the deep engine's builder-discipline precision + breaks itsduct::cmd → Nonetest), OR (b) a SYNTACTIC-MODE macro classifier candor-scan uses for entry macros the typed classifier leaves to verbs (keeps the deep engine pristine; more infra — and note scan's effect attribution runs throughclassify()at the call-join, so a scan-only path needs threading there too). Likely generalizes beyond duct (any builder-macro whose terminal verb carries the effect). -
candor-scan silent-pured BUILDER-CHAIN verbs whose entry it can't type —
ureq::get(url)….call()(FOUND by the real-world oracle 2026-06-17; FIXED same day by GENERALIZINGscan_builder_entry_effectinto an entry→effect TABLE — ductcmd/sh→Exec + ureqget/post/…→Net; the oracle drives new rows. Closed duct + ureq together; the table is the systematic "generalize" fix.) Original finding: candor-classify puts ureq's Net on the.call()VERB (lib.rs:348); the syntactic scanner can't type the chain from theureq::get()entry, so.call()is missed — and because ureq is CALIBRATED it isn't disclosed blind → silent-pure (worse than the uncalibrated case, which discloses). This is the SAME family as duct but via a FUNCTION entry, not a macro — so it CONFIRMS the family generalizes, and the real fix is broader than the duct point-patch: over-approximate builder-chain ENTRIES generally for the syntactic engine (extendscan_builder_entry_effect— e.g.ureq::get/post/request→ Net — and ideally drive it from a small data table of "entry → effect where the typed classifier defers to a terminal verb"). The deep engine catches all these via the typed verb, so it stays precise. The systematic version of this is the "generalize the macro-blindness fix" backlog direction. -
[~] Soundness fuzzer — "never silently under-reports" is now a CI gate, not a hope (
soundness/, Bet 1 phase 1 of the improvement roadmap). A hand review found multiple trust-contract violations the unit/integration suite missed (Box<dyn Fn>callbacks, non-local callbacks,Arc<dyn>dispatch — candor reported them PURE instead ofUnknown). The fuzzer generates crates that thread a known effect through every call form known to under-report (closures,dyndispatch, generic/boxed callbacks, the receiving side) and asserts every reachable function iseffect-or-Unknown, never silent-pure. Verified to have teeth: reintroducing the historicalresolve_callee_ => Nonehole makes everyrecv_boxedseed fail. Phase 2 shipped — a dynamic oracle (soundness/oracle.sh): RUNS each generated program understrace, confirms the effect executed (its marker in the trace, filtering runtime startup syscalls), and asserts candor's static prediction (main's transitiveinferred) over-approximates the kernel-observed effect — ground truth that trusts nothing about the generator. CI runs 60 construction seeds + 40 oracle seeds/push (oracle is Linux-only;oracle_check.pydecision logic unit-validated). Extended: a cross-crate variant (run_cross.sh— a lib→bin boundary exercisingDefPathHashcross-crate propagation; teeth-verified by disablingload_cross_reports), plus macro forms (a call inside amacro_rules!expansion, and a macro-DEFINEDsinktesting the #5 macro-fn-visibility fix; teeth-verified by reverting it). CI now runs 60 construction + 40 cross + 40 oracle seeds/push. Now also: arbitrary-self-type trait-object forms (arc_dyn+ a cross-crateArc<dyn LibTrait>variant — #4 teeth: revertingis_dyn_receiverfails the non-local arc seeds), and per-function oracle attribution (oracle_pf.sh— instrument the chain witheprintlnentry/exit markers, reconstruct the call STACK at each effect syscall, and pin a runtime-proven effect to the EXACT on-stack function, not justmain). CI runs 60 construction + 40 cross + 40 whole-program-oracle + 40 per-function-oracle seeds/push. -
Database clients.
sqlx/rusqlite/postgres/tokio_postgres/diesel/redis/… now classifiedDb(execution verbs only, not query building; best-effort, tune via CONFIG). -
tokio::net/std::os::unix::netUnix sockets →Ipc, no longer conflated withNet. -
tokio::process→Exec. The async mirror ofstd::process(which was covered) — spawning a subprocess viatokio::process::Command/Childwas classified pure, a silent under-report of subprocess execution (the dangerous direction). Closed to match the existingtokio::fs/tokio::netcoverage;tokio::processhas no pure data types to over-flag. Unit-tested. -
randover-report fixed — now verb-precise. Whole-craterand→Randflagged its pure distribution constructors (Uniform::new,Normal::new) and deterministic-seed constructors (seed_from_u64) as effectful. Now matched to the calls that actually consume randomness — the entropy sources (OsRng/thread_rng/rng/from_entropy) and generation verbs (gen*/random*/fill*/sample*/next_u*).getrandom/fastrandstay wholesale (effectful end-to-end). Removes the false positives without losing the generation coverage. Unit-tested. -
const/static initializers now reported (a
static X = effectful()performs its effect), with macro-generated items (e.g. tracing__CALLSITEstatics) filtered out viaspan.from_expansion()— that filter was needed; without it the report flooded. -
memmap2 →
Fs. -
Trait dispatch (dyn + generic) over local traits — broke through with Class Hierarchy Analysis: a call to a locally-defined trait method now adds edges to all impls, so effects propagate through
dynand generic dispatch (sound over-approximation). On ebman this resolved the LLM feature and droppedUnknown100→92, of which only 6 are now purely Unknown.CANDOR_PARANOIDremains the opt-in for the residual non-local generic-dispatch gap. -
[~] Closures / callbacks — the statically-resolvable half is closed. A named function passed as a value (handed to a combinator —
iter().map(parse)—thread::spawn, stored as a callback, registered) now adds acaller → fnedge, so its effects propagate even though the call happens inside unseen library code. Previously dropped (thePath-to-FnDefvalue wasn't a Call), a silent under-report inconsistent with inline closures (already charged lexically). Over-approximates in the safe direction; local targets only; ui-tested. Residue (deferred, the genuinely MIR-hard part): the receiving side — animpl Fn/fn-pointer parameter invoked inside a function — keeps an honestUnknown, because pinning its concrete target needs interprocedural closure-flow (effects riding in function types), a MIR-level pass. An effectful inline closure already lands on its lexical owner, so the live hole is now just "effect arrives only through an unknown callback parameter." Small, characterized. -
[~] stdio / println. Decided against for now:
std::io::stdout/println!is pervasive and low-signal (especially for TUIs); would add noise without authority-level value. Reconsider if a use case appears.stdin(real input) could be added later as its own effect. -
(candor-java) Validate Scala and Groovy on real bytecode. DONE (2026-06-13). Scanned scala-library 2.13.8 + cats (Scala) and the groovy 4.0.28 runtime + groovy-json (Groovy): both parse without crashing; the dynamic surface (Scala broad-collection dispatch, Groovy MOP) lands in honest Unknown; real effects attributed to genuine sources. Fixed two engine bugs found en route — System.getProperty miscounted as Env, and unbounded CHA over deep hierarchies (now the §4 ≤12-or-Unknown bound applies to ALL dispatch). App-level cats = all-Unknown, ZERO fabricated effects; groovy-json = 9 real Fs. Promoted to the validated tier in the README. Original item:
-
(candor-java) Validate Scala and Groovy on real bytecode. The README claims them by bytecode-lowering only; the validation pass Kotlin got (real okhttp/ktor/coroutines bytecode, dispatcher entry points flagged, no crashes on coroutine state machines) hasn't been run for either. Do the same: one real Scala codebase (lambda/
Future-heavy — closures lower toinvokedynamic; traits/mixins) and one Groovy one (a Gradle plugin is a natural fixture — Groovy's MOP/dynamic dispatch should land inUnknown, verify it does rather than silently passing). Document the result in the README either way. Until this lands, the marketing page (candor.poly.io) deliberately lists Scala/Groovy at lowering-confidence only — promote them to the validated tier when it does.
-
Under-report seam residuals (deferred from the 2026-06-18 under-report round; candor-scan). The round FIXED the FFI-call→Unknown disclosure + Drop-glue edge (shipped 0.5.11). Deferred, honestly: (1) the GENERAL "any unresolvable bare call → Unknown" — rejected for now because it FLOODS (measured 80 false-positive Unknowns on tokio: closure-param invokes, macro-defined locals, cfg-gated fns, re-exports); needs a way to distinguish a genuinely-foreign bare call from a syntactically-unresolved-but-pure local one (e.g. seed from extern/glob-import provenance) before widening disclosure. (2) [STALE — SHIPPED as R48
94f333c, decls.rs:731] bare localmacro_rules!body effects (the transcriber token-soup parse is fragile — risks fabrication). (3) Deref/Index/operator implicit-coercion edges (no syntactic call node; needs type-directed coercion the syntactic backend lacks). -
[~] Model the modern async-HTTP / TLS / QUIC stack as Net (+ a few more) — MOSTLY DONE 2026-06-17 (
4c6d66afound it, calibrated same day: hyper/hickory_resolver/h3/quinn/tokio_rustls/native_tls→Net, tokio_vsock→Ipc, rustls_native_certs→Fs, rlimit→Env — verb-keyed + crate-gated in classify (num_cpus was initially modeled Env but REVERTED to pure: it's a near-pure CPU-topology query, std's equivalent thread::available_parallelism is pure, and Env would spray over every thread-pool ctor); on oha: Net 30→40 fns, disclosed deps 32→25, fabrication-probe clean. REMAINDER: hyper_util + native_tls calls in oha stay disclosed because they're a bare.request()/.connect()on an UNTYPED builder receiver — the syntactic scanner can't formhyper_util::…::request, and (unlike ureq::get) the entry is a constructor-then-method, so neither the verb rule nor scan_builder_entry_effect catches it. That's the candor-scan builder-chain-receiver-typing boundary; honest — disclosed, not silent. The deep engine catches all of these via typed resolution.) Original finding: (soundness/realworld/method: scan a real corpus, read candor's κ-ledger DISCLOSED-dep list, have an independent classifier pin each to effect-or-pure). Run onoha: candor honestly DISCLOSES (invisible, NOT silent) but doesn't MODEL a stack that is candor's core value (Net): hyper, hyper_util, hickory_resolver (DNS), h3, quinn, h3_quinn, tokio_rustls, native_tls → Net (high-confidence; the lower layer reqwest/ureq build on — the hand-picked recall corpus missed it). Also: tokio_vsock → Ipc; rustls_native_certs → Fs (loads OS trust store); rlimit → Env (num_cpus considered but left PURE — topology query, not Env). Low-confidence (verify the called API): humantime (Clock via now-formatter), clap (Env via args). The classifier CORRECTLY left url/kanal/serde_json/bytes/ratatui/http/aws_sign_v4/rand_regex PURE (disclosure right). These are honest-but-incomplete (P2 precision), not silent under-reports. Calibrate verb-keyed (like reqwest) + add the builder ENTRIES to scan_builder_entry_effect so candor-scan catches them too. NOTE: the DEEP engine has the same gap (shared classifier), so this helps both.
The curated classifier's structural weakness is that κ tries to know the PACKAGE ECOSYSTEM. The
2026-06-11 ledger work (the receipt now NAMES unlisted deps the code calls — Rust via Cargo.toml,
TS via resolved imports) makes the blind spot visible; the endgame is to make most of it
derivable: scan the dependencies themselves and chain the reports (the CANDOR_DEPS/hash
mechanism that already exists, spec §2). A dep's effects derive from ITS calls into the builtin
frontier (std/node:/java.*), which is bounded and slow-moving — κ then only has to know builtins +
the FFI floor, and the treadmill becomes a cache-refresh. Pieces: (1) candor-scan already analyzes
unbuilt third-party crates from ~/.cargo/registry/src — add a --deps mode that scans the
lockfile's tree and emits sibling reports; (2) candor-ts: scan node_modules dist JS via --allow-js
(half the ecosystem ships JS — its require("fs") calls are exactly the builtin frontier);
(3) JVM: analyze the dep jars on the classpath, not just project classes (it's the same ASM pass).
SLICE 1 SHIPPED (2026-06-11 late): candor-scan now emits hash (crate#qual) and CONSUMES
CANDOR_DEPS (files/dirs of sibling reports; unambiguous tail2-then-leaf join; stale-version →
Unknown per §2.1; inherited effects + all four literal surfaces; a chained dep counts as covered in
the κ ledger). End-to-end verified: the ledger names the blind spot → one dep scan closes it →
settle inherits Db+tables, check_rates Net+host across the crate boundary, on stable. The JVM
ledger and TS interface-CHA also landed same-day. SLICES 2+3 SHIPPED (same night): --deps walks Cargo.lock, scans every
registry dependency's unbuilt source in-process (the self-gate's own deny Exec forbade the
spawn-yourself shortcut) into .candor/deps/, then chains the root scan — MEASURED on pgman:
328/328 deps in 75s one-time (~0.23s/dep, cached thereafter), the κ ledger dropping 12 unlisted →
1 (the path dep, the documented skip), 7 fns gaining effects through the chain. An all-pure dep's
EMPTY report registers as covered (its emptiness is the purity claim — serde_json was briefly
misnamed a blind spot). TS CJS units shipped too (module.exports/exports.foo/object-literal
exports are units named the way consumers resolve; jsonwebtoken 4→8 fns, sign reads Clock; the
@types/ join mismatch fixed). REMAINING: JVM classpath-jar scanning + chaining verification;
path/git deps need manual scans (no registry checkout); dist-JS minified bundles unprobed.
These were dismissed as hard limits; they're really expensive or risky, not impossible. The one
genuine floor is undecidability (no sound+complete effect set in general) — and the answer there is
sound over-approximation (Unknown), which candor already does.
-
[~] Closure-flow — the
impl Fnreceiving side: BOUNDED SLICE SHIPPED; full MIR pass deferred (docs/closure-flow.md). Shipped (no MIR needed): a free fn that invokes a callback param defers itsUnknown;check_crate_postresolves it from the concrete fns passed at the HOF's call sites — all named → edge the HOF to them and drop the redundantUnknown(effects + host detail propagate through the HOF, "CHA for callbacks"); any closure / fn-ptr, or never called locally → the soundUnknownstands. Bounded to free fns (arg index == param index) and named callbacks (a closure's effects are already captured lexically on its definer). Integration-tested (resolved drops Unknown; closure-passed keeps it). Residue (deferred, the genuinely MIR-hard part): methods (self offset), and removing theUnknownfor closure callbacks (needs un-folding the lexical charging / per-instance MIR). The original scoping + the trigger for the full pass are in the note. A function that invokes a callback parameter keeps an honestUnknown(its target isn't pinned at HIR). Scoping it surfaced the decisive fact, measured not assumed: the effects are not missed — an inline closure's body is charged lexically, and a named fn passed as a value gets a call edge (the passing side, already shipped), soNet/Fs/etc. land correctly on the definers and their callers. The only residue is theUnknownthe HOF (and its callers) carry — sound, and in the common case redundant (the real effect is already captured elsewhere). So this isn't a soundness fix; it's reducing redundantUnknownnoise on higher-order call paths. Three buildable routes exist (effect-polymorphic signatures / per-monomorphization MIR via theInstance::try_resolvewe already use / closure-flow dataflow), none a different project — but the payoff is precision on a safe base. Recommendation: don't build yet; the trigger is a measurement — ifaudit'sUnknowncount on real callback-heavy crates traces mostly to HOF invokers (not genuine dynamic dispatch / FFI), prototype the per-mono route behind a flag against a 3-shape fixture corpus. (Pushback was right that "needs a MIR engine" framed a road as a wall; scoping then showed the road isn't worth walking until the noise is shown to matter.) -
Macro-generated effects — narrow the blanket filter. DONE + VALIDATED (2026-06-13). The blanket skip was already narrowed (skip macro-generated consts/statics — where the tracing
__CALLSITEstatic flood lives — but ANALYZE macro-generated FUNCTIONS). Validated on a real macro-heavy crate (tracing + async_trait + serde derive): anasync_traitmethod's inner I/O shows Fs; atracing-using fn that ALSO does Fs shows both (not hidden); a pure tracing fn shows only Log (correct — it IS a log framework); a#[derive(Serialize)]struct is pure with no false effects. The DefKind heuristic aligns with the noise/real boundary in practice (the flooding macros generate statics; the fn-generating ones — async_trait/serde — are real), so the ExpnData-identity allowlist below is NOT needed: it would add complexity + re-flood risk for no measured benefit. (Validation answered the "tune against a corpus" question: current is sufficient.) -
Macro-generated effects — narrow the blanket filter (original framing, superseded above). Today
span.from_expansion()skips all macro output (added because compiler-internal/tracing __CALLSITEexpansions flooded the report). But every expansion carries its macro's identity (ExpnData/DefId), so we can filter only known-noise macros and analyze the rest (so anasync_trait/derive/decl-macro that generates effectful code becomes visible). The blocker is re-flooding risk, which needs a real-codebase corpus to tune — a validation problem, not an impossibility. (Pushback: the user-vs-noise distinction is decidable from the expansion's identity.) -
Literal
Nethost detail — shipped (engine + report + spec). Full host-by-runtime-value is undecidable, but the literal subset is extractable. Done: a directly-classifiedNetcall carrying a string-literal address/URL records the host (host[:port], scheme/path/userinfo stripped), propagated on the same call graph asfsdetail (propagateis now generic over the element), surfaced as the report's optionalhostsfield and rendered byshowasNet*(api.example.com). Honest by construction: a runtime address yieldsNetwith no host (absent ≠ "no network"); a header value / verb is filtered out (net_host_literalrequires a dotted name orhost:port). Spec'd in candor-spec §2 (never a completeness claim —Netkeeps the I/O claim;hostsonly narrows it with what's provably visible). Unit + integration tested. Follow-up: candor-java parity; more host-bearing APIs (e.g. URL builders) if a real case wants it. (Pushback was right: "not statically knowable" only held for the runtime subset.) -
Smarter generic-dispatch over foreign types (currently assumed pure to avoid flooding;
CANDOR_PARANOIDis the opt-in). A choice, not a limit: could assume-pure only when the dispatch's trait bounds exclude all known-effectful traits, and markUnknownotherwise — tightening the default without the paranoid-mode noise. (Pushback: a tunable tradeoff, not a wall.) -
Entry-point handling in strict mode.
mainno longer raises AS-EFF-001 (it's the root that legitimately holds the whole capability bundle). -
Reachability / dead-code elimination. CHA + the new named-fn-callback edges made the call graph much more complete (edges through local trait-object/generic dispatch, and through fns passed by name to combinators/
spawn/registries), but it's still missing the unknown-callbackimpl Fn/std-dynedges, so reachability would still mislabel some callback-reached code as dead. Closer to soundness than before, but not there yet — deferred until that residual closure-flow gap closes. -
[~] Finer
Fsgranularity (read vs write). Non-breaking refinement shipped: each report entry now carries an optionalfs: ["read"|"write"], derived from the verb of every directly-classifiedFscall (fs::write→write,File::open→read,fs::copy→both;OpenOptions::openleft unannotated since its direction is runtime-flag-decided) and propagated through the call graph in a separate fixpoint that never touches the effect set. TheFseffect itself is unchanged, so no baseline regresses (verified: the self-guard stays clean) and the field is omitted when unknown.cargo candor showrenders it (Fs*(write),Fs(read,write)) andshow --jsonexposes it. Still deferred (the breaking part): splittingFsinto first-classFsRead/FsWriteeffects with a capability-subtyping relation (Fs ⊇ FsRead, FsWrite) — that needs the vocabulary + token-subtyping work and does break committed baselines (Fs→FsWritereads as a gained effect → spurious AS-EFF-005). Cross-crateFscarries no detail (the dependency's report doesn't record it). (Net-by-host is not statically knowable — won't do.) -
Cross-crate effect propagation (CRITIQUE §8 — closed). Each report entry carries a stable
DefPathHash; a dependent crate loads its dependencies' reports keyed by it (surviving reexport-shortened paths) and inherits their already-transitive effects. Fixed a real consumer whosebinunder-reported theDb/Net/Execit performs through itslib. -
Devirtualize concrete trait calls (CRITIQUE §9 — closed). A method call on a concrete (non-
dyn) receiver resolves to its single impl instead of CHA-expanding to every impl — removing the over-report where a pureself.applies()inherited a sibling rule's effect (104 fns de-over-reported on gitui, no soundness loss). -
cargo candor explain <fn>— effect provenance. Traces the call path that gives a function each effect:mainhasNetbecausemain → middle → leaf, andleafcallsstd::net::TcpStream::connectatmain.rs:1. Turns an effect set into a story you can follow to its source. Engine records effect sites (callee + span) underCANDOR_EXPLAIN; a BFS over the call graph finds the nearest source per effect. (Cross-crate and unresolvable calls are labelled as such — the path stops at the boundary.) Used by P0 §3.
- Recognise cap-std capability types in
declared_caps(and its operations inclassify): a project on cap-std now gets conformance against real, unforgeable capabilities for free, with candor as the visibility layer. Validated insample-capstd/. Compile-time enforcement stays cap-std's job. (Mapped: Dir→Fs, Pool/TcpStream→Net, SystemClock→Clock, UnixStream→Ipc; extend the smallcapstd_captable for more.)
- Distribution: repo is public (git is the channel —
--git/git clone, as AGENTS.md uses). - crates.io distribution — vendored
span_lint, dropped the only git dep (the old "can't" was wrong). The prior note said crates.io is impossible becauseclippy_utilsis a git dependency — but candor used clippy_utils for exactly one function (diagnostics::span_lint, a thin wrapper overLintContext::emit_span_lint). Done: vendored those few lines (minus clippy's docs-page link) intosrc/lib.rs, removed the dep. Verified0git sources inCargo.lock;cargo packagenow packages candor and stops only oncandor-reportnot yet being on crates.io (routine multi-crate release ordering), with no git-dep blocker. Added aversionto thecandor-reportpath dep so the manifest is publish-ready. ui tests confirm byte-identical diagnostics. The nightly + rustc-dev toolchain is still needed to build (rustc_private) — that was never the crates.io blocker. (Pushback was right: I'd called this fundamental; it wasn't.) - Nightly fragility (
rustc_privatepinsnightly-2026-04-16) — the bump process is now a step-by-step inCONTRIBUTING.md(pick matching nightly+clippy_utils rev, fix rustc_private breakage, re-bless ui, re-baseline the self-guard). - Automated nightly bump — shipped (
.github/workflows/nightly-bump.yml). The pin can't be removed while we're a dylint lint, but the migration is now a bot: a weekly (+ manual) workflow that pins a candidate nightly, runs the full build/test, re-blesses the ui fixtures and re-baselines the self-guard, and opens a PR if green — or fails loudly and files a tracking issue if the nightly brokerustc_private(the case that needs a human). Vendoring clippy_utils already removed the old "pick a matching clippy_utils rev" step, so a bump is now just "pick a nightly, fix any rustc_private breakage". (Pushback item: "expensive maintenance", not a hard limit — now a notification.) - Test coverage — unit (pure logic) +
ui_testfixtures with blessed.stderr(copied from the framework-saved file, since compiletest has no bless) + scriptedtests/integration.sh(AS-EFF modes, cross-crate, version stamping, audit) +test-receipt.sh(the bash receipt). 23 unit · 5 ui · 15 integration · 10 receipt, all gated in CI. - JSON output via serde (correct escaping for any path/loc; replaced the hand-rolled escaper).
- Report provenance / versioning.
build.rsstamps the source commit + toolchain into the dylib (a#[used]candor-build-version=tag), the report envelope, and the calibrated sidecar;cargo-candorand the receipt read the true dylib version (not the source tree's HEAD), so a pulled-but-not-rebuilt engine can no longer masquerade as current and mask a stale baseline. - v0.2 self-describing report envelope
{ candor: {version, toolchain}, functions: [...] }. All readers accept the legacy v0.1 bare array during migration (candor-spec §2). -
cargo candor auditat-a-glance profile — effect tally, unresolvable-call list, coverage gaps, broadest-surface functions;--allkeeps the full per-function lint. -
cargo candor audit --coverage— make the classifier ceiling auditable (the principled partial-fix for "silent under-report via an uncovered dep", the most dangerous gap). The default coverage check only warned for crates matching thecandor-suspectname heuristic, so an effectful crate whose name doesn't look effectful slipped through silently.--coveragenow lists every external crate candor saw called but has no effect rules for — calls into them are assumed pure, so any I/O they perform is under-reported. Can't eliminate the ceiling (candor can't know an unanalyzed crate is effectful), but converts it from silent to visible — candor's honesty thesis applied to its own coverage. Fixed a latent false-positive en route: path-matched runtimes (tokio/async_std/mio, matched by module path not crate name) were absent fromCALIBRATED_CRATESand would have been mislabeled blind spots; a newpath_cratesfield in thecalibrated.jsonsidecar marks them covered. Default also gained a one-line count hint;candor-suspectwidened (subprocess/dns/serial/socket families). Integration-tested. - candor-java: adopt the v0.2 envelope + first tests/CI — done. It now emits the
{ candor: {version, toolchain}, functions }envelope (git hash baked in at build time viabuild-info.properties, readers still accept v0.1 bare arrays), and ships a 26-checktest/ smoke.shbehavioural suite (over real bytecode fixtures) gated in.github/workflows/ci.yml(JDK 21). Reached near-parity with the Rust impl: cross-jar hash/inheritance + version-trust,calls, theshow/where/callers/map/diffquery layer (--json), fs read/write detail, lambdas/method-refs, and constructors. - Engine-level version-aware cross-crate trust (candor-spec §2.1 SHOULD):
load_cross_reportsnow reads each sibling report'scandor.version; on a mismatch with the running engine it downgrades the inherited effects toUnknown(can't trust analysis by rules this engine may have changed). Legacy v0.1 reports have no version → trusted as before. Tested (mismatch → Unknown, match → effects as-is). - De-duplicated the coverage
SUSPECTheuristic — now a singlecandor-suspectfile at the clone root, read by bothcandor-run.sh(viaCANDOR_HOME/ its own location) andcargo-candor(viaCANDOR_DIR), with a graceful skip if missing. No more two-copy drift. - Ported the tooling/query layer from bash+Python to a Rust CLI binary. The engine (
lib.rs) must be Rust (arustc_privatedylint lint), but the wrapper —cargo-candor's diff / show / where / callers / audit logic — was bash with embedded Python for JSON. That was the fast, zero-install choice, but a recurring glue-bug source (the sidecar/report glob collision, quoting, state-hash matching) with duplicated logic (report-reading re-implemented in nearly every Python snippet; theSUSPECTheuristic copy-pasted). Done: a Cargo workspace now holds the lint plus two no-rustc_privatecrates —crates/candor-report(the report structs + envelope-or-bare-array parsing, the single source of truth the lint and CLI both depend on) andcrates/candor-query(the read-onlyaudit/show/where/callers/map/diffcommands, one typed binary over those structs).cargo-candordispatches to it and is now python-free (606 → 355 lines, 251 lines of inline Python deleted). The port was verified byte-for-byte against the Python it replaced (identical human output for every command;diff --jsonidentical content, now deterministically ordered instead of Python's hash-order). Thin bash remains only for genuine shell glue (orchestratingcargo dylint, the fast-path freshness check,watch). (The Stop-hook receiptcandor-run.shand the MCP stdio server stay in Python — they're hook orchestration / a protocol server that already delegates report logic tocargo-candor, not duplicated query logic; folding them ontocandor-queryis a possible follow-up, not required for DRY.)
-
Beyond code: field transfers (assessed 2026-06-11). candor's kernel is field-agnostic: a closed vocabulary of consequential actions + a resolvable invocation graph + transitive propagation + an
Unknownhonesty marker + a deterministic policy gate. The transfer test (all four required): (a) the graph is mechanically resolvable — once extraction needs an LLM, determinism dies and you have a lint, not a gate; (b) the effect vocabulary is small and closed; (c) leaves are classifiable at a boundary; (d) change is frequent enough that a gate pays. Ranked candidates: 1. Agent fleets (orchestrators → subagents → tools; effects = tool capability classes;tools:declarations are static, delegation is CHA-shaped, an undeclared MCP tool isUnknown) — same buyers, spec nearly ports as-is. Exploration PUBLISHED: candor-agents — tested (25 checks + a teeth-verified soundness fuzzer), validated on the 36k-star wshobson/agents fleet (headline: 182/192 agents run with ambient authority), combined fleet+code mode linked via the Exec boundary. 2. IaC / CI-CD (Terraform modules, Actions workflows; effects = cloud-API classes; an unpinned third-party action =Unknown; OPA gates the plan, nothing gates the composition graph transitively). 3. Data lineage / privacy (dbt/Airflow DAGs; effects = data categories — PII/PHI; purpose-limitation as a deny rule; an unparseable stage =Unknown, never silently clean). 4. Access control is the existence proof at org scale (IAM reachability analysis — same math). Database development (assessed 2026-06-11, ranks between fleets and IaC): the catalog IS the graph (pg_depend; proc bodies via libpg_query — the engine's own parser), units = functions/procs/views/triggers, the literal surface = per-relation read/write (the hosts/cmds/paths analog), dynamic SQL = the reflection analog → Unknown, triggers need clinit-style edge synthesis. The queries: "which procs transitively WRITE ledger.entries" (append-only as a gate), whatif on migrations, rewire on dropped view edges. Prior-art gap is real (lineage tools do pipelines, not the proc call graph + gate + Unknown). First step SHIPPED (2026-06-11,794ec38+ javae3e6c55+ spec9877b43): thetablesliteral surface +allow Db in <scope> <table>gate, both engines, conformance-locked. Remaining: the DB-side engine (pg_depend/libpg_query) and --link at the Db boundary ("this handler transitively writes payments.ledger, through the app AND the database"). Contracts/SOPs fail test (a) today. The launch landed 2026-06-11; these are now eligible, in rank order, as capacity allows. -
[~] Controlled eval of edit quality (not just analysis cost) with independent ground truth and multiple trials — the gate on P0 (see P0 §5). The pilot (
EVAL.md) showed consumability + efficiency, that a source-only agent can beat the report where the classifier has a gap, and (Trial 5) that candor's edit-feedback lifts non-local awareness on one task. Now scaled — first batch shipped:eval/scaled/is a pre-registered, reproducible 3-task × 2-arm × 2-trial harness (fixtures with candor-verified ground truth, blind judge, falsification clause). Batch 1 (eval/scaled/RESULTS.md) found a completeness gap but was confounded — it also exposed its own metric mis-spec, fixture leakage, and an answer-key contamination bug (all fixed). Batch 2 (eval/scaled/RESULTS-v2.md) — clean, pre-registered: completeness as primary, de-leaked fixtures, weaker model (Sonnet), N≈9/arm, contamination fixed and fully re-run. Result: control completeness 7% vs treatment 100% (binary 0.17 vs 1.00); neither falsification condition triggered — candor's edit-feedback takes a realistic agent from naming ~0 of the non-local callers to all of them, for ~5% extra tokens, a large lift consistent across 3 tasks. Batch 3 (eval/scaled/RESULTS-v3.md) — the untested cell, shipped: a large fixture (tasks-v3/orderflow— Net propagating to 16 non-local fns across 9 files / 3–5 call-graph layers, vs 7/4 in the small tasks) × a frontier agent (Opus-class), 2 arms × 3 trials, blind frontier judges, pre-registered (fixture + 16-fn denominator committed before the run). Result: control completeness 6% vs treatment 79% (conservative; ~100% under the lenient blanket reading 2 of 3 judges applied), gap 0.73 — neither falsification condition triggered. Decisive new finding: a frontier model does not close the gap — the Opus control still named only the one helper it edits past (1/16) and wrote "callers unaffected"; the missing thing is call-graph enumeration, not capability, which is exactly what candor supplies. The 6%→100% effect is not a small-fixture / weak-model artifact. Remaining: more tasks/power at this scale, and edit-quality-beyond-summary measures (does the agent act on the propagation, not just report it). -
Effect-aware PR review —
examples/candor-pr-review.yml: a workflow that POSTS the per-function effect delta vs the baseline as a PR comment + step summary (the review-time sibling of P0's edit-time loop; both powered bycargo candor diff). It informs rather than blocks (pair withcandor-guard.ymlto also fail); an AI reviewer can consume the same viadiff --json. -
Formal semantics —
candor-spec/SEMANTICS.md: the effect lattice, call-site resolution rules (CLASSIFY/CROSS/DEVIRT/CHA/EXEMPT/UNKNOWN), the transitive least-fixpoint, cross-crate composition, the conformance predicates, and the conditional-soundness properties (with the two honesty caveats). The implementation was then verified against it clause-by-clause.
Unknown/AS-EFF-003 · CANDOR_RULES (né CANDOR_CONFIG) · CANDOR_NO_AMBIENT/AS-EFF-004 · CANDOR_PARANOID ·
CANDOR_BASELINE/AS-EFF-005 · ICE hardening · raw-socket + HTTP + Rand + Db + Ipc classification ·
const/static initializers (macro-filtered) · main entry-point exemption · unit tests ·
cargo-candor wrapper · CI + downstream guard workflow · self-guard ·
CHA: see through dyn/generic dispatch over local traits.
Latest pass: cross-crate propagation (DefPathHash) · devirtualization · report provenance &
versioning · v0.2 self-describing envelope · cargo candor audit at-a-glance profile · formal
SEMANTICS.md + a clause-by-clause code↔spec audit · remediated a real consumer's stale baseline.
Hardening pass: Python→Rust query port (candor-report + candor-query) · Fs read/write detail ·
frictionless install.sh + stable ~/.candor home · dylib/query resolution by newest-mtime (was the
head -1 glob) · self-guard trusts the baseline's own siblings (no spurious cross-engine downgrade) ·
fixpoint profiled on ripgrep (negligible).
- [~] declared_caps now peels common wrappers — a capability behind
Option<&Fs>/Vec<&Fs>/Box/Result/ a tuple is recognised (was a false AS-EFF-001).caps_in_tyrecurses into type arguments (bounded depth). Residual: a cap behind a user STRUCT FIELD (aCaps { fs: &Fs }bundle) still isn't seen — that needs recursing into ADT field types, not just generic args. - Macro-generated functions are no longer skipped — the blanket
from_expansion()filter was narrowed to consts/statics (the tracing__CALLSITEnoise); macro-generated functions (async_trait/derive/decl-macro) are analyzed and reported, held by the fuzzer's macro forms. - const/static initializers don't propagate as callees — their init effects are reported
standalone but not inherited by code that references the const (we only follow
Fn/AssocFncall edges, and a const reference isn't a Call expr anyway). - Baseline key is
def_path_str— not guaranteed unique; identical stringly-named items (rare) could collide in the guard. Names are the only stable cross-run key, so this is inherent. - [STALE — the worklist SHIPPED in the 0.23.1 sweep; see propagate.rs:19,50 and the 2026-08-05 audit at the top]
Effect/fs fixpoint is naive iterate-to-convergence, not a worklist (O(rounds × V × E)per crate). Measured negligible: on ripgrep (52k lines) the largest crate (rg: 1179 fns / 3302 edges) fixpoints in 0.42 ms, ~0.7 ms total across the workspace — ~0.017% of the run. Per-crate cost is dominated by call resolution + rustc, not propagation, so a worklist rewrite isn't worth it.
Status (2026-06-21): item 1(d)
unknownWhyvocabulary divergence is DONE (harmonised to the canonical reflect/native/dispatch/callback + a conformance check, PART 10). Item 3 is partly done (conformance now covers 8 effects + the dispatch frontier +containmentPART 11; Clipboard/Ipc and the desugared-call generative forms remain). Items 1(a/b/c/e/f) envelope-field divergence (undeclaredis a DECISION, scan.rs:1743) and 4 (MCP tool-set — VOID) are still open.
These four are NOT simple bugs — they are cross-engine consistency / coverage efforts that need a deliberate design pass (some touch the spec + all 4 engines + conformance + releases). The clear bugs the same sweep found are already fixed (java serialization soundness 0.5.7; candor-query audit silent-pure
- whatif empty-target; ci self-gate stale-binary; mcp arg-validation; candor-agents crash + strict gate).
-
Report ENVELOPE cross-engine field divergence. A consumer written against one engine breaks on another: (a) candor-scan (rust) never emits
declared/undeclared/overdeclared(java/swift/ts always do, as[]); (b) rust OMITSdirectwhen a fn has no own-body effects (others emit[]), so a consumer can't tell "no direct effect" from "untracked"; (c) rust emits exact-DUPLICATE entries (tokio: 2 fns appear twice, identical hash/loc/inferred — cfg-gated re-emission; DEDUP-by-fn before writing, others don't dup); (d)unknownWhytoken vocabulary diverges 4 ways (javadispatch-broad:/dispatch-broad-ext:, tscall:/accessor:, swiftcontentsOf:, rustcallback:— spec defines only reflect:/native:/dispatch:/callback:); (e) java envelope coverage field ispackages:[](array) vspackage:""(string) elsewhere; (f)unresolvedis presence-only on rust/ts vs explicit bool on java/swift. The dedup (c) is a clear small fix; the rest needs a SPEC decision (which envelope fields are mandatory vs optional, and a singleunknownWhyvocabulary) then per-engine alignment + a conformance check on the envelope shape. candor's whole pitch is cross-language-consistent reports, so this matters. NOTE: do NOT blindly add declared/undeclared to rust — confirm whether rust supports the declaration-annotation feature at all first (it may be a genuine semantic gap, not a missing field). -
candor-scan (syntactic, stable) silent-pure on macro-hidden effects. A
libc::socket(...)inside an idiomaticsyscall!{...}declarative-macro wrapper (socket2/mio) comes back PURE — the stable backend doesn't expand localmacro_rules!, so the libc callee is invisible (candor-classify WOULD map it to Net). This is the documented SYNTACTIC-FLOOR limitation (absence≠pure; the nightly HIR engine, which expands macros, is the sound gate). NOT a candor-scan bug to "fix" by guessing — but consider whether candor-scan can raise Unknown when a function body contains an UNEXPANDABLE local-macro invocation it can't see through (sound over-approximation), vs the current silent omission, and whether the κ ledger discloses it. Verify the disclosure story; don't add noisy Unknown-on-every-macro. -
Conformance coverage gaps. The 4-way suite exercises only 6/10 effects (Fs/Net/Exec/Env/Clock); Db/Rand/Log/Clipboard/Ipc have ZERO cross-engine coverage (deliberately scoped out in expected.json — they need per-language deps / are structurally asymmetric, so each engine tests them in its own suite). And the generative matrix has NO desugared-call forms (custom-iterator for-of, disposal, operators, subscripts), so the ~14 implicit-call holes fixed 2026-06-15 are gated ONLY by per-engine fuzzers, not conformance — a cross-engine regression of them wouldn't be caught. Worth: add desugared-call fixtures to the generative differential, and at least Db/Log (the common ones) to the cross-engine set.
-
MCP tool-set divergence across engines. ts exposes 8 tools (impact/where/reachable/path/callers/ show/map/whatif), rust 5 (effects/where/callers/whatif/diff), java 4 — and the arg key is
fn(ts) vsfunction(python). An agent/skill written against one engine's candor MCP breaks on another, defeating the "same prompt everywhere" promise. Align: a canonical tool set + arg key across all three servers (the python servers shell out to candor-query which already has impact/reachable/path/show/map, so it's mostly wiring), update the schemas + mcp.json.example + READMEs.