Skip to content

Releases: tombaldwin/candor-swift

candor-swift v0.23.1

Choose a tag to compare

@tombaldwin tombaldwin released this 20 Jul 12:11

Changelog

All notable changes to candor-swift are recorded here. Format loosely follows
Keep a Changelog; candor is pre-1.0, so minor versions may include
behavioural changes (always in the soundness-increasing direction — see the §4 trust contract).
A heading marks a report- or verdict-affecting change: it changes report bytes or gate
verdicts, so an engine upgrade across it is baseline-invalidating (regenerate any saved baseline
with the new build — the AS-EFF-005 guard refuses a cross-build baseline by design).

[0.23.1] — 2026-07-20

performance — quadratic per-function loop-invariant removed (no output change)

The per-function analysis loop rebuilt Set(freeFnByName.keys) at every CallCollector construction —
freeFnByName is fixed after decl-aggregation and never mutated in the loop, so this was O(freeFns) × N =
O(N²) on any function-heavy corpus (ms/function rose 0.07 → 0.40 across 500→10000 funcs). Hoisted to a
single build before the loop → flat ~0.05 ms/function, 8.4× faster at 10k functions. Report output
verified byte-for-byte identical (this is a loop-invariant hoist, not a semantic change); swift test green.

⚠ soundness — sync callback-invoker opaque-arg (Swift arm of the four-way parity fix)

Fixes a silent under-report (cardinal sin): an OPAQUE closure passed to a SYNCHRONOUS higher-order
invoker — Sequence/Collection.forEach/map/filter/compactMap/flatMap/reduce/first/contains/allSatisfy/ sorted/min/max/… (and their bare-self forms in a Collection extension) — read PURE, when the
invoker CALLS its closure argument in-thread. xs.forEach(cb) where cb is a fn-typed param is the
exact sibling of the already-correct direct call cb() and must contribute Unknown (callback:<arg>).
INLINE closure literals (xs.forEach { … }) keep their analyzed effect (charged lexically to the
passer — never reach the guard) and RESOLVABLE named callables (xs.forEach(namedFn)) keep their
resolved effect via the fn-ref edge — only OPAQUE args disclose, so over-disclosure is low. Matches
candor-java's SYNC_CALLBACK_INVOKERS opaque-arg guard (c755acd). A/B on swift-argument-parser:
+2 direct sites (InputOrigin.forEach, ErrorMessageGenerator.unknownOptionMessage) + 3 honest
transitive reaches; zero fabrication, zero inline-closure flood; report bytes otherwise unchanged.

[0.23.0] — 2026-07-20

Spec floor → 0.23. Soundness-increasing, report-shape-neutral:

  • cross-package protocol dispatch (interfaceUnion, the 0.23 rung): a chained consumer's protocol method
    resolves to the conformer's effect (gated behind CANDOR_WORKSPACE_CHAIN). PART 18 conformance.
  • ⚠ opaque closure → synchronous invoker (arr.forEach(cb)/map/filter/… with an opaque closure
    param) discloses Unknown — the four-way sync-callback rung (PART 1 sync_callback_opaque). Inline
    closure literals keep their analyzed effect (no over-disclosure).
  • protocol-union emission guarded against same-tail-type name collision.

[0.22.0] — 2026-07-18

Spec floor → 0.22 (the verify oracle rung, shipped on the java/ts arms). candor-swift declares 0.22; the
report and verdict schema are unchanged from 0.21, so this engine's output is byte-identical across the bump. No
functional change to the Swift engine.

[0.19.0] — 2026-07-17

