Skip to content

Dark Mode — the ballot drop door: hand-carried ballots arriving at an Atlas (forward-first, thresholded custody, mandatory fragmentability) #86

Open

Description

@tiliv

Milestone: Dark Mode. Sibling to #85 (the registration drop door — how an Atlas admits a Tell's listing) and to #71/#72/#73. This is the ballot drop door: the same building, a different door — how a hand-carried anecdote.ballot/v1 reaches an Atlas and what the Atlas does with it. It is door #2 of the three unbuilt doors named in anecdote.channel/docs/ballot-mesh.md ("The Atlas door, with the three-rule table").

Not yet deep — this serializes intent so a cold context can pick it up; the shapes below are the shape, not a settled spec. In the register of #85.

What already exists (do not rebuild)

The mesh under this door is largely built; what's missing is the admission behavior at the Atlas.

  • anecdote.channel/composer/ballot.mjs — anecdote.ballot/v1: device-signed answer that travels as a person, not a POST. ts (age stamp) lives inside the signature ("evidence, not an honor claim"). ballotId = content hash. turnInSubmission() projects a carried ballot back into the ordinary tell.submission/v1 idiom (a comment on the poll's canonical issue), so admission at a live home stays the Tell's bin/authz call.
  • satchel.mjs (pin/slush), meet.mjs + meet-carrier.mjs (trade choreography + the QR/fountain physical leg), quell.mjs, transfer.mjs/carrier.mjs (verify-from-anyone transport; packLayout/verifyLayout attest a set; the ok vs trusted split).
  • quell.mjs — isAuthorQuell (terminal), host quell (de-listing, loses to freshness via supersededBy), stillLive().

The model (why this door is different from the fast internet)

Pipe presumed bad, nodes presumed good. On the fast internet a silent node is presumed dead and the pipe is presumed good. Here it inverts: the pipe is what we presume broken; the nodes are people-run and as-good-as-they-can-be, holding a backlog until we meet again. Loss of contact is not death. Two towns that can't reach each other still distribute the same poll and still collect data; when they meet, the whole backlog arrives.

Consequences that fall out of this model rather than being imposed:

  • Lateness does not exist, symmetrically. A ballot can't be "late" because the Atlas carrying it can be equally late turning it in. quell.mjs already encodes the ballot half ("lateness is a position on a signed timeline, not a state"); the collector is on the same timeline as the ballot. A signed close date is still a standing quell; what's rejected is the idea of a late bucket.
  • Intake is the only signal of physical origin we have. In the offline origin, "this ballot came in here" is statistically "this ballot was near here." Commingling data with where-it-landed is not noise — it is the one piece of ground truth the system produces.
  • The proximity guarantee emerges for free. A cut-off Atlas naturally picks up the ballots it advertised, because those are the ones walking back toward it. So every Atlas behaving as willing to pick up is what lets us honestly say "listen to everyone; someone holds your shard."

The thesis: deliver poison, judge at the source

The Atlas is not the abuse gate, and making it one would violate two hard rules at once: boundary-canon's "witness of documents, not a judge of shapes" and OPEN-QUESTIONS §A's "a signature proves ownership, never fitness." So the mesh deliberately accepts and forwards likely-poison. Abuse resistance lives elsewhere:

  • Cost asymmetry. A synthetic ballot must fabricate a shocking amount of commingled context — met-records, co-signed meet artifacts, the ambassador-by-signature, the signed QR the ambassador showed — a chain of circumstantial events that are alibis for each other. The quality of the attestation is what lets a ballot rise to acceptance. The threshold raises fabrication's cost — never to infinity (collusion faking a meet is unsolvable by crypto; thresholds price it).
  • Judgment at the source, under the community's own constitution. A poisoned vote reaches the source, and the source — via the pile's constitution and the bin/judge / ATLAS_MATCH_CMD / govern seam — says "this doesn't work for us," with criteria the community can change.
  • The promise we keep. Never block; but you get to know it didn't work (finalize-submissions 👍/👎, read back by egress-github interpretStatus).

The honest answer to "it can't be gameable": we don't stop poison at the door — that would be a global gate we reject; we make it expensive to fabricate, fully signed so it's provable, and judged at the source. The timeline-as-artifact (trajectory, revocations, commingled proximity) is what lets a reviewer smell the synthetic.

The two acts (forward is the law; custody is the exception)

A ballot arriving triggers two distinct acts with two distinct risk profiles, and only one is dangerous.

Forward — promiscuous, safe, the default law

Relaying a signed ballot down the wire commits the Atlas to nothing: it verifies-from-anyone and waits on someone's accept (accept.mjs). A spam ballot forwarded is harmless flood. Any Atlas receiving a ballot does nothing but forward it, first, always — this is settled, not open. "Send it to a neighbor" is the hard search: it buys "ask all of their friends one hop further," and since an Atlas should already hold its neighbors' public poll listings, "do I know a live door for this?" is a local lookup that the forward extends outward. Flooding to all connected Atlases remains a good default move.

Take custody — thresholded, reversible, gated (the named open question)

Provisioning a stand-in pile and collecting a tally is the only act that exposes the Atlas to injection, and it is not yet specced. Proposed shape:

  • Custody must never depend on certainty, because certainty doesn't exist: boundary-canon promises "reachability from your roots — never completeness." "No home exists anywhere" is unknowable and can't be the trigger.
  • The trigger is a local, honest threshold — a conjunction the Atlas can evaluate alone: (1) it holds the poll object the ballot carried (the signed QR, ambassador-by-signature) so it knows the poll is real and what it's for; (2) stillLive() over every door it knows says no live door; (3) mass or scope-fit — either it is accumulating un-homed ballots for the same (scope, poll) (a shard forming in its hands whether it wants it or not), or its own boundary claim contains() the ballot's scope (bisect on-device — see "settled" below). Mass beats time as a trigger because time is meaningless here; accumulation is the honest signal of "I am, in fact, where this is landing."
  • Custody is reversible, because provisioning is itself a signed listing (the Atlas as attested provisioner, provisioner_spec — see data-pile). That drops it into Dark Mode — the Atlas drop door: registration admission + the three-rule table #85's freshness/quell arbitration: a truer home appearing later with a fresher signed listing supersededBys the stand-in. Taking custody is never terminal or exclusive.
  • The spam bound. Custody is gated behind the ejected decision seam (same shape as ATLAS_MATCH_CMD / bin/judge / govern) with a conservative honest default: forward-only; provisioning requires PR-consent or a configured judge; unattended custody is OFF by default, mirroring bin/match's honest needs-judgment that matches nothing. Even a poisoned shard that gets through is only "signed junk that landed at Atlas X" — provable, attributable, discarded downstream, because the Atlas never claims fitness, only witnesses arrival.

⚠ Open, and the least-certain part of this issue: whether the custody threshold should be driven primarily by mass or by scope-fit (the Atlas holding a boundary claim), and whether custody should ever fire without the Atlas holding a boundary claim at all. This is the section to sharpen before any PR builds it.

Dedup is content-id, never identity

Dedup hides subtleties worth dwelling on:

  • Content-id dedup only. The same signed artifact arriving twice (ballotId match) collapses — that's just the mesh converging. Two different signed ballots from one signer are not a double-vote to squash.
  • There is no one-answer-only integrity constraint — there can't be one offline, and often you don't want one: availability polls ("pick more than one time to meet"), general-sentiment trajectory, changed-mind revisions. The device may decline a second submission as a courtesy ("I have your receipt, you don't need another"), but that is UX, not enforcement.
  • Default leans count-all-with-sufficient-claims. How differing answers from one signer are counted is the constitution's decision, never the Atlas's. (bin/authz enforces no dedup today; OPEN-QUESTIONS flags "ballot-box integrity" as unsolved by design.)

Fragmentability is the actual guarantee (defrag is a trap)

The instinct to congregate every shard back to the original owner and evict it from ourselves is wrong — it increases data-loss risk. Once all the data is in one hand you cannot get it back, and the promised public report may simply never come.

  • THE ALL-CAPS FAILURE: if the data pile collecting the answers leaves the Tell it was originally addressed for, then all digital ballots fail AND all handmade ballots fail too — the Atlas would collect the handmade ones without trouble but have nowhere to forward them. This is the failure fragmentation exists to insure against.
  • The guarantee we want: any poll asked can be answered, and it does not matter if the owner vanishes.
  • Therefore, anything advertised/distributed on an Atlas must be fragmentable. The Atlas keeps its shard as durable redundancy; "handback" to the owner is share a copy (a PR-as-consent pickup the two sides negotiate), never move-and-delete. Forwarding to a live home still happens when one exists (and the Atlas should know how to forward late-arriving ballots to the Tell hosting the pile), but the shard stays.
  • Aggregation is the price/benefit of discoverability. A private poll on a Tell that is on no Atlas is not subject to aggregation — that was marked off early. Aggregation comes from the discoverability an Atlas gives. Fragmentation is what makes the promised aggregate report resilient to an owner who vanishes or collects everything and never reports — the report can be reconstructed from discoverable fragments (the "shattered shards"), not held hostage to one party's goodwill.

Provenance

Record by (who handed it over) as provenance — not authority, not identity theater. The device signature with ts inside is evidence. The poll object carried inside the ballot (the signed QR the ambassador showed) is what lets a ballot rise to acceptance; the Atlas holds no keys and no piles and "cannot read or prove much" — by design, not a gap.

Settled / rejected

  • Rejected: containment-as-authority. No Atlas-to-Atlas "give me every Atlas in the state and I'll fan out" pipe. It violates VISION ("neighbors, not a graph … authority never leaks sideways … even the state is just an Atlas") and boundary-canon ("witness of documents, not judge of shapes"; "'The Colorado atlas' is a convention the architecture refuses to enforce").
  • Kept: "contains" lives client-side as bisect() over signed boundary claims (anecdote.channel/composer/bisect.mjs, already built — contains() point-in-polygon, bisect() returns every containing constituency). A state Atlas may act only as an advisory recommender (§M: "it only ever proposes; the local merge disposes") — a convenience pointer, never a container that owns a list or grants transitive reach.
  • Off by default: unattended custody.
  • Non-goal: physical-proximity spam at the intake. Stopping someone physically standing at the intake is a physical-security problem ("how do we hire security"), not a crypto question. Explicitly out of scope.
  • Guard: an archival/forwarding mechanism must never quietly become "skim all the data / hand the Atlas away as a pipeline to some other server." (See open question below.)

New open question: Atlas archival

Because every Atlas now carries non-negligible datasets after not very long (fragmentation means it keeps shards). Add to OPEN-QUESTIONS:

  • What does Atlas archival look like, and how do we keep the reports despite archiving?
  • Archival is a flag, not a stop on the poll: "this is going down to a reserve, and that reserve is now in charge of reporting on it." It does not necessarily quell the poll.
  • Transparency is the guard. The mechanism must be so simple and transparent that it is unremarkable when someone watches it happen — precisely so it can never become covert exfiltration/pipelining of an Atlas's poll data to some other server.

Coupled / deferred

Home

Filed here in the Atlas-design / dark-mode series (as #85 chose) so it survives a context switch. True home is likely atlas.anecdote.channel (now in scope); cross-link/migrate when the atlas repo is in play.

Activity

  1. tiliv commented on Jul 6, 2026

    @tiliv
    CollaboratorAuthor

    Update — the Atlas-addressed pivot (higher ground; extends this issue, doesn't overwrite it)

    A later realization resolves the "how can anyone open a hand-carried ballot to count it" worry — by dissolving it. Recorded here so a cold context reads it before building on the sections above.

    Ballots are signed, not encrypted. In anecdote.channel/composer/ballot.mjs, answer is a plaintext field and signBallot is attest (Ed25519 sign), not encrypt. "Sealed" is the wax-seal sense — tamper-evident and attributable, you still read the letter. So an Atlas can read and count. What it "cannot read or prove much" of is the genuineness of context (it holds no keys to the proximity web, no piles), never the answer text.

    The pivot: Atlas-addressed QRs. What an Atlas advertises should be an Atlas-addressed QR (re-signed, Atlas-fronting, original instigator inside for lineage), not the Tell-addressed original. Answering it routes to the Atlas and the data becomes public by intent, so that what the public thing may be used for is defined by license, not by encryption. This makes the confidentiality/availability fork moot: there is no lock. Protection relocates to (1) pseudonymity, (2) a constituency view-gradient (you see aggregates for your own constituencies, and peers above you in aggregate only — never their direct rows), (3) the operator's license.

    Two artifact classes, and the crisp answer to "when to hold":

    • Tell-addressed poll — self-hosted, answers go home to the Tell, discoverable-if-on-an-Atlas but not Atlas-distributed (the old "photocopy/advertising" resolution still holds here).
    • Atlas-addressed poll — the Tell elects to push; the Atlas fronts it and stores answers public under license.

    So: an Atlas holds data iff it fronts an Atlas-addressed poll — or it caught the echo of one and archives it (lineage-provable but locally unrecognized → direct-to-archive). Dedup becomes arrival behavior (copies converge by ballotId as they arrive), not an after-the-fact pass.

    Split out into their own issues so this one stays the drop-door reference:

    Net effect on this issue's Confidentiality discussion: chosen posture is public-by-construction + view-gradient + license, superseding the encrypt-to-tally option (which would have re-broken the owner-can-vanish guarantee).


    Generated by Claude Code

  2. tiliv commented on Jul 6, 2026

    @tiliv
    CollaboratorAuthor

    Implementation plan — grounded in the current code

    Sequenced into PR-sized slices. The carrier/mesh half is already built in anecdote.channel/composer/; #86 is mostly Atlas-side glue + one pure resolver, no new crypto.

    Already built (reuse, don't rebuild)

    • composer/ballot.mjs — buildBallot/signBallot/verifyBallot/ballotId/turnInSubmission
    • composer/transfer.mjs (verifyTransfer/verifyLayout, ok/trusted split), carrier.mjs (carrierSession), meet-carrier.mjs (unpackMeetOffer, the "one voice" refusal)
    • composer/meet.mjs receiveMeet (quells verified + quelled ballots pruned before landing), satchel.mjs arrived() / applyQuells / dedup-by-ballotId
    • accept.mjs (friend gate + signed encounter records), route.mjs (never-blocked-only-routed), quell.mjs (stillLive/supersededBy/isAuthorQuell)
    • data-pile bin/pile-new --provisioner (stamps provisioner/provisioner_spec; custody rule — a provisioner never touches the age identity)
    • tell turnInSubmission → tell.submission/v1 → bin/authz

    Slice 0 — Attest before you act (atlas; text only)

    The Atlas CONSTITUTION growth clause requires attesting new conduct in words before running it. Amend CONSTITUTION.md (accept hand-carried, verify-from-anyone ballots at one signed door; forward what I can't home; never judge fitness; may stand up a stand-in tank addressed to an absent owner while holding no key that opens it; custody reversible by freshness) + add notes/ballot-door.md (the three-branch table + entitlement/witness stance). Also narrows the "I do not register data-piles directly" clause with the attested stand-in exception. Risk: none; unblocks all.

    Slice 1 — Arrival → turn-in (minimum case) (anecdote.channel; composition)

    New pure resolver composer/drop.mjs — resolveDrop({ballots, listings, quells, myScopes, authorKidFor}) → {turnIn, shrugQuellBack, floodOnward} (the three-rule table on quell.stillLive + satchel.arrived). Wire turnIn → turnInSubmission → existing egress; admission stays bin/authz. Tests composer/drop.test.mjs (real Ed25519, one case per branch + dedup + tamper). Risk: low.

    Slice 2 — The Atlas drop door: receive + verify + forward-first (atlas; greenfield, the settled law)

    New bin/drop imitating bin/match: bare invocation, ATLAS_* I/O, verify via the atlas's own stdlib verifyAttested (as in dump.mjs/atlas-index.mjs), dedup by content-id. Three branches: known live door → stage to examine-not-merge drop/<scope>/<poll> branch; quelled/closed → emit the quell back (the shrug); unknown → _data/drop-archive/<scope>/<poll>/<ballotId>.json (content-addressed → dedup is arrival behavior) + one-hop flood to _data/atlases.yml. Ejected ATLAS_DROP_CMD, honest default = forward-only, never custody. Wrap in .github/actions/drop + drop.yml; add bin/drop.test.mjs and its path to test.yml. Risk: medium; every piece follows a precedent.

    Slice 3 — Thresholded custody (atlas + data-pile; the OPEN question, gated; depends on #87)

    Fires only via PR-consent or a configured judge: trigger = holds the #87 fronted-poll object + stillLive says no live door + mass/scope-fit. Runs data-pile bin/pile-new --provisioner <atlas> --recipient <instigator-age-from-#87> → a stand-in pile the Atlas can't open; the Atlas signs the delivery manifest (sign ≠ decrypt). Reversible via supersededBy; adoptable when the instigator reappears. Risk: high; last.

    Two decisions this surfaced

    1. Cross-repo sharing (Slice 2): recommend bin/drop reuse the atlas's own stdlib Ed25519 verify (reimplement ballotId) over importing anecdote composer/ — low coupling, matches dump.mjs.
    2. Custody recipient (Slice 3): data-pile forbids the provisioner holding the age identity, and the owner is exactly who's unreachable — so a stand-in pile can only be sealed to an age recipient carried in Dark Mode — the Atlas-fronted poll + network-addressed ballot: the transformed Tell-derivative, collectible on any lineage Atlas #87's fronted-poll object; absent that, the door degrades to archive-only. → Dark Mode — the ballot drop door: hand-carried ballots arriving at an Atlas (forward-first, thresholded custody, mandatory fragmentability) #86-custody hard-depends on Dark Mode — the Atlas-fronted poll + network-addressed ballot: the transformed Tell-derivative, collectible on any lineage Atlas #87 carrying the instigator's age recipient (should fold into Dark Mode — the Atlas-fronted poll + network-addressed ballot: the transformed Tell-derivative, collectible on any lineage Atlas #87's artifact shape).

    Sequencing: Slice 0 → (Slice 1 ∥ Slice 2) → Slice 3 after #87. The open mass-vs-scope-fit threshold only blocks Slice 3.

    See also: #87 (fronted poll / the recipient dependency), #88 (Antidote), NONPROFIT.draft-0.md / ANTIDOTE.draft-0.md.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions