Repository navigation
Dark Mode — the ballot drop door: hand-carried ballots arriving at an Atlas (forward-first, thresholded custody, mandatory fragmentability) #86
Description
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 ordinarytell.submission/v1idiom (a comment on the poll's canonical issue), so admission at a live home stays the Tell'sbin/authzcall.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/verifyLayoutattest a set; theokvstrustedsplit).quell.mjs—isAuthorQuell(terminal), host quell (de-listing, loses to freshness viasupersededBy),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.mjsalready 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/governseam — 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 byegress-githubinterpretStatus).
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 claimcontains()the ballot's scope (bisecton-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 listingsupersededBys 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, mirroringbin/match's honestneeds-judgmentthat 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 (
ballotIdmatch) 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/authzenforces 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
- Dark Mode — the Atlas drop door: registration admission + the three-rule table #85 — the freshness/quell/ownership arbitration this custody-as-listing rides on.
- Dark Mode 1 — signed Atlas snapshot export + verify-on-ingest ("real at one time") #71 / Dark Mode 2 — the sneakernet: stacked snapshot drop-off, cascading push #72 — signed snapshot export + the sneakernet; the heartbeat/backlog structure this leans on.
- Dark Mode 3 — the Atlas offline frontend: your local copy, everything public, the matcher running for you #73 — the offline Atlas frontend; consumer of what lands here.
- OPEN-QUESTIONS §A (the summonable judge / constituency solve — the custody decision seam), §C (aggregation → composite "reusable report" across Atlases; the
bin/rollup→tell.digest/v1shard shape andtell.poll.summary/v1merge shape; "registry of bounded concepts"), §E (matcher addressing), §M/§N (neighbors-not-a-graph, liveness-as-observed-claim). anecdote.channel/docs/ballot-mesh.md"The doors (unbuilt, in order)" — this issue is door #2.tell.anecdote.channel/docs/sealed-credential.md"The offline parallel" — the presence-plus-possession authority a ballot constitutes; the threshold-of-diverse-meet-artifacts policy.
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
Add NONPROFIT.draft-0.md — the operator's constitution / license-to-f…
tiliv commentedon Jul 6, 2026
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,answeris a plaintext field andsignBallotisattest(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":
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
ballotIdas they arrive), not an after-the-fact pass.Split out into their own issues so this one stays the drop-door reference:
anecdote.atlaspoll/v1): the transformed Tell-derivative, verifiable on any lineage Atlas. The real build seed.NONPROFIT.draft-0.md(this branch / forthcoming PR) — the operator's constitution + the public-not-open-source license the whole posture rests on.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
tiliv commentedon Jul 6, 2026
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/turnInSubmissioncomposer/transfer.mjs(verifyTransfer/verifyLayout, ok/trusted split),carrier.mjs(carrierSession),meet-carrier.mjs(unpackMeetOffer, the "one voice" refusal)composer/meet.mjsreceiveMeet(quells verified + quelled ballots pruned before landing),satchel.mjsarrived()/applyQuells/ dedup-by-ballotIdaccept.mjs(friend gate + signed encounter records),route.mjs(never-blocked-only-routed),quell.mjs(stillLive/supersededBy/isAuthorQuell)bin/pile-new --provisioner(stampsprovisioner/provisioner_spec; custody rule — a provisioner never touches the age identity)turnInSubmission→tell.submission/v1→bin/authzSlice 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) + addnotes/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 onquell.stillLive+satchel.arrived). WireturnIn→turnInSubmission→ existing egress; admission staysbin/authz. Testscomposer/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/dropimitatingbin/match: bare invocation,ATLAS_*I/O, verify via the atlas's own stdlibverifyAttested(as indump.mjs/atlas-index.mjs), dedup by content-id. Three branches: known live door → stage to examine-not-mergedrop/<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. EjectedATLAS_DROP_CMD, honest default = forward-only, never custody. Wrap in.github/actions/drop+drop.yml; addbin/drop.test.mjsand its path totest.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 +
stillLivesays no live door + mass/scope-fit. Runs data-pilebin/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 viasupersededBy; adoptable when the instigator reappears. Risk: high; last.Two decisions this surfaced
bin/dropreuse the atlas's own stdlib Ed25519 verify (reimplementballotId) over importing anecdotecomposer/— low coupling, matchesdump.mjs.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