Reason-scoped Unknown policies (SPEC §6.2): deny E Unknown[reflect,dispatch,indirect,native,unresolved,setup]
narrows the Unknown part of a deny to a fixed reason-class vocabulary, with the dynamic/* aliases and
config .candor/config unknown-alias <name> = <class…> names. Bare deny E Unknown is unchanged
(Unknown[*]); an unrecognized reason maps to unresolved; the class propagates transitively. An AS-EFF-006
--gate-json verdict whose effects include Unknown carries a reasonClass array. Report bytes
unchanged. Also: a scan-level SETUP warning — a Package.swift that declares dependencies but has no
fetched .build/checkouts prints a "run swift build" remediation (the analog of a missing node_modules).

[0.18.0] — 2026-07-16

spec 0.18 — the trust-trio

candor-swift now declares spec 0.18 (specVersion; engine candor-swift-0.18.0). A pinned-tool-surface
rung (no report/verdict change), pinned four-way in the conformance suite:

  • --strict advisory-verb CI gate: fix-gate, gains, unverified are advisory (exit 0); --strict
    makes each a CI gate (exit 1 while a finding remains). gains rejects a --policy (exit 2), naming the
    scan-time deny <E> gained gate (AS-EFF-005).
  • mostly-Unknown disclosure: the scan opener (emitSurface) + tour never say "nothing hidden" over a
    ≥⅓-Unknown graph; tour --json carries an additive unknown: {count, total}.
  • Hardening from a Fable-model code review: the scan opener gained the ⅓-Unknown gate it was missing (only
    tour had it); gains --strict added.

[0.16.0] — 2026-07-16

⚠ The callgraph-aware baseline guard + Unknown-only advisory ⟨0.16⟩

The AS-EFF-005 regression ratchet (CANDOR_BASELINE) now keys existence on the baseline CALLGRAPH
sidecar, not the report. The report OMITS pure functions, so the pre-⟨0.16⟩ guard read a formerly-
PURE function turning effectful as "new" (exempt) and let it escape — the sharpest supply-chain
shape. ⟨0.16⟩ the sidecar lists every analyzed function (pure ones included, SPEC §2.2), so a
pure→effectful transition is now caught: [AS-EFF-005], exit 1.

  • Callgraph-keyed existence. A function present in the baseline callgraph sidecar but absent from
    its report is a KNOWN-PURE function; gaining a real boundary effect against it is a violation. When
    no sidecar accompanies the baseline the guard degrades to report-only existence (a stderr note,
    exit unchanged) rather than failing; a corrupt sidecar fails closed (exit 2).
  • Unknown-only gain is advisory, not exit 1. A corpus test found the guard, on real dependency
    bumps, firing only on gained-Unknown — resolution noise, not a capability gain. Unknown is the
    §4 trust marker (pure policies exclude it), so failing pure→Unknown broke CI on innocuous bumps.
    Now the ratchet (exit 1) fires only on gaining a REAL boundary effect; an Unknown-ONLY gain is
    disclosed as one advisory note, exit unchanged (--gate-json stays ok: true). A real+Unknown
    gain still fails and reports only the real effect. Pinned by conformance PART 15c.

⚠ report/verdict-affecting — an upgrade across 0.16 is baseline-invalidating (regenerate the
saved baseline, callgraph sidecar included, with the new build); and ⚠ the spec string changed
to 0.16 — a consumer pinning spec == "0.15" must accept 0.16.

[0.15.0] — 2026-07-15

⚠ The coverage envelope + privacy-manifest conditionality ⟨0.15⟩ (candor-spec/COVERAGE-DESIGN.md)

The wikipedia-ios false-confidence find (a real-world corpus-testing find, SOUNDNESS-LOG 2026-07-15):
"what the scan couldn't see" — the κ ledger — now travels WITH the report and conditions the
verdicts, instead of evaporating on stderr. Before: privacy-manifest --verify answered ok: true
with no caveat over 19 invisible modules; after: conditional: true + coverage: { uncovered: 19 },
exit code unchanged. Pinned by conformance PART 4s.

  • coverage envelope field. The κ-coverage ledger (the stderr classifier doesn't cover line)
    as data: "coverage": { "uncovered": [ { "name", "calls" }, … ] } — same modules, same import
    counts, same order; OMITTED entirely when nothing is uncovered, so a fully-covered report keeps
    the exact pre-0.15 document shape (verified byte-identical against the prior release binary
    before the version bump; the shipped envelope differs only in its version/spec strings).
  • privacy-manifest --verify is coverage-conditional. When the report's ledger is non-empty (or
    any fn carries invisible), the JSON verdict gains conditional: true +
    coverage: { uncovered: N, modules: [...] }, and the human output appends the
    ⚠ verdict is conditional on N uncovered modules… line. Exit code UNCHANGED (disclosure, not a
    gate); both keys absent on a fully-covered report.
  • --gate-json advisory coverage note. The structured verdict carries the same small
    coverage block when the ledger is non-empty — VERDICT-PRESERVING (ok/violations/exit computed
    exactly as before; the ⟨0.9⟩ provable-purity auto-disclosure precedent).
  • gains --json re-discloses coverage. The CURRENT report's envelope coverage block rides the
    answer verbatim when present, plus coverageDelta: { nowUncovered, noLongerUncovered } when the
    baseline's uncovered NAME SET differs (names only). Human TSV unchanged (pinned surface).
  • Per-fn invisible, module-qualified precision. A member call whose confidently-resolved
    receiver root IS a blind imported module (SomeSDK.doThing()) now attributes invisible with
    exactly that module — precise, not file-granular, so the sweep-[33]/[36] no-flooding guard for
    member calls on stdlib/κ-pure receivers is untouched. (Report bytes change only for that shape —
    an added disclosure, the sound direction.)

Host-resolution recall — constant strings + literal heads

Two recall improvements to the §1 host-refinement path (Llm/Db/Net uniformly), matching
candor-java; both conservative — any ambiguity stays bare Net, no fabrication:

  • Constant-string resolution (conformance PART 4q). A host built from a string constant —
    let apiBase = "https://api.openai.com"; URL(string: "\(apiBase)/chat") (interpolation, bare
    ref, or const-left concat) — now resolves through the host-refinement path exactly like an inline
    literal. A module/global/static or local let NAME = "literal" is indexed; a var, runtime value,
    or non-const first segment stays bare Net, and ambiguous same-name consts resolve to nil.
  • Literal-head extraction (conformance PART 4r). An interpol...
Read more

candor-swift v0.23.0

Choose a tag to compare

@tombaldwin tombaldwin released this 20 Jul 08:51

Changelog

All notable changes to candor-swift are recorded here. Format loosely follows
Keep a Changelog; candor is pre-1.0, so minor versions may include
behavioural changes (always in the soundness-increasing direction — see the §4 trust contract).
A heading marks a report- or verdict-affecting change: it changes report bytes or gate
verdicts, so an engine upgrade across it is baseline-invalidating (regenerate any saved baseline
with the new build — the AS-EFF-005 guard refuses a cross-build baseline by design).

[Unreleased]

⚠ soundness — sync callback-invoker opaque-arg (Swift arm of the four-way parity fix)

Fixes a silent under-report (cardinal sin): an OPAQUE closure passed to a SYNCHRONOUS higher-order
invoker — Sequence/Collection.forEach/map/filter/compactMap/flatMap/reduce/first/contains/allSatisfy/ sorted/min/max/… (and their bare-self forms in a Collection extension) — read PURE, when the
invoker CALLS its closure argument in-thread. xs.forEach(cb) where cb is a fn-typed param is the
exact sibling of the already-correct direct call cb() and must contribute Unknown (callback:<arg>).
INLINE closure literals (xs.forEach { … }) keep their analyzed effect (charged lexically to the
passer — never reach the guard) and RESOLVABLE named callables (xs.forEach(namedFn)) keep their
resolved effect via the fn-ref edge — only OPAQUE args disclose, so over-disclosure is low. Matches
candor-java's SYNC_CALLBACK_INVOKERS opaque-arg guard (c755acd). A/B on swift-argument-parser:
+2 direct sites (InputOrigin.forEach, ErrorMessageGenerator.unknownOptionMessage) + 3 honest
transitive reaches; zero fabrication, zero inline-closure flood; report bytes otherwise unchanged.

[0.23.0] — 2026-07-20

Spec floor → 0.23. Soundness-increasing, report-shape-neutral:

  • cross-package protocol dispatch (interfaceUnion, the 0.23 rung): a chained consumer's protocol method
    resolves to the conformer's effect (gated behind CANDOR_WORKSPACE_CHAIN). PART 18 conformance.
  • ⚠ opaque closure → synchronous invoker (arr.forEach(cb)/map/filter/… with an opaque closure
    param) discloses Unknown — the four-way sync-callback rung (PART 1 sync_callback_opaque). Inline
    closure literals keep their analyzed effect (no over-disclosure).
  • protocol-union emission guarded against same-tail-type name collision.

[0.22.0] — 2026-07-18

Spec floor → 0.22 (the verify oracle rung, shipped on the java/ts arms). candor-swift declares 0.22; the
report and verdict schema are unchanged from 0.21, so this engine's output is byte-identical across the bump. No
functional change to the Swift engine.

[0.19.0] — 2026-07-17

Reason-scoped Unknown policies (SPEC §6.2): deny E Unknown[reflect,dispatch,indirect,native,unresolved,setup]
narrows the Unknown part of a deny to a fixed reason-class vocabulary, with the dynamic/* aliases and
config .candor/config unknown-alias <name> = <class…> names. Bare deny E Unknown is unchanged
(Unknown[*]); an unrecognized reason maps to unresolved; the class propagates transitively. An AS-EFF-006
--gate-json verdict whose effects include Unknown carries a reasonClass array. Report bytes
unchanged. Also: a scan-level SETUP warning — a Package.swift that declares dependencies but has no
fetched .build/checkouts prints a "run swift build" remediation (the analog of a missing node_modules).

[0.18.0] — 2026-07-16

spec 0.18 — the trust-trio

candor-swift now declares spec 0.18 (specVersion; engine candor-swift-0.18.0). A pinned-tool-surface
rung (no report/verdict change), pinned four-way in the conformance suite:

  • --strict advisory-verb CI gate: fix-gate, gains, unverified are advisory (exit 0); --strict
    makes each a CI gate (exit 1 while a finding remains). gains rejects a --policy (exit 2), naming the
    scan-time deny <E> gained gate (AS-EFF-005).
  • mostly-Unknown disclosure: the scan opener (emitSurface) + tour never say "nothing hidden" over a
    ≥⅓-Unknown graph; tour --json carries an additive unknown: {count, total}.
  • Hardening from a Fable-model code review: the scan opener gained the ⅓-Unknown gate it was missing (only
    tour had it); gains --strict added.

[0.16.0] — 2026-07-16

⚠ The callgraph-aware baseline guard + Unknown-only advisory ⟨0.16⟩

The AS-EFF-005 regression ratchet (CANDOR_BASELINE) now keys existence on the baseline CALLGRAPH
sidecar, not the report. The report OMITS pure functions, so the pre-⟨0.16⟩ guard read a formerly-
PURE function turning effectful as "new" (exempt) and let it escape — the sharpest supply-chain
shape. ⟨0.16⟩ the sidecar lists every analyzed function (pure ones included, SPEC §2.2), so a
pure→effectful transition is now caught: [AS-EFF-005], exit 1.

  • Callgraph-keyed existence. A function present in the baseline callgraph sidecar but absent from
    its report is a KNOWN-PURE function; gaining a real boundary effect against it is a violation. When
    no sidecar accompanies the baseline the guard degrades to report-only existence (a stderr note,
    exit unchanged) rather than failing; a corrupt sidecar fails closed (exit 2).
  • Unknown-only gain is advisory, not exit 1. A corpus test found the guard, on real dependency
    bumps, firing only on gained-Unknown — resolution noise, not a capability gain. Unknown is the
    §4 trust marker (pure policies exclude it), so failing pure→Unknown broke CI on innocuous bumps.
    Now the ratchet (exit 1) fires only on gaining a REAL boundary effect; an Unknown-ONLY gain is
    disclosed as one advisory note, exit unchanged (--gate-json stays ok: true). A real+Unknown
    gain still fails and reports only the real effect. Pinned by conformance PART 15c.

⚠ report/verdict-affecting — an upgrade across 0.16 is baseline-invalidating (regenerate the
saved baseline, callgraph sidecar included, with the new build); and ⚠ the spec string changed
to 0.16 — a consumer pinning spec == "0.15" must accept 0.16.

[0.15.0] — 2026-07-15

⚠ The coverage envelope + privacy-manifest conditionality ⟨0.15⟩ (candor-spec/COVERAGE-DESIGN.md)

The wikipedia-ios false-confidence find (a real-world corpus-testing find, SOUNDNESS-LOG 2026-07-15):
"what the scan couldn't see" — the κ ledger — now travels WITH the report and conditions the
verdicts, instead of evaporating on stderr. Before: privacy-manifest --verify answered ok: true
with no caveat over 19 invisible modules; after: conditional: true + coverage: { uncovered: 19 },
exit code unchanged. Pinned by conformance PART 4s.

  • coverage envelope field. The κ-coverage ledger (the stderr classifier doesn't cover line)
    as data: "coverage": { "uncovered": [ { "name", "calls" }, … ] } — same modules, same import
    counts, same order; OMITTED entirely when nothing is uncovered, so a fully-covered report keeps
    the exact pre-0.15 document shape (verified byte-identical against the prior release binary
    before the version bump; the shipped envelope differs only in its version/spec strings).
  • privacy-manifest --verify is coverage-conditional. When the report's ledger is non-empty (or
    any fn carries invisible), the JSON verdict gains conditional: true +
    coverage: { uncovered: N, modules: [...] }, and the human output appends the
    ⚠ verdict is conditional on N uncovered modules… line. Exit code UNCHANGED (disclosure, not a
    gate); both keys absent on a fully-covered report.
  • --gate-json advisory coverage note. The structured verdict carries the same small
    coverage block when the ledger is non-empty — VERDICT-PRESERVING (ok/violations/exit computed
    exactly as before; the ⟨0.9⟩ provable-purity auto-disclosure precedent).
  • gains --json re-discloses coverage. The CURRENT report's envelope coverage block rides the
    answer verbatim when present, plus coverageDelta: { nowUncovered, noLongerUncovered } when the
    baseline's uncovered NAME SET differs (names only). Human TSV unchanged (pinned surface).
  • Per-fn invisible, module-qualified precision. A member call whose confidently-resolved
    receiver root IS a blind imported module (SomeSDK.doThing()) now attributes invisible with
    exactly that module — precise, not file-granular, so the sweep-[33]/[36] no-flooding guard for
    member calls on stdlib/κ-pure receivers is untouched. (Report bytes change only for that shape —
    an added disclosure, the sound direction.)

Host-resolution recall — constant strings + literal heads

Two recall improvements to the §1 host-refinement path (Llm/Db/Net uniformly), matching
candor-java; both conservative — any ambiguity stays bare Net, no fabrication:

  • Constant-string resolution (conformance PART 4q). A host built from a string constant —
    let apiBase = "https://api.openai.com"; URL(string: "\(apiBase)/chat") (interpolation, bare
    ref, or const-left concat) — now resolves through the host-refinement path exactly like an inline
    literal. A module/global/static or local let NAME = "literal" is indexed; a var, runtime value,
    or non-const first segment stays bare Net, and ambiguous same-name consts resolve to nil.
  • Literal-head extraction (conformance PART 4r). An interpolation/concat whose FIRST literal
    segment itself completes scheme://authority/"https://api.openai.com/v1/\(path)" — now
    extracts the host and fires the refinement (was bare Net). A split authority, interpolated
    port, or unterminated authority stays bare Net; a CDN-style head stays Net (no fabrication).

spec 0.15 — the coverage-envelope rung (§2)

candor-swift now declares spec 0.15 (specVersion in main.swift; the envelope + --gate-json
verdict carry it). 0.15 is another tier-2 (pinned-tool-surface) rung, additive over 0.14: it admits
the envelope coverage ledger and the coverage-co...

Read more

candor-swift 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-swift 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-swift 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-swift 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

Choose a tag to compare

@github-actions github-actions released this 16 Jul 19:49

Full Changelog: v0.17.0...v0.18.0

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.

candor-swift 0.16.0

Choose a tag to compare

@tombaldwin tombaldwin released this 16 Jul 08:46

The callgraph-aware baseline guard (spec 0.16): a formerly-pure function that turns effectful is caught as a gain (keyed on the baseline callgraph sidecar), rather than exempt as "new code". The ratchet fires only on a real boundary-effect gain; an Unknown-only gain is disclosed as advisory (validated by a real-world corpus test). Floor now 0.16, conformance-pinned four-way.

candor-swift 0.15.0 — coverage + privacy-manifest conditionality

Choose a tag to compare

@tombaldwin tombaldwin released this 15 Jul 11:26

candor-swift 0.15.0 — declares spec 0.15.

  • The coverage envelope + privacy-manifest conditionality — the κ ledger travels with the report, and privacy-manifest --verify now marks its verdict conditional: true (+ a human ⚠) when uncovered modules could hide sensor usage — a verdict that carries its caveat instead of a false all-clear (the wikipedia-ios find). Exits unchanged. --gate-json gains the verdict-preserving advisory; gains adds the ledger + delta.
  • Host-resolution recall — constant-anchored ("\(apiBase)/chat") and literal-head ("https://api.openai.com/v1/\(p)") hosts now fire the Llm/Db/Net refinement.

⚠ Report bytes change for the new surfaces. Wire-compatible with 0.14.