Theatre Register

In flight

What is being built right now and is therefore not yet true. Every entry names what exists on disk, what does not, and how to tell when it has landed.

Written 2026-08-16 and rewritten twice the same afternoon, because seven agents were building at once and the route count went from 14 to 115 while this page was open. That is the argument for tools/doccheck.py in one sentence: nobody can hold a system this size in their head for the length of time it takes to write about it.

Open: the page log is on Postgres but unverified against one

The Postgres migration moved three writable stores and left the page log behind with CADB_NO_PAGELOG set, because several analytics queries selected a bare column beside an aggregate — legal in SQLite, rejected by Postgres. That was the right conservative call.

It has since moved. web/observe.py connects through web/dbkit.py like the other three, and four faults were fixed on the way. One was a real bug rather than a dialect wart: COALESCE(m.state, "open")double quotes are an identifier in Postgres, so that filter was reading a column that does not exist.

CADB_NO_PAGELOG should stay set anyway, until somebody runs tools/test-postgres.py against a real DATABASE_URL. Nobody has. "Correct on Postgres" is currently inference from reading web/dbkit.py, not observation — which is decisions.md §15 applied to the thing that was written to satisfy §15.

findings/postgres.md §7 says the page log did not move, and explains why. That section is now out of date.

tools/check-observe-port.py guards it: all 21 readers over a fixed dataset, diffed against a stored fingerprint, so a query change that moves any number fails with a diff naming it. It is in tools/test-postgres.py's SUITES and it asks a different question from the rest of that file — those ask whether the code runs on Postgres, this asks whether it returns the same numbers.

Open: search results give the same evidence on nearly every row

/shows?q=sondheim returns 19 rows. 17 of them print the identical evidence line, "In the credits: Stephen Sondheim." Measured 17 August 2026.

An evidence line exists to tell a reader why this row is here and therefore which of two similar rows to click. When it says the same thing on every row it has stopped answering that and become decoration that costs a line of vertical space each time. It is the same shape as the earlier problem where every row reported matching on "title, the synopsis" — accurate on each row, useless across the set.

The fix is specified in findings/search-results.md §7 item 8 and is unbuilt: compute the evidence line for every row on the page, and where one value covers most of them, say it once at the top instead of on each row. That is the only open item on the search pages; the index-pollution bug that sat beside it in the notes is fixed and rebuilt.

Written but not connected

Plates. Removed 2026-08-16 on the owner's instruction. The composed-plate idea is dead; do not revive it. Shows with no picture keep the labelled stand-in in related_image().

Cropping. web/cropping.py is now imported by web/app.py, so nothing here is orphaned and orphans passes for the first time since it was written.

The check reported both of these while they were unwired. A module nobody imports is the quietest kind of unfinished work: it is written, it is committed, it passes every test anyone runs on it, and it does nothing.

Landed since this page was first written

All of these were "no routes, no module found" a few hours ago. They are in docs/routes.md now, documented from the live URL map.

