Releases: tombaldwin/candor-java
Release list
candor-java v0.23.1
Changelog
All notable changes to candor-java are recorded here. Format loosely follows
Keep a Changelog; candor-java is pre-1.0, so minor versions may
include behavioural changes (always in the soundness-increasing direction — the §4 trust contract).
⚠ marks a verdict-affecting change — a gate/guard/report that was green may read differently
after upgrading; review policies and regenerate baselines with the new build.
[0.23.1] — 2026-07-20
A performance + classifier-soundness patch (spec unchanged at 0.23). The analysis engine loses two
super-linear cliffs (both output-preserving), a model-SDK over-classification is fixed, and a follow-up
review closed a silent under-report that the first cut of that fix had opened.
-
Performance — the analyze pass is no longer super-linear in class count.
Cha.chaTargets
(class-hierarchy dispatch resolution) is a pure function of the fixed post-load hierarchy, but it was
recomputed from scratch at every call site that dispatches a given method on a given declared type —
so its cost (CHA fan-out × superchain walk) compounded as both call sites and hierarchy depth grew.
Memoizing it by(owner,name,desc)flattens the curve from ~2.7 ms/class to ~1.0 ms/class: a
4585-class corpus scans in 4.4s vs 12.5s (2.85× faster), with report output verified byte-for-byte
identical (it is a cache, not a semantic change). Interactive edit-time scans were already fast; this
is the batch/large-repo lane. Only thejava -jar/native analyze pass — no gate or report change.
A follow-up pass on the reporting stage (Surface.nearestSourceBFS: pre-sort each function's callees
once instead of at every BFS node;HashSetfor the visited set) takes the same corpus to 3.3s —
3.77× over the original end-to-end — again byte-for-byte identical. -
Performance — the propagation fixpoint is no longer O(V²) on deep call chains.
computeFixpoint
re-swept every caller on every pass, so its pass count equalled the longest back-to-front call chain —
O(V²) on deep whole-program graphs (a controller→service→repo→… layering, or generated code). Replaced
the sweep with a worklist over a callee→callers reverse index: a function is reprocessed only when a
callee actually gained an effect. Same monotone least fixpoint → output byte-for-byte identical
(verified across the library corpora and a 4000-deep synthetic chain;FixpointTest+ full suite green).
~1.8× on a 12000-deep chain, growing with depth; shallow corpora unaffected. Mirrors the equivalent
worklist fix in the Rust engine. -
⚠ Llm model-SDK precision — a builder/constructor no longer fabricates
Llm+Net, WITHOUT opening an
under-report. The ⟨0.13⟩ model-SDK surface fired on any call into a curated provider package, which
over-classified pure construction (new OpenAiService(...), Spring AI's fluentcc.prompt().user(..)
read['Llm','Net']though they dispatch no request). A constructor (<init>) is now excluded, and the
Spring AI ChatClient fluent builders are carved out by a denylist (SPRING_AI_CHATCLIENT_BUILDERS)
layered over the retainedorg.springframework.ai.blanket — so every real dispatch (chat, streaming
terminal,EmbeddingModel.call, and anything not explicitly listed as a pure builder) still
classifiesLlm+Net; a builder we forget to list merely over-reports (safe), never silently drops an
effect. (The first cut narrowed dispatch to an allowlist and silently under-reported streaming/embedding
calls — caught in review and corrected before release; regression anchorsspringAiChatClientStream+
springAiEmbeddingCallnow pin it.) Closes the twokappa_libs_probefabrications
(openaiServiceBuilderPure,springAiPromptBuilderPure).
[0.23.0] — 2026-07-20
Spec floor → 0.23 (the cross-package interface-dispatch rung; report/verdict schema unchanged). This
release folds in a large classifier-soundness wave driven by the reconcile-against-reality loop — running
the transitive candor verify oracle against Apache commons-dbcp2/compress/vfs2 to surface real silent
under-reports on code we did not write, then fixing the classifier — plus a value-provenance precision layer
and an oracle refinement. All changes are soundness-increasing (§4 trust contract) and regression-gated.
- ⚠ super-call through a generic intermediate superclass now propagates the inherited method's effect
(was silently dropped — dbcp2 pool-lifecycleClock). - ⚠ opaque callback → synchronous invoking HOF (
forEach/forEachRemaining/ifPresent, matched
owner-agnostically soList.forEachis covered) disclosesUnknown— the four-way sync-callback rung
(conformancesync_callback_opaque). - ⚠ filter/buffered stream
read/write/skip/flush/closethat delegates to a wrapped sink of
unknown type disclosesUnknown(compress/vfs2 Monitor/Filter streams). - ⚠
AccessController.doPrivileged(action)now runs itsPrivilegedAction/PrivilegedExceptionAction's
run()— the wrapped effect no longer orphaned (vfs2PrivilegedFileReplicator). - ⚠ value provenance: a stream-consuming utility (
IOUtils.read/copy/…, Guava streams) that reads a
stream the method did not itself open disclosesUnknown; whole-program, a project stream field proven
bound only to in-scope concrete opens stays precise (construction-carried suppression). Closes the
readFullyclass as honest disclosure without abandoning the source/sink stance. candor verifyoracle — coverage crediting: transitive attribution stops once the stack walk crosses
an unanalyzed frame, so it no longer false-positives on a library's unmodelled-dependency effects (sound,
zero masking — keys on analyzed-set membership, not string prefixes; a credited frame is disclosed via the
coverage envelope, not a silent green).- cross-package interface dispatch (interfaceUnion, spec 0.23): a chained interface method resolves to the
impl's effect across packages. - Four soundness bugs in the new analysis code, found by adversarial code review, fixed + regression-pinned.
[0.22.0] — 2026-07-18
Spec floor → 0.22 (the verify oracle rung; report/verdict schema unchanged from 0.21). candor-java folds in
a candor verify (JVM -javaagent) fix: the agent's overload-key pre-pass no longer counts compiler-generated
synthetic bridge methods, so a generic/covariant override (e.g. executeTask(T) beside the erased
executeTask(Object) bridge) keys the same bare name the report uses — closing a spurious cardinal-sin
false-positive that misfired on any generic task/visitor/callable/comparator override. Found on the public corpus
(zip4j's integration suite). Verify-only; report and verdict bytes are unchanged.
⚠ candor verify agent transparency fix — the instrument no longer perturbs the program under test. The
-javaagent's EffectTransformer built its ClassWriter with COMPUTE_FRAMES, whose getCommonSuperClass
resolves supertypes by class-loading the application's types mid-transform; force-loading a class whose
supertype is being defined on the same loader raises LinkageError: attempted duplicate class definition. On
Apache commons-io's FileUtilsTest this broke 194/195 tests — the oracle crashing the code it is meant to
observe. The Trace.emit injection is frame-neutral (it pushes two constants and pops them via the call — no
branch target, no local), so the javac-emitted stack-map frames stay valid and need no recomputation: the writer
now uses COMPUTE_MAXS only, eliminating all mid-transform class-loading. This is strictly better than both prior
states (no crash, and no silent skip of classes an earlier getCommonSuperClass couldn't resolve). Found while
measuring the agent's runtime overhead on a real suite (≈1.02× / +2.4 % on commons-io's filesystem tests);
regression-gated by VerifyOracleTest.agentDoesNotBreakClassLoadingViaFrameRecomputation (a fixture whose
control-flow merge is over its own subclasses — the minimal reentrancy trigger, confirmed to fail under the old
COMPUTE_FRAMES build). Verify-only; report and verdict bytes are unchanged.
⚠ Classifier: a java.io FILTER-stream close/flush no longer reads silent-pure — the runtime oracle found this on Apache commons-compress. A FilterOutputStream/FilterInputStream/FilterReader/FilterWriter close/flush DELEGATES to a wrapped stream of unknown concrete type (reached e.g. via super.close() from a filter subclass), performing the actual write/close syscall on a sink candor cannot resolve — so it now reads Unknown (disclose, never fabricate Fs: the wrapped sink may be in-memory). candor already caught the file open at the concrete ctor (new FileOutputStream); this closes the deferred-close half. Conformance-clean; +10 Unknown on commons-compress (modest — the pattern is filter-subclass-close, not ubiquitous). Resolves 2 of the 4 commons-compress finds (CompressFilterOutputStream.close, ZipArchiveOutputStream.destroy). Regression: StructuralDispatchTest.filterStreamCloseDelegatesToUnknownWrappedSink.
⚠ Classifier: an OPAQUE callback handed to a SYNCHRONOUS invoking HOF (Iterator.forEachRemaining, Stream.forEach, Optional.ifPresent, Iterable/Collection/Map.forEach) no longer reads silent-pure — the runtime oracle found this on Apache commons-compress. candor already disclosed Unknown for an opaque task handed to an executor (es.submit(param)), and edged an inline lambda/method-ref (its body captured at the indy) — but an opaque (field/param) Consumer handed to a synchronous invoking HOF was neither edged nor disclosed → the caller read pure though the HOF invokes the callback for its effect. The opaqueTaskHandoff disclosure now also fires for these synchronous invokers (Rules.SYNC_CALLBACK_INVOKERS), covering the ...
candor-java v0.23.0
Changelog
All notable changes to candor-java are recorded here. Format loosely follows
Keep a Changelog; candor-java is pre-1.0, so minor versions may
include behavioural changes (always in the soundness-increasing direction — the §4 trust contract).
⚠ marks a verdict-affecting change — a gate/guard/report that was green may read differently
after upgrading; review policies and regenerate baselines with the new build.
[0.23.0] — 2026-07-20
Spec floor → 0.23 (the cross-package interface-dispatch rung; report/verdict schema unchanged). This
release folds in a large classifier-soundness wave driven by the reconcile-against-reality loop — running
the transitive candor verify oracle against Apache commons-dbcp2/compress/vfs2 to surface real silent
under-reports on code we did not write, then fixing the classifier — plus a value-provenance precision layer
and an oracle refinement. All changes are soundness-increasing (§4 trust contract) and regression-gated.
- ⚠ super-call through a generic intermediate superclass now propagates the inherited method's effect
(was silently dropped — dbcp2 pool-lifecycleClock). - ⚠ opaque callback → synchronous invoking HOF (
forEach/forEachRemaining/ifPresent, matched
owner-agnostically soList.forEachis covered) disclosesUnknown— the four-way sync-callback rung
(conformancesync_callback_opaque). - ⚠ filter/buffered stream
read/write/skip/flush/closethat delegates to a wrapped sink of
unknown type disclosesUnknown(compress/vfs2 Monitor/Filter streams). - ⚠
AccessController.doPrivileged(action)now runs itsPrivilegedAction/PrivilegedExceptionAction's
run()— the wrapped effect no longer orphaned (vfs2PrivilegedFileReplicator). - ⚠ value provenance: a stream-consuming utility (
IOUtils.read/copy/…, Guava streams) that reads a
stream the method did not itself open disclosesUnknown; whole-program, a project stream field proven
bound only to in-scope concrete opens stays precise (construction-carried suppression). Closes the
readFullyclass as honest disclosure without abandoning the source/sink stance. candor verifyoracle — coverage crediting: transitive attribution stops once the stack walk crosses
an unanalyzed frame, so it no longer false-positives on a library's unmodelled-dependency effects (sound,
zero masking — keys on analyzed-set membership, not string prefixes; a credited frame is disclosed via the
coverage envelope, not a silent green).- cross-package interface dispatch (interfaceUnion, spec 0.23): a chained interface method resolves to the
impl's effect across packages. - Four soundness bugs in the new analysis code, found by adversarial code review, fixed + regression-pinned.
[0.22.0] — 2026-07-18
Spec floor → 0.22 (the verify oracle rung; report/verdict schema unchanged from 0.21). candor-java folds in
a candor verify (JVM -javaagent) fix: the agent's overload-key pre-pass no longer counts compiler-generated
synthetic bridge methods, so a generic/covariant override (e.g. executeTask(T) beside the erased
executeTask(Object) bridge) keys the same bare name the report uses — closing a spurious cardinal-sin
false-positive that misfired on any generic task/visitor/callable/comparator override. Found on the public corpus
(zip4j's integration suite). Verify-only; report and verdict bytes are unchanged.
⚠ candor verify agent transparency fix — the instrument no longer perturbs the program under test. The
-javaagent's EffectTransformer built its ClassWriter with COMPUTE_FRAMES, whose getCommonSuperClass
resolves supertypes by class-loading the application's types mid-transform; force-loading a class whose
supertype is being defined on the same loader raises LinkageError: attempted duplicate class definition. On
Apache commons-io's FileUtilsTest this broke 194/195 tests — the oracle crashing the code it is meant to
observe. The Trace.emit injection is frame-neutral (it pushes two constants and pops them via the call — no
branch target, no local), so the javac-emitted stack-map frames stay valid and need no recomputation: the writer
now uses COMPUTE_MAXS only, eliminating all mid-transform class-loading. This is strictly better than both prior
states (no crash, and no silent skip of classes an earlier getCommonSuperClass couldn't resolve). Found while
measuring the agent's runtime overhead on a real suite (≈1.02× / +2.4 % on commons-io's filesystem tests);
regression-gated by VerifyOracleTest.agentDoesNotBreakClassLoadingViaFrameRecomputation (a fixture whose
control-flow merge is over its own subclasses — the minimal reentrancy trigger, confirmed to fail under the old
COMPUTE_FRAMES build). Verify-only; report and verdict bytes are unchanged.
⚠ Classifier: a java.io FILTER-stream close/flush no longer reads silent-pure — the runtime oracle found this on Apache commons-compress. A FilterOutputStream/FilterInputStream/FilterReader/FilterWriter close/flush DELEGATES to a wrapped stream of unknown concrete type (reached e.g. via super.close() from a filter subclass), performing the actual write/close syscall on a sink candor cannot resolve — so it now reads Unknown (disclose, never fabricate Fs: the wrapped sink may be in-memory). candor already caught the file open at the concrete ctor (new FileOutputStream); this closes the deferred-close half. Conformance-clean; +10 Unknown on commons-compress (modest — the pattern is filter-subclass-close, not ubiquitous). Resolves 2 of the 4 commons-compress finds (CompressFilterOutputStream.close, ZipArchiveOutputStream.destroy). Regression: StructuralDispatchTest.filterStreamCloseDelegatesToUnknownWrappedSink.
⚠ Classifier: an OPAQUE callback handed to a SYNCHRONOUS invoking HOF (Iterator.forEachRemaining, Stream.forEach, Optional.ifPresent, Iterable/Collection/Map.forEach) no longer reads silent-pure — the runtime oracle found this on Apache commons-compress. candor already disclosed Unknown for an opaque task handed to an executor (es.submit(param)), and edged an inline lambda/method-ref (its body captured at the indy) — but an opaque (field/param) Consumer handed to a synchronous invoking HOF was neither edged nor disclosed → the caller read pure though the HOF invokes the callback for its effect. The opaqueTaskHandoff disclosure now also fires for these synchronous invokers (Rules.SYNC_CALLBACK_INVOKERS), covering the JDK types and commons-io's IOIterator.forEachRemaining(IOConsumer) / IOStream.forEach (the exact shape ArchiveInputStream.forEach uses). Only OPAQUE args are affected — an inline lambda or a freshly-constructed impl keeps its edged effect (no over-disclosure); full conformance suite green. Regression: StructuralDispatchTest.syncCallbackInvokerOpaqueArgReadsUnknown. ⚠ may add Unknown to a function passing an opaque callback to such a HOF.
⚠ Classifier: a super.method() call to an effect through a GENERIC intermediate superclass no longer reads silent-pure — the runtime oracle found this cardinal-sin vein on Apache commons-dbcp2. A super-call compiles to INVOKESPECIAL owner=<direct superclass>; when that class does not declare the method (it is inherited from higher up — e.g. PoolableConnection extends DelegatingConnection<Connection> extends AbandonedTrace, super.setLastUsed() → owner DelegatingConnection, which inherits setLastUsed from AbandonedTrace where Instant.now() is the Clock leaf), candor edged to owner.method — a non-existent node — so the callee's effect was silently lost and the caller read pure (not even Unknown). The special/static edge now resolves to the nearest superclass that actually declares the method (via nearestConcreteSuper, the same resolution the virtual path uses), falling back to the raw owner when it declares the method itself or the target is external. Fixed 5 of 7 silent Clock under-reports the candor verify oracle caught on commons-dbcp2 (the remaining 2 are the harder _pool.borrowObject → activateObject framework-callback (A3) class). Regression: StructuralDispatchTest.superCallThroughGenericIntermediateSuperclassPropagatesEffect. ⚠ may add a previously-missed effect to a function that made such a super-call.
⚠ candor verify now attributes effects TRANSITIVELY — the oracle falsifies candor's core (transitive) claim, not only leaf classifications. candor's report is transitive (a function that reaches an effect is effectful); the shipped oracle, however, attributed each runtime effect only to the direct enclosing method (Trace.emit(enclosing-fn, effect), a transform-time constant). It was therefore structurally blind to a transitive cardinal sin: a caller that reaches an effect through a dropped/dynamic edge and is reported pure was never tested (the effect landed on the leaf; the caller's observed set was empty ⇒ vacuously held). Demonstrated minimally — seed middle (a caller of an Fs leaf) as pure and the old oracle reported 0 violations, holds. Trace.emit(effect) now walks the live stack and attributes the effect to the enclosing method and every analyzed caller on it, exactly matching candor's transitive report, so a caller-level miss is caught (app.Main.middle: ran {Fs} but declared {pure}). Overload-correct: EffectTransformer registers each analyzed method's runtime stack key (owner#name#descriptor → candor qual) at transform time, since a bare stack frame carries no overload descriptor for the qual. Effect: on Apache commons-io's filesystem suite the oracle now checks 133 functions (was 30) — the transitive callers, not just the I/O primitives — and still holds with 0 cardinal-sin violations, attribution-complete. This reintroduces a per-leaf stack walk (the transform-time con...
candor-java v0.21.0 — spec 0.21
spec 0.21 — the completeness manifest. The report envelope carries analyzed: {count, digest} (distinguish provably-pure from never-seen: pure count = analyzed.count − |functions|) and unanalyzed: [{path, reason}] (the target source candor could not parse). The sharp fix: a configured gate over incompletely-analyzed source now fails closed — exit 2 with a machine-legible {ok:false, incomplete:true, unanalyzed} verdict, instead of a green report a CI/agent read as an all-clear over unseen code. Additive; pinned four-way in gen_completeness.py.
candor-java v0.20.1
0.20.1 engine patch (spec stays 0.20) — grew the curated TELEMETRY_HOSTS set from a real-repo dogfood of 0.20: adds posthog.com, plausible.io, usefathom.com, heapanalytics.com, fullstory.com, hotjar.com, logrocket.com, cloudflareinsights.com (single-purpose analytics / session-replay / RUM). These now classify known-telemetry, so deny Net[unknown-host] tolerates them instead of false-flagging. Shared verbatim four-way; no vocabulary/schema change.
candor-java v0.20.0 — spec 0.20
spec 0.20 — the Net destination-class rung: a per-function netClass field (known-telemetry/known-partner/unknown-host) and a deny Net[unknown-host] security gate (egress only to known destinations; fail-closed on a masked/runtime host). Plus a reason-class query surface (blindspots --stats/--class, unverified --class). Additive over 0.19. Pinned four-way in conformance (parsepolicy netClasses + the net-destination-class differential).
candor-java 0.19.0 — spec 0.19
spec 0.19 — reason-scoped Unknown policies (deny E Unknown[class], the reasonClass verdict field, config unknown-alias). Additive tool-surface rung; no report-schema change. See CHANGELOG.md.
v0.18.0 — spec 0.18 (the trust-trio)
candor spec 0.18 — the trust-trio floor.
A pinned-tool-surface rung (no report/verdict change), pinned four-way in the conformance suite:
--strictadvisory-verb CI gate —fix-gate/gains/unverifiedare advisory (exit 0);--strictmakes each a CI gate (exit 1 while a finding remains). A typo'd flag is rejected loud (exit 2), never a swallowed disarmed gate;gainshas no--policy(a passed one names the scan-timedeny <E> gainedgate, AS-EFF-005).- surface/
tourmostly-Unknown disclosure — never "nothing hidden" (nor atour --json{"reaches":[]}) over a ≥⅓-Unknown graph. - Hardened by a Fable-model code review (caught two latent cardinal-sin edges: an un-gated scan opener; a single-dash flag swallow).
See CHANGELOG.md for the per-engine detail.
v0.17.0
candor 0.17 — the UX-audit engine round + spec floor bump. Query target validation (where/callers fail loud on a bad target, TIER-2 conformance), candor-ts node-fetch recall + prose-at-TTY output, empty-scope remedy text, and the /code-review fixes. Full four-way conformance: OK. See the spec §8 changelog + each CHANGELOG. Native binaries (candor-macos-arm64/linux-x64/macos-x64) attach automatically via native.yml.
candor-java 0.16.0 (reference)
The callgraph-aware baseline guard (spec 0.16, reference engine): a formerly-pure function that turns effectful is caught as a gain (keyed on the baseline callgraph sidecar). The ratchet fires only on a real boundary-effect gain; an Unknown-only gain is advisory (corpus-validated). Floor now 0.16, conformance-pinned four-way.
candor-java-0.16.0-all.jar— the fat jar (JRE 17+)- native binaries (candor-macos-arm64 / candor-linux-x64 / candor-macos-x64) are built + attached by CI
candor-java 0.15.0 — the coverage envelope (reference)
candor-java 0.15.0 — declares spec 0.15.
- The coverage envelope — the κ ledger travels with the report, computed one shared way (stderr/envelope/gate can never disagree);
--gate-jsongains a verdict-preserving coverage advisory;gains --jsonadds the ledger +coverageDelta. - Literal-head host extraction — a URL built by runtime string concatenation with a literal host base (
new URL("https://api.openai.com/v1/" + p)) now recovers the host from the bytecode (both javac concat shapes) and fires the Llm/Db/Net refinement.
Fat-jar attached. Wire-compatible with 0.14.