Repair the published docs site, and make its drift fail the build - #632
Merged
Conversation
The home page said seventeen bounded contexts while the architecture page one click away generated eighteen from the code model, and the model page promised two reserved BCs above a table showing one. Both numbers were hand-typed, and both went stale the day budget shipped its Allocation aggregate. Neither is hand-typed now. arch:count grows a planned-bc kind that reads len(_PLANNED_ROWS), so the reserved-BC clause tracks the table it introduces, and the home page carries the same kind=bc marker the model page already used. That marker only expands on pages the hook reaches, and the hook was gated on the architecture/ prefix alone, so a marker on the home page would have rendered its stale checked-in body forever while looking generated. The gate now admits index.md, and the marker walk covers it, so the guard and the gate cannot drift apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Model page lists budget/allocation as Active, and that page's status legend says Active means the aggregate ships and is listed under Modules. It was not: /architecture/modules/budget/ returned 404, with no nav entry and no card, for the four weeks since the Allocation aggregate shipped. Every other of the eighteen BCs had a page, so the one BC a reader could not follow was the one governing what the deployment is allowed to spend. The page is written from the aggregate's own docstring, which already held the FSM, the five slices, and the rule that balance is never stored but folds from the inference ledger. Its slices table is the generated marker, so the surface cannot drift from the routes. A fitness test now asserts the three things that must move together for a BC to be readable: the page exists, the nav points at it, and the landing grid cards it. Nothing checked any of them before, which is why a promise the site makes about itself could go unmet in silence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
get_fixture_pidinst registers GET /fixtures/{fixture_id}/pidinst and an
MCP tool of the same name, but it carries neither command.py nor
query.py, and the introspector required one of the two. So a shipped
public surface rendered nowhere on the equipment page, and the prose
went on describing get_asset_pidinst as the only way to reach the
PIDINST serializer.
The failure was silent, which is the part worth fixing: a slice that
falls through the discovery rule produces no build error, just a table
one row short. Discovery now also accepts route.py plus handler.py, the
shape a read slice too thin for a query takes.
This is the only slice in the tree the old rule dropped, verified by
counting both rules over every BC: 294 before, 295 after, the single
difference being equipment/get_fixture_pidinst. The pinned equipment
slice count moves with it in the same commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each module page's Examples section carries an HTML comment claiming the
block was extracted from the contract tests. It was not, and sixteen of
them had drifted far enough that a reader copying one got a 422 or a 404.
Three documented endpoints did not exist in any form: /procedures/{id}/steps
is /activities, /runs/{id}/readings is /observations, and /plans/{id}/wires
is /add-wire with a sibling /remove-wire rather than a DELETE. The rest were
field-level: decision sent actor_id where the request takes decided_by,
federation sent scope_set for scopes, caution sent {target_kind, asset_id}
for the {kind, id} discriminator, safety sent id for an External binding's
value, campaign sent id for an external ref's value, two batch endpoints
were documented as a single flat entry rather than an entries list, methods
omitted the required execution_pattern, and equipment and recipe used
affordance names (Move.Continuous, captures_images) that are not members of
the thirty-value enum the site's own reference page publishes. Federation
told the caller to send signed_at, which the handler stamps from the clock
port and the model rejects.
Every body is now validated against the JSON Schema FastAPI publishes for
that route, and a test does the same on every run so the examples cannot
drift from the surface again. Verified the guard by breaking one example
deliberately: it fails on a defect the API would reject, and stays quiet
for an extra field on a model that permits extras, which is the API's own
semantics rather than a gap in the check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two whole aggregates were publishing endpoints with no explanation anywhere on their page. Trust's opening sentence generated "six aggregates" from the code model and then named five: Ratification, the co-signature gate, appeared nowhere in the docs despite three live endpoints and its own enforcer agent. Agent had the same hole for LanguageModel, whose five slices rendered in the generated slices table while every hand-written section covered only the Agent aggregate. Eighteen shipped events were missing from seven Events tables, some for two months. Those tables are the only per-event reference on the site, so an integrator reading the log met types the docs did not list. The count came from diffing the introspected model against each page rather than from the audit's estimate, which was high by two. The agent fleet was stated three ways, none of them right: five on the modules card and in decision's prose, nine on the agent page, thirteen seeded in the composition root. It reads thirteen now, with the four undocumented agents added and the two that register unconditionally called out, because a kill switch and a consequence gate that an operator can forget to switch on are not what they claim to be. Also: agent budget caps are enforced, not declarative, and the page said the opposite of a safety-relevant truth. Run advertised response bodies it does not return. Equipment pointed at two serializer files that moved into a package. Standards listed Supply as planned and DSSE PAE as pending, both shipped. Federation's card rendered a raw icon shortcode, the only unresolved one on the site. Every module page linked to a "Reading the examples" section that did not exist, and the landing page sent readers to an Idempotency column no slices table has. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fifty-six model pages ended by promising an explanation: a runbook and live experiment view would be invention, "see the note on the index". The note was deleted in July when the index pages became generated, and the sentence kept pointing at an anchor no page has carried since. It was the most repeated broken link on the site. The generator emits the section again rather than editing 56 files, keyed on maturity so every non-pilot beamline gets it and 2-BM, which really does have both pages, does not. Thirty-five Open questions pages sent the reader to a link labelled Inventory. The stages migration dissolved that page everywhere except 2-BM, and the href was repointed while the label was not, so the link resolved cleanly to a page holding no device tables at all. Nothing flagged it: not a 404, not a missing anchor, so both the build and a link checker passed a link that lied about its destination. They name the device pages now. 2-BM's real Inventory link is untouched. The last five were renamed headings the inbound links never followed: two on the 2-BM Controls page, three on ID32, and one pointing at a 2-BM beam-alignment section that was never written, now aimed at the Beam mode section where the blocker it means is actually tracked. The build reports zero missing anchors, down from sixty-one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 2-BM pages published physical values with nothing behind them. Nine Hexapod performance figures appeared in one prose table and nowhere else in the repo, though git shows they once carried a datasheet attribution and a caveat that they are dominant-axis envelope figures rather than per-DoF measurements. Both were dropped when the zone pages were consolidated. The values move into the descriptor with that attribution restored verbatim, matching how the Rotary entry beside them already cites its own datasheet. The three objective focal lengths get the opposite treatment, because nothing can source them: they appear only in CORA's own scenario, which requires the field, and the descriptor, the catalog row and the staff pages are all silent. They are withdrawn from the published tables and asked as DET-14 rather than left reading as recorded fact. The filter-foil table published its Position values in the Slot index column, so it told an operator to command index 106. Both paddles are now indexed 0 to 4, and the operational upstream m17 paddle is published for the first time, having existed only in the executable model. MODE-3 was the sharpest correction. 2bm-docs#171 did close as completed on 2026-06-16, but not by fixing the substitution: upstream took Path C and deleted the 2bma:table1 record, so its composite axes no longer exist in Channel Access. CORA still models MirrorTable as a virtual_pose asset over that record. The pages now say what the tracker actually contains and ask the question that follows, which is whether to re-model onto the raw motors, not when the fix will land. Shutter PV strings are unchanged, as intended. The descriptor notes and the recipe step tables now say what the bare prefixes are: device-level names whose concrete record leaf is still unconfirmed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
These three sections state CORA's own rules and picks, so drift here is expensive: a reader follows them into the code and finds something else. The catalog hub called the Role registry closed at five and named five, while the generated Roles page beside it listed six. Shutter graduated on exactly the rule-of-three trigger that paragraph describes, three Methods needing to command a shutter as a bound contract, and the page still filed shutters under the deferred Conditioner cluster it had left. Two columns the descriptor validates were also being dropped on the way to the page: executor_shapes, which the hub elevates to a governance rule, and the per-method note that carries the Role-versus-Family binding rationale. The stack pages under-described what ships. The ControlPort speaks Tango as well as EPICS, with a production adapter and its own extra, and a Tango-floor reader would have concluded otherwise. globus-sdk, h5py and botorch back three more ports and appeared on no stack page at all. Two rows read Deferred for things that shipped: the container image, which three other pages already document, and the boot-time schema-version assertion, which build_kernel performs before anything that can write exists. Cedar was presented as the Decision BC's policy language; there is no Cedar dependency and no policy to author, only a borrowed noun. The reference pages had four wrong facts and two missing children. conventions.md named capability.settings_schema as the settings declarer, which resolves to a real aggregate in the wrong BC; it is Family. It also guaranteed that exactly one slice is exempt from the subject guard, where three are. layout.md published four of the eight Port carve-outs and pointed at a registry that has doubled. patterns.md cited a slice renamed away from its re- prefix. The Reference index listed eight of its ten pages, omitting Affordances, which the Catalog links into, and Naming, which is the rule set reviewers are sent to. Counts that had drifted are refreshed, and the one I could not derive cleanly is gone rather than replaced with another guess. The docs version pin for mkdocs-material is likewise removed rather than corrected: it already disagreed with the two places that install it, and a third copy is a third thing to drift. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d not Four things a reader needs existed only in files the site never rendered. SECURITY.md carries the disclosure channel and the sharpest statement of the X-Principal-Id trust contract; the README carries the only copy of the commands that install and start CORA; CITATION.cff carries the citation metadata. All three are now staged or generated rather than retyped, so the site cannot disagree with them: run.md is sliced from the README's own sections and fails the build loudly if either heading is renamed, and the cite page is rendered from CITATION.cff at build time. Eighty Open questions pages asked beamline staff to confirm facts and told them nothing about how to answer or what Blocks-go-live means. Only 2-BM did, and it keeps its own richer version. The rest get a shared footer appended by the hook, so no hand-authored file changed and a new deployment inherits it. The footer carries no anchors: on 83 pages, one bad fragment is 83 bad fragments. Two pages were reachable only by search. The projects memo is internal working material that addresses the maintainer directly and cites private memory filenames; it is excluded. The module-page template moved out of docs/ entirely, because not_in_nav only hides a page from the nav while leaving it built, sitemapped and indexed, which is how a placeholder skeleton became publicly searchable. The scrubber demo's 29 KB script and its data file stop shipping on all 802 pages. The footer names the license. It links nothing else: mkdocs does not depth-rewrite copyright links, so the relative one I tried first resolved against the current page and 404d everywhere except the root. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every fix in this branch was findable only by reading the rendered site. Nothing in CI could have caught any of it, and that is the part worth changing. mkdocs 1.6 ships a validation config this project never set. Turning on links.anchors escalates a dead fragment from INFO, which --strict ignores, to WARNING, which it fails on; that class alone accounted for 61 of the audit's findings. nav.omitted_files does the same for a page that builds and enters the sitemap and the search index while no nav entry reaches it, which is how an internal memo became publicly searchable. Both are clean now only because the earlier commits cleared them, and both were verified by breaking the build on purpose and watching it fail: a planted anchor and a planted orphan page each aborted the run. The docs workflow only ran on push to main, so a broken build was discovered after it shipped. It now builds on pull requests too. The deploy job gets `if: github.event_name != 'pull_request'` in the same change, because it has `needs: build` and no other condition, so adding the trigger alone would have published every PR's build to the live site. The talks decks and the artifact upload are skipped on PRs: nothing deploys from there, and a chromium download per deck is a lot to pay for a docs check. The no-em-dash rule covered tracked Python and not the pages that face readers, which is where the rule is actually about. Sixty-six em dashes across three published files were table cells meaning "not applicable" and now say n/a. The markdown check ships without an allowlist: the Python side has one because the rule was retrofitted onto a large corpus, whereas markdown starts clean, so a future hit is new and should be fixed rather than recorded. Its helper is git-aware and keeps the GIT_DIR strip that pre-commit needs, which the migration-file sibling omits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
While this branch was in flight, #626 pointed the 2-BM shutter PVs at the records that exist: the descriptor now carries S02BM-PSS:FES:BeamBlockingM and the SBS twin rather than the bare prefixes. The earlier commit here added a caveat built on the opposite premise, that the pv is a record prefix and not a readable channel. The rebase merged without a conflict and left that text sitting two lines under a pv it now contradicts, which is the failure mode a clean merge is worst at showing. The caveat now describes the full record and its inverted read, and keeps the part that is still true: the leaf name is CORA-proposed and awaits APS PSS sign-off. The recipe step tables say the same, so their open and closed values read as the operator's words for == 0 and == 1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A verified sweep of the 801-page published site produced 125 findings. This fixes them, and adds the guards that would have caught them.
The pattern
Generated content was correct; the hand-typed content beside it had drifted. Budget shipped as the 18th BC on 2026-07-13 and the fallout was visible five ways, including a live 404 from a table row whose own legend promised it was navigable. Nothing in CI could catch any of it: the 61 dead-anchor messages were INFO, which
--strictignores, and no test asserted that a shipping BC has a page.Commits
e8ae45c8ebarch:count kind=planned-bcand widens the hook gate so a home-page marker actually expandse2b03a0ae2f8d9a7882e_slices()discoversget_fixture_pidinst, a shipped REST+MCP surface that rendered nowhere. Verified it admits exactly one slice: 294 to 295f05885b51157ea3a977561ecce5df9b0584c62dfd53b259175Family.settings_schema79481287b96eafe60257validation.links.anchors,nav.omitted_files, docs build on PRs, em-dash rule extended to published markdownc8241b13a8Verification
make docs-buildclean with both validation keys on, 0 warnings.pytest tests/unit tests/architecture: 43,249 passed, 622 skipped.Worth your attention
A real finding about the beamline, not the docs.
2bm-docs#171closed as completed by taking Path C:2bma:table1was deleted, not repaired, so its composite axes no longer exist in Channel Access. The pilot descriptor still modelsMirrorTableas avirtual_poseasset over that record. Documented and raised as MODE-3; not re-modelled, since that is a design decision with test implications.Three items are noted rather than fixed, by explicit decision: the MirrorTable model above, the objective focal lengths that survive in the scenario fixture, and the three coexisting shutter PV forms.
Three findings were dropped with reasons: an MCTOptics claim the repo cannot verify, the em-dash and emoji findings (refuted, they were table-cell placeholders and a text dingbat), and un-hiding the blog.
An adversarial verification pass over the riskiest commit found eight defects in my own work before it was committed, including a safety claim that was wrong in the unsafe direction.
🤖 Generated with Claude Code