Releases: tombaldwin/candor-swift
Release list
candor-swift v0.23.1
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 behindCANDOR_WORKSPACE_CHAIN). PART 18 conformance. - ⚠ opaque closure → synchronous invoker (
arr.forEach(cb)/map/filter/… with an opaque closure
param) disclosesUnknown— the four-way sync-callback rung (PART 1sync_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:
--strictadvisory-verb CI gate:fix-gate,gains,unverifiedare advisory (exit 0);--strict
makes each a CI gate (exit 1 while a finding remains).gainsrejects a--policy(exit 2), naming the
scan-timedeny <E> gainedgate (AS-EFF-005).- mostly-Unknown disclosure: the scan opener (
emitSurface) +tournever say "nothing hidden" over a
≥⅓-Unknown graph;tour --jsoncarries an additiveunknown: {count, total}. - Hardening from a Fable-model code review: the scan opener gained the ⅓-Unknown gate it was missing (only
tourhad it);gains --strictadded.
[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.Unknownis 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-jsonstaysok: 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.
coverageenvelope field. The κ-coverage ledger (the stderrclassifier doesn't coverline)
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 itsversion/specstrings).privacy-manifest --verifyis coverage-conditional. When the report's ledger is non-empty (or
any fn carriesinvisible), the JSON verdict gainsconditional: 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-jsonadvisory coverage note. The structured verdict carries the same small
coverageblock when the ledger is non-empty — VERDICT-PRESERVING (ok/violations/exit computed
exactly as before; the ⟨0.9⟩ provable-purity auto-disclosure precedent).gains --jsonre-discloses coverage. The CURRENT report's envelopecoverageblock rides the
answer verbatim when present, pluscoverageDelta: { 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 attributesinvisiblewith
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 locallet NAME = "literal"is indexed; a var, runtime value,
or non-const first segment stays bareNet, and ambiguous same-name consts resolve to nil. - Literal-head extraction (conformance PART 4r). An interpol...
candor-swift v0.23.0
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 behindCANDOR_WORKSPACE_CHAIN). PART 18 conformance. - ⚠ opaque closure → synchronous invoker (
arr.forEach(cb)/map/filter/… with an opaque closure
param) disclosesUnknown— the four-way sync-callback rung (PART 1sync_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:
--strictadvisory-verb CI gate:fix-gate,gains,unverifiedare advisory (exit 0);--strict
makes each a CI gate (exit 1 while a finding remains).gainsrejects a--policy(exit 2), naming the
scan-timedeny <E> gainedgate (AS-EFF-005).- mostly-Unknown disclosure: the scan opener (
emitSurface) +tournever say "nothing hidden" over a
≥⅓-Unknown graph;tour --jsoncarries an additiveunknown: {count, total}. - Hardening from a Fable-model code review: the scan opener gained the ⅓-Unknown gate it was missing (only
tourhad it);gains --strictadded.
[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.Unknownis 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-jsonstaysok: 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.
coverageenvelope field. The κ-coverage ledger (the stderrclassifier doesn't coverline)
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 itsversion/specstrings).privacy-manifest --verifyis coverage-conditional. When the report's ledger is non-empty (or
any fn carriesinvisible), the JSON verdict gainsconditional: 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-jsonadvisory coverage note. The structured verdict carries the same small
coverageblock when the ledger is non-empty — VERDICT-PRESERVING (ok/violations/exit computed
exactly as before; the ⟨0.9⟩ provable-purity auto-disclosure precedent).gains --jsonre-discloses coverage. The CURRENT report's envelopecoverageblock rides the
answer verbatim when present, pluscoverageDelta: { 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 attributesinvisiblewith
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 locallet NAME = "literal"is indexed; a var, runtime value,
or non-const first segment stays bareNet, and ambiguous same-name consts resolve to nil. - Literal-head extraction (conformance PART 4r). An interpolation/concat whose FIRST literal
segment itself completesscheme://authority/—"https://api.openai.com/v1/\(path)"— now
extracts the host and fires the refinement (was bareNet). A split authority, interpolated
port, or unterminated authority stays bareNet; a CDN-style head staysNet(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...
candor-swift 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-swift 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-swift 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-swift 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
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.
candor-swift 0.16.0
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
candor-swift 0.15.0 — declares spec 0.15.
- The coverage envelope + privacy-manifest conditionality — the κ ledger travels with the report, and
privacy-manifest --verifynow marks its verdictconditional: 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-jsongains the verdict-preserving advisory;gainsadds 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.