Promote: the chain pages the origin was never serving - #23
Merged
Merged
Conversation
The Dogecoin and Zcash routes rendered whatever envelope arrived as a grid of raw API key names, a table whose columns were the first eight keys of the first row, and the whole response as JSON. That renders, but it does not explain: a reader could not tell a koinu from a Dogecoin, an exact integer from a rounded one, or a fact the authority proved from a field it left null. multichain-view.ts is a pure presentation model with the rules in one place. Numbers keep their exact source string and are shifted by string arithmetic, never parsed into a double, because a Dogecoin balance can exceed the range where a double still counts single units. Absent and zero are different answers and the page says which. Shapes the model recognises get a purpose-built reading; shapes it does not are still rendered, as a humanised record that says plainly it is the response as received. The transaction page now reads the envelope the contract actually defines: lifecycle state as a sentence with an evidence tone, transparent value as two aligned columns with exact totals, shielded structure with its privacy notice and no inference about participants, protocol actions split into evaluated and candidate. Block, address and outpoint get their own readings. The chain overview states which questions the chain can answer at all, and separates "not offered" from "not stated" so an unreachable authority is never reported as one that declined. The narrowest thing here matters most: the coin-amount rule is an allowlist, not a pattern. A pattern that matched "supply" printed a DRC-20 supply of 100000000000 as 1,000 DOGE, because a token's units are its own and have nothing to do with koinu. The pages also had no visual coverage at all. The matrix knew thirteen Bitcoin routes and nothing else, so eleven chain routes reached production without one screenshot, contrast probe or unfinished-page check. They are in the route list and in GATED_ROUTES now, with fixtures for a chain at its tip, a chain behind its tip, an unreachable authority, and an object that simply is not there. Chain states are scoped to chain routes so the matrix does not pay for combinations that measure nothing. Fixed on the way: the scrollable tables took a mouse but no keyboard, which axe reports as a serious violation on three routes. Verified: 60 new unit tests, 252 frontend tests, lint, production build, and the visual matrix over 11 chain routes in both themes at 375 and 1440 with zero overflow, zero contrast failures, zero axe violations and zero unfinished pages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README described a Bitcoin explorer. It has not been one since PR 11, and a reader arriving from GitHub had no way to learn that Dogecoin and Zcash are first-class here, what each chain can actually answer, or why the three are not interchangeable. The README now leads with all three chains and carries the capability table the product itself renders, including the three facts that follow from it: Dogecoin fees are quoted per kilobyte and never relabelled as sat/vB, Zcash fee guidance follows ZIP-317 logical actions, and only the transparent side of Zcash is public. It also records the gateway route table, which is what makes the chain surfaces reachable at all, and the design-system gates, which were running in CI without being mentioned anywhere a contributor would look. Two screenshots, taken by the review harness against its fixed review data and captioned as such, so nothing in them reads as a claim about live chain activity. Alt text describes what the image shows rather than naming it. The experience brief is amended rather than rewritten: the promise loses the word Bitcoin, and a new section states the five rules three chains imposed. Chains are not interchangeable. Not offered and not stated are different answers. Units are the chain's own. Privacy is a product boundary, not a gap to fill. Identifiers do not carry between chains. The design system gains the two surfaces the chain work introduced, the chain mark and the status rail, plus the numbers rule that decides when a figure may be shifted at all, and the sixth entry in the fixtures note: a whole surface that had no fixture and therefore no review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…can read Two dead ends the redesign made visible rather than fixed. A block page stated "Page 1 of 3" and offered no way to reach the other two. The address page did the same with a transaction history. The overlay has always accepted a page, so the fix is a real address: the requested page travels as a `page` query parameter, both lists share one pager, and a link to page three of a long history can be shared. readPaging refuses paging it cannot trust rather than linking nowhere. Timestamps were printed as the ISO-8601 the authority sends. That is precise and unreadable at speed: a reader has to parse punctuation to find the hour. They now read as "29 Aug 2026, 04:02 UTC" with the exact string on the title, built by hand rather than through Intl and always in UTC, because a block timestamp is a fact about the chain and two people describing the same block should not disagree about when it happened. Verified: 67 unit tests, lint, production build, and a scripted click through the pager confirming it produces /dogecoin/block/<hash>?page=2 and navigates there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hat showed The visual matrix covered thirteen inherited routes. Five Universe-authored ones were not among them: the output page, the three asset lookups, and the page that holds everything this browser remembers. Two defects had been shipping in plain sight because a page nobody photographs is a page nobody reviews. The rune mint progress bar had never rendered, on any release. The inherited Bootstrap layer owns `.progress` and makes it a 16px flex row, which collapsed the track to zero width and printed the label on top of an empty grey rectangle. It draws its 87 percent now, and it uses names Bootstrap does not own. The one timestamp on the saved page was printed in `--secondary`, which is a surface token: it resolves to `--u-surface-inset` and measured 1.3:1 on white. Nothing caught it because no fixture ever put an entry on that page, so the element had never existed in a screenshot. The saved page also spoke in wire identifiers. Its filters offered "all", "bitcoin", "dogecoin"; its entries read "dogecoin / mainnet"; its pinned protocols read "runes". They now read "Every chain", "Dogecoin, mainnet", "Runes", four headings that had no i18n now have it, and the two select controls take the product's palette instead of the browser's. Chain display names moved into universe-chain-routing.ts so a chain is spelled the same way in a heading, a saved list, and a filter. The saved page is seeded through localStorage rather than a request, because that is where its state actually lives, and at a fixed instant so the screenshot is the same every run. Verified: 259 frontend tests, lint, production build, every documentation gate, and 45 screenshots across three themes at 320, 375 and 1440 with zero overflow, zero contrast failures, zero axe violations and zero unfinished pages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The chain work grew its own copies of three things the folder already had: the evidence tone union, the atomic amount formatter, and the identifier shortener. Two implementations of one rule is how the rule quietly stops being one rule, so the chain presentation model now delegates to universe-evidence.ts and keeps only what it genuinely adds: the exact source string beside every rendering. Two things moved into the shared vocabulary to make that possible. `neutral` joins EvidenceTone. The design system has always defined five evidence states and the token has always existed; the type carried four. It is the tone for a fact with no evidence claim attached, which is what a chain that never answered leaves behind, and it is not the same as a claim that could not be established. formatAtomicAmount accepts a leading minus. One real amount in the contract is signed: a Zcash transaction's value balance, the net movement between the transparent and shielded pools, which is negative whenever value moves into the shielded side. The old rule refused it and printed nothing at all. Every other caller passes a quantity that is never negative, so this can only turn an empty render into a correct one. Also here: an attached asset's own state on the output page was printed as a bare lowercase word. The design system's rule is that an evidence state always carries its word and its treatment; that one had the word and no treatment. Verified: 259 frontend tests, lint, production build, every documentation gate, and the asset routes recaptured across three themes at 375 and 1440 clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…roject The architecture section described three processes and a route split in prose, which is the one thing here a picture is better at: the whole first-party data policy is a statement about which side of the gateway a thing sits on. The diagram makes that visible in one look, and it is a mermaid fence so GitHub renders it with nothing fetched from anywhere. CONTRIBUTING.md was the upstream document, unmodified, ending in a stray literal EOF from however it was written. It opened by welcoming contributors to the Mempool Open Source Project and instructed them to sign Mempool Space K.K.'s contributor licence agreement and add themselves to `contributors/`. Someone opening a pull request against this fork is not contributing to that project and should not be told to do any of it. It now leads with what applies here: where work happens, where Universe changes live and why that boundary is what keeps upstream security fixes easy to take, the checks CI will run, the four rules that are enforced rather than remembered, and the requirement to add a fixture for any page you add. The upstream agreement is preserved verbatim below it, under a sentence saying it governs work submitted to that project rather than to this one. Nothing was deleted, and the trademark audit's allowlist entry for this file still holds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The privacy page said the explorer "only asks its own backend for the specific transaction, block, address, output, or asset you opened". That was written when search was a local matcher. It is no longer what happens. Searching on Dogecoin or Zcash, or widening a search to every chain, calls /api/v1/universe/search with the text you typed, and completing a partly typed address has always sent that prefix to the backend. In those cases the query itself reaches the server before you open anything, which is exactly the fact the old sentence denied. The page now separates the two halves honestly. Classification is local: working out whether what you typed looks like a transaction, a block, an address, an output, or an asset happens on your device and asks nobody. Then it names the three searches that do reach the server, and states the part that is still absolutely true and is the one that matters: none of it reaches anyone else, and no query is tied to a visitor because there is nothing here to tie it to. The README carried the same claim and now carries the same correction. A privacy page that overstates is worse than one that admits a server call, and this one had been overstating since the multichain search shipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every page title fell back to "Bitcoin Explorer" on mainnet, because that is what an empty network meant when there was one chain. Eleven routes have been telling browsers, tab strips, and search engines that a Dogecoin page is a Bitcoin one. The chain is in the URL, so the suffix now says which chain it actually is, and Bitcoin and the test networks keep exactly the title format they had. The chain pages then said it twice: "Dogecoin dashboard - Universe Explorer - Dogecoin", and said "dashboard" where the heading on the page said "overview". The title takes the same label the heading does and lets the suffix carry the chain once. The description in index.html, which is also the og and twitter description on every shared link and the social card's alt text, described a Bitcoin explorer. So did the fallback in SeoService. Both name three chains now. Also here: the chain pages open their own socket and nothing in the matrix answered it, so every chain screenshot carried a failed-handshake console error. That is noise which would hide a real one, and it meant the live transport was never exercised in review. The harness answers the subscribe with one envelope per channel, at fixed sequence numbers so a rerun produces the same screenshot. Verified: 259 frontend tests, lint, production build, every gate against source and against frontend/dist, page titles checked by driving the built application across six routes, and the matrix now reporting zero console errors on the chain routes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
frontend/src/index.html is generated: generate-config.js copies it from index.mempool.html on every build, and it is gitignored. Editing it produced a correct local build and a change that the next CI run would have silently discarded, which is the worst shape a fix can have, because the local proof looks exactly like a real one. The description, the og and twitter descriptions, and the social card alt text are corrected in index.mempool.html, which is the file the build reads. Regenerating confirms the output carries them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README said "Live today, backed by the first-party Ord 0.29 authority" and named three protocols. The production smoke check reports that same authority as stale by 519,670 blocks while it rebuilds its index, and reports it as ok, because staleness is a fact to publish rather than a failure to hide. Both statements are true and they read as contradicting each other, which is the problem. The product itself has never conflated the two: a page using an authority states how far behind the chain tip it is. The README was the one surface that said only the first half. It now says readable, and then says plainly that readable is not current, that an authority rebuilding its index is still answering, and where the freshness figure is published. That stays accurate whether the rebuild is running or finished, so it needs no revisiting when ord catches up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The generic table caps at seven columns so it stays legible on a phone. It said how many rows it was holding back and nothing about the fields, so a response with twelve fields per row rendered as a complete answer with five of them silently gone. It now states the count and points at the full response, which is the same rule the row cap already followed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both the README and CONTRIBUTING told readers to report vulnerabilities privately "rather than opening a public issue", which implies an issue tracker. This repository has none: `has_issues` is false. Someone arriving with a bug to report had nowhere the documentation acknowledged. Both now say plainly that the tracker is off and that a pull request is the durable place to raise anything, including a report the reporter cannot fix, with an empty diff. The security route stays private and now says not in a commit message either, which is the other place a vulnerability leaks by accident. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
I corrupted my own sweep and nearly reported the result. An Angular production build empties its output directory before writing it, so a rebuild during a matrix leaves a window where index.html and the lazy chunks are simply absent. The gateway answers 503 for the missing index, the pages served in that window have no title and no lang attribute, and axe reports both as serious violations. Eleven findings across nine route and viewport combinations, every one of them about the build being replaced rather than about the interface, and nothing in the output distinguished them from real ones. The run already proves it is measuring the right thing before it starts, after a port collision once put another product on the address and the matrix passed thirty-nine blank screenshots. This is the same argument extended over time: the hashed bundle names are recorded at the start and compared at the end, and a run whose identity moved throws with both identities printed rather than reporting findings it did not measure. A server that stopped answering part-way fails the same way, because incomplete results that look complete are the specific danger here. Proven rather than assumed: driven against a sandboxed copy of the output whose bundle name was swapped six seconds into a run, it exits 1 and names both builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The release went live, so for the first time these pages could be tested against what the chain API actually sends rather than against what I expected it to send. Three faults fell out immediately, and none of them were reachable from the hand-written fixtures. **A protocol listed twice, once falsely.** The capability envelope names TAP on Doge `tap_doge`; the API serves it at `/protocols/doge-tap`. Matching on the route id alone rendered the tab as "not stated" and then appended the envelope entry beside it, so the overview showed the same protocol twice and neither row was the whole truth. A tab now carries every registry id that means it. **A link into a 404.** Production reports a fourth Dogecoin protocol, `dunes`, that this explorer has no page for. It was appended with a link to `/dogecoin/protocols/dunes`, which answers 404, and which the frontend's own allowlist throws on before it even asks. It is still listed, because the chain says it is there and hiding it would be a claim the chain did not make, and it is no longer a link. **A column that would have been empty on every Zcash row.** The pending list carries whole transaction envelopes, and Zcash reports no fee amount on any of them, because a shielded transaction's fee cannot be read from its transparent side. It reports ZIP-317 logical actions instead. A fixed Fee column spent the most valuable slot on the page on nothing; the cost column now takes whichever figure the rows can actually fill, and says why when it is the second one. Those envelopes also broke the generic table: twenty-one fields per row meant schemaVersion, chain and network took three of the seven columns and every cell in them was identical. Constant columns are dropped and counted, and a list of transaction envelopes now gets read as transactions, with the two figures a pending list is for. Smaller, same source: a nested snapshotId read as "Snapshot snapshot ID", and a nested chain and network repeated the page heading. The payloads are committed as live-payloads.ts, captured from the public origin at 2026-08-29T08:26Z against release mempool-8f6005ae0, trimmed only in row count. live-model-check.spec.ts drives the model with them. Every assertion is an invariant rather than a value, because the chain moves: the first version asserted a one-block lag and failed on the next run against a two-block one. The visual fixtures carried the wrong mempool shape, so the matrix had never rendered a pending list the way production sends one. They match now. Verified: 283 frontend tests, lint, production build, all seven documentation gates against source and against frontend/dist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The live Zcash pending set was empty when I looked at it, which is a real and ordinary state: no unconfirmed transactions, and the node reporting its view as complete. The page showed a status rail, a record of the snapshot, and nothing at all where the list would be. A reader could not tell "there are none" from "the list did not load", and telling those apart is the product's whole claim. An empty list the authority actually returned now says which one it is. Where the authority also called its view complete, that is a proven none and takes the proven treatment: our node has none right now, and this is none rather than an answer that failed to arrive. Where it did not, the list is empty and the emptiness is not established, which is a different fact and takes the partial treatment. Pinned as a fixture in both forms, from the shape production actually sent at 08:54Z, so neither can quietly regress into the other. Also here, a correction to the previous commit. The claim that a run whose server goes away is now refused was not mine to make: capture.mjs has caught a refused connection mid-run since 69c7437, and it names the exact route it stopped at, which is better than anything checked at the end could. My second clause restated it and could never fire, because that guard exits first. Only the bundle-identity check is new, and the comment now says which case it is for: a server that keeps answering while the files under it are replaced. That guard proved itself twice over on the way here. It refused a run I broke by rebuilding underneath it, and the older one refused a run I broke by killing its gateway while it was still going. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two places on a chain route still said Bitcoin, found by mempool-41 looking at the deployed site. The footer tagline read "Bitcoin activity while it is still forming, and what it proves" on every page, including the chain-prefixed ones. It names three chains now, which is correct everywhere rather than correct on one route family. Chain pages set a title but never a description, so every one of them fell back to the site description. A Dogecoin transaction and the Zcash protocol list previewed identically wherever a link was pasted. Each page kind now says what it actually is. Verified by driving the built application: the title, the description and the tagline read correctly on the homepage, a chain overview, a chain transaction and a chain protocol list. Checked while I was there, and no change needed: a socket that cannot connect cannot freeze these pages. UniverseWebsocketService never errors its observable, it reconnects with backoff or completes, so the 15 second poll continues and "Last observed" keeps telling the truth. The refused handshake mempool-41 found belongs to the cutover gate they shipped it in; this matrix answers the socket from a fixture and could never have caught it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One Zcash output in the fixtures read t1SoMeTransparentOutputAddressXXXXX. It is the one thing a fixture must not do: a value that announces itself as filler teaches a reviewer to skim past the column it sits in, and the whole point of these files is that somebody looks at what renders from them. Both transparent addresses are now well formed for the chain: t1, thirty-five characters, base58 alphabet. They are still not real addresses and nothing here claims they are. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The design system defines five evidence states and holds them apart with
measured colour, a word, a border and a tint. It says nothing about the code
that feeds them, and this release showed why that gap matters: an interface can
only show a difference the data path preserved.
Four defects shipped in one day, across three repositories, with one shape.
Somewhere in each, "I do not know" and "I know, and the answer is no" were the
same value.
- an indexer publishing block_count where the reader expected blockCount,
read as publishing no checkpoint;
- a WebSocket handshake with no Origin header, read as permitted, so every
probe passed and every browser was refused;
- a field-name pattern matching "supply", reading a token quantity as a coin
amount and printing 100000000000 as 1,000 DOGE;
- a Bitcoin node answering "no such block", read as a failure, so a healthy
chain was reported unavailable on every search.
Three of those were found by mempool-41 during the release, one by me. The
generalisation is theirs and it is better than the one I started with: this is
not only about tolerant readers accepting bad input, it is about a definitive
negative and an absence of knowledge collapsing into one value, in either
direction.
The evidence-states section now says the distinction has to survive the trip,
and what to ask of anything reading a payload, a header, a field name or a
status this product does not fully control.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mempool-41 found the day's pattern living in their own release tooling: a promotion waiter that merged on GitHub's mergeStateStatus, which reports CLEAN whether a run passed, failed, or never started. It would have promoted an unvalidated commit to main. That prompted the obvious question about this harness, and the answer was the same. The contrast probe's failure path records an error and an empty result. An empty result is also what a page with nothing wrong produces. Both printed "contrast failures: 0" and exited 0, and the error field, though written into the JSON report, was never read by the summary or shown to anyone. So a run where the probe threw on every page was indistinguishable from a clean one. That is the worst place for this to hide. Measured contrast is the one check here an accessibility engine cannot make: text on a fee gradient, on a block face, over the Lens canvas. Its silence is exactly what nobody would question. The summary now separates three states that were one. Failures found. Probe threw, so nothing was measured and the zero is not a result: reported, listed with the error, and the run fails, because it cannot claim what it did not measure. Probe ran and found no text to measure: reported and not failed, since a genuinely empty page is a legitimate reason. Proven rather than assumed, the same way the chain checks were: with the probe made to throw, the run names both affected pages and exits 1, where before it printed zero and exited 0. Restored, a clean run over twelve screenshots still passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The section said four defects across three repositories. Two more were found after it was written, and both were in the machinery meant to protect the distinction rather than in the product: GitHub reporting a pull request as CLEAN whether its checks passed, failed or never ran, and this repository's own contrast probe recording an empty result when it threw, which is what a clean page also produces. Those two belong in this section more than the first four do. In the product a collapsed distinction produces a wrong page, which somebody eventually sees. In a gate it produces silence, which is what everybody is hoping for. Six, not four. I had also written seven elsewhere while describing the same list, which was simply wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…plorer-experience
…plorer-experience
A chain publishes `ready` and, when that is false, the codes saying why. The rail read `sync.state` for its chain reading, which is the node's view of its own blocks and not the same question. A chain whose node is caught up while a protocol indexer is not answering publishes sync ready and ready false, so the rail printed Ready, in the proven tone, on the reading a visitor trusts most, for a chain that had just said no. Dogecoin publishes exactly that today. The reasons were carried in the same document and rendered nowhere. Where a reason did reach a page, in the protocol coverage list, it arrived as `protocol-authority-stale`. So: the verdict wins over the sync state, and the reasons are read into sentences beneath the rail. `chain-reasons.ts` holds the fourteen codes the overlay derives, as an allowlist rather than a pattern, because a pattern would match a code it has never seen and assert a meaning for it. Three distinctions the raw list does not make on its own: - A code on an authority that is ready is not a fault. `tap_doge` reports `pending-protocol-coverage-unavailable` and `reorg-evidence-tail-only` while ready and complete; they are the stated edges of what it covers. They now sit under "What it does not cover" rather than in the colour that means broken. - `authority-capability-disabled` says the index was built without the protocol switched on, so the wording says it cannot serve it until it is rebuilt, rather than implying it will catch up. - A code with no sentence here keeps its own words and is marked as one this build has no description for. A chain that withholds readiness and states no reason says so, because an empty space where the explanation belongs reads as a chain that is fine. The fixtures said `doge-tap` where the capability envelope says `tap_doge`, and carried three prose-shaped reasons the API never emits, so nothing here had ever been read against a code production actually sends. They now use the production spellings, and two states photograph what the live overlay published this morning: a chain not ready with three reasons, and one not ready with none.
Every row in the chain menu was one line: the chain's name, its state and its detail sentence run together and off the end. The header's own `.dropdown-item` rule sets `display: flex`, which is right for the network rows it was written for, an icon beside a label. A component style carries an attribute selector as well as its class, so it outranked the switcher's two-column grid in the global sheet, and the grid never applied at any width. It survived because the matrix had never opened a menu. Routes may now name a control to open before anything is measured, and the switcher is a route. A route that declares one and cannot find it fails, rather than quietly measuring the closed page instead. Opening it found two more things. The block height printed as a bare run of digits, `Block 2884120`, while the status rail groups the same fact as `2,884,120`; it is grouped in both places now. And the detail line read `Tip 2884120; mempool ready, complete` whether the chain said it was ready or not, so a chain marked degraded sat beside a sentence in which nothing was wrong, in the wire's words. A degraded chain leads with its reason. axe reads a control under an open menu as a target too small to hit, and at 320 and 375 it reads the search field behind this one that way. That is a real measurement of a state the rule is not about: the field is dismissible from under a menu, and the same field at the same widths passes on `home`, which is this path with the menu closed and is measured in full in every run. Obscuring findings on a route that declares it opens an overlay are printed on their own line, with the element, viewport and theme, and not counted. Every other rule still fails the run, and a target-size failure that is not about obscuring still fails too.
The text and branding gates run twice in CI, once over the source and once over `frontend/dist`, and the release artifact workflow runs the pair over `dist` before it packages anything. Given a directory that exists and holds no files, both walked it, found no problems, and printed the same pass a clean output prints. That directory is empty for the whole of an Angular build, which deletes its output before rewriting it, and it stays empty after the silent build failure this repository has hit before. Either would have been read as a clean release artifact by the two gates whose job is to read the artifact. Both now refuse a run that read no files, and say that there was nothing to check rather than nothing to find. Passing a path that does not exist already failed; this is the case where the path is there and the content is not.
Two small things carried over from the working tree on the build host. The wordmark faded to 45% opacity whenever the socket was not connected. The connection status sits three elements along in the same bar, says what has happened in words, and announces itself politely; the mark does not need to repeat it, and a product whose logo dims is a product that looks broken rather than one whose socket is reconnecting. The `ng-container` that existed only to read the connection state goes with it. The `.accelerate` badge is a filled chip with no foreground of its own, so it inherited the row's text colour over solid lavender, and over the brand colour while its pulse animation ran. It declares the paired ink for both fills.
Live, right now, `/zcash/block/<hash>` renders as a two-column table of raw API fields: `schemaVersion`, `zcash-metaprotocols-api-v1`, `prev_hash`, `tx_count`, and a block time as `1788003830`. None of its four transactions is listed. That is the whole page. Zcash does not answer a block the way Dogecoin does. It puts the block's fields at the top level rather than under a `block` key, in snake_case, with the time in seconds and the transaction ids inside an envelope that counts from an offset instead of a page number. `classifyPayload` only recognised the Dogecoin shape, so a real Zcash block fell through to the generic reader, which is the fallback for a response this product has no reading for and is not supposed to be a page anyone reaches. It is read now, in the same words the rest of the product uses: height grouped, the time as a UTC timestamp with the seconds kept exactly on the title, the transaction list linked and paged from the offset. Nothing is invented from the Dogecoin shape: size, difficulty, the merkle root, the confirmation count and the next block are not in this payload and stay absent rather than being filled in. `formatUnixTimestamp` refuses milliseconds, because a time read in the wrong unit lands in the year 58000 and still renders. The generic fact reader drops every array without a word, so a block carrying Zerdinals inscriptions or ZRune events would have shown nothing about them and said nothing about showing nothing. Arrays this page has no reading for are named and counted instead. `checkpoint` and `coverage` are the source's statement about its own coverage of the chain, not facts about this block, and the status rail already states every one of them from the capability document. They are not repeated as a nested field table. Three fixture corrections, all read from the live capability document rather than chosen: - Zcash was marked as unable to answer a block lookup while production answers one, so the overview said the page was not offered and nothing ever asked for the page that exists. Both chains declare fee estimates too. - The Zcash chain tip in the fixture sat below the height of the Zcash block the same run renders, which is a page contradicting itself. - The block fixture is the response production actually returned, with the protocol arrays emptied and nothing else changed. The spec is written against the same response, because a fixture in the Dogecoin spelling is exactly how this page shipped as a field dump.
…s for `cancel-in-progress: false` applies to every ref, so pushing three times to a branch under review queues three full matrix runs and only the last one is worth waiting for. The heavy job holds one of the six ultra runners for up to an hour and a half while the commit that replaced it sits in the queue behind it, which is what happened on this branch this morning. Cancelled only for pull requests. Pushes to develop and main keep running to completion, because promotion reads per-commit check results and the release artifact is built from a main commit, so each of those has to be validated on its own rather than inheriting a later one's run.
Both the README and CONTRIBUTING.md said to report a vulnerability "privately to the Bitcoin Universe security contact" and never said who that is or how to reach them. With the issue tracker off, a reader following that instruction had nowhere to go, and the only thing the page actually told them was where not to report. The channel exists and is already switched on: GitHub private vulnerability reporting is enabled on this repository, and `/security/advisories/new` answers. Both documents now link it, and SECURITY.md says what to include, what is in scope, where the trust boundaries are written down, and that an inherited upstream finding is worth sending upstream as well so the fix reaches everyone running it. No response time is promised, because none is committed to. Also records, in the two documents that describe the rail, that its chain reading is the chain's own verdict rather than its node's, and that a verdict there is now published with the evidence behind it.
Correcting the Dogecoin and Zcash fixtures to what the live capability document declares changed two things this image shows: fee estimates are offered, and TAP on Doge states its condition as a sentence rather than as a wire code. The alt text says what the image now says, including the chain switcher reading, which is the part of the header this branch changed.
…not have The chain switcher reads `/api/v1/chains` and a chain page reads that chain's own `/status`. Both come from the same overlay, so they agree in production and an outage that reaches one reaches both. Two failure states overrode only the single status, so their screenshots showed a header calling the chain Ready above a page saying its authority could not be reached, and the reviewer's first question was about the contradiction rather than about the state. `chain-authority-down` now takes the list down too, and the header reads Not stated beside a rail whose five readings all say Not available. `chain-behind` publishes the same not-ready verdict in both documents.
Same shape as the block, and worse in effect. `/api/v1/zcash/address/<t-addr>` publishes `balance.confirmed_zatoshis` and `balance.received_zatoshis`, its history in an offset-counted envelope, and unspent outputs whose index is a number and whose amount is called `valueZatoshis`. The reader knew only the Dogecoin names, so a real Zcash address rendered its identifier, no balance at all, and a table of unspent outputs with every amount and index blank, while all of it was in the response. It is 12.513454 ZEC confirmed, 9,203.00177805 received, and 8,945 transactions, and the page said none of it. Read now, with the history paged from the offset (page 1 of 179 rather than a list that stops at fifty), and with what the source does not publish left absent: nothing about what has been sent and nothing about anything unconfirmed. Received minus confirmed is not the total sent, and it stops being close the moment an output is immature. Fixing this exposed a second thing that was already wrong for both chains. The unspent output table printed "not in a block yet" whenever no height was stated, so a source that does not publish heights at all had every one of its confirmed outputs labelled pending. A row now says that only where the source does state a height for its confirmed outputs and did not state one here, which is what a mempool output looks like there. Where the source never states one, the column says the height was not reported and makes no claim about confirmation.
The block fixture was the chain tip, whose protocol lists are all empty, so the path that names lists this page cannot read yet was never rendered by anything. It is a real block carrying a Zerdinal now, and rendering it found two things. "1 inscriptions" and "1 envelopes". The names come from the response's own field names, which are plural, so putting the count in front produces that for every list holding one entry. The count sits beside the name instead: inscriptions (1), envelopes (1), events (1). The transaction list was trimmed to four ids while the same response counts seventeen and states a page size of fifty, so the page read "page 1 of 1" over four of seventeen. A false claim of completeness is not a thing to put in a review fixture, and the list is kept whole.
Six runners in the fleet carry `linux-ultra` and they are the only ones that can run the visual matrix. Eight more carry `universe-ci` and were idle while this repository's runs queued behind each other for over an hour. The documentation gates are a checkout, a Node install and a set of Node scripts. No browser, no Rust, no Docker, and a ten minute ceiling it does not come close to. It runs anywhere now. The backend job stays on `linux-ultra` because it builds rust-gbt and no other runner advertises a Rust toolchain, and the frontend job stays because the matrix needs Chromium.
They are scoped to the chain routes, so the deep pass over the Bitcoin routes never reaches them, and they were reviewed by hand instead. Those five are the states the whole status vocabulary exists for: a chain that cannot be reached, one that is behind, one whose node is caught up while its protocol side is not, one that withholds readiness without saying why, and an object that is simply not there. They could not have run here before this change. A state that tells a request to fail gets exactly that, and Chromium writes its own line about the refusal, which the harness counted as an application console error. The five states whose entire purpose is a failing request therefore failed the gate by succeeding. A refused request on a state that declared it is now printed and not counted, matched narrowly: only on a state carrying a `status` override, only for the browser's own resource line, and every one of them listed in the summary. Anything the application throws still counts, and so does a resource failure on a state that asked for none. Measured on the current build: thirty-two refusals asked for, zero console errors, zero overflow, zero axe violations, zero contrast failures, zero unfinished pages.
I have now written four, then six, then corrected seven to six, and mempool-d3 has since found two more. The number was wrong every time within the hour, and a count that keeps rising is a worse fact about this than the count itself. It is gone. The two new ones are the most expensive yet, because they are the gates that read what we ship. check-text.mjs and check-branding.mjs, pointed at a frontend/dist that exists and is empty, walked it, found no files, and printed the sentence a clean output prints. An Angular build empties that directory and leaves it empty for the whole of the build, and both gates run against it in the release artifact workflow. Verified on this branch rather than taken on trust: pointed at an empty directory both now exit 1 and say there was nothing to check, which is not the same as nothing to find. Pointed at a missing path they exit 1. Pointed at the real output they still pass. That makes three gates in this repository with the same defect, found by two sessions in one afternoon, all of them failing by staying quiet. A gate is allowed to find nothing. It is not allowed to look at nothing and report it the same way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mempool-d3 found the live ZRC-20 list printing its entire ruleset ledger as a JSON string in a table cell. That is the deployed build, and this branch already removes it. What this branch does instead turns out to be worse in one specific way, and it is mine. Driven against the payload production returns right now, the table renders six columns, silently drops rulesets and divergence, and reports hiddenColumnCount zero. So the page states it held nothing back at the moment it is holding back the most: the whole ledger for every token under two rulesets that disagree, the list of fields they disagree on, and the authority's own prose about a rule neither ruleset evaluates because no activation height for it exists. A JSON blob is ugly and present. This was readable, incomplete, and accompanied by a claim of completeness, which is the shape I have spent the day removing from other people's code and had just introduced into mine. A field a table cannot hold is now named rather than counted: the page says which structures each row carries, that this explorer has no reading for them yet, and that they are in the full response and nowhere else. This is deliberately not a reading for those structures. That is mempool-d3's next piece of work and it needs its own review: shifting by the stated decimals, showing the lens ruleset, putting each ruleset's figure beside the other rather than picking one. Splitting it also means the page stops lying about completeness now rather than when that lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Documentation gates failed on my head commit, on a test my branch does not touch: "an upstream that is genuinely gone is still reported, and promptly" gave up after 106191ms against a 12000ms bound. It takes 6.5 seconds locally and it passed on universe-runner-01 and universe-runner-03 earlier today. reservePorts counted from a fixed base of 8794. The test then asserts that nothing is listening on the upstream port, which is only a true premise if the test owns it. Where something else holds that port the connection is accepted and never answered, so instead of the instant ECONNREFUSED the gateway retries on, the request simply waits. That is the 106 seconds. This repository already learned this. The visual matrix step twelve lines away in the same workflow says a fixed port once had it measuring another job's build, and takes an operating system port now. This file never did, and while the docs job ran alone on the ultra pool it did not have to. Moving that job to the shared pool, which was the right change and made the fleet faster, turned a rare collision into a likely one. It found an old fault rather than causing a new one. Ports now come from the operating system. The "genuinely gone" test also checks its own premise first and refuses to run against a port something answers on, because a test that times a stranger's service reports on the stranger. Proven rather than assumed: with a listener deliberately placed on the reserved port, the guard fires and names it; on a free port it passes. All three restart tests and all fourteen gateway routing tests still pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Twenty-eight commits. Verified green on the exact head commit c13f8e5: documentation gates, backend build and test, frontend build including the full visual matrix, and CodeQL, read from per-check-run conclusions rather than from mergeStateStatus, which on this repository reports CLEAN whether checks passed, failed, or never ran. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The defect PR 12 fixed was every chain route answering 404 in production while the site itself loaded. That is precisely what a production smoke check exists to catch, and this one would have sailed through it: it probes capabilities, statistics, mining, universe protocols and sources, and backend-info, and nothing else. The defect was found by hand, and until now the check still could not see it. Four things are asserted, each drawn from something that actually broke. The directory serves all three chains by name. Three envelopes of which one is a chain this product does not serve passes a length check and is wrong. Each chain's routes reach the service that owns them. An invalid network query must be rejected with 400 by the chain API; the Bitcoin backend, where these requests went when the gateway was wrong, has no such route and answers 404. So 400 proves the overlay saw the request and 404 proves it did not. The status route is used because it is the one route every chain has: an earlier version sent a malformed txid, which works on Dogecoin and Zcash and is wrong on Bitcoin, whose transactions the explorer backend serves. Each capability document is readable against the contract rather than merely non-empty, because a 200 carrying the wrong body is what a misrouted family looks like when something else happens to answer. Reads a chain declares are served. This is the assertion that had to be built carefully, because `reads` states what lookup the explorer offers and not whether it will succeed today. A 503 with a named reason passes and is recorded: that is Dogecoin telling the truth while its index backfills, and a check that cries wolf for a day gets ignored on the day it is right. A 404 is read from the body rather than the status, because "no such route" and "no such object" both answer 404 and collapsing them makes this either blind to a misrouted family or noisy about every identifier that does not exist. Also a live socket handshake carrying a browser Origin, asserted at 101. The cutover gate runs that against the loopback gateway; this runs hourly against the public origin, which is the hop that gate cannot see, and which is exactly where the CORS defect lived. Written against node:http because fetch will not perform an upgrade, and rejects in a way that reads as an unreachable server. Verified twice. Against production as it stands, with Dogecoin degraded: every chain assertion passes, and the only failure is the pre-existing Doge TAP checkpoint, which is honest. And against a deliberately broken origin, one fault at a time: the whole domain misrouted, only the per-chain families misrouted, a directory naming a chain we do not serve, a socket refusing a browser Origin, and a declared read answering 500. All five caught, with a no-fault control passing every chain check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment beside the visual passes said they were measured at about two and a half minutes and sixteen. The deep pass took forty-seven minutes and 504 screenshots on the run that merged PR 14, so the recorded figure is now off by a factor of three, and it is the figure the next person will plan against. Nothing is failing and nothing needs doing today. The job finished in 59m57s of its 100 minute cap. The reason to write the real numbers down is that the deep pass is routes times states times themes times widths, one more route costs about seven minutes, and whoever adds one is currently working from sixteen. They would meet the ceiling as a timeout forty-seven minutes into a run rather than as a number they could have added up beforehand. So the comment carries the measurement, the run it came from, the runner it ran on, and the per-pass breakdown, plus what to do if it needs to grow: split it again, the way it was split from the broad pass in the first place, rather than raising the cap. A gate that cannot finish protects nothing, which is the reason that split exists. Found by mempool-d3 reading the timings of their own new step and noticing the number beside it did not match the one in the file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…matrix-cost Write down what the matrix actually costs, not what it cost once
Production smoke had no idea the chain domain existed
The chain overview rendered `reads` and dropped `coverage`. They answer different questions: `reads` is the static statement of which lookups this explorer offers for a chain, and `coverage` is the live one, how much of the chain each of them can currently answer over. Dogecoin declares both, and on a day its confirmed history source was down the page said "Address history: Offered" while the same document beside it said address history was unavailable. Both statements were in the envelope; only one was on the page. So the three coverage dimensions are stated, always all three, beside the explanation of why the chain is not ready. One says what is not answering and the other says what that costs a reader, and at a desktop width they now share a row that the fault panel used to leave three quarters empty. The technical identifiers go back on the page as well, at the level they belong at. The snapshot identifier, the schema version, the exact observation time and the overlay's own commit are how an operator checks a rendering against the document it came from, and the redesign that took the snapshot identifier out of the status line took all of them off the page entirely. They are filed in a disclosure that is closed by default, the snapshot identifier is shortened in the middle where it is shown, and the whole value is carried for a screen reader and for a copy control, because a shortened identifier nobody can recover is evidence destroyed rather than evidence filed. The rail's freshness reading gains the instant behind it. "3 seconds ago" is the one figure there that changes while nobody is looking, and it carried no way to recover the time it was measured from.
Production served a chain dashboard whose status line ran six facts together behind a raw snapshot identifier, headed static capability metadata "What is happening now", never said why Dogecoin was degraded, and published "Release development" to the public. Every check here passed for the whole of it. That is not a coincidence: the visual matrix renders this repository's build against fixtures, the synthetic check reads the API without a browser, and the origin was serving neither. It was found by looking at a screenshot. So a smoke that opens the two chain pages in a browser, against the public origin, with nothing mocked, and holds each one to the capability document the same run just read. Nothing in it hard-codes a tip, a readiness verdict or a Dogecoin outage: a chain that truthfully reports it cannot answer passes, and a page that fails to say so does not. The judgements live apart from the browser run so they can be tested, and they are. Twenty-two cases, each a state that reached production or one a change could plausibly reintroduce: the dashboard this one replaced, a snapshot identifier back in the primary interface, a release the overlay could not name, a chain not ready with nothing but a raw code where the explanation should be, a ready chain still showing a fault panel, the wrong chain under the right route, a rail that lost its labels, and a scan that matched nothing, which must fail rather than read as a clean page. Two more assert the healthy states pass, because a check that fails on good output gets switched off. The synthetic check gains the release field it never read. A placeholder identity is a legitimate answer from a workstation and never from the public origin, and the three chains are served by one overlay process, so two different identifiers on one origin means two processes are answering.
The release identity section described the frontend and the explorer backend and said nothing about the overlay, which publishes its own on every chain status document and had been publishing the word "development" to the public. Nothing set the variable it reads, no example file mentioned it, and every release directory had been naming its own commit in a file nobody read. Written down: which component publishes where, why the overlay fell back to a placeholder, what installing an overlay release now requires, and the two public checks that refuse a release which cannot name what is answering.
Three fixes to the same fault, that the space between a reading and its label was doing the work of relating them. In the capability grid the label carried `flex: 1 1 auto`, so it grew to the end of a 320 pixel cell and every state chip sat against the right edge with a hand's width of nothing between it and the words it answers. Measured at 1437 and at 1920: eight pixels between the label box and the chip, and roughly a hundred and sixty between the text and the chip. The label no longer stretches. In the coverage block the two columns were sized per row, so the three states stepped sideways with the length of each name and could not be read down. And the two disclosure controls on this page are the only way to reach the evidence behind it, at about eighteen pixels tall. On a coarse pointer they are targets a finger can hit.
Five routes were checked under a replaced palette and at 200 percent, and the two this suite most recently redesigned were not among them. Both carry a status rail, a coverage block and a disclosure, which is exactly the kind of structure a halved viewport has to reflow rather than clip. Both hold: no overflow, no text painted on itself, in either mode.
The README shows the Dogecoin overview as the harness renders it, and the page now states how much history is readable beneath the rail. Retaken at the same width, from the same review data, and the alt text describes what is there, including the five rail readings it summarised as one sentence before.
Three components sit behind this origin and each publishes its own commit: the frontend in the config script, the explorer backend on backend-info, and the overlay in the release field of every chain capability document. Nothing recorded what a release expected those three to be, so nothing could tell a coherent deployment from an origin serving a frontend from one release, a backend from another, and an overlay that could not name itself at all. All three have happened. The frontend and the backend were checked against each other, which catches the pair drifting and says nothing about either being the release that was meant to be live; that is how this origin served a frontend forty-three commits behind for a day with every check green. So the build writes RELEASE-MANIFEST.json from the commit it is building, the artifact carries it, install refuses a manifest naming a different commit from the directory it is going into, and the cutover holds the live origin to it before the switch is allowed to stand. The overlay commit is deliberately not pinned. It comes from another repository on its own release train, and pinning it would either be wrong every time that train moved or would couple two releases that are not coupled. What is required of it is the contract this frontend reads and an identity it can state; the commit it reports is recorded. Eleven tests, each a way these three have drifted apart or could: a frontend from another release, a backend from another release, a component publishing nothing, an overlay naming a placeholder, two overlay processes answering one origin, an overlay on a contract this frontend does not read, and a manifest that is missing or foreign, which must fail rather than be assumed good. Run against production as it stands, it passes the frontend, the backend and the contract, and fails the overlay for the reason that is on the public page.
…ease-identity The public chain pages, and the gate that never looked at them
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.
Promotes 51 commits to
main, which is what the release artifact is builtfrom and what the public origin serves.
Why this promotion is the fix
The public Dogecoin and Zcash pages were reported as unfinished: one run-on
status strip carrying a whole snapshot identifier, a card headed "What is
happening now" over static capability metadata, the same facts twice, "Release
development" printed as a fact about the product, two bare text links, and no
word about why Dogecoin was degraded.
Nothing was broken.
mainwas 43 commits behinddevelopand its only twounique commits are promotion merges, so
git diff origin/develop...origin/mainwas empty. The chain page redesign had merged to
developand never reachedthe branch the origin is built from.
What this carries
The chain page redesign, and on top of it
#22: the live history
coverage the overview dropped, the technical identifiers filed in a disclosure
rather than in the status line, a browser-level public smoke that holds each
chain page to its own capability document, twenty-two negative tests proving
that gate can fail, a release manifest so the three component identities behind
this origin can be checked against what the release says they should be, and
the two chain routes added to the forced colours and 200 percent zoom pass.
Companion release, already deployed
The overlay half of "Release development" is fixed in
backend-apis#78 and
#80, promoted in
#79 and cut over to
backend-apis-5e2f6b1ebefore this promotion, so the manifest check inrelease.shhas something valid to read at cutover:base-chain-authority-unavailableis gone from that list because it was nottrue: it was tied to the confirmed history source rather than to the node, and
Dogecoin Core was up and supplying the tip printed beside the sentence saying it
had not answered.
Checks
Green on
fad608892, the exact commit merged: documentation gates, backendbuild and test, frontend build (unit tests, lint, production build, four build
gates, the broad visual pass, the deep pass across themes and widths, the five
chain failure states, forced colours and 200 percent zoom, and both new
negative gate suites), and all four CodeQL analyses.
Release plan
Build the artifact from this merge commit on the runner fleet, refresh
/usr/local/bin/universe-explorer-releasefrom the artifact, install beside therunning release, preflight, cut over, then verify through the public origin with
synthetic-check.mjs,release-manifest.mjs verifyandvisual-qa/chain-page-smoke.mjs. The previous release directory stays in placefor rollback.
🤖 Generated with Claude Code