Skip to content

Releases: tombaldwin/candor-java

candor-java v0.23.1

Choose a tag to compare

@tombaldwin tombaldwin released this 20 Jul 12:11

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 the java -jar/native analyze pass — no gate or report change.
    A follow-up pass on the reporting stage (Surface.nearestSource BFS: pre-sort each function's callees
    once instead of at every BFS node; HashSet for 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 fluent cc.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 retained org.springframework.ai. blanket — so every real dispatch (chat, streaming
    terminal
    , EmbeddingModel.call, and anything not explicitly listed as a pure builder) still
    classifies Llm+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 anchors springAiChatClientStream +
    springAiEmbeddingCall now pin it.) Closes the two kappa_libs_probe fabrications
    (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-lifecycle Clock).
  • ⚠ opaque callback → synchronous invoking HOF (forEach/forEachRemaining/ifPresent, matched
    owner-agnostically so List.forEach is covered) discloses Unknown — the four-way sync-callback rung
    (conformance sync_callback_opaque).
  • ⚠ filter/buffered stream read/write/skip/flush/close that delegates to a wrapped sink of
    unknown type discloses Unknown (compress/vfs2 Monitor/Filter streams).
  • AccessController.doPrivileged(action) now runs its PrivilegedAction/PrivilegedExceptionAction's
    run() — the wrapped effect no longer orphaned (vfs2 PrivilegedFileReplicator).
  • ⚠ value provenance: a stream-consuming utility (IOUtils.read/copy/…, Guava streams) that reads a
    stream the method did not itself open discloses Unknown; whole-program, a project stream field proven
    bound only to in-scope concrete opens stays precise (construction-carried suppression). Closes the
    readFully class as honest disclosure without abandoning the source/sink stance.
  • candor verify oracle — 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 ...

Read more

candor-java v0.23.0

Choose a tag to compare

@tombaldwin tombaldwin released this 20 Jul 08:51

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-lifecycle Clock).
  • ⚠ opaque callback → synchronous invoking HOF (forEach/forEachRemaining/ifPresent, matched
    owner-agnostically so List.forEach is covered) discloses Unknown — the four-way sync-callback rung
    (conformance sync_callback_opaque).
  • ⚠ filter/buffered stream read/write/skip/flush/close that delegates to a wrapped sink of
    unknown type discloses Unknown (compress/vfs2 Monitor/Filter streams).
  • AccessController.doPrivileged(action) now runs its PrivilegedAction/PrivilegedExceptionAction's
    run() — the wrapped effect no longer orphaned (vfs2 PrivilegedFileReplicator).
  • ⚠ value provenance: a stream-consuming utility (IOUtils.read/copy/…, Guava streams) that reads a
    stream the method did not itself open discloses Unknown; whole-program, a project stream field proven
    bound only to in-scope concrete opens stays precise (construction-carried suppression). Closes the
    readFully class as honest disclosure without abandoning the source/sink stance.
  • candor verify oracle — 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...

Read more

candor-java v0.21.0 — spec 0.21

Choose a tag to compare

@tombaldwin tombaldwin released this 17 Jul 14:57

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

Choose a tag to compare

@tombaldwin tombaldwin released this 17 Jul 13:21

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

Choose a tag to compare

@tombaldwin tombaldwin released this 17 Jul 12:58

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

Choose a tag to compare

@tombaldwin tombaldwin released this 17 Jul 09:23

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)

Choose a tag to compare

@tombaldwin tombaldwin released this 16 Jul 19:53

candor spec 0.18 — the trust-trio floor.

A pinned-tool-surface rung (no report/verdict change), pinned four-way in the conformance suite:

  • --strict advisory-verb CI gatefix-gate/gains/unverified are advisory (exit 0); --strict makes each a CI gate (exit 1 while a finding remains). A typo'd flag is rejected loud (exit 2), never a swallowed disarmed gate; gains has no --policy (a passed one names the scan-time deny <E> gained gate, AS-EFF-005).
  • surface/tour mostly-Unknown disclosure — never "nothing hidden" (nor a tour --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

Choose a tag to compare

@tombaldwin tombaldwin released this 16 Jul 16:19

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)

Choose a tag to compare

@tombaldwin tombaldwin released this 16 Jul 08:47

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)

Choose a tag to compare

@tombaldwin tombaldwin released this 15 Jul 11:26

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-json gains a verdict-preserving coverage advisory; gains --json adds 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.