AreaWhere it is
The identification search/find, web/search.py, web/templates/find.html
Accounts, SSO, sessions/account/*, /lists, /favourites, web/accounts.py
The public API36 rules under /api/, web/api.py
Contributor licence and experience/contribute, /licence, /moderate, /changes, web/contribute.py
Sharing and feedsweb/sharing.py for meta tags, /feeds and four Atom routes
Admin and observability/admin/*, web/adminviews.py, web/adminauth.py, web/observe.py
The newsletter/newsletter/*, web/newsletter.py
Privacy, cookies, terms/privacy, /cookies, /terms
Filter and sort on listsweb/listing.py, now imported by web/app.py

The LATCH reversal is recorded, not silent. brief.md struck through "LATCH controls wherever a list appears" on 2026-08-13 and corrected it to "LATCH is how we decide what order a page is in, not a control we hand the reader". The owner overrode that on 2026-08-16 — "we need filter and sort options when look at lists" — and flows.md §LATCH carries the override at the top of the section with the original correction kept beneath it, because its reasoning still governs which controls exist rather than whether they exist. Two measurements survive and constrain the build: era is a pure function of year, so era and decade are one axis with two names; and type is a filter, never a sort.

Two admins, now divided by job

tools/admin/app.py is a stdlib HTTP server on port 8010 with its own store at contrib/edits.sqlite3, built on 14 August, described in docs/contributing.md. web/adminviews.py is a Flask blueprint mounted on the main app at /admin. Both review proposals.

The ambiguity is not theoretical. The blueprint was originally called admin.py, and because web/ sits first on sys.path, it shadowed the tools/admin/ package that emit.py, overlay.py, ship.py and inbound.py all import as from admin import store. The outbound path to broadway-data broke the moment anything imported the web app first. The contribute agent found it and it was renamed to adminviews on 2026-08-16.

Settled 2026-08-16, and the split is by job rather than by system. Moderating — approving, rejecting, hiding a comment, suspending an account — is authoritative at /admin. Authoring and shipping — the propose form, the lead-image picker, the changeset bands, inbound diffing, bundle export — is still only on port 8010.

There is one queue, viewed two ways: both read and write the same contrib/edits.sqlite3 through the same tools/admin/store.py, so a decision made in either place is the same row.

Port 8010 is the outstanding item. It has no session, no second factor and no CSRF, which was right for a queue one person worked on a laptop and is wrong for anything reachable from the internet. It should be retired once the authoring screens have homes. Until then: run it when you need them, and do not expose it. docs/routes.md carries the full division.

Thirteen check suites, and now one command

.venv/bin/python tools/checkall.py            # everything
.venv/bin/python tools/checkall.py --quiet    # failures and the total
.venv/bin/python tools/checkall.py doccheck   # substring filter

Thirteen independent suites, each written by whoever owned the thing it guards. tools/checkall.py finds them by glob rather than by list, so a new one is picked up without anyone remembering to register it. It is deliberately unowned — its own docstring says to rename it, move it into a Makefile or fold it into CI.

It does not require an exit convention, because it cannot yet. Most suites exit 0 whether they passed or not, so exit status alone cannot be trusted; it judges on the exit code and the printed summary. Either alone is wrong — a suite crashing on import prints no summary, and a suite printing "3 failed" while exiting 0 must not read as passing. If the suites later agree to exit non-zero on failure, this keeps working and the summary match becomes belt-and-braces.

Anything it cannot parse counts against the run, reported as UNPARSED and never waved through, because the failure it exists to catch is a suite quietly ceasing to be run. Silence is not success.

It found a third outcome nobody had modelled. check-derived.py prints "nothing to check" when the build artefacts are absent, which is the ordinary state on a laptop — forcing that into pass or fail would either hide it or cry wolf, so it is labelled skip and counted apart. On its first run it also surfaced a doccheck failure nobody was watching.

A runner is the second line of defence, not the first. tools/check-blur.py runs as step 2 of deploy/build.sh, ahead of the derivatives, because the leak it guards is in that script's own pipeline: an unblurred original would be built, uploaded to the bucket, and cached by Cloudflare for a year. Wiring a check to the thing it actually protects beats collecting it into a list. And documenting a gate is neither.

Do not assert a test count from outside. A check that fires when a suite grows fires on good news, and teaches whoever hits it to update the number without reading why it moved.

The suites stay separate files on purpose. tools/doccheck.py is about documentation disagreeing with code; tools/check-blur.py is about an asset disagreeing with its licence. Collapsing them would make failures harder to read and each check harder to explain, which is the opposite of the point.

For the owner: mint real identifiers, and do it soon

This is the one item on this page that is a decision rather than a status.

ids.md specifies minted, permanent, never-reused identifiers, and the rules are honoured for 237 album ids and broken for 21,672 others. Works, productions, people and venues are slugs derived from titles. 122 person ids carry a positional disambiguation suffix like adolph-deutsch-2, and broadway-data ships a merge-duplicate-people.ts that will move them.

The work is a change across broadway-data, tools/build-db.py, and a slug-to-id redirect layer whose behaviour ids.md §Slugs already specifies.

The argument for doing it now is cost, and it only goes one way. The expense of unstable identifiers scales with how many people have stored one. That number was zero until 2026-08-16, when a public, keyless API went live over those exact ids. It will not be zero for long, and ids.md exists because castalbumdb.com's maintainer watched this happen and could not undo it.

The API is honest about it in the meantime — meta.identifiersStable: false on every list response, an identifiers block at /api/v1, and a stability notice on the reference page. Honesty is not a fix.

Documentation that describes something that is not built

ids.md describes an identifier scheme that exists for albums only. Every other id in the database is a broadway-data slug. docs/model.md has the detail. This mattered less when nothing external consumed our identifiers; the API now does, so /api/v1/works/<wid> takes the-black-crook rather than the W-0001A the document specifies. ids.md also contradicts itself on identifier length, which tools/mint-albums.py noticed and resolved and nobody wrote back.

START-HERE.md points at a file called docs/CONTRIBUTION-PATH.md, which has never existed on disk. The contribution path is documented at docs/contributing.md instead, and START-HERE.md should be corrected to point there.

schema.md models Recording, Song, Edition and Discrepancy. None of the four is a table. Discrepancy is the significant absence, because decisions.md §10 requires discrepancies as first-class, sourced, queryable records, and what exists is a set of one-off checks in web/app.py that each write their own sentence. See docs/model.md.

tools/check.mjs checks design/site/, the 7,332 static HTML files the old generator produced. The served site is web/app.py and those files are no longer the product. The check still passes, against the wrong thing.

Known data faults, reported and not fixed

These are in broadway-data and are worked around here rather than corrected at source, because the source is read-only.

  • Venue ids and production theatre references live in different id spaces. 11,463 productions reference a theatre by IBDB numeric id while venues.json is keyed by slug, so 87% of productions cannot reach a venue. tools/build-db.py joins 5,758 of them by name. broadway-data issue #95.
  • Seven London venue records were written over New York theatres. Adelphi, Apollo, Lyric, Palace, Lyceum, Garrick, Playhouse, Phoenix — every one of those names exists on both Broadway and Shaftesbury Avenue. On 15 August seven were recategorised west-end rather than corrected, putting 262 Manhattan productions in London. web/app.py:wrong_place() tests the address, because the category and the coordinates are both wrong. broadway-data issue #90.
  • The people table contains rows that are not people. 1994, 510-performances and 534-p all hold person records.
  • West End production ids are malformed. 02-has-exceeded-29-500-performances-at-the-st-martin-s-2024-original-westend is an id. The West End importer was open at the time of writing.
  • 220,345 cast identifiers are recorded and 25,424 resolve to a person — 11.5%. The rest are rendered from the identifier itself and flagged known: False, because dropping them would silently shrink every cast list on the site. That rendering is right for 87% of the ones that can be checked; the misses are punctuation, so Adam Chanler-Berat comes back as Adam Chanler Berat.