Skip to content

candor-swift v0.27.0

Choose a tag to compare

@tombaldwin tombaldwin released this 09 Aug 19:52
· 151 commits to main since this release

[0.27.0] — 2026-08-07

  • An EMPTY SCAN now reaches the machine channel. no Swift sources under <target> exited raw, so a
    consumer reading --gate-json - got nothing — an ordinary CI accident (a path that moved) and the
    last cause in this engine still exiting that way. Pinned by conformance PART 36 (b17).

  • An UNREADABLE config left a stale green at the FILE sink, while the same cause streamed its
    refusal correctly. The §3.3.1 pre-pass reads the config to learn what the sink must not overwrite and
    runs BEFORE arming; its own comment said that read was "LENIENT — no exit, no diagnostic", which was
    an assumption about the reader rather than a property of it. Three exit sites in that reader could
    fire, so the process died before arming and a previous run's ok: true survived on disk — the exact
    outcome arming exists to prevent. Third engine with this shape (ts and agents had it), and the first
    where only ONE of the two sink forms was affected: the stream was already right, which is why it took
    a file-sink probe to see. Pinned by conformance PART 36 (b15).

  • …and the guard now enumerates a dep directory exactly as the LOADER does. The first repair
    registered the directory's files with a FLAT read beside a RECURSIVE loader walk, so a report one
    level down stayed unguarded — and for --deps, which writes one subdirectory per name@version, the
    nested layout is the ORDINARY one. A guard that enumerates differently from the loader guards a
    different set of files. One enumeration now serves both.

  • ⚠ A --gate-json sink INSIDE a deps DIRECTORY destroyed the operator's dep report. deps
    accepts a directory — --workspace writes .candor/deps/ and hands that back, so it is the common
    spelling — and the loader walks it and reads each report inside. The §3.3.1 sink-over-input guard
    registered only the DIRECTORY, which never equals a file within it, so --gate-json <depdir>/lib.json
    was unguarded: arming destroyed the report, the run chained the wreckage and exited 0 with ok: true
    written over the input. All four engines. The FILE spelling of this channel had been guarded for a
    release; the directory spelling had not, and no row posed it. Now pinned by conformance PART 36 (b14),
    which asserts both the refusal AND that nothing was written.

  • A gate-adjacent flag given NO VALUE now reaches the machine channel. --policy, --out and
    --gate-json with a missing value exited raw, so --gate-json - got nothing — the last cause in this
    engine still doing that after every other had been routed, and the one §3.1 names beside the unknown
    flag. rust and ts already answered it. Found by sweeping the causes a user can TRIGGER rather than by
    reading exit sites, which is a different list and a shorter one.

  • The configured-dep refusal reaches the machine channel. depsFail exited raw, so --gate-json -
    got nothing and a file sink kept the armed placeholder rather than the reason. PART 35 has pinned the
    EXIT CODE for this cause in four engines for a release; nothing pinned the CHANNEL until PART 36 (b8).

  • The gate verb registers its stream sink before anything can exit, and writes it once.
    refuseGateAndExit already knew how to write - to stdout; the gate verbs pre-pass never put it in
    the sink list, so an exit-2 during argument parsing left stdout empty. Registering it then exposed the
    mirror — the flag loop registered the same sink again, and one exit-2 wrote the refusal document
    TWICE. Deduped at the write, which covers every appender rather than the two that exist today.

  • ⚠ MODULE IDENTITY IS NOW PER FILE, AND HONOURS THE DEPENDENCY GRAPH. The rung the entry below
    promised, built and reviewed three times. It replaces the withdrawal: the improvement that entry gave
    up is here, on evidence rather than on filesystem shape.

    The question the disclosure channels ask is per FILE: can THIS file's package — or THIS file's
    Xcode target — import THAT module?
    It was being answered per SCAN: a name claimed anywhere silenced
    it everywhere. That mismatch is what nine earlier review rounds kept rediscovering, and no bound on
    the derivation fixes it, because the derivation was answering the wrong question.

    A file's owning package can import its own targets plus, through each .package(path:) it declares,
    the PRODUCTS those local packages expose — the product is the unit of exposure, the target is the unit
    of import, and they are not interchangeable. An .xcodeproj target has no Package.swift, so its
    answer comes from the closure the --target resolver already walked: the products it links, plus the
    graph behind those specific products, since Xcode puts the whole reachable graph on a target's import
    path. Anything unreadable at any step — a computed path:, a non-literal targets:, no owning
    package at all — yields no claim, so the module stays named.

    Measured, 20 --target scans across NetNewsWire, IceCubesApp and firefox-ios. THIS entry's change
    leaves NetNewsWire and firefox untouched. Their gains come from two OTHER entries below — an Xcode
    target being a module, and the path-normalization fix — so on the released build firefox's Client
    reads 17608 analyzed functions and 51 uncovered modules, against 17562 and 55 at the start of this
    release's work. NOT "against 0.26": the released 0.26 engine has no --target flag at all and cannot
    produce those numbers. Every before/after figure in this section is measured against 430c5ef, the
    commit this thread started from, which is already past 0.26. What THIS one moves is IceCubesApp's app target: 48 uncovered modules to 38, and all
    ten names removed have analyzed functions in the same report
    (StatusKit 284, Account 109,
    DesignSystem 46, …). The 279 entries that leave functions with
    them carried no inferred effect between them — each held only an invisible hedge for a module this
    run had read.

    The invariant that makes this safe to state: every name that can be claimed internal has passed
    analyzedTargets, so a module is called internal only if a file under that target's real source root
    was read in this run. Absence is still never a claim of purity for anything unread.