Skip to content

Promote: the chain pages the origin was never serving - #23

Merged
bitcoinuniverseadmin merged 51 commits into
mainfrom
develop
Aug 30, 2026
Merged

bitcoinuniverseadmin merged 51 commits into
mainfrom
develop

Conversation

@bitcoinuniverseadmin

Copy link
Copy Markdown

Promotes 51 commits to main, which is what the release artifact is built
from 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. main was 43 commits behind develop and its only two
unique commits are promotion merges, so git diff origin/develop...origin/main
was empty. The chain page redesign had merged to develop and never reached
the branch the origin is built from.

$ curl -s https://explorer.bitcoinuniverse.io/resources/config.js
  window.__env.GIT_COMMIT_HASH = '521a091';
$ readlink -f /opt/universe-explorer/current
/opt/universe-explorer/releases/mempool-521a0917f      # origin/main

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-5e2f6b1e before this promotion, so the manifest check in
release.sh has something valid to read at cutover:

$ curl -s https://explorer.bitcoinuniverse.io/api/v1/chains
bitcoin  ready=False release=5e2f6b1ee reasons=['protocol-history-unavailable']
dogecoin ready=False release=5e2f6b1ee reasons=['confirmed-history-authority-unavailable',
                                                'protocol-history-unavailable']
zcash    ready=True  release=5e2f6b1ee reasons=[]

base-chain-authority-unavailable is gone from that list because it was not
true: 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, backend
build 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-release from the artifact, install beside the
running release, preflight, cut over, then verify through the public origin with
synthetic-check.mjs, release-manifest.mjs verify and
visual-qa/chain-page-smoke.mjs. The previous release directory stays in place
for rollback.

🤖 Generated with Claude Code

Bitcoin Universe and others added 30 commits August 29, 2026 06:28
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>
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.
bitcoinuniverseadmin and others added 21 commits August 29, 2026 12:05
…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
@bitcoinuniverseadmin
bitcoinuniverseadmin merged commit 1b12b26 into main Aug 30, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant