Skip to content

Promote edge to stable: Polis 2.43.1 - #3342

Merged
colinmegill merged 16 commits into
stablefrom
edge
Nov 5, 2026
Merged

colinmegill merged 16 commits into
stablefrom
edge

Conversation

@github-actions

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

Copy link
Copy Markdown
Contributor

Release plan

Version: 2.43.1

Reason: no new selected migrations

Previous stable: f46391b539997689e01d1c0c40f5dcd38d494dcb

Candidate: c3dadc86fe467bd8a70e1eac61cf2017789a4381

release.txt additions:

  • none

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 16 commits November 2, 2026 19:10
…ly when testConnection is routed (#3322)

The deploy's health check expected no handler switches unless the version
route was selected, so a box serving testConnection from Rust, which needs
POLIS_API_CONNECTION_READS=1, failed validation. The check now expects that
switch exactly when testConnection is in RUST_API_ROUTES, alongside the
existing version rule. Every other check is unchanged. New cases cover the
switch enabled, missing, unexpected and combined with other routes.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ame checks run on our own machines (#3324)

Each workflow job's commands move into a make target (with shell entrypoints
where a job needs one), keeping its service setup, pinned runtimes, test
selections and artifact handling as they were. The coordinator workflow stays
inline because its required-job control checks the literal commands.

The pinned Node runtime is set up before the TLS server checks, the final
Delphi replay keeps the PostgreSQL backend, and the API job resolves its
source SHA before any test or build. A README section documents the
prerequisites for running the same checks locally.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…tedb never meets the image's temporary server (#3327)

The PostgreSQL image initialises with a temporary server that listens on the
Unix socket only, then restarts. pg_isready on the socket could succeed against
that temporary server, and the createdb that followed failed with a missing
socket while it restarted. Waiting for TCP readiness waits for the real server.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… regains two routes (#3336)

The identity, Treevite and POST-comments recorders matched each case's route
hits by the route's global registration ordinal, so mounting one more
Express app before the routes shifted every id and made live Node disagree
with every committed fixture. The recorders now discover the mounted apps
and resolve a hit by its method and path, so a new app leaves the fixtures
untouched; the re-recorded fixtures differ from the old ones only in route
identity (every old id was one too high), with the comparators, expected
differences, cases and policies byte-identical.

The route census regains GET /api/v3/version, which was registered but
missing, and names the participation route's middleware as registered; the
census check now passes on the source it describes. The legacy census test
is updated for the repaired census; two tests that walked all registrations
select them explicitly.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…running server (#3340)

* recordings: cases name their route by method and path, and the census regains two routes

The identity, Treevite and POST-comments recorders matched each case's route
hits by the route's global registration ordinal, so mounting one more
Express app before the routes shifted every id and made live Node disagree
with every committed fixture. The recorders now discover the mounted apps
and resolve a hit by its method and path, so a new app leaves the fixtures
untouched; the re-recorded fixtures differ from the old ones only in route
identity (every old id was one too high), with the comparators, expected
differences, cases and policies byte-identical.

The route census regains GET /api/v3/version, which was registered but
missing, and names the participation route's middleware as registered; the
census check now passes on the source it describes. The legacy census test
is updated for the repaired census; two tests that walked all registrations
select them explicitly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci: the committed route census is checked against the source and the running server

Nothing compared the committed route census with the routes the server
registers, which is how one route went missing from the census and a
middleware name drifted without anyone noticing. A new local check,
make check-route-census (also called verbatim by a hosted workflow),
regenerates the census from source with the unchanged extractor and fails
on any difference, naming each changed route and printing the regeneration
command before the diff, then starts the characterization app and runs the
runtime coverage check against it. The check is strict: any change to the
route file requires a regenerated, committed census. Unchanged edge fails
both halves; with the repaired census it passes; a route added without a
census row, a stale middleware name, a runtime-only route and a route
removed but left in the census each fail it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

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

Production's server log group already retains 365 days, but the stack still
declared one month. The next CDK deploy that touched the group would have cut
retention to 30 days and deleted eleven months of request logs. The stack now
declares the retention production has. Deploying it changes nothing.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… the runner (#3329)

Two database tests in post_treevite_waves_tests.rs failed intermittently with
PostgreSQL timeouts, on hosted runners and on a quiet x86 box:

- allocator_lock_and_real_fk_locks_do_not_cycle timed out (57014) in fixture
  DDL, before either lock probe ran: setup used the serving connection with its
  500 ms statement budget.
- concurrent_creates_and_owner_budget_are_serialized raced: the second create
  could reach the owner lock after the first had committed, or time out waiting
  for it, so the test sometimes observed no serialization at all.

Setup and lock probes now use a test-only connection with 30 s / 10 s
statement and lock timeouts; connect() and the real writer pool keep the
serving settings. The concurrent test holds the owner advisory lock in an
observer, sees both creates waiting on it before releasing, and joins both
workers before asserting.

Test-only; no handler, schema or serving change. Under 8-process load plus 8
CPU workers, 600 pairs: concurrent creates passed 348 before and 600 after.
Handler mutations still fail the tests: a real lock cycle or a member-row
FOR UPDATE fails the allocator test; removing the owner lock, or taking it
after the cap check, fails the concurrent test.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…d the route's own deadline (#3331)

next_comment::tests::slower_reads_cannot_occupy_next_statement_slots failed
on a quiet x86 box at 75.9 ms against its 75 ms bound, with an HTTP 200 that
met the route's own deadline: the test compared end-to-end wall time,
including client and scheduling time, against the route deadline.

The test now proves the property directly. While every slower pool is held
full, it asserts each slower pool's slots are all occupied and its own
next-statement pool is untouched, and requires an HTTP 200 (completion
inside the route's deadline, no Node fallback) on at least one of up to four
loaded requests. Slot counts are derived from the pools instead of being
hard-coded. The CPU-probe sampling and the unused libc dev-dependency are
removed. Wall time is printed for diagnosis only.

Test-only; no route, pool, deadline or serving change. Under 8 CPU workers
and 4 concurrent test processes: 200/200 pass; quiet: 10/10. Mutations still
fail it: a 100 ms slowdown in read() fails 10/10 quiet and 20/20 loaded at
the deadline check; one permit budget shared by every pool fails at the
occupancy check.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…instead of sleeping (#3339)

The test for the whole-generations cache expected a refresh after a sleep
and unwrapped the result, so a slow or timed-out database read (a
PostgreSQL statement timeout on the math bundle read, as one hosted run
showed) surfaced as an unwrap on None with no explanation. The test now
times each read with the clock the cache itself uses and asserts that a
cached generation is never served after it was stored plus three seconds
and never replaced before, with no sleep; a read that returns nothing is
retried only when the pool's own counters show a database failure, and
unexpected data fails at once. Lifetimes of 1000, 2800, 3200 and 8000 ms
each fail it; a wrong environment key, a dropped generation without a
database failure and a cache that never reuses fail too. Test module only.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ch's state (#3332)

* polis-api: the startup line names every route it serves and each switch's state

Until now the bind-success event said only the address and the math label, so
nobody could tell from the logs which routes this process serves or which
typed routes are switched on. The event now carries a catalog: every route
family with its method and path patterns, and for each switched route the
switch's environment variable and whether it reads on or off. Only on/off
states are logged, never a value.

The catalog is logging-only metadata beside dispatch; dispatch itself is
byte-identical. Tests keep the two honest: every catalog entry's method and
path must reach its own dispatch family (not the hand-back), every dispatch
key must have a catalog entry whose family is its module, Treevite paths are
derived from the contract's operation table, and every switch name in the
catalog must be one its route module actually reads. Removing an entry,
adding a dispatch arm without one, naming a switch nothing reads, or swapping
two routes' switches each fails a test. The scanner's limits are written at
the top of the test file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* polis-api: the startup catalog tests check each entry's switch and match family names exactly

The switch-to-reader check worked per family, so the two math-membership
entries could swap their switches without a failure; and the reader match
used substrings, so one family name matched inside another. Each entry is
now checked against its own startup state field, names have identifier
boundaries, and a regression covers the comments, moderation-comments and
participant-comments readers. Test module only; the catalog, the line and
dispatch are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* polis-api: the startup catalog scanner checks name boundaries on the original source

The reader scanner checked identifier boundaries on whitespace-stripped
source, so a keyword merged with the name after it and valid reader forms
(if state.x, match m::, &mut state.x, return m::) were rejected, a false
failure only. The structural lookup keeps the compact form; byte offsets let
the exact-name check inspect the original whitespace. Sixteen keyword cases
across spaces, tabs, LF and CRLF; the earlier mutations still fail. Test
module only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ent route owns its pool (#3335)

* polis-api: a test over the real startup wiring proves the next-statement route owns its pool

The next-statement isolation tests exercise a pool they build themselves, so
nothing failed if production wiring handed that route another route's pool,
either where the pools are built or in a handler's choice. Startup
construction (config, the completed App and the refresh task) now lives in
one function that main calls; its statements moved verbatim and eight
startup cases give identical output and exit status before and after.

A new test runs that constructor in a child process against the real
configuration, checks the next-statement pool is a distinct object from
every other configured pool (97 of them) with its exact limits (read-only,
two slots, 1 ms acquire, 50 ms statement, 300 s lifetime), and drives the
real next-statement and comments dispatch to check which pool each used.
Handing the route the common pool, the comments pool or the comments
limits, reassigning a pool later, or making a handler read another route's
pool each fails it. Handlers are unchanged; the participation path gains no
per-request work.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* polis-api: the wiring test derives its pool list from the App, so a new pool must be classified

The startup wiring test listed the configured pools by hand, so a pool added
later went unchecked until someone added it. One macro now derives the pool
references and an exhaustive classification for every App field, with no
rest pattern, nested owners included; an unclassified field fails to
compile, a pool classified as disabled or as sharing the next-statement
pool fails the intended assertion, and the earlier mutations still fail.
Helpers compile only under test with database tests; production source is
unchanged and eight startup cases give byte-identical output.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* ci: local make targets for the five hosted suites that had none

Delphi Python Tests, the polis-cli format, clippy and test job, the
PostgreSQL Delphi writers, the Delphi result reads and the production
importer each get a check-* target and a ci/local entry point. Each runs
the commands its workflow runs, so a Docker host can run these suites
the same way it runs the others.

- check-delphi-python-tests runs the workflow's eight jobs in order. It
  builds the postgres and delphi test images once, runs the six shards
  from those images, then runs the combine's own tests and the combine.
  DELPHI_PYTHON_LEG runs one of those jobs. COMPOSE_PROJECT_NAME names
  the images, the shard projects and the results directory.
- check-polis-cli requires Python 3.12 and runs scripts/test-polis-cli.sh.
- check-dynamo-writers and check-production-import take the project name
  and PostgreSQL port their workflows set, refuse a project that is
  already in use, and run their scripts.
- check-delphi-result-reads requires Node 24.22.1 and runs
  scripts/test-delphi-result-reads.sh --ci.

The hosted workflows are unchanged. docs/local-checks.md lists the new
targets. On an arm64 Docker host each target passed with the same test
counts as its latest hosted run.

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

* ci: tests for the local entry points that fail on their known defects

ci/tests/test_local_entry_points.py runs the real ci/local entry points in a
throwaway Git checkout with stub docker, cargo, node and Python executables
first on PATH and stub suites. No Docker is needed. Against the current
scripts, 21 of its checks fail, one for each known defect:

- The Delphi target's full run can pass on the previous run's results when
  those cannot be replaced.
- A failed image build, a refused or failed shard rerun, a changed source or
  rebuilt images still leave the previous run's results to be combined.
- A shard leg runs with no images from its run.
- Its results directory can be pointed outside .ci-out or through a
  symlinked .ci-out.
- check_project in runtime.sh accepts a project when any of its three
  Docker queries fails.
- The polis-cli target checks python while its launcher runs python3, and it
  passes on an inherited CARGO_TARGET_DIR, as the result reads target does.
- The writers target lets its launcher remove raw containers it did not
  create, accepts an existing cargo volume, never removes the one it
  creates, and writes proof logs outside .ci-out. The import target also
  writes its proof outside .ci-out.

The commits that follow fix each one.

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

* ci: local project checks fail closed, and the CLI target checks python3

check_project in ci/local/runtime.sh tested only the output of its three
Docker queries. When a query failed with no output, the project counted as
free. This came in with the earlier change that added the local entry points
(PR #3324). Each query's exit status is now checked, and a failed query
refuses the project with a message, as an occupied one already did.

check_python now takes the interpreter to check (default python, as before).
The polis-cli target checks python3, which its launcher runs, and unsets an
inherited CARGO_TARGET_DIR so the crates build where the hosted job builds
them.

Two helpers for the entry points that follow:
- check_absent refuses containers or volumes, by exact name, that a launcher
  creates with plain docker run, and fails closed the same way.
- check_out_dir creates .ci-out/<name> and refuses a name or symlink that
  would put results outside .ci-out.

The ProjectCheckTests and PolisCliTests cases in
ci/tests/test_local_entry_points.py now pass.

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

* ci: Delphi target results are bound to their run and never read stale

The Delphi Python tests target recorded each shard's exit status in a file.
Its full run called the shard step where the shell ignores errors, and then
trusted those files. When an earlier run's files could not be replaced, the
target combined the earlier run's results and passed, whatever the current
shards did. A failed image build, a refused or failed shard rerun, a changed
source or rebuilt images also left the previous run's results to be combined.

Each job now runs in its own process with strict error handling, and the
results are bound to the run by receipts in .ci-out/<project>:
- The images job is the start of a run. It first removes every receipt,
  then writes images.ok after the build succeeds. images.ok holds a new run
  id, the source identity (HEAD, uncommitted changes, untracked files) and
  the two image ids.
- A shard job first removes its own receipt, before the project check. It
  then runs only against this run's images and an unchanged source, and
  writes shard-I.ok only after it exits 0. A shard can therefore be retried
  within a run.
- The combine accepts only six shard receipts of this run, source and
  images.
- The full run takes the shard results from the shard processes' own exit
  statuses, runs every shard, and combines only when all six exited 0.
- Receipts are written atomically, and any failure to remove or write one
  fails the job.
- The results directory is always .ci-out/<project>. The DELPHI_PYTHON_OUT
  override is refused.

The DelphiPythonTests cases in ci/tests/test_local_entry_points.py now pass.

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

* ci: writers, result reads and import targets own what they remove and write

The Delphi writers launcher creates its proof, dynamo and build containers
and a cargo registry volume with plain docker run. When it exits it removes
the two containers by name, whoever created them, and it never removes the
volume. Its entry point checked only the Compose project, which those
resources are not part of.

- The writers target now refuses to start while any of those containers or
  the volume already exists. Everything its launcher removes is then its
  own, and the target removes the cargo volume the run created.
- The result reads target refuses the same way for the two containers its
  launcher removes.
- These checks fail closed when Docker cannot list containers or volumes.
- The writers and import proof logs now go to .ci-out/<project>, through a
  directory check that refuses a symlink out of .ci-out.
- The writers and result reads targets unset an inherited CARGO_TARGET_DIR.
  Their launchers run queue-rs/target/debug/polis-migrate, so a shared
  target directory would run an old binary or none.

The launcher scripts are unchanged. All 30 cases in
ci/tests/test_local_entry_points.py now pass. docs/local-checks.md describes
the run receipts, the results directory and the test command.

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

* ci: tests for the remaining identity, run and results-directory defects

Ten new cases in ci/tests/test_local_entry_points.py fail against the
current entry points:

- Identity:
  - The Delphi source identity hashes git's partial output when git cannot
    read a changed tracked file, so it matches the clean source and the
    combine passes.
  - Image inspection that prints plausible ids and then fails is accepted
    by both the images job and a shard.
- Runs: when a new run cannot remove the previous run's receipts, a combine
  run on its own still accepts them, because nothing names the run the
  caller expects. A job run on its own may also omit the run.
- Results directories:
  - A combined directory replaced by a symlink, or a file link inside it,
    makes the combine write outside .ci-out.
  - A link planted in the writers results directory makes the launcher
    overwrite a file outside it.
  - The import target accepts a results directory that already exists.
- Cleanup: when Docker cannot answer during cleanup, the writers target
  reports success while its cargo volume may remain.

The 30 earlier cases still pass.

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

* ci: the Delphi target's source and image identities fail closed

The source identity hashed the combined output of several git commands. When
one of them failed (git diff cannot hash a changed tracked file it cannot
read), the hash covered the partial output and could equal the clean
source's. The comparisons ran inside command substitutions that ignore
errors. Image inspection kept only its formatted output, so a docker call
that printed two ids and then failed was accepted.

- source_id hashes HEAD, the diff against HEAD and the untracked files'
  names and contents separately. Each git command's exit status is checked,
  and nothing is printed unless all of them succeeded. The diff is taken
  with no external diff, textconv or colour.
- image_ids requires docker to succeed and to print exactly two sha256 ids.
- Every use reads these values through helpers that fail the job when they
  cannot be computed, and only then compares them.

Three of the new cases in ci/tests/test_local_entry_points.py now pass: an
unreadable changed source, and image inspection failing after printing ids,
in the images job and in a shard.

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

* ci: Delphi jobs run on their own name the run they belong to

The run id the images job made up proved only that the receipts agreed with
one another. When a new run could not remove the previous run's receipts
(for example, a read-only results directory), its images job failed. A
combine run on its own then still accepted the previous run, because it had
no way to know which run the caller meant.

DELPHI_PYTHON_RUN now names the run: a value the caller chooses, new for
every run.
- Each job run on its own (DELPHI_PYTHON_LEG set) requires it.
- The images job records it in images.ok, and shards record it in their
  receipts.
- Shards and the combine refuse an images.ok of any other run, and the
  combine refuses shard receipts of any other run.
- The full run uses the given value or makes a new one, prints it, and
  passes it to its jobs.
- Receipts the new run could not remove still name the earlier run, so
  they are refused.

Two more of the new cases in ci/tests/test_local_entry_points.py now pass: a
combine asked for a new run refuses the previous run's surviving receipts,
and a job on its own without a run is refused.

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

* ci: no write under a local results directory can follow a planted link

The entry points checked only .ci-out and the project directory under it.
A link already waiting further down was followed: a rust.log link in the
writers results directory made the launcher's tee overwrite a file outside
it, and a combined directory or file link in the Delphi results directory
made the combine write outside .ci-out.

- check_out_dir takes "fresh": the directory must not exist yet and is
  created empty, so nothing can be waiting in it. The writers and import
  targets use it for their proof logs. Their launchers already refuse to
  reuse a project's proof, and a rerun now needs a new project or the old
  directory removed. Every step in check_out_dir is checked explicitly,
  since it runs inside a command substitution.
- The Delphi combine removes its combined directory before each combine,
  and fails if it cannot.
- Receipts are written to a file mktemp creates, then renamed into place.
  A link at a receipt's name is replaced, not followed.
- Shard directories were already replaced by the shard script.

Four more of the new cases in ci/tests/test_local_entry_points.py now pass:
a planted combined directory, a planted file link inside it, and existing
writers and import results directories.

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

* ci: the writers target fails when it cannot confirm its cargo volume is gone

The writers entry point removes the cargo registry volume its run created.
It treated any failed docker volume inspect as "no such volume". So when
Docker could not answer during cleanup, the target passed and the volume
could remain unreported.

The cleanup now lists volumes and checks that Docker answered. It removes
the volume when the listing shows it, and fails the run when the listing or
the removal fails, saying the volume may remain.

The last new case in ci/tests/test_local_entry_points.py now passes: all 40
pass.

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

* docs: local checks name the run, the fresh results and what stays outside

docs/local-checks.md now describes:
- DELPHI_PYTHON_RUN, which a Delphi job run on its own requires, and how
  the combine refuses receipts of any other run.
- The new, empty results directories the writers and import targets
  require.
- The cleanup check on the writers target's cargo volume.
- That these ownership checks belong to the make targets, not to the
  launcher scripts called directly.
- What the suites behind these targets still write outside .ci-out: crate
  targets, server/node_modules, the importer's launcher-mount test resources
  and host caches.
- That no hosted job runs the entry-point tests yet.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… and refuses ephemeral ports (#3338)

On a macOS work box the ephemeral port counter (49152-65535) is shared by
the whole machine and IPv6 sockets are given ports already held on IPv4.
The compose PostgreSQL port is published on IPv4 only, and a localhost
address tries [::1] first, so a test suite's own listener could take the
PostgreSQL port mid-run ("Connection terminated unexpectedly") or a pg
client could connect to itself and wait forever. Three intermittent
integration failures matched the counter passing the port to the second.

The harness now uses 127.0.0.1 in both default database URLs, and refuses
before any container starts when the PostgreSQL port or any published
compose port is 49152 or above, dynamic, or crosses that range, saying why.
The three clients in the Treevite accept-lock test get a five-second
connect timeout so a wrong connection reports instead of hanging. Test
logic, deadlines and lock budgets are unchanged; hosted CI, on Linux with
port 5432, is unaffected. With a competing dual-stack listener held for the
whole run, unchanged edge fails at migration and the fixed harness passes
(2429 tests).

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

* recordings: cases name their route by method and path, and the census regains two routes

The identity, Treevite and POST-comments recorders matched each case's route
hits by the route's global registration ordinal, so mounting one more
Express app before the routes shifted every id and made live Node disagree
with every committed fixture. The recorders now discover the mounted apps
and resolve a hit by its method and path, so a new app leaves the fixtures
untouched; the re-recorded fixtures differ from the old ones only in route
identity (every old id was one too high), with the comparators, expected
differences, cases and policies byte-identical.

The route census regains GET /api/v3/version, which was registered but
missing, and names the participation route's middleware as registered; the
census check now passes on the source it describes. The legacy census test
is updated for the repaired census; two tests that walked all registrations
select them explicitly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* clients report what paints red in the browser console to one server route

Until now no Polis client reported errors anywhere and no server route
accepted such reports, so a broken response or a broken page was visible
only in a participant's browser console. Every page of the admin console,
the report, the legacy participation client (including vis2 and the embed)
and the alpha participation client now carries a dependency-free reporter
that catches uncaught errors and unhandled rejections, script, style, image
and font load failures, console.error and console.warn, failed fetch and
XMLHttpRequest calls (status 0 or 400 and above; 304 is success) and CSP
violations. Each distinct position is its own report, carrying a stable
signature (client, kind, status or message category, and the location with
the bundle's content hash removed) and a count; repeats batch after one
second or on page hide; a page sends at most 50 signatures, 20 sends a
minute and 200 per tab session, by sendBeacon or a credential-less keepalive
fetch to the client's own origin only.

The server gains POST /api/v3/clientErrors, handled before the body parser
and authentication. It reads no cookie, identity or database, touches no
pool or lock, caps the body, admits a report only when both a per-address
limit (burst 10, then 1 a minute; IPv4 address or IPv6 /64, taken from the
X-Forwarded-For entry before a recognized Cloudflare edge) and a per-process
limit (burst 100, then 1 a second) admit, always answers 204, and writes one
structured warn line, client_error, with no address in it. The participation
path shares nothing with it.

Triage is deliberate: a Rust binary, render-client-errors, turns an exported
Logs Insights result into a table of signatures with their message, count
and pages; precise rows by default, an opt-in cross-build grouping. Nothing
feeds an automated fixer.

The route census gains the new route; the recorders already name routes by
method and path, so no fixture changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

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

* ci: every nginx upstream must be a container the production web role runs

The 2.43.0 deploy answered 502 for every static asset at the origin because
nginx sent those paths to a file-server container that production web boxes
never start; the hotfix sent them back through Node. Nothing would have
caught the same mistake again.

A new check, `make check-nginx-upstreams` (also called verbatim by a hosted
workflow), parses every upstream host from the nginx templates and the
rendered Rust route locations, reads the services the web role starts from
the deploy hook and the Compose file, and fails naming the upstream, the
config file and the hook line when an upstream names a service the web role
does not run. An empty, commented exception list is the only way around it.
The 2.43.0 shape fails it; edge passes. The hook reader joins continued
lines, tokenizes only the container-start lines, finishes one-line helpers,
and names a line in every error. The README states what it does not read:
includes of files that are not .conf or .template, and shell constructs it
cannot follow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci: the nginx upstream check's hook reader follows compose commands, continued lines and one-line helpers

The reader matched compose only as a bare docker-compose or docker compose
followed by config, so the hook's own /usr/local/bin/docker-compose start
was neither refused nor counted, while plugin-style forms (a COMPOSE array,
a hook moved to docker compose, command -v) were refused. It now matches the
executable's basename, skips the global options before the subcommand, and
accepts config or no subcommand. It also stops a comment's trailing
backslash from swallowing the next line, closes one-line helper functions
(including those ending in "& }"), refuses direct docker compose starts,
reviews docker by absolute path, parses spaced and function-keyword
declarations, and names the line in every error, including the ROLE_IMAGES
one. Six fixture groups cover these; the 2.43.0 shape still fails naming
file-server and its template.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

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 5, 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 7 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 7365 71%

@colinmegill
colinmegill marked this pull request as ready for review November 5, 2026 00:33
@colinmegill
colinmegill merged commit e62b94c into stable Nov 5, 2026
100 of 104 checks passed

This branch was successfully deployed

1 active deployment
release — c3dadc86 Deployed Nov 5, 2026 by colinmegill via publish #2
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