Repository navigation
server: the database declares its vote convention; a server refuses to start against a convention it was not built for - #2944
Open
colinmegill wants to merge 20 commits into
Open
colinmegill wants to merge 20 commits into
colinmegill wants to merge 20 commits into
Conversation
… functions, views, ledger) — P-078 PR-A Schema change, ruling R-A. No data change: the seed row (version 0, agree_value -1) states today's storage convention, so every reader that consults it computes what it computes today. 000023_vote_convention.sql adds, in one transaction: the vote_convention singleton (updated in place) with its append-only history and a monotonic guard; the schema_migrations ledger with 000000-000022 backfilled as pre-ledger and this file's own row (sha256 of the file without its ledger-marker line); vote_convention_current() (STABLE, SECURITY DEFINER); vote_semantic()/vote_storage() (IMMUTABLE, STRICT); vote_insert() (SECURITY DEFINER, lock_timeout 2 s as a function SET clause, FOR SHARE, expected-version check, the 000006 rule maintains votes_latest_unique); the votes_semantic and votes_latest_unique_semantic views; grants to the 000021 coordinator roles when they exist, recorded in the ledger row. vote_insert is revoked from PUBLIC. Named SQLSTATEs P0780-P0784, P0789. down/000023_drop_vote_convention.sql drops exactly those objects and refuses (P0789) once the convention has moved or a later migration is in the ledger. down/test_000023_down.sh runs 11 cases on a disposable PostgreSQL 17. docs/vote-convention.md: grants, restore-detection rule, runbook, and how the server and engine readers bind to the row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
… view grants, ledger checksum CI Review fixes, batched into one change of 000023's bytes: - vote_insert refuses (P0791) when the convention row is missing instead of storing NULL votes; DELETE/TRUNCATE on vote_convention refuse (P0790); TRUNCATE on the history refuses (P0781); a row put back after a forced removal must continue the history (the guard now also runs on INSERT); the guard stamps changed_at/changed_by. - The coordinator observer no longer gets SELECT on the two semantic views (they read votes with the owner's rights). - Catalog comments and the ledger note carry no internal ids. - server/postgres/check_ledger_checksums.py and a CI workflow recompute the self-checksum of every migration from 000023 on; the runbook gains the operator pre-apply check. - docs: the un-flip's precondition (every writer through vote_insert) and the proposed table locks for its preamble; 55P03 scope; READ COMMITTED; pre-ledger rows are asserted, not observed; down-file precondition. - Tests (15): a writer blocked by the flip then writes +1 under version 1; restore detection against the held un-flip file when present; the permanence cases; the checker. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
colinmegill
added a commit
that referenced
this pull request
Oct 3, 2026
…e amended lock order - convention_ddl_sha256 binds PR-A's 000023_vote_convention.sql (draft #2944, 351628cf...). Its exact bytes replace the earlier fixture under ci/probe_box/fixtures/unflip/ until PR-A lands; the tests apply the real 000023 and then the held 000024. - The held 000024 takes the plan's amended lock order (section 2a): LOCK TABLE vote_convention IN EXCLUSIVE MODE before the FOR UPDATE, so vote_insert() FOR SHARE readers cannot starve it, and LOCK TABLE votes, votes_latest_unique IN SHARE ROW EXCLUSIVE MODE before the UPDATEs, so a writer that bypasses vote_insert() is fenced while reads continue. New migration digest and ledger self-checksum; registry pins updated. - A test holds the transaction open and shows a direct INSERT and vote_insert() both failing with 55P03 while reads and the convention read continue, and both succeeding after the rollback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
… a convention it was not built for Failing first. Pins, before the change that makes them pass: - server: a startup check that reads public.vote_convention_current() and refuses no table, no row, an unknown contract and the other sign, naming the exact declare command; the import worker refuses before consuming. - engine: the same decision for the math poller, the math pipeline stage and the UMAP stage; the stage fakes answer the two startup queries. - coordinator: the same decision as a pure function; the schema census grows by the three tables migration 000023 adds (625 columns). - migration 000023: the row is written only on an empty database; a database holding votes is left undeclared with the DECLARE_NEEDED notice; the declare operation at both signs and its refusals; the shell entry point's status line; the down file from seeded, undeclared and declared databases; the restore rule knows "undeclared". - the sign lint treats the migration, its reversal, the operation and this test as the database's own chokepoint. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…o start against a convention it was not built for Migration 000023 (P-078 PR-A, ruling R-A 2026-10-05) gives the database one row, public.vote_convention, naming which stored vote value means "agree". - The migration writes the row ONLY on an empty database (version 0, agree -1, operation seed-empty). A database that already holds votes is left undeclared, with NOTICE DECLARE_NEEDED naming the one command. - The operator declares an existing database once: make vote-convention-declare AGREE=-1 (server/bin/vote-convention-declare.sh, server/postgres/operations/vote_convention_declare.sql). AGREE=+1 writes (1, +1) as the first state for a deployment that reversed its own signs. make vote-convention-status reports GUARD_NEEDED / DECLARE_NEEDED / GUARDED. - Every reader of votes checks the row before its first vote read and refuses to start on no table, no row, an unknown contract, or a sign other than the one it was built for: the server (index.ts), the import worker, the math poller, the math pipeline stage, the UMAP stage and the commentgraph CLI, the Delphi job poller, and the coordinator (at startup and at every source snapshot). The messages name the command and the guide (docs/vote-convention-upgrade.md). - The row and its history record the operation that wrote them and the operation file's ledger checksum; vote_convention_current() carries the contract version; the monotonic guard admits (0, -1) and a declared (1, +1) as first states; the down file reverts an undeclared database too. - The ledger checker covers operation files; the coordinator's typed rows and census include the three new tables; the characterization harness leaves the permanent tables alone; the queue crate's fixture accepts later server migrations. - Docs: docs/vote-convention-upgrade.md (what other deployments see and run), docs/vote-convention.md, docs/migrations.md, docs/upgrading.md, CHANGELOG, README. No vote value changes. No read, write, export or API response changes. Applying the migration to production is a separate, explicit step by the owner, before the release that reads the row is deployed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
colinmegill
marked this pull request as ready for review
October 6, 2026 03:18
Hosted CI's vote-sign lint flagged server/bin/vote-convention-declare.sh: it names the sign it writes (AGREE=-1 / +1) and exists only to run the declare operation, which is already registered as the database chokepoint. The local lint run had preceded `git add`, and the lint scans tracked files. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…job table on edge; 000024 is reserved) The migration, its reversal, the down test and its compose file move from 000023 to 000025, with every reference, pin and the two ledger self-checksums (the migration's and the declare operation's) regenerated. The chain neither requires nor records 000024, which another change holds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…n-wt # Conflicts: # queue-rs/tests/jobs_integration.rs
…rapper, and the lock the migration takes Corrections from the operating-surface review of #2944: - The migration ledger records what the catalog shows for every earlier file (000000..000023: 'verified' when the file's signature object is present, 'unverified' when it is not, the probe named in the note) instead of asserting them as applied. The migration refuses (P0780) without the vote tables from 000000 and 000006 and needs nothing later; 000024 is neither required nor recorded. The down file keys its later-migration refusal on ledger-era rows (64-hex checksums). - The header and the guide say what the file locks: AccessShareLock on votes and votes_latest_unique until COMMIT (the empty-table checks), compatible with ordinary traffic, rolled back after the 5 s lock budget behind an ACCESS EXCLUSIVE holder; nothing on the vote tables is rewritten. - server/postgres/bin/apply-migration.sh covers 000025: seal from down/000025-files.sha256, PostgreSQL 17, the login owns the vote tables, the vote tables exist, no convention object exists yet, no old transaction, free disk; the budgets; a post-check of the ledger row and the convention state (DECLARE_NEEDED / GUARDED). Docs route the apply through it. - run_delphi.py runs polismath/check_vote_convention.py as its first stage, before the reset that removes previous results, so a job launched directly or by an older poller refuses with nothing changed. The commentgraph CLI's test-postgres exits 1 on a refusal and prints the operator message; the docs claim only that command. - The guide says the checks are made at startup (and at the job boundary), that the import worker and the Python processes check DATABASE_URL only, and that this release does not serve a +1 database. - The down test covers the chain 000000..000023 then 000025, the verified ledger, a copy without the dormant 000019/000021/000023, the prerequisite refusal and the wrapper (preflight-only, an old transaction, an ACCESS EXCLUSIVE holder, the apply, a second run, the seal). The census pins 000023 and the renumbered 000025. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
… and the renumbered 000025 The census test enumerates the migrations directory and binds every file's bytes, so it had been red since 000023_create_delphi_foundation.sql merged without a census. Measured on PostgreSQL 17.11 with the full chain applied to a fresh database: eight new relations (delphi_jobs, delphi_job_aliases, delphi_job_inputs, delphi_current, delphi_job_guards, delphi_provider_requests, polis_queue_logs, delphi_foundation_install), the four columns 000023 adds to the 000019 tables, and the constraints and indexes they change. 95 relation types, 694 columns. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…e standard library; the commentgraph CLI builds a real PostgresConfig Found by the composed-image rehearsal: run by file path from run_delphi.py, polismath/check_vote_convention.py had its own package directory first on sys.path, where polismath/types.py shadowed the standard library's types and every run crashed before the check. The script drops that entry before its imports and run_delphi.py sets PYTHONPATH before the stage. The commentgraph CLI's test-postgres passed a dict where PostgresClient needs a PostgresConfig (pre-existing on edge), so its refusal path was unreachable in the image. The guide states the exit codes and which lines show at the default log level. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
… first, with its own refusal rows Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
Edge brought 000024 (the large worker class, polis-queue/3). The queue integration test keeps the prefix check, now 000000-000024; files after it (the vote convention, 000025) are in no template. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
… the census measures it Edge merged 000024 (the large worker class, polis-queue/3), which this branch had treated as held elsewhere. Corrections, nothing else: - 000025's ledger gains a row for 000024_create_polis_queue_large_class, probed by public.polis_queue_large_class_install (verified when present, unverified with the probe named when not, as for every earlier file). Comments, docs/migrations.md, docs/vote-convention.md and the checker's docstring say 000000..000024. Resealed: the ledger self-checksum and down/000025-files.sha256. - test_000025_down.py: the chain is 000000..000024 (no 000020); the pol.is shaped copy leaves out 000019, 000021, 000023 and 000024; the wrapper case expects 24 verified earlier files. - The coordinator schema census, measured again on PostgreSQL 17.11 with 000000..000025 applied to a fresh database: the only differences are 000024's (polis_queue_large_class_install; the widened kind, stage, worker_class and contract checks; pq_stage_large; the /3 default). 96 relations, 697 columns; 000025 pointers moved up one line. - The apply wrapper's header and docs/vote-census.md (which named the vote convention migration by its old number) are updated. The wrapper still covers 000019, 000023 and 000025; 000024 has no apply policy there and is left to its own change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…fore 000025 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
… Rust tests The required campaign compares the Rust test names it runs with coordinator-rs/ci/inventory-v2.json exactly. tests/vote_convention.rs added seven tests that the inventory did not list, so the campaign's candidate gate failed on 'unexpected' names (on this branch since those tests were added). The identities are appended; no existing entry changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
… not refused The startup check exited 1 on any failure to read the convention, including ECONNREFUSED while postgres was still starting. Under compose (the e2e stack, make start) the server then restarted with a new address, and nginx, which resolves `server` once, answered 502 for every request: the hosted E2E run failed that way (server log: "failed to read the database's vote convention connect ECONNREFUSED", then a restart; every spec's cy.visit got 502). The read is now retried every 2 s for about a minute when the database does not answer, as the server tolerated before the check existed. A refusal (no table, no row, the other sign, an unknown contract) still exits at once. Checked on the build box with the e2e compose stack: postgres held back 20 s, the server logs "could not read the database's vote convention (attempt 1 of 30); retrying in 2 s: connect ECONNREFUSED ...", then "vote convention: version 0, agree stored as -1" and "Server started"; restarts 0; nginx GET / and /api/v3/participationInit 200. One forced server restart on the unchanged stack reproduces the 502 (new container address, nginx upstream unreachable). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
Edge brought the math poller's large class as a queue child (#2973) and the per-class CDK worker groups (#2974). One conflict in delphi/scripts/math_poller.py: _build_service keeps the vote convention check before it builds the service, with edge's simplified constructor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
This was referenced Oct 6, 2026
…row; the large-class child checks it The row is the only truth for the stored vote sign: STORAGE_AGREE_VALUE never overrides it, and a build for the other sign refuses. Seven campaign tests had reversed the votes with a raw UPDATE and launched the coordinator at +1 while the row still said (0, -1), so this PR's coordinator refused them. - conftest: declare_convention() moves the row to the next version with the other sign (the transition its triggers admit, recorded in the history); reverse_votes() stores every vote with the other sign and leaves the row. - Polarity through database ingress (three fixtures), the rebuild schedule and the tie key: votes reversed AND the row declared +1, then the +1 run; the -1 build is shown to refuse (#mismatch). The "negative" control is the opposite votes under the same declaration (votes reversed again, +1 run), instead of the same votes read under the wrong sign. - The node reader's deliberately different third namespace is the same conversation with every vote reversed (row unchanged), not a run under the other sign. - math_poller --job (the large-class rebuild child): the same startup check as the poller, before it builds the service that reads votes; a refusal exits 2 (EXIT_JOB_ENV_INVALID) with the operator message. Test: tests/poller/test_rebuild_child_convention.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
Edge gave the apply wrapper a policy for 000024 (#2975). One conflict in server/postgres/bin/apply-migration.sh: the wrapper now covers 000019, 000023, 000024 and 000025, each with its own seal, rights, chain, rows, lock line and post-check, unchanged from the two sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
Delphi Coverage Report
|
colinmegill
added a commit
that referenced
this pull request
Oct 7, 2026
#2978 is now built on #2944 and merges after it: 000026 records itself in the migration ledger 000025 creates, so the combined apply wrapper lands here, carrying #2944's 000025 entry and adding 000026's. Conflicts, resolved: - server/postgres/bin/apply-migration.sh: #2944's wrapper (000019, 000023, 000024, 000025) plus the 000026 entry, with the corrections that follow in the next commit (every file of the seal, the ledger self-checksum, 000026's rights, chain on 000025's ledger row, post-check on the ledger). - queue-rs/tests/jobs_integration.rs: the 000000-000024 chain check from #2944; the jobs_v4 template applies 000025 then 000026. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
colinmegill
added a commit
that referenced
this pull request
Oct 7, 2026
Brings the corrected 000026: its ledger row, the durable dead-job breaker and retention by reachability (policy as data, dry run by default, tombstone then purge), with #2944's vote convention and the combined apply wrapper underneath. No conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Observable changes:
Corrections from the connections map
The job-system connections map found that this PR's apply wrapper (000019, 000023, 000024, 000025) and #2978's (000019, 000023, 000024, 000026) each lacked the other's migration, and that 000026 had no ledger row, so this PR's 000025 down could not see it. Resolved without changing this branch:
Where the combined wrapper lands: DRAFT — schema change — server: migration 000026, queue retention and the parked read (P-083, P-086) #2978. DRAFT — schema change — server: migration 000026, queue retention and the parked read (P-083, P-086) #2978 is now built on this branch (merged, not rebased) and merges after it. It carries this PR's 000025 entry unchanged and adds 000026's, so the two PRs do not conflict: this PR keeps the 000019/000023/000024/000025 wrapper, and DRAFT — schema change — server: migration 000026, queue retention and the parked read (P-083, P-086) #2978's version is a strict extension of it.
Down order. 000026 now inserts its own
schema_migrationsrow (self-checksum, the rule this PR introduced). This PR'sdown/000025_drop_vote_convention.sqlalready refuses while the ledger records a later migration, so over 000026 it refuses and changes nothing; 000026's down runs first and removes its row, then 000025's down runs. Proven in DRAFT — schema change — server: migration 000026, queue retention and the parked read (P-083, P-086) #2978'stest_000026_down.sh(l), (i) and (p) on mm2.Census. This PR's census (000000..000025, 96 relations, 697 columns) stays as is; DRAFT — schema change — server: migration 000026, queue retention and the parked read (P-083, P-086) #2978 re-measures the combined chain.
No file in this PR changed; this branch already contains edge
7987554ff.pol.is (our deployment): none at runtime once the row exists. The deploy itself writes nothing: production holds votes, so migration 000025 creates the table and leaves it empty, printing
DECLARE_NEEDED; the owner then runs the declare command once (one row, one transaction, no vote touched). Both are the owner's explicit steps, before the images of this release are deployed: until the row exists, the new server, import worker, math poller, Delphi job poller, the Delphi job itself (before its reset) and the coordinator refuse to start (exit 1; the poller exit 2) with the messages below, while the running images never read the row and keep working. The checks are made at startup (and at the job boundary); a process already running when the row changes is not stopped by this release. After the declaration each component logsvote convention: version 0, agree stored as -1at startup and nothing else changes: no vote value, no read or write path, no export byte, no API response. The vote-path recordings replay byte-identical (no expected-difference entry).Other deployments (and any dev or prodclone volume that already holds votes): on upgrade, applying the migration prints
DECLARE_NEEDED, and the new containers refuse to start until the operator runsmake vote-convention-declare AGREE=-1once; old containers keep working meanwhile. A fresh install needs nothing (the migration seeds version 0). A fork that reversed its own signs can declareAGREE=+1, which records that fact; this release does not serve a +1 database (its components refuse it rather than read it inverted). Nothing else.New operator commands:
make vote-convention-status,make vote-convention-declare AGREE=-1|+1 [REASON=...](server/bin/vote-convention-declare.sh,server/postgres/operations/vote_convention_declare.sql); the apply of 000025 to an existing database goes throughserver/postgres/bin/apply-migration.sh(the wrapper that already applies 000019 and 000023). New guidedocs/vote-convention-upgrade.md;CHANGELOG,README,docs/upgrading.md,docs/migrations.md,docs/vote-convention.mdupdated.A Delphi job run against an undeclared database now stops before its reset stage, so a conversation's previous results are kept; before this PR the reset ran first. The commentgraph CLI's
test-postgresexits 1 (not 0) when the database refuses.The server waits for Postgres at startup. The startup convention check queries the database. If Postgres is not answering yet, the server retries every 2 s instead of exiting. A normal start costs one query and no delay; a cold start waits up to a minute. Without the wait, the server exited during a cold start, came back at a new address, and nginx answered 502 until it was restarted. A refusal of the convention itself (no table, no row, the other sign) still exits at once.
The large-class rebuild job checks the convention too. The rebuild job (
math_poller --job, from math poller: oversized conversations are jobs on the Postgres queue (P-073 r2) #2973) makes the same check before it reads a vote. A refusal exits 2.The characterization harness's
seedno longer truncatesvote_convention,vote_convention_historyandschema_migrations(they refuse TRUNCATE and are not application data).Renumbered to 000025
The draft was
000023_vote_convention.sql. Edge has since merged the Delphi job table as000023_create_delphi_foundation.sql(#2968) and the large worker class as000024_create_polis_queue_large_class.sql(#2971), so this migration, its reversal, its down test and its compose file are 000025. Every reference, pin and both ledger self-checksums were regenerated: migration ledger checksum3c39aadd679d9c5aa4f4d5c1a42cf39cd0b15659cb1112ccdad4d8f438c10047, declare operation60e17ec16e1e035c1645575978f1028c0f4f08c253880f6c51222f3377ece20c. The two files are sealed inserver/postgres/migrations/down/000025-files.sha256, and the wrapper checks that seal before it sends anything.queue-rs'sjobs_integrationchain assertion requires 000000..000024 as a prefix and accepts the files after it.server/postgres/bin/apply-migration.shis extended for 000025. Edge's apply wrapper: 000024 (polis-queue/3) gets its seal and preflight #2975 gave it 000024, and the two are merged.Corrections from the operating-surface map
The second reviewer's map of this PR's operating surface (review round 2) ranked what had to change inside the PR before pol.is could take it. Each item, and what changed:
pre-ledger, "assumed applied", and a database that had never run 000019 still got a 000019 row. Now000025_vote_convention.sqlprobes the catalog for every earlier file (000000..000024) and records what it found: checksumverified(the file's signature object is present) orunverified(it is not), the probe named in thenote, plus a NOTICE with the counts (server/postgres/migrations/000025_vote_convention.sql, the ledger section). The migration's own prerequisites are the vote tables from 000000 and 000006 only; without them it refusesP0780and creates nothing. The down file refuses on ledger-era rows (64-hex checksums) rather than on the backfill (down/000025_drop_vote_convention.sql). Test:test_000025_down.pycases 16 (full chain: every earlier file verified), 17 (a copy shaped like pol.is, without the dormant 000019/000021/000023/000024: those four rowsunverifiedwith their probe, 000025 applies, declares and reverses without them) and 18 (no vote tables: P0780, nothing created).AccessShareLockonvotesandvotes_latest_uniqueuntil COMMIT (its empty-table checks): compatible with ordinary traffic, and behind anACCESS EXCLUSIVEholder it waits its 5 s per-lock budget and rolls back. The migration header,docs/vote-convention-upgrade.md#guardanddocs/vote-convention.mdsay exactly that; the wrapper prints it; the down test proves it (case 19: an ACCESS EXCLUSIVE holder onvotesmakes the apply exit 5 after the budget with the catalog unchanged).server/postgres/bin/apply-migration.sh(server: the Delphi job table (foundation migration, flag off; DynamoDB keeps running every job) #2968) covers 000025: preflight seal (down/000025-files.sha256), server (PostgreSQL 17), rights (the login owns the vote tables and can CREATE inpublic:vote_insertand the views run with the owner's rights), chain (the vote tables exist; nothing later required), rows (none of the thirteen objects exists yet: a first install, not a repair), xacts, disk; then the budgets (lock_timeout5 s,statement_timeout60 s,transaction_timeout120 s,idle_in_transaction_session_timeout30 s) ahead of the file; after it, the post-check (the ledger row with a 64-hex checksum) and the convention state (DECLARE_NEEDEDwith the next command, orGUARDED) with the verified/unverified counts. Exit codes 0/2/3/4/5/6 as for the queue files. The runbook, the guide anddocs/migrations.mdroute the apply through it. Test: case 19 (--preflight-onlyapplies nothing; an old transaction refuses at exit 4; the lock holder; the apply; a second run refused at exit 4; the seal verifies).run_delphi.pyranreset_conversation.py(which removes a conversation's previous results) before its first guarded stage, so a job launched directly, or by an older poller that never checked at boot, could erase results and then print "Nothing has been changed". Nowdelphi/polismath/check_vote_convention.pyis the job's first stage, before the reset (delphi/run_delphi.py); a refusal exits 1 with the operator message andNothing has been reset or changed. Tests:delphi/tests/test_run_delphi_exit_codes.py::test_undeclared_convention_refuses_before_the_reset(the stage order: onlyconventionran),test_check_vote_convention.py(the three refusals and the pass, through a fake client), and the job-child protocol tests carry the new first stage.test-postgresnow prints the operator message to stderr and exits 1 (delphi/umap_narrative/polismath_commentgraph/cli.py); only that command is claimed as gated at the CLI (lambda-localis gated where it reads votes, in the UMAP stage'sfetch_conversation_data). Test:delphi/tests/test_commentgraph_cli_refusal.py.DATABASE_URLand a differingREAD_ONLY_DATABASE_URL, the import worker and the Python processes check the one database they read (this body's earlier "both URLs" for the worker was wrong); a fork that declares+1is recorded, not served, by this release. The plan carries the same table.Not done here, on purpose (they belong to Colin's deploy step or to later PRs, as the map says): the deploy-day execution itself, +1 serving, tombstones and the write fence, the backup format, the flip rehearsal, the runner rewrite.
Rehearsal on composed images
This was run on a build box with images built from this PR's head (
57bd51aa8): the server image and the Delphi image. The pre-release server image was built from edge288163928.The database was PostgreSQL 17.11 with the chain 000000..000024 applied and a generated fixture loaded: 5 participants, 5 statements and 25 votes stored in the original convention. Each component was run as its stock command would run it, against that database, and given 60 s:
--once)run_delphi.py --zid=501)test-postgres)The box has no AWS credentials or MinIO, so (b), (d) and (e) fail later on those once they are past the check. Before every step and after it, I took a
pg_dump --schema-only(pg_dump's random\restrictlines set aside) and the sha256 of every vote row.1. Images first: the release images against the database before 000025. Every component refused, and nothing changed.
The full message is:
No "Resetting" line appears in (e): the reset never ran.
2. The migration through the wrapper, then the declaration, then the images.
Between the apply and the declaration, all six components refused again with the same exit codes. The message is:
Then the declaration, a check, and the images:
3. The down path. The release containers were all gone first (0 left besides postgres).
The server's
vote convention:andServer startedlines are info level. The stock image logs at warn, so these runs setSERVER_LOG_LEVEL=info.What this is
Migration
000025_vote_convention.sql: one table with at most one row naming the stored vote sign (agree_value; today -1, "version 0"), its append-only history, the migration ledger,vote_convention_current()((version, agree_value, contract_version)), the pure conversions,vote_insert()and the two semantic views, as drafted, amended to plan P-078 for other deployments:NOTICE: vote convention: DECLARE_NEEDED ...; nothing reads a sign from the dataCHECK (contract_version = 1)vote_convention_current(); an unknown contract is refusedoperationandoperation_checksum(which file wrote it)pre-ledgerrowsverified/unverifiedfrom the catalogundeclared; a first-state +1 declaration =declared-agree-plusWhere the refusal lives:
server/index.tsandsrc/workers/start-import-worker.ts(throughserver/src/votes/dbConvention.ts; the server onDATABASE_URLand a differingREAD_ONLY_DATABASE_URL, the worker onDATABASE_URL);delphi/scripts/math_poller.py,delphi/polismath/run_math_pipeline.py,delphi/run_delphi.py(first stage,polismath/check_vote_convention.py), the UMAP stage (run_pipeline.fetch_conversation_datathroughstorage.PostgresClient.require_declared_convention), the commentgraph CLI'stest-postgresanddelphi/scripts/job_poller.py(throughdelphi/polismath/utils/vote_convention_boot.py);coordinator-rs/src/vote_convention.rsat startup forrun/onceand inside every source snapshot transaction.math/(Clojure) is retired and untouched. The sign each build is built for is its chokepoint'sSTORAGE_AGREE_VALUE; no literal sign is written in the checks.Decisions taken as recommended
Each C-question the plan leaves to the owner, with the plan's recommendation taken; veto any line.
vote_import_batch(), the fence-marker SET clauses and the tombstone/fence functions are not in this PR (they belong with the flip tool and the server surface PRs).polis-data-backup/2): yes for the plan; nothing in AWS changes here.run-migrations.shis unchanged in this PR; an existing database takes 000025 through the apply wrapper instead.byod_import_jobs, in a later schema PR; nothing here.Taken under the delegation for D-level choices, beyond the plan's list:
AGREE=+1writes (1, +1) as a first state without the renames, tombstones and fence (those are the flip tool's), so this release's components refuse such a database rather than read it inverted; the engine's per-cycle "absent row means version 0" fallback inRowConventionSourceis unchanged (plan PR 4), the refusal being at startup; the migration, its reversal, the declare operation and the down test are registered as the database's own chokepoint in the vote-sign lint (0 new literals, no allowlist entry); the number is 000025 (above).The three upgrade paths, proven earlier on a build box with real databases
The built server (
node dist/index.js) and the math poller (scripts/math_poller.py --once) against PostgreSQL 17.11 containers (recorded at the previous head; the messages are unchanged except the migration number).(a) A fresh database. The migrations run at container init; the row is seeded; both start.
(b) A database with existing votes and no row (another deployment upgrading): the chain applied, one vote inserted, then 000025.
The server, exit code 1:
The math poller, exit code 2, the same text with
(math poller). Thenserver/bin/vote-convention-declare.sh --status:The declaration (
server/bin/vote-convention-declare.sh -1):Then the server starts (
vote convention: version 0, agree stored as -1/Server started on port 55483), the poller's--onceexits 0, and the stored vote is untouched (-1).(c) A row naming version 1 (agree +1) with a server built for version 0 (declared with
AGREE=+1). The server, exit code 1:The math poller, exit code 2, the same text with
(math poller). (The coordinator's decision is the same function, unit-tested; its live run needs a TLS database and was not exercised.)The message when the migration is not applied at all:
Polis cannot start (server): this database has no vote_convention table, so it does not record which stored vote value means "agree". Migration 000025 has not been applied. Nothing has been changed. Next: back up the database, apply server/postgres/migrations/000025_vote_convention.sql (see docs/migrations.md), then, if the database already holds votes, run "make vote-convention-declare AGREE=-1". Guide: docs/vote-convention-upgrade.md#guardTest evidence
Since the run below:
b38925a5fnames the seven Rust tests incoordinator-rs/tests/vote_convention.rsin the coordinator campaign's inventory.1a1404255adds the startup wait above. I checked it on the build box with the e2e compose stack: postgres was held back 20 s; the server restarted 0 times; nginx answered 200 for/and for/api/v3/participationInit. One forced server restart on the previous head reproduces the 502.d47cbe2c3merges edge (math poller: oversized conversations are jobs on the Postgres queue (P-073 r2) #2973, cdk: one worker group per queue class; demand and alarms read from the queue; manifest remnants removed #2974). It had one conflict indelphi/scripts/math_poller.py; the check stays in_build_service. Delphi pytest after the merge: 3851 passed, 0 failed.5f99152c8makes the campaign tests go through the convention row and adds the check to the rebuild job. On the build box at this head:conversation-stats"until", 3 intrashes); I re-ran those two suites alone and they passed 6/6.sign_lint: 0 new literals.83ebe3967merges edge again (apply wrapper: 000024 (polis-queue/3) gets its seal and preflight #2975 gave the apply wrapper a policy for 000024, plus math poller: the capacity line reports large_parked (scale-in reads it) #2977, delphi image: carries the polis-jobs daemon (queue-rs), --version, image witness #2979 and deploy hooks: delphi-large and delphi-worker restart only polis-jobs.service #2980). One conflict, inserver/postgres/bin/apply-migration.sh: the wrapper now covers 000019, 000023, 000024 and 000025, each with its own policy. After the merge, on the build box:test_000024_down.sh: all checks pass, including its wrapper case.test_000025_down.sh: 24 passed, 0 failed, 1 skipped.Coordinator campaign: the sign changes through the row. The ruling is that the declared row is the only truth, and
STORAGE_AGREE_VALUEnever overrides it. Seven campaign tests used to reverse the votes with a rawUPDATEand launch the coordinator at +1 while the row still said (0, -1). They now go through the row:#mismatch).No witness needed re-pinning. The reversed votes read under the right sign are byte for byte what the wrong-sign read produced, so every polarity witness on the build box (macOS arm64) equals its pin:
0b5f690d35ba…4fa3b33d193d…523b48455a31…23c7933c7b10…50a50a0b1c11…6fbd8cf04653…c40f36ef591f…aec5181c022a…01a9fdf049f7…,e2b674af0ba3…,686486e4e1b1…On the build box the campaign passed 290 of 291 cases. The one failure,
test_step2_review_controls.py::test_actual_pca2_route_serves_generation_zero, also fails on edge itself: it expects ETag"0", and since7d452d41cthe route sends"rustproto-0". It is not fixed here, so "Coordinator S1/S2 required" stays red until edge fixes it.All of these were run on the build box at head
57bd51aa8, with the same entry points the workflows use. Hosted CI is the second witness.psql -f; the wrapper has no policy for it), 000025 (wrapper: 24 verified, DECLARE_NEEDED), then the declaration.server/postgres/migrations/down/test_000025_down.sh(PostgreSQL 17.11, the real chain 000000..000024 then 000025): 24 passed, 0 failed, 1 skipped (the held un-flip file is not in the tree). It covers:votesrolls the apply back on its 5 s budget (exit 5, catalog unchanged); a second run exits 4; the seal verifies;npm test -- --ci --coverage --maxWorkers=2): 1723 passed, 1 skipped, 0 failed.conversation-stats"should accept until parameter". That case compares the host clock with database timestamps, and the same case failed once on the previous head too.npm run contract:checkandnpm run lintare clean.origin/edge(vote path, collective statement, routing, job views): ok.main.cjs replay→postgres schema the recordings read changed: public.vote_convention_current(): absent at record time, now defined. The other 51 cases replayed, and its node tests pass.expected-differences.jsonadmits only per-case text entries, and the golden guard forbids re-recording together withserver/postgres/changes.generate.py --checkpasses, andmain.cjs replayreproduces 272 cases, every recording byte for byte.delphi/tests): 3902 passed, 250 skipped, 44 xfailed, 2 xpassed, 0 failed.ci/vote_convention/run.sh):sign_lint.py: 0 new, no allowlist entry. Its 30 stale entries are pre-existing.test_gate.py: 32 OK.check_ledger_checksums.py: ok for the migration and the operation.cargo clippy --all-targets -- -D warningsandcargo test --locked(every suite, including the schema census at 96/697) are clean.jobs_integrationtests against PostgreSQL 17 pass.Applying this migration to production is a separate, explicit step by the owner
Runbook in
docs/vote-convention.md(production runbook): check the checksums and the seal, apply the file through the wrapper as the owner role (printsDECLARE_NEEDED), run the declare command once (printsGUARDED v0 agree -1), verify through the runtime connections, then deploy this release. Rollback: the down file, after stopping the components that read the row; the old images never needed it. The deploy-day order is migration → declare → images; the images refuse until the row exists, and nothing in the deploy hooks enforces the order, so the hold before any container stop is the operator's checklist.Not in this PR
Any flip; the rehearsal of the flip (#2942); the read-only preflight and the author's-vote witness (plan PR 2); the runner hardening (PR 2); the server and engine surfaces over the views and
vote_insert()(PRs 3–5); the handshake's structural check, the transition assertion, the write fence and the tombstones (PR 9); the backup format (PR 8); a continuous (per-cycle) check in any component.🤖 Generated with Claude Code
https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s