Skip to content

Promote edge to stable: Polis 2.43.0 - #3321

Merged
colinmegill merged 43 commits into
stablefrom
edge
Nov 2, 2026
Merged

colinmegill merged 43 commits into
stablefrom
edge

Conversation

@github-actions

@github-actions github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Release plan

Version: 2.43.0

Reason: new selected migrations

Previous stable: c341a0e715e87f153785b46f478fcf4fdd785403

Candidate: 44f8b560395e40a6e960f9653745fe35ef2130e0

release.txt additions:

  • 000045_page_ids_width.sql

Merge the reviewed version preparation into edge, then refresh this plan for the resulting edge SHA before merging the promotion with a merge commit.

colinmegill and others added 30 commits October 25, 2026 12:36
…und, so another process cannot answer them on macOS (#3278)

Supertest 7 starts the app with listen(0), which binds the IPv6 wildcard, then
sends its requests to 127.0.0.1 on the port it read back. macOS lets an IPv4
listener hold the same port number, so on a developer machine with other
services in the ephemeral range a test request could be answered by a foreign
process: an Express 404 from another app, a 502 from a local relay, or a
socket hang-up. Linux refuses the shared port, so CI never saw it. The test
harness now maps Supertest requests to the family the server actually bound,
only on macOS and only when ::1 exists; globalSetup binds and requests on
matching IPv4 addresses; four regression cases prove the failure and the fix.
No product code or assertion changes.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…3279)

