A hand-rendered artifact may not carry a fact that moves
Affects: docs/architecture.mmd, scripts/check-diagram-current.mjs, scripts/publish-diagram.mjs, public/architecture.mmd, public/architecture.png, app/engineering/page.tsx, package.json
What went wrong
Two defects, and the second one is the interesting one.
The first is familiar. docs/architecture.mmd is 250 lines describing the request path from a browser through to Postgres — every middleware in the order it runs, both pricing engines and why they cannot import from each other, the row-level-security boundary, the SECURITY DEFINER function that resolves an inbound Stripe delivery to a tenant. docs/architecture.png is what render-diagram.command produces from it. Neither is served. Next.js publishes public/, and docs/ is a directory in a repository that returns 404 to anyone who is not signed in. /engineering has been arguing from that diagram since August 2026 and no reader could open it.
That is the fifth time: the OpenAPI document (D-069), the migration SQL (D-070), the API reference (D-071), the coverage run (D-086), and now the diagram. Each existed, each was generated rather than typed, each was correct, and each was unfetchable. The rule this repository keeps re-learning is three words long. Publish the bytes.
The second defect is worse, because publishing the file as it stood would have published something false. Three nodes carried their own readiness on their faces:
stripe["Stripe … awaiting STRIPE_SECRET_KEY"]
quickbooks["QuickBooks Online … awaiting tenant consent"]
salesforce["Salesforce … awaiting tenant consent"]All three were drawn dashed amber under a class named unbuilt. Every one of those labels was wrong at the moment it was checked. GET /api/connectors reported stripe=platform_account_live — it had been finalising real test-mode invoices for a day, and /integrations server-renders one. The other two reported operator_credentials_missing, which is a further-back state than the consent screen the label described: no tenant can reach a consent screen because the operator has not supplied client credentials.
check:diagram passed throughout. It compares the byte count of the source against a stamp written by the renderer, which catches an image that has fallen behind its source and cannot catch a source that has fallen behind reality.
The decision
The labels are not corrected. They are removed, and the class of claim is banned from the artifact.
lib/connector-readiness.ts already states the rule about itself: whether a connector can transact "differs per connector and changes with a deployment's environment, which is exactly the shape of fact that must not be typed into a page." That reasoning applies to a diagram with more force, not less. A page is re-rendered on every request from a live reading. This diagram is a PNG produced by a script that drives Chrome on one particular Mac, because mermaid measures every label with getBBox and there is no Chromium for the arm64 Linux this repository builds on. A fact that changes when somebody sets an environment variable in Vercel cannot live in an artifact whose correction requires physical access to a laptop.
So the provider nodes now state only what is structurally true and will stay true: which trigger fires the call, whether the credential is platform-wide or per-tenant, and which object the adapter writes. One annotation node points at GET /api/connectors and /integrations — readiness appears as an address rather than as a value.
The unbuilt class is renamed thirdparty and recoloured from amber to teal. The old name asserted a fact about our code — that these paths did not exist — which was never true of any of the three and had become indefensible about Stripe. Dashed now means "the far side of this edge is not ours", which is a property of the node rather than a status of it.
The guard
check:diagram grows two assertions beyond the byte comparison:
- 1.No node label may contain status vocabulary —
awaiting,not built,unbuilt,coming soon,not yet,currently dark,no code,planned,todo. Comments are exempt, because the diagram's header has to be free to quote the labels it removed; thereadinessnode is exempt because its entire content is a forwarding address. - 2.
public/architecture.mmdandpublic/architecture.pngmust match theirdocs/originals byte for byte.
The first rule is blunt on purpose. It cannot tell a correct status from an incorrect one — nothing available at build time can, and giving this script network access to find out would make the build depend on a third party being up. What it can tell is that a status is being asserted in the wrong artifact, which is the defect that occurred and the one that will recur.
The alternative that was rejected
Keeping the readiness labels and having the check verify them against GET /api/connectors at build time. That fails on its own terms: it makes every build depend on the API being reachable, it turns a provider outage into a red pipeline, and it still leaves the published PNG wrong for however long it takes somebody to re-render it on the Mac. The correct number in an artifact that cannot be updated quickly is a correct number with a fuse on it.
Also rejected: rendering the mermaid in the browser from the published source, via the mermaid CDN build. It would remove the manual render step entirely, which is genuinely attractive. It would also put the architecture diagram behind JavaScript, and every other piece of evidence on /engineering is in the pre-script markup on purpose.
Why both the source and the render are published
The PNG is what a person looks at. The mermaid source is what a person checks — it diffs, and it pastes into mermaid.live without going through us. Publishing only the image would republish the original problem one level down: an artifact you can see and cannot verify.
That reasoning only holds if the source is actually readable when fetched, and on the first deployment it was not. .mmd has no registered media type of its own, and the platform's table resolves the extension to application/vnd.chipnuts.karaoke-mmd — a genuine IANA registration for an unrelated karaoke format. A browser given that type downloads the file, and the global X-Content-Type-Options: nosniff header forbids it from recovering by inspecting the bytes. So next.config.mjs declares text/plain; charset=utf-8 for /architecture.mmd specifically. The charset is not decoration: the labels contain · and —, and a browser guessing latin-1 renders those as mojibake in the one artifact whose entire purpose is being legible.
Four aliases ship with it — /architecture and /diagram to the section that explains the picture, /architecture.mermaid and /diagram.png to the files. Same split as D-069 and D-070 use for the spec and the schema: an extension is a request for the file, a bare noun is a request for the page. Before they existed every one of those addresses answered with the 404 page, which is an unambiguous "there is no diagram here" delivered to somebody who went looking for one.
What would have to change for this to be wrong
If the render ever becomes reproducible in CI — a Chromium for arm64 Linux, or moving the render to an x86 runner — then the diagram stops being a hand-maintained artifact and the argument for banning perishable facts weakens. It does not vanish: a diagram regenerated per build could carry live readiness honestly, but it would then be making a claim that /integrations already makes better, in a picture, without a legend explaining what the colours mean.
The honest weakness of the byte-count comparison is unchanged and is documented where it lives: an edit that changes the diagram without changing its length passes. Mermaid source is line-oriented and real edits add or remove nodes, edges and label text, so it catches the case that occurred and every case shaped like it.