Seventy files of comments, docstrings and documentation carried references
to a private planning tracker: review round numbers, item numbers, plan and
finding names, and working labels that meant nothing to a reader of this
repository. Each is reworded to state the behaviour or the reason directly,
or dropped where it only pointed at the tracker. Public pull request numbers
stay. No code, string literal, test assertion, snapshot, migration or
workflow step changes; files whose bytes are pinned by a checksum are left
untouched.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…arting it (#3280)

The CodeDeploy group for the web boxes moves to the WITH_TRAFFIC_CONTROL
deployment style on the web target group: BlockTraffic deregisters a box
and waits for the deregistration delay, the install hooks run, ValidateService
passes, and AllowTraffic registers the box again after two healthy checks.
Today a box is restarted while still receiving traffic, which costs a few
dozen 502s per deploy. The math and Delphi boxes move to their own
deployment group with no load balancer, deployed alongside the web one, and
the deploy role may create deployments in both groups. The web target group
checks health every 10 s with a 5 s timeout, two checks to become healthy
and three to become unhealthy, and a 60 s deregistration delay.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…and Rust recognises the XID token kind (#3281)

A Rust test mints anonymous, XID and standard-user participant tokens with the
Node server's claim set and verifies them with the same options the server's
express-jwt uses, against recordings made by the Node signer; a Jest test mints
every kind with the real Node signer on each run, compares header and payload
with the recording, and verifies Rust-minted tokens. Refusals are pinned on
both sides: exp at now, wrong audience or issuer, future nbf, RS384, unsigned
alg none, and HS256 signed with the public key. polis-auth gains the XID token
kind for verification only, with the request-xid precedence rules the server
applies, and a table test of request identities. Nothing is served or minted
by Rust; no production key is involved.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ag, off by default (#3282)

The inactive coordinator added eleven resources, an ingress rule on the
database security group and two stack parameters with no default, so a deploy
from a clean checkout stopped on the missing parameters and production was
deployed from a hand-edited working copy instead. The coordinator is now
synthesized only when -c enableCoordinatorInactive=true is passed. Without it
the stack contains none of its resources, parameters or output, and a clean
checkout synthesizes what production runs. With it the template is unchanged.
Once a deploy has included the flag it must be passed on every later deploy,
or the coordinator's resources are deleted; the documentation says so.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…d to it (#3283)

* deploy: the Rust API service runs on the web boxes with nothing routed to it

polis-api is added to the production compose profile on the web boxes and
started after Node, nginx and the participation client, with no route
selected and the nginx map unchanged, so nothing served changes. It connects
to Postgres over verified TLS with the same pinned certificate bundle the
other images carry, checked at build and at start; refuses to start without
TLS; is capped at half a CPU and 512 MiB with eight read-only connections;
and reports a health document with its version, commit, routing state and
pool bounds. The deploy's validation reads that health: while no route is
selected a failing service is logged and the deploy passes; once a route is
selected it fails the deploy. Stopping the stack stops the service too, so a
deploy with the service switched off or a rollback leaves nothing running.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* deploy tests: the fake docker login reads its password from stdin

after_install.sh now runs with pipefail, so a fake docker login that exits
without reading --password-stdin lets the fake aws ecr get-login-password die
of SIGPIPE on the Linux runners, failing ten hook tests with exit 141. The
real docker reads stdin; the fake now does too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…pationInit's objects as the server serves them today (#3284)

* polis-contracts: the participant token, the auth envelope and participationInit's objects as the server serves them today

Three typed entries describe current server behaviour so the Rust side can
be checked against it: the auth envelope the server injects into response
bodies (which responses carry it, including error bodies; a falsy existing
value replaced in place; expires_in fixed at one year), the participant JWT
header and the three claim kinds exactly as the server mints them, and
participationInit's ptpt, xInfo and conversation objects with their key
order following the bootstrap schema's column order. The conversation entry
names the supported legacy table layout and rejects it explicitly. A test
decodes the recorded server-minted tokens and re-encodes them byte for byte,
so a change in the server's claims surfaces here. Nothing served changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-contracts: no expect in library code

polis-api compiles schema.rs into its binary, where clippy denies expect();
literal_u32 now builds the JSON number directly, which cannot fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ly while the section is hidden (#3287)

The old condition tested a jQuery selection object, which is truthy even
when it contains no elements. At or above the minimum participant count,
every person update on a visible section therefore scheduled a fade whose
completion called initialization again. Changed dimensions could rebuild
the visualization, register the person listener again, and request more
comments through the immediate listener update.

Check the selection length so only a hidden section takes this fade path.
The test covers counts at and above the minimum: a hidden section fades
and initializes, while a visible section schedules neither, even after a
width change. It also verifies that the reveal's listener broadcast does
not schedule another fade. Ordinary person updates still request comments;
the visible section adds no request through re-initialization. Resize,
tab, and initial reveal behavior are unchanged.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…nly probes for half-made identities (#3285)

Thirty-five fixed-label counters record what the Rust side does with each
request's identity (verified, refused, handed back to Node and why, admitted
by membership), exposed as an identity object on the internal health
document; nothing on the request path waits on them. Every route uses one
table of hand-back reasons, and an admitted OIDC request on the votes read
is no longer counted as a hand-back. Three read-only SQL probes count the
half-made states the server's join path can leave behind: users created
and never joined, participants in xid-required conversations whose user has
no xid under the owner, and OIDC mappings without a user; each is counts
only, bounded by a statement timeout, and tested against the migrations.
Nothing served changes.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
… sides (#3286)

Sixty recordings of the server's identity behaviour, made on the sealed
characterization stack: the anonymous first vote, bare-xid first contact on
participationInit, votes and comments with the allow-list off and on and
with xid_required, XID tokens in their own and another conversation, the
standard-user join, OIDC first use and repeat, Treevite accept and login,
the permanent cookie, a refused first join that leaves a user with no
participant, and a failed xid binding that leaves an unbindable participant;
plus the refusals (wrong audience, wrong issuer, unsigned, wrong algorithm,
expired). The recorder compares each run against the committed fixture and
re-records only when asked; a Jest test replays the fixture against the live
server; a Rust test verifies the twenty recorded tokens with the polis-auth
verifier. A new workflow runs the same local entry point in hosted CI.
Nothing served changes.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…min account (#3288)

An external identity used to resolve directly to its linked user and could
issue or reuse a participant token with that user's account privileges.

Refuse XID authentication for anyone who owns a conversation, has the owner
flag, moderates a site, is listed in ADMIN_UIDS, or has an OIDC mapping. Check
both new credentials and existing XID tokens, including cookie conversion.
Return HTTP 403 with polis_err_xid_owner_account and log the account class
without identity values.

Plain participants still resume their identities, and first contact still
creates a participant account. Existing identity bindings are unchanged.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…the deployment (#3289)

A failed polis-api image pull used to stop the deployment even when no Rust
route was selected. Resolve routing and the start switch before pulling,
catch that unrouted pull failure, log it once, and skip Rust startup so the
Node deployment can finish.

Selected Rust routes still require a successful pull. Migration and Node
image pulls, invalid manifests, and other deployment failures stay fatal
with their existing exit codes.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…r or admin account (#3290)

* polis-auth: an external identity never resolves to an owner, moderator or admin account

Check XID accounts against current primary database privileges before using
verified claims. Match the Node refusal body and class order, and expose the
same guard for admission and token issuance. Count refusals by class without
logging identity values.

Pin refusals and plain participation to Node recordings. Test the predicate
against migrated PostgreSQL tables and recheck tokens after privilege changes.
No routes, schema or Node behavior change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-api: the health identity-key test counts the event list instead of a literal

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…d never served (#3291)

* polis-auth: participant admission as one transaction, switched off and never served

Commit the user, participant, XID binding, invite consumption and prepared
recovery code together. Lock the invite before admission and keep the existing
participant-number lock until commit. Roll back failed attempts in full.

Preserve anonymous conversation scope, owner bypass and the owner-account XID
refusal. Keep bcrypt, token signing and child-invite issuance outside the
participant-number lock. No route, Node handler or schema changes.

Test concurrent invite and identity joins, failures at each write boundary,
and shared admission results from the Node recordings. Format, clippy and
the polis-auth tests pass through the shared CI entry point.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-auth: the admission recordings test passes the account check that verification now requires

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-auth: import the unavailable type from the crate root in the recordings test

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…es everything or nothing (#3292)

* embed pages: page ids up to 2,000 characters, and a first visit creates everything or nothing

Every visit to a page with an id over 100 characters returned a 500 and
left an orphan empty conversation. Widen page_ids.page_id and
page_ids.site_id to varchar(2000), keeping the unique site and page pair.

Create the conversation, page mapping and invite in one transaction so
all three succeed or all three roll back. When first visits race, roll
back the losing insert and redirect both visitors to the winner.

Reject missing, empty or overlong site and page ids with a clear 400
before writing anything, while preserving the existing URL parsing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* migrations: the first-deploy report includes the page id width migration

The report builder lists the migrations that sit outside the adoption set; the new migration joins that list and the generated report is refreshed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* coordinator: the schema census includes the page id width migration

Measured against a fresh PostgreSQL 17 with the whole migration chain applied: the two page_ids columns widen to 2,000 characters; nothing else changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* migrations: the forward proof's additive ledger lists the page id width migration

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…t role (#3296)

* comments: a participant's statement never looks up the owner's account role

The old comment path made two external calls to the identity provider on
every comment to find the conversation owner's account and read its role.
Remove the management client, the owner-role helper, the remaining Pro-gated
comment-list branch, and the unused management package.

Moderation emails still use the account email stored in the database, and
comments still schedule notification tasks. Login configuration is unchanged.

The report's moderation level filter, which only applied when the owner carried a paid role, now applies to every conversation; no account is looked up for it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* comments: the moderation-list test expects the level filter to apply without an owner lookup

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The post-comments recordings guard treated every file under server/characterization as a recording. The route inventory and its scope are a census of the registrations the code declares, and the identity recordings map observed route hits through that inventory, so a route removal has to update it in the same change or every identity case misreports its route hits. The inventory and scope files are exempted; recordings, cases, fixtures and comparison rules stay guarded.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…wo forms as Node (#3299)

* polis-api: participant signing keys read once at start, in the same two forms as Node

Load and validate the RSA pair before startup. Use the public handle for
participant verification and keep a private handle for future signing.
Report loaded status and a public-key fingerprint in health without key bytes.

Cover value and path settings, escaped newlines, boot refusals, and Node
signature and fingerprint agreement. No token issuance or route changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-api: absent signing keys are reported, not fatal, unless a participant route is selected

A box that routes no participant traffic to the service (and every fixture that starts it without keys) boots and reports participantKeysLoaded false; a present but malformed, unreadable or mismatched pair still refuses to boot, and so does a selected participant route without keys.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-api: the public key alone is enough to verify, and the earlier public-key path setting is accepted

A process that only verifies participant tokens needs no private key; one that is configured must be valid and must match. The fixtures and the deploy set POLIS_API_PARTICIPANT_PUBLIC_KEY_FILE, which now stands in for JWT_PUBLIC_KEY_PATH when neither JWT_PUBLIC_KEY form is set.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
… shared recorder modules (#3303)

* ci: the post-comments recordings guard covers its own profile and the shared recorder modules

The guard treated every file under server/characterization as a post-comments recording input, so a server change that must update another profile's cases (identity, votes, collective-statement, jobviews) in the same pull request could never pass, although each of those profiles has its own replay workflow and re-record procedure. The guard now covers the post-comments directory and the shared recorder modules at the top of the tree, which is what its header describes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* ci: the route inventory tool and its unit test are census files too

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
… keys refreshed off the request path (#3297)

* polis-auth: participant admission as one transaction, switched off and never served

Commit the user, participant, XID binding, invite consumption and prepared
recovery code together. Lock the invite before admission and keep the existing
participant-number lock until commit. Roll back failed attempts in full.

Preserve anonymous conversation scope, owner bypass and the owner-account XID
refusal. Keep bcrypt, token signing and child-invite issuance outside the
participant-number lock. No route, Node handler or schema changes.

Test concurrent invite and identity joins, failures at each write boundary,
and shared admission results from the Node recordings. Format, clippy and
the polis-auth tests pass through the shared CI entry point.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-auth: the admission recordings test passes the account check that verification now requires

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-auth: import the unavailable type from the crate root in the recordings test

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-auth: single-sign-on first use as one transaction, and provider keys refreshed off the request path

Provision verified provider identities with the existing email and name rules,
owner defaults, and social-login mapping replacement. Roll back the whole
attempt on uniqueness races and reuse the bounded admission transaction policy.

Load provider keys from a boot snapshot and refresh them in a separate task.
Unknown keys refuse immediately; refresh attempts are rate limited, failures
retain the previous keys, and identity counters expose misses and refreshes.

Add migrated PostgreSQL, Node recording, and local provider rotation tests.
Keep the new functions unrouted and disabled.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-auth: the provisioning recording pins the current Node source

The recorded file gained a connection parameter on its join-code helper when the page id change merged; the single-sign-on provisioning path it records did not change, so the pin moves to the current bytes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ff (#3301)

Build the Rust version response with the same script and inputs as Node.
Keep its JSON bytes, unknown-provenance fallback and no-store header.
Add separate process and nginx switches, with both off by default, and
compare version responses in the deploy health check when selected.

Record the Node contract and test image parity, HEAD, conditional requests
and routing fallback. The route does no database or participant work.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ptance, switched off and never served (#3304)

* polis-api: POST /api/v3/votes for admitted participants, durable acceptance, switched off and never served

Add the reviewed vote handler for existing conversation-bound participants.
Keep first contact, new participants, bare XIDs and cookie recovery on Node.
The independent switch defaults off; nginx routing is unchanged.

Return the stored vote with HTTP 200 when star or next-comment work fails.
Report follow-on degradation in the body. Decide the same-choice retry rule
in the handler's Rust transaction: lock the latest vote key, compare the
stored choice and append only on change. No migration or new Postgres
function is needed. Report uncertain commits separately and never hand a
dispatched vote to Node.

Preserve independent bounded pools, capped best-effort notifications and
parser hand-back. Add health counters, explicit pre-dispatch markers,
current Node witnesses and database/replay tests. Node keeps its deliberate
append-on-every-request behavior in the contracts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-api: the health field count includes the votes handler's entry after the merge

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…me (#3305)

A live vote or comment can return before its background interaction write
finishes. The tests sometimes read the old interaction timestamp in their
baseline and the new one after the import correctly refused the job.

Wait for the real interaction writes before taking that baseline or returning
the downloaded body to the import worker. Settle pending writes on failures,
and await closure of the application pools so Jest's worker can exit.

The tests still prove that occupied targets refuse imports without changing
existing rows, live votes survive in history and latest, and zero-batch jobs
refuse a vote arriving during download without completion side effects.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…replays (#3306)

* characterization: Treevite recordings, every route and refusal, both replays

Record all nine Treevite routes, authentication and validation refusals,
login round trips, child issuance, identity conversion and a real database
race. Preserve the current partial state as an explicit security delta.

Keep generated login codes in memory and compare normalized wire bytes and
committed rows against the pinned fixture in two fresh replays.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* characterization: the Treevite recorder names the fields it discards

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…3307)

* polis-auth: participant admission as one transaction, switched off and never served

Commit the user, participant, XID binding, invite consumption and prepared
recovery code together. Lock the invite before admission and keep the existing
participant-number lock until commit. Roll back failed attempts in full.

Preserve anonymous conversation scope, owner bypass and the owner-account XID
refusal. Keep bcrypt, token signing and child-invite issuance outside the
participant-number lock. No route, Node handler or schema changes.

Test concurrent invite and identity joins, failures at each write boundary,
and shared admission results from the Node recordings. Format, clippy and
the polis-auth tests pass through the shared CI entry point.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-auth: the admission recordings test passes the account check that verification now requires

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-auth: import the unavailable type from the crate root in the recordings test

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-auth: single-sign-on first use as one transaction, and provider keys refreshed off the request path

Provision verified provider identities with the existing email and name rules,
owner defaults, and social-login mapping replacement. Roll back the whole
attempt on uniqueness races and reuse the bounded admission transaction policy.

Load provider keys from a boot snapshot and refresh them in a separate task.
Unknown keys refuse immediately; refresh attempts are rate limited, failures
retain the previous keys, and identity counters expose misses and refreshes.

Add migrated PostgreSQL, Node recording, and local provider rotation tests.
Keep the new functions unrouted and disabled.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-auth: the provisioning recording pins the current Node source

The recorded file gained a connection parameter on its join-code helper when the page id change merged; the single-sign-on provisioning path it records did not change, so the pin moves to the current bytes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* identity: Node and Rust admitting participants together, under load

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
… directly (#3302)

Keep the existing API and page routes on their selected backends. Send known
assets and public documents directly to the file-server, preserving their
bytes, cache headers and compression.

Return the file-server-compatible error body locally for unmatched paths.
Keep the application catch-all registered while page routes still use it.

Cover route selection, static files, error bodies and the absence of unknown
requests in the application log. Verify reloads with concurrent participation
traffic and save both rendered route selections.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* retire: the spam-check client and the permanent-cookie recovery

The spam check had not been on the comment path for some time. A source
search confirms that Akismet only constructed its client and verified its
key at boot. Remove that startup call, dependency, setting, and references.

The permanent-cookie recovery had not recovered a participant in a month
of production logs. A request with only the old pc cookie and no token now
follows the normal first-contact flow and joins as a fresh participant.
Keep the database columns and replace the recovery tests with a regression
that checks the fresh participant and its signed token.

No other code reads req.cookies, so remove the cookie parser too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: the admission model, identity recordings and replay pins follow the cookieless server

Treat a request with only an old cookie as first contact. Keep both cookie
entries in the admission matrix: no external ID follows anonymous entry,
and a denied external ID follows new denied-ID entry.

Keep both identity cookie cases and record fresh users and participants.
The XID case now creates a binding to the new token's user. Preserve JWT
verification and the fixture's source hashes.

The collective-statement index changes only egress.refusedAtServerLoad:
rest.akismet.com:80 is gone because the retired spam-check client no
longer verifies its key at startup. All 51 case recordings are unchanged.
Two fresh recordings are identical and replay matches byte for byte.

Validation on disposable Docker services: all 2,400 admission cases match,
all six setup unit tests pass, both identity replays match all 60 cases,
and all 123 identity and token tests pass. Both route-inventory hashes
still match the source. No runtime or participation-path code changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: the collective-statement egress pin is re-recorded in its own change after this merges

The replay index records which hosts the server tried to reach at load; with the spam-check client gone that list is empty. The recordings guard requires that pin to move in its own change from edge, so it stays as recorded here and follows in a one-line re-record.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…y registration index (#3308)

Removing an earlier route shifts the observer's registration indexes even
when Treevite behavior has not changed. Map hits through the enabled route
inventory to stable route ids, remove duplicates, and sort numerically.

Rewrite the fixture by mapping its existing indexes through the inventory,
not by re-recording. Preserve every other byte of the fixture.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…m-check client's removal (#3309)

Re-record the collective-statement profile after the spam-check client was
removed. The server no longer attempts its load-time key check, so the
load-time egress refusal list is empty.

Two recordings are identical and two fresh replays match all 51 cases.
Every case file and hash is unchanged; only the egress pin moves.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* retire: the tutorial, onboarding and marketing pages

Remove POST /api/v3/tutorial and the unused page shells and redirects.

No successful request since July 7: the tutorial write, /bot/install*,
/bot/support*, /pwreset*, /other-conversations*, /user/create*,
/inboxApiTest*, /hk/new*, /s, /s/CTE and /nabi. Some of these registrations
were shadowed by broader page routes.

Remove the remaining onboarding shells: /createuser*, /user/login*,
/user/logout*, /settings*, /conversation/create*, /pwresetinit*, /inbox*,
/hk* and /s/*. Remove the marketing and demo pages: /gov*, /company,
/contrib*, /demo, /demo/<id> and /bot*. These served successful requests
in the September 26 through October 25 access-log window. The aggregate
check does not retain their exact last-success timestamps.

Remove /about, /football and /pdf redirects. All three returned redirects
in that same window; exact last-success timestamps are not in the check.

Remove the tutorial CLI operation and docs, legacy demo router entry and
analytics mapping, dead tutorial store method, unused page wrappers and
redirect helper. Remove the seven matching Rust page handlers, contracts,
read settings, nginx switches, health counters and obsolete test fixtures.
Retire the 29 stable route identities and check their 404 responses through
the real static-file proxy and the generic API fallback.

Keep /home, /privacy, /tos, /donate, /signin and /signout. /feeds/*,
/api/v3/votes/famous, /polis_site_id*, /report_bundle*, /report_style* and
the six topicMod routes stay.
No table or column changes; vote, comment and next-statement work is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: the retired-page test accepts the proxy's 502 where no file server runs, and the differential harness no longer expects the tutorial step

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: the characterization inventory moves to its own change from edge

The recordings guard requires the route inventory, its scope and its cases to change in a separate pull request from the code they describe; those updates follow once this merges.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: the route inventory and its scope move with the code again

The recordings guard now treats the inventory and scope as the census they are, so the retired entries, scope and cases return to this change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-api: the health field count follows the retired page routes after the merge

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
colinmegill and others added 13 commits October 29, 2026 02:44
…tly backups; the upgrade and restore recipe (#3312)

* self-hosting: a rebuild never deletes a database without a dump; nightly backups; the upgrade and restore recipe

Check the selected database in an isolated copy before removing any project
volumes. Refuse populated databases unless data loss is explicitly confirmed,
and save a verified host dump before proceeding. Keep the backups volume and
volumes belonging to other projects.

Add dump and restore commands, nightly dumps with fourteen-file retention,
and a startup refusal for legacy database volumes. Document upgrades and
restores, including the PostgreSQL 13 to 17 transition. Keep these services
out of the production composition.

Exercise refusal, dump failure, restore failure, round-trip row counts,
password migration, retention, connection cleanup and rebuilds on real
PostgreSQL and Compose containers.

Review round 2 asked for three changes, applied here: host dump files under ./backups/ are ignored by git; the guide creates ./backups before copying a nightly dump out; the test script's negated checks are explicit failures (a negated pipeline is exempt from errexit).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* self-hosting: the three review fixes (ignored dumps, the backups directory, explicit test failures)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…not on time (#3315)

Hold append, notification and star work at explicit database gates, and observe
commit and pool completion before checking durable rows. Attribute dispatch
creation to each concurrent caller so only a verified pre-dispatch handback
may be retried. Keep a real sixteen-caller first-append race with bounded,
reported retries of entirely empty rounds.

Keep eight narrow readers and both real read limits while all four writer
slots stay held. Retry only read-pool acquisition timeouts, at most three
attempts per reader, with unchanged writer evidence required on every attempt.
Keep the held-append response between the real dispatch deadline and that
deadline plus 500 ms. Observe vote and notification commits independently of
the star gate and retain the 600 ms notification promptness check.

Warm fixture connections, retain concurrent notification pressure, and cancel
an outstanding route when checking detached vote durability. The production
handler, pools, deadlines, vote conversion and fallback behavior are unchanged.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…off (#3311)

* polis-api: Treevite wave creation as one typed transaction, switched off

Create the wave and its root and member invitations in one transaction.
Serialize wave numbers and owner-wide capacity checks with advisory locks.
Keep Node response shapes and member top-up behavior, with overridable code
ceilings checked before inserts and separate bounded minting resources.

Add recording replay and database tests for numbering races, quota refusals,
collision retries, rollback and participation isolation. Keep routing off.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-api: a wave create fills lost child allocations of earlier waves

After issuing the new wave's codes, the same transaction looks for
members of earlier child waves whose parent invite was consumed but whose
own allocation is short, and fills the deficit under the existing
per-(wave, member) advisory lock, counting every code state first so no
member is granted twice. At most 100 deficient allocations are filled per
create; the rest wait for the next create or accept. The response still
reports only the new wave's codes, as Node's does; repairs are logged as
a count. A repair failure rolls back the new wave.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…d off (#3313)

* polis-api: Treevite wave creation as one typed transaction, switched off

Create the wave and its root and member invitations in one transaction.
Serialize wave numbers and owner-wide capacity checks with advisory locks.
Keep Node response shapes and member top-up behavior, with overridable code
ceilings checked before inserts and separate bounded minting resources.

Add recording replay and database tests for numbering races, quota refusals,
collision retries, rollback and participation isolation. Keep routing off.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-api: a wave create fills lost child allocations of earlier waves

After issuing the new wave's codes, the same transaction looks for
members of earlier child waves whose parent invite was consumed but whose
own allocation is short, and fills the deficit under the existing
per-(wave, member) advisory lock, counting every code state first so no
member is granted twice. At most 100 deficient allocations are filled per
create; the rest wait for the next create or accept. The response still
reports only the new wave's codes, as Node's does; repairs are logged as
a count. A repair failure rolls back the new wave.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-api: Treevite accept and login as typed write handlers, switched off

Add independent admission and crypto capacity for invitation acceptance and
return-code login. Keep expensive work outside the admission transaction and
retain durable acceptance when follow-on work is delayed.

Enforce invitation age, login-code expiry, anonymous identity boundaries, and
second-invite burning without replacing a live recovery credential. Sign new
participant sessions for one day and top up child-wave invitations idempotently.

Keep the new route switch disabled and leave gateway routing unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* ci: the polis-api test step names the admission fixture's database; the replay harness drops an unused argument

The Treevite write tests reuse the admission fixture from polis-auth, which
reads POLIS_AUTH_TEST_DATABASE_URL. The polis-api test step only set its own
variable, so those tests panicked on the hosted runner while passing on any
host that had both. Point the fixture at the same disposable database; it
creates its own schema per process.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ched off (#3314)

GET /api/v3/comments/participant returns, for one conversation, the two
fields the participation page reads from the statement list: txt and tid,
in Node's property order. Visibility is exactly Node's plain participant
list (active, the conversation's moderation rule, positive velocity,
created order, the default cap); no query key can widen it. Identity is the
comments twin's hybrid resolution: anonymous allowed, cookies, malformed and
foreign-conversation tokens refused.

The route has its own one-slot read and identity pools with short bounds
and makes no writes. It is behind its own switch, off by default, and has
no nginx map entry, so nothing is served by this change. Recordings against
unchanged Node compare the whole projected body: 426 of 426 over 71
conversations are byte-identical; the topic agenda's expanded call is listed
in deltas with its reason.

The boot health test counts two new fields.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…helpers, old bundles and embed demos (#3294)

* retire: the share, summary, explore and one-time pages, the implicit helpers, old bundles and embed demos

Remove GET /share/<id>, /summary/<id>, /explore/<id>, /ot/<id>,
/iip/:conversation_id and /iim/:conversation_id. Each has no successful
request since July 7 in the access-log check through October 26.

Remove /embed, /embedPreprod, /embedReport, /embedReportPreprod and their
.html files, plus /styleguide and /dist/admin_bundle.js. The preproduction
and report demos, styleguide and old admin bundle have no successful
request since July 7. /embed had five 200 responses in the July 7 to
October 26 window; the check did not record their latest timestamp.
/embed.html had requests in that window; its last successful timestamp
was not recorded separately.

Remove /__webpack_hmr, /cached/*, GET and ALL /font/*, and the old
/*embed*js/* proxy registration. The hot-reload endpoint had two 200
responses during the final 30 days of that window; the check did not
record their latest timestamp. The other three paths have no successful
request since July 7. Remove the unreachable catch-all proxy registration
and the never-enabled missing-file branch, which had no calls.

Remove the six corresponding Rust routing switches, nginx templates,
link handlers, dedicated pools, counters, contracts and conformance cases.
Drop their process settings and the legacy one-time-link router entry.
Move the nineteen registrations to the retired inventory and verify the
retired pages and demo filenames return 404 through the remaining proxy.

Keep feeds, famous votes, site-id integrations, the live embed.js,
conversation pages, home, privacy, terms, donate, signin, signout, report
bundles and styles, all six topic moderation routes, and every table and
column. The participation path gains no work, locks or waits.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: the retired-page test has its own file name and accepts the proxy's 502 where no file server runs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: the characterization inventory moves to its own change from edge

The recordings guard requires the route inventory, its scope and its cases to change in a separate pull request from the code they describe; those updates follow once this merges.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: the route inventory and its scope move with the code again

The recordings guard now treats the inventory and scope as the census they are, so the retired entries, scope and cases return to this change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-api: the health field count follows the retired page routes and the key custody fields

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: drop the file fetcher and static-port settings the share-bundle retirement orphaned

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…3295)

* retire: the share, summary, explore and one-time pages, the implicit helpers, old bundles and embed demos

Remove GET /share/<id>, /summary/<id>, /explore/<id>, /ot/<id>,
/iip/:conversation_id and /iim/:conversation_id. Each has no successful
request since July 7 in the access-log check through October 26.

Remove /embed, /embedPreprod, /embedReport, /embedReportPreprod and their
.html files, plus /styleguide and /dist/admin_bundle.js. The preproduction
and report demos, styleguide and old admin bundle have no successful
request since July 7. /embed had five 200 responses in the July 7 to
October 26 window; the check did not record their latest timestamp.
/embed.html had requests in that window; its last successful timestamp
was not recorded separately.

Remove /__webpack_hmr, /cached/*, GET and ALL /font/*, and the old
/*embed*js/* proxy registration. The hot-reload endpoint had two 200
responses during the final 30 days of that window; the check did not
record their latest timestamp. The other three paths have no successful
request since July 7. Remove the unreachable catch-all proxy registration
and the never-enabled missing-file branch, which had no calls.

Remove the six corresponding Rust routing switches, nginx templates,
link handlers, dedicated pools, counters, contracts and conformance cases.
Drop their process settings and the legacy one-time-link router entry.
Move the nineteen registrations to the retired inventory and verify the
retired pages and demo filenames return 404 through the remaining proxy.

Keep feeds, famous votes, site-id integrations, the live embed.js,
conversation pages, home, privacy, terms, donate, signin, signout, report
bundles and styles, all six topic moderation routes, and every table and
column. The participation path gains no work, locks or waits.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: the dormant API routes, the unused dependencies and settings

Remove users/invite, snapshot, metrics, sendEmailExportReady,
math/correlationMatrix, mathUpdate, contributors, auth/deregister,
dataExport, dataExport/results and math/pca. These paths had no successful
request since July 7 in the retained access logs.

Remove notifyTeam, whose last successful call was September 21. Remove
dummyButton, which had two successful calls during September 26 through
October 26; the access-log summary does not record their exact timestamps.

Remove the matching CLI operations and documentation, the legacy button
and metrics callers, report correlation fetching, and the Rust legacyPca
handler, contract and routing switch. Mark all thirteen routes retired in
the characterization inventory and replace their cases with a shared 404
test. Unknown API GETs return 404 without going to the static-file proxy.

Remove aws-sdk v2 and the unused Mailgun, nodemailer, oauth, simple-oauth2,
request, googleapis, aiplatform, vertexai, mimelib, nan and p3p direct
packages and their unused type packages. Remove the periodic export test,
comment-language backfill and inactive PCA polling loop, together with
WEBSERVER_USERNAME, WEBSERVER_PASS, RUN_PERIODIC_EXPORT_TESTS,
ADMIN_EMAIL_DATA_EXPORT_TEST, BACKFILL_COMMENT_LANG_DETECTION and the
unused Mailgun settings.

Keep axios, dd-trace and ENCRYPTION_PASSWORD_00001 because they still have
readers. Feeds, famous votes, site-id integration, live pages, report
assets, topic moderation, database tables and columns stay in place.
Votes, comments and next-statement handling gain no new work or locks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: the retired-page test has its own file name and accepts the proxy's 502 where no file server runs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-cli: the math help test no longer expects the retired correlation and refresh leaves

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: the characterization inventory moves to its own change from edge

The recordings guard requires the route inventory, its scope and its cases to change in a separate pull request from the code they describe; those updates follow once this merges.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: the characterization inventory moves to its own change from edge

The recordings guard requires the route inventory, its scope and its cases to change in a separate pull request from the code they describe; those updates follow once this merges.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: the route inventory and its scope move with the code again

The recordings guard now treats the inventory and scope as the census they are, so the retired entries, scope and cases return to this change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: the route inventory and its scope move with the code again

The recordings guard now treats the inventory and scope as the census they are, so the retired entries, scope and cases return to this change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: leave the collective-statement harness untouched; its two stale settings go with the next re-record

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-api: the health field count follows the retired page routes and the key custody fields

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: the CLI command floor and the importance check follow the retired operations

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* Merge edge into retire/dormant-api; the health field count follows the retired legacy pca switch

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: leave the characterization harness settings in place; they go with the next re-record

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* retire: drop the file fetcher and static-port settings the share-bundle retirement orphaned

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* ---

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ng 202 and writing nothing (#3319)

An admitted vote keeps its place in the writer queue after the response
has returned. At 60 seconds, log the vote identifiers and count a writer
acquisition alarm, then keep waiting on the same acquisition until a slot
is available. The alarm never discards the vote and there is no wait ceiling.
Reads and admission keep their 100 ms acquisition budget. Pool capacity,
connection and statement limits, and the one-second response deadline stay
unchanged. A waiting dispatch holds no database connection or blocking worker.

Hold all four writer slots past a test alarm and prove that the dispatch
survives, appends once, publishes its receipt and schedules notification.
Require each concurrent caller's own completion and durable receipt before
accepting 202. Preserve replay integrity and notification scheduling.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…go builds are locked (#3317)

* deps: installs run no package scripts, lockfiles carry integrity, cargo builds are locked

Use frozen npm installs with lifecycle scripts disabled at local lockfile-backed build and test entry points. Install the pinned Cypress browser explicitly where end-to-end tests need it.

Add registry URLs and integrity to the legacy participation lock without changing package versions. Refuse missing Anthropic SDK at runtime and direct environment builders to the existing pinned requirements.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* ci: the coordinator workflow's server install keeps the prefix before the flag, as its campaign test asserts

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* deps: the routing recordings workflow keeps its install line; that change moves to its own pull request

The recording rules and product code may not change in one pull request (the routing guard enforces it).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ackage scripts disabled (#3318)

The same install hygiene as the rest of the repository, in its own pull request because the routing guard forbids changing the recording rules together with product code.


Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…hed off (#3316)

* polis-api: the participant statement list as a typed read route, switched off

GET /api/v3/comments/participant returns, for one conversation, the two
fields the participation page reads from the statement list: txt and tid,
in Node's property order. Visibility is exactly Node's plain participant
list (active, the conversation's moderation rule, positive velocity,
created order, the default cap); no query key can widen it. Identity is the
comments twin's hybrid resolution: anonymous allowed, cookies, malformed and
foreign-conversation tokens refused.

The route has its own one-slot read and identity pools with short bounds
and makes no writes. It is behind its own switch, off by default, and has
no nginx map entry, so nothing is served by this change. Recordings against
unchanged Node compare the whole projected body: 426 of 426 over 71
conversations are byte-identical; the topic agenda's expanded call is listed
in deltas with its reason.

The boot health test counts two new fields.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-api: the moderation statement list as a typed read route, switched off

Add a moderator-only statement list with the fields and pagination read by
the moderation page. Verify authority before reading statements or counts.
Use independent bounded read-only pools and leave the route disabled.

Parse numeric query values like Node, preserve large offsets, and return its
Bad Request response for malformed or repeated parameters. Compare every
query and refusal probe against Node and retain reasons for deliberate
differences. Require a limit and one moderation state so omitted values
hand back before any database read. Keep participation pools and the next-statement behavior intact.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-api: the health census is 138 fields with the moderation list route

The merge resolution counted the participant list's two fields twice: once from edge and once from this branch's history. Edge reports 136; this route adds two.

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…napshot that ends sessions within a minute (#3310)

* polis-api: Treevite login-code revoke and reissue, and a revocation snapshot that ends sessions within a minute

Add explicit owner and site moderator actions to revoke or replace a participant's login code. Keep the routes off by default and prepare replacement credentials on a bounded crypto worker before the write transaction.

Load revoked participants at startup and refresh them every thirty seconds on a separate read connection. Check signed participant sessions against the in-memory snapshot and return a terminal unauthorized response so a fallback cannot bypass revocation.

Add coverage for legacy year-long tokens, authorization, replacement credential derivations, failed refresh retention, and vote statement counts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

* polis-api tests: the statement-counting proxy in the revocation test disables Nagle on both sockets

The proxy forwards each PostgreSQL protocol header and body as separate
writes. With Nagle and delayed acknowledgements on Linux, those small writes
stall long enough to spend the vote's real 250 ms admission deadline, and the
vote answered 502 inside the test only. Every assertion and all production
code are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss9NfEMZS9Qm4FZ2npXDrx

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Delphi Coverage Report

File Stmts Miss Cover
polismath/init.py 2 0 100%
polismath/benchmarks/bench_pca.py 128 107 16%
polismath/benchmarks/bench_repness.py 81 65 20%
polismath/benchmarks/bench_update_votes.py 38 28 26%
polismath/benchmarks/benchmark_utils.py 34 0 100%
polismath/components/init.py 1 0 100%
polismath/components/config.py 165 70 58%
polismath/conversation/init.py 2 0 100%
polismath/conversation/conversation.py 1293 264 80%
polismath/conversation/group_vote_counts.py 38 2 95%
polismath/conversation/manager.py 131 42 68%
polismath/database/init.py 1 0 100%
polismath/database/dynamodb.py 392 176 55%
polismath/database/postgres.py 414 95 77%
polismath/delphi_storage/init.py 1 0 100%
polismath/delphi_storage/codec.py 295 22 93%
polismath/delphi_storage/embedding_spool.py 58 6 90%
polismath/delphi_storage/golden_corpus.py 60 9 85%
polismath/delphi_storage/import_report.py 51 3 94%
polismath/delphi_storage/legacy_import.py 171 16 91%
polismath/delphi_storage/postgres.py 49 22 55%
polismath/delphi_storage/production_import.py 612 131 79%
polismath/delphi_storage/resource.py 179 32 82%
polismath/delphi_storage/scan_pacing.py 77 0 100%
polismath/delphi_storage/writer.py 196 13 93%
polismath/empty_output.py 29 2 93%
polismath/engine_adapter.py 317 265 16%
polismath/job_child/init.py 364 51 86%
polismath/job_child/census.py 74 9 88%
polismath/job_child/input_bundle.py 101 7 93%
polismath/pca_kmeans_rep/init.py 5 0 100%
polismath/pca_kmeans_rep/clusters.py 257 38 85%
polismath/pca_kmeans_rep/corr.py 98 17 83%
polismath/pca_kmeans_rep/group_k_smoother.py 28 0 100%
polismath/pca_kmeans_rep/legacy_kmeans.py 197 8 96%
polismath/pca_kmeans_rep/pca.py 162 19 88%
polismath/pca_kmeans_rep/repness.py 226 5 98%
polismath/poller/init.py 4 0 100%
polismath/poller/admission.py 318 15 95%
polismath/poller/backfill.py 936 32 97%
polismath/poller/capacity.py 485 27 94%
polismath/poller/capacity_queue.py 248 13 95%
polismath/poller/coordinator_bridge.py 317 317 0%
polismath/poller/math_writer.py 113 3 97%
polismath/poller/promotion.py 112 4 96%
polismath/poller/readiness.py 368 13 96%
polismath/poller/rebuild_child.py 133 5 96%
polismath/poller/service.py 756 54 93%
polismath/poller/worker_pool.py 140 0 100%
polismath/queue/executor.py 221 111 50%
polismath/regression/init.py 4 0 100%
polismath/regression/clojure_comparer.py 188 20 89%
polismath/regression/comparer.py 891 640 28%
polismath/regression/datasets.py 126 27 79%
polismath/regression/recorder.py 36 27 25%
polismath/regression/utils.py 130 85 35%
polismath/replay/init.py 2 0 100%
polismath/replay/attribution_capture.py 49 0 100%
polismath/replay/axis_continuity.py 347 24 93%
polismath/replay/certify.py 999 59 94%
polismath/replay/crosslang.py 128 2 98%
polismath/replay/decision_ties.py 51 8 84%
polismath/replay/diagnostics.py 50 7 86%
polismath/replay/driver.py 73 1 99%
polismath/replay/dsn_admission.py 36 1 97%
polismath/replay/equiv_query.py 10 0 100%
polismath/replay/event_ingress.py 95 1 99%
polismath/replay/fixture_bundle.py 815 62 92%
polismath/replay/fixture_config.py 266 11 96%
polismath/replay/fixture_extract.py 285 12 96%
polismath/replay/fixture_generate.py 103 1 99%
polismath/replay/fixture_samples.py 157 14 91%
polismath/replay/fixture_selection.py 119 0 100%
polismath/replay/fixture_survey.py 147 11 93%
polismath/replay/legacy_pca.py 29 17 41%
polismath/replay/polarity.py 319 25 92%
polismath/replay/poller_equiv.py 776 99 87%
polismath/replay/prodclone.py 193 2 99%
polismath/replay/real_data.py 146 10 93%
polismath/replay/schedule.py 205 10 95%
polismath/replay/served_math.py 159 4 97%
polismath/replay/shard_bench.py 189 65 66%
polismath/replay/stagecompare.py 890 161 82%
polismath/replay/stages.py 256 40 84%
polismath/replay/stepcompare.py 86 1 99%
polismath/replay/store.py 120 11 91%
polismath/replay/tie_capture.py 107 19 82%
polismath/replay/types.py 58 6 90%
polismath/run_math_pipeline.py 284 93 67%
polismath/tools/engine_rebuild.py 230 11 95%
polismath/types.py 6 0 100%
polismath/utils/cli_flags.py 15 0 100%
polismath/utils/clj_hash.py 44 0 100%
polismath/utils/env_flags.py 13 0 100%
polismath/utils/general.py 63 39 38%
polismath/utils/output_profile.py 66 2 97%
polismath/utils/serialization.py 10 5 50%
polismath/utils/vote_convention.py 159 5 97%
umap_narrative/700_datamapplot_for_layer.py 514 470 9%
umap_narrative/701_static_datamapplot_for_layer.py 310 310 0%
umap_narrative/702_consensus_divisive_datamapplot.py 435 275 37%
umap_narrative/801_narrative_report_batch.py 657 209 68%
umap_narrative/803_check_batch_status.py 268 148 45%
umap_narrative/llm_factory_constructor/init.py 2 0 100%
umap_narrative/llm_factory_constructor/model_provider.py 232 106 54%
umap_narrative/narrative_data.py 178 55 69%
umap_narrative/numerical_stages.py 80 14 82%
umap_narrative/polismath_commentgraph/init.py 1 0 100%
umap_narrative/polismath_commentgraph/cli.py 268 268 0%
umap_narrative/polismath_commentgraph/core/init.py 3 3 0%
umap_narrative/polismath_commentgraph/core/clustering.py 108 108 0%
umap_narrative/polismath_commentgraph/core/embedding.py 104 104 0%
umap_narrative/polismath_commentgraph/lambda_handler.py 219 219 0%
umap_narrative/polismath_commentgraph/schemas/init.py 2 0 100%
umap_narrative/polismath_commentgraph/schemas/dynamo_models.py 80 6 92%
umap_narrative/polismath_commentgraph/tests/conftest.py 17 17 0%
umap_narrative/polismath_commentgraph/tests/test_clustering.py 74 74 0%
umap_narrative/polismath_commentgraph/tests/test_embedding.py 55 55 0%
umap_narrative/polismath_commentgraph/tests/test_storage.py 87 87 0%
umap_narrative/polismath_commentgraph/utils/init.py 9 7 22%
umap_narrative/polismath_commentgraph/utils/converter.py 283 48 83%
umap_narrative/polismath_commentgraph/utils/group_data.py 332 279 16%
umap_narrative/polismath_commentgraph/utils/storage.py 609 481 21%
umap_narrative/reset_conversation.py 162 53 67%
umap_narrative/run_pipeline.py 355 203 43%
umap_narrative/topic_naming.py 148 11 93%
Total 25600 7366 71%

@colinmegill
colinmegill marked this pull request as ready for review November 2, 2026 22:27
@colinmegill
colinmegill merged commit 11c756d into stable Nov 2, 2026
89 of 94 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant