Cosmic Bull

Rendered from catalog/DISCOVERY_SITE.md at commit 6a510c665a53 in the project repository. The committed file is the source of truth; this page is a rendering of it.

Discovery and architecture — cosmicbull.net, first public version

The objective: build the first public version of cosmicbull.net as the human-facing interface to the verified Cosmic Bull ecosystem, "without creating a second source of truth", reusing the existing generated artifacts and live chain-derived information, and preserving every existing verification and provenance guarantee.

This is the §7 discovery record for that decision. It is the third in the series: DISCOVERY.md covered the catalog manifest, DISCOVERY_APIDOCS.md covered the API reference and deferred a site with a stated condition. That condition has now been met, and this record is what the deferral was waiting for.

Every measurement below was taken against live pearl-1 on 2026-09-22 and is reproducible with the commands shown. Where a claim is inherited from a search rather than measured, it says so.


1. Verdicts

ShapeVerdict
(a)Reuse an existing ecosystem package explorer instead of buildingNOT AVAILABLE — §3.1
(b)Embed / iframe gnoweb inside the siteIMPOSSIBLE — §4.1
(c)Fetch chain state from the browser at page loadREFUSED — §4.2
(d)Crawl gnoweb into a static mirrorREFUSED — §4.3
(e)A new on-chain realm backing the siteREFUSED — §4.4
(f)Commit the built HTML into the repositoryREFUSED — §5.2
(g)A static site generated offline from the committed, already-verified artifactsBUILD — §5

The shape of the answer is set by one measured fact and one design rule.

The measured fact is §4.1: gnoweb sends X-Frame-Options: DENY and no CORS headers at all. It cannot be embedded and it cannot be read from a browser on another origin. Whatever the site does with gnoweb, it does with hyperlinks.

The design rule is §5.1: the site is a pure function of committed artifacts and never talks to a chain. That is the whole answer to "no second source of truth" — not a policy anyone has to remember, but a property of the build.


2. What already exists, inside the project

Searched: catalog/portfolio.json, catalog/api/, catalog/*.md, docs/*.md, pearl/*.md, tools/, and all committed .gno under pearl/, p/, r/.

ArtifactWhat it holdsNature
catalog/portfolio.json20 packages: path, kind, role, category, deploy tx + height + creator, gas, storage, primary_file_sha256, byte count, imports, dependencies, consumers, registry registration, limitations, record pointersprovenance fields chain-attested and proved by verify_catalog.py; summary / category / role / reusable / limitations human-authored
catalog/api/*.md21 pages, 344 exported symbols — signatures, doc comments, types, methods, crossing flagschain-derived from vm/qdoc by gen_api_docs.py
pearl/DEPLOYMENT.md2,175 lines: 16 deployment sections, cost measurements, live-verification transcripts, adversarial results, closure amendments, explicit limitation sectionshuman-authored prose over measured facts
catalog/applications.md, catalog/primitives.mdper-application and per-primitive narrative, audit dispositionshuman-authored
docs/*.mdarchitecture, security, deployment, toolchain, dependency-closure and discovery standardshuman-authored
tools/verify_catalog.py · gen_api_docs.py · verify_depclosure.pythe three gates, each with its own test suiteexecutable

There is no HTML emitter anywhere in tools/ — confirmed by grep -rilE '<html|<!doctype|text/html' tools/, which returns nothing. Nothing in the project renders anything for a browser today.

.evidence/ holds the raw closure-gate runs but is gitignored, so it does not exist in a fresh clone. The site must not read it. The digests that matter are transcribed into pearl/DEPLOYMENT.md.

Classification: REUSABLE EXISTING PRIMITIVE, all of it. The site needs to author no facts whatsoever. Every column it wants already exists in a committed file. That is the finding that makes a small site possible.


3. What already exists, outside the project

3.1 Ecosystem explorers and registries

HitWhat it isClassification
gnoweb on pearlhttps://pearl.testnets.gno.landThe chain's own web interface. Serves all 20 of our packages today: rendered output, source browser, $help call builder, state explorerREUSABLE EXISTING PRIMITIVE — link to it, never rebuild it
GnoScan — gnoscan.ioBlock explorer. Network selector offers mainnet, staging and custom only. Pearl is not among themNOT RELEVANT (wrong network, and a block explorer is not a package portfolio)
gnoverse/mygnoscanRealm inspector — source, imports, dependents, callers, dependency graph. The most conceptually adjacent project foundRELATED IMPLEMENTATION — a dynamic service needing a tx-indexer and a database; whether pearl is configured was not verified
moul/gno-contracts gnocontracts previewCrawls a locally-booted gnodev into a static tree. Live at moul.github.io/gno-contracts-previewsRELATED IMPLEMENTATION — see §4.3
gnolang/gno PR #6194 misc/gnopreviewThe upstream version of the same crawler, for per-PR previews. OPEN, unmergedRELATED IMPLEMENTATION — not depend-able
moul/gnopmWorkspace version manager, not a public registryNOT RELEVANT
gnoverse/gno-explorerAggregate counts; last push 2026-03-19, README still create-next-app boilerplateNOT RELEVANT

No public package-registry website for gno.land was found — nothing analogous to npmjs.com or pkg.go.dev. The curated gnoverse/awesome-gno list was read in full: its Tools section names gnodev, the debugger, GnoScan, tx-indexer, Supernova, editor plugins, gnopls, gno-mcp, gnobro and gnovanity, and contains no static site generator, no documentation-site tooling and no portfolio template.

No relevant existing implementation was found in the searched sources for the thing the site is actually for: presenting deployment records, transaction hashes, byte-match verification, audit dispositions and dependency-closure verdicts alongside the packages they describe. Every tool above shows what is deployed. None shows what was proved about it.

3.2 Limits of this search — stated, because they bound the verdict


4. The four refusals and the one impossibility

4.1 Embedding gnoweb — IMPOSSIBLE, measured

curl -sS -o /dev/null -D - https://pearl.testnets.gno.land/r/<ns>/service_registry \
  | grep -iE '^(x-frame-options|content-security-policy|referrer-policy)'

returns

content-security-policy: default-src 'self'; script-src 'self' https://sa.gno.services; ... form-action 'self'
referrer-policy: no-referrer
x-frame-options: DENY

DENY is unconditional — not SAMEORIGIN. No origin can iframe any gnoweb page. This is not a limitation to work around; it is the correct answer, and it settles the question of how the site relates to gnoweb: by hyperlink, and only by hyperlink.

4.2 Fetching chain data from the browser — REFUSED

Two facts, both measured:

curl -sS -o /dev/null -D - -H 'Origin: https://cosmicbull.net' \
     'https://pearl.testnets.gno.land/r/<ns>/service_registry$source' | grep -ic '^access-control'
# -> 0

curl -sS -o /dev/null -D - -H 'Origin: https://cosmicbull.net' \
     'https://rpc.pearl.testnets.gno.land:443/status' | grep -i '^access-control'
# -> access-control-allow-origin: *

So gnoweb is unreadable from a browser on another origin, and the RPC node is readable. A live-fetching site is therefore technically possible against the RPC.

It is refused anyway, and the reason is the objective's own constraint rather than a technical one. A page that queries the chain at load time can display a value that no gate ever checked. The portfolio's provenance fields are trustworthy precisely because verify_catalog.py proved them; a number fetched in the browser three months from now carries none of that and is indistinguishable on screen from one that does. That is exactly a second source of truth — the same fact, arriving by a second path, with no gate on it.

The live view already exists and is authoritative: it is gnoweb, one hyperlink away. Reproducing it in a weaker form on our own page would trade a verified claim for an unverified one and call it freshness.

There is a second reason, smaller but real: that access-control-allow-origin: * is infrastructure configuration, not a documented API contract. Where it is set — node or edge — could not be determined. Building a page that goes blank when someone changes a proxy header is a poor trade for data we already hold.

4.3 Crawling gnoweb into a static mirror — REFUSED

The precedent exists and works: gnocontracts preview and the unmerged upstream misc/gnopreview both crawl gnoweb into a self-contained static tree, and both have already paid for the hard parts — the render-argument page explosion, and noindex on pages that are near-duplicates of real gno.land pages.

It is refused here for three reasons:

  1. It is the second-source-of-truth problem in its purest form. A crawled mirror is a copy of gnoweb's output that begins drifting the instant it is written, looks exactly like the real thing, and has no gate that can detect the drift.
  2. It inverts the value. Those crawlers exist because their target chain is ephemeral — a gnodev that will not exist tomorrow. Ours is a live public testnet with a hosted gnoweb. The original is available; mirroring it is strictly worse than linking to it.
  3. The upstream tool is unmerged and its input is a git diff, not a list of deployed paths. It could not be used as-is regardless.

What is adopted from that prior art is its URL-slugging discipline: no output path may contain $, : or &, because a URL tolerates them and a static host may not. The site's own URLs are plain directory paths for the same reason.

4.4 A new on-chain component — REFUSED

The objective says "Do not create a new on-chain registry or duplicate existing Gno infrastructure unless discovery establishes a genuine need."

Discovery establishes the opposite. The site is read-only over artifacts that already exist. It has no state, no users, no writes and no authority. There is nothing for a realm to hold.

DISCOVERY.md §5 already refused a catalog realm on four grounds, and DISCOVERY_APIDOCS.md §6 added a fifth. All five still apply. A sixth is specific to this objective: our service_registry is already live and already carries the four primitives' registrations, and portfolio.json already records them under registry — a fact verify_catalog.py checks as C11/C12. A site realm would be a third copy of a pointer that is already recorded twice and checked once.

Nothing is deployed by this objective. No bytes, no transactions, no registrations. §7a's Gate B does not apply, because there is no deploy.


5. What is built, and why it is the smallest thing

5.1 The central property: the build is offline and hermetic

tools/gen_site.py reads exactly three kinds of input, all committed:

catalog/portfolio.json   -> every structured fact (provenance, deps, limitations)
catalog/api/*.md         -> the chain-derived API reference
the committed *.md records -> deployment, audit, architecture, discovery prose

and writes site/. It opens no socket. It imports no networking module, and a test asserts that.

This is the architecture, not an implementation detail. It means:

The provenance guarantees are not merely "preserved". They are preserved structurally — the site has no path by which it could weaken them.

5.2 site/ is a build output, not a committed artifact — and why that differs from catalog/api/

catalog/api/ is committed, and checked for drift. Doing the same for site/ was considered and refused, because the two cases are not alike:

So site/ is gitignored. The generator, its templates, its tests and its documentation are committed; the output is one command away. This removes the drift surface entirely rather than policing it — the smaller and stronger move.

The cost is that a production host must run one build command. That is a real cost and it is accepted; hosting is a separately-authorized step that has not been taken.

5.3 The markdown renderer is bounded by measurement, not by hope

Rendering the committed records means implementing markdown, which is a notorious place for bugs to live. The scope was therefore measured across all 39 committed .md files rather than assumed:

ConstructOccurrences
fenced code (bare / go / sh)471 / 395 / 14
table rows844
ATX headings (h1–h4 only)816
unordered list (depth 0 / depth 1)543 / 32
blockquote177
thematic break129
ordered list110
indented code29
inline code / bold / link / emphasis4,022 / 1,303 / 317 / 253
backslash escapes (\_ \[ \* \ <`)104
HTML comments (the GENERATED banner)21
raw inline or block HTML0

That is the entire grammar the renderer must handle, and it is small. The one apparent <pkgpath> HTML tag in catalog/api/cointest2.md is a backslash escape emitted by vm/qdoc, not a tag.

Critically, this is enforced rather than recorded: the test suite re-derives that inventory from every committed .md on every run and fails on a construct the renderer does not handle. If a future record introduces a footnote or a nested blockquote, the tests break before the site silently renders it as literal text.

5.4 The one thing the site adds that nothing else has

For every package, the page prints the command that lets the reader check the central provenance claim themselves:

curl -sS 'https://pearl.testnets.gno.land/p/<ns>/feeledger$download&file=feeledger.gno' \
  | shasum -a 256
# 8723 bytes, sha256 bb2e1058973d...

$download returns the deployed bytes verbatim. This was verified for all twenty packages: the fetched bytes' sha256 and length equal the primary_file_sha256 and primary_file_bytes recorded in portfolio.json, 20 matching, 0 mismatching.

That is the difference between a site that asserts its packages are verified and one that hands the visitor the means to confirm it against the chain in a single command, without trusting us at all. It costs no infrastructure — it is a curl line on a static page.

One operational note, learned by measurement: pearl's edge returns HTTP 403 to Python's default urllib User-Agent while serving curl normally. The commands printed on the site use curl for that reason.


6. gnoweb views — verified, including two that look real and are not

Measured on /r/<ns>/service_registry:

SuffixStatusBytesVerdict
(bare)20050,832real — Render()
$source200122,730real — source browser
$source&file=<f>200287,774real — deep-links one file
$help200135,799real — godoc + maketx builder
$help&func=<Name>20056,085real — filters to one function
$download&file=<f>20019,781real — raw deployed bytes
?state20051,124real — state explorer
$info20050,837NOT A VIEW
$funcs20050,838NOT A VIEW

$info and $funcs return HTTP 200 and within six bytes of the bare render page, because gnoweb dispatches on known webquery keys and silently falls through to the render view for anything else. A 200 is not evidence that a view exists. The site links none of them.

$help&func=<Name> is the most useful of these for the objective's "learn how to interact with it": it filters the call builder to one function — 56 KB against 135 KB for the unfiltered page. The site deep-links it from every exported function of every realm. It fails open on an unknown name, so the names are taken from the chain-derived API reference rather than typed.


7. Page structure

Six page kinds, each with a single job:

PathJobEvery fact from
/What this is; the verification model in one screen; the countsportfolio.json
/packages/The full inventory, filterable, with kind, role, category, size, depsportfolio.json
/packages/<name>/Identity · provenance + the check-it-yourself command · API · dependencies · limitations · records · live linksportfolio.json + catalog/api/<name>.md
/verification/What is proved, by which tool, and what is not coveredportfolio.json + the records
/records/<doc>/The committed records, renderedthe .md files
/start/How to import a primitive and how to call a realmportfolio.json + the API reference

Curated prose is labelled as curated wherever it appears, matching the manifest's own about field, which already says summary, category, role, reusable and limitations are human-authored and not chain-attested. A site that renders a chain-attested tx hash and a human-written summary in the same visual style would erase the distinction the whole project is built on.


8. Limits of this record


9. Amendment, 2026-09-22 — the public-exposure hardening pass

Section 8's last limit said the site runs on localhost only. Before any future hosting, a public-exposure audit of the built output was performed, and the build hardened so the site is safe to publish without access to the project repository — which is private, a fact the first version's build did not account for: it pinned 152 GitHub links into that repository, every one a 404 for a public visitor and every one naming the private path.

Every rendered page was scanned (not assumed) for repository URLs, emails, machine paths, credential shapes, loopback URLs, endpoints and addresses. Findings, by category:

ExposureCategoryDisposition
Chain identifiers: pkgpaths, tx hashes, heights, digests, g1… addresses, gnoweb/RPC linkspublic chain datakept — this is the site's substance
Links to the five upstream application repositories (fee_split, timelock_guardian, upgrade_registry, permission_registry, service_registry)intentionally public — verified "private": false via the GitHub API on 2026-09-22kept — authoritative source for the five pipeline applications
github.com/gnolang/gno quoted as the GNOROOT Go module pathpublic upstreamkept
152 generated links into the private project repository (footer, per-package source links, record headers, the verification tools table, record cross-links to non-record files)private/repository-dependentremoved — replaced by unlinked file paths pinned to the build commit; sitemd renders a withheld link as its bare label
The private repository's path in two lines of pearl/DEPLOYMENT.md proseprivate/repository-dependentredacted at render time to a visible marker, counted and disclosed on the affected page; the committed record is unchanged
CLAUDE.md and docs/CONTRIBUTING.md rendered as recordssensitive operational — agent operating rules, authorship policy, and the only email addresses (personal and noreply) in any committed Markdownwithheld from the record set entirely
Stale pages surviving in site/ from earlier buildsunnecessary exposurepruned — a rebuild now converges the directory to exactly its own output, and refuses (exit 2) on a foreign file it never wrote

What enforces it, rather than remembers it:

Stated limits: S8 checks are shape checks — a secret that reads as prose has no greppable form; the real defense stays upstream (key material never enters the repository, CLAUDE.md §1). The five-repository allowlist encodes their visibility as measured on 2026-09-22; if one is ever made private, the allowlist is wrong until edited, and nothing detects that automatically. The repository remains private and the site remains undeployed; neither changed in this pass.


10. Amendment, 2026-09-22 — the site becomes the ecosystem's public identity

Same day, third pass, operator-directed: evolve the verified site into the public home of the Cosmic Bull ecosystem, without rebuilding it and without weakening §5's architecture. The build remains offline and hermetic; every new page is generated from the same committed inputs; nothing was retyped.

What changed, and what deliberately did not:


11. Amendment, 2026-09-22 — brand order and the Airdrop / Faucet page

Operator-directed refinement, fourth pass, same constraints. §10 recorded that a faucet/distribution nav entry was not implemented; the operator has now explicitly directed one, informational only, so this amendment supersedes that line rather than rewriting it.


Operator-directed, fifth pass. Two presentation rulings; no mechanism changed.


13. Amendment, 2026-09-22 — the Sun line removed from the homepage

Operator-directed, sixth pass: the homepage no longer opens with the Sun line that §10 introduced; the hero now leads directly with Gno Application Factory. The sun-gold accent and hero glow remain the visual motif, and the airdrop page's $SHAMS section now says simply "named for the sun". Test B1 inverted to pin the removal on the homepage (the historical amendments above still quote the line, which is why the assertion is homepage-scoped, not site-wide); B4 keeps the sun emoji off the page while still asserting the bull's position in the brand.


14. Amendment, 2026-09-22 — the bull image replaces the emoji

Operator-directed, seventh pass: the brand mark in the header and the footer is now the operator's Cosmic Bull artwork (a 400×400 JPEG, committed at tools/site_assets/CosmicBull.jpg, sha256 80f5b8e5d2fe…), rendered as a small circular image after the name; the bull emoji is gone from the site chrome. The image is copied verbatim into the build as bull.jpg — a local asset, deliberately: the same artwork exists on IPFS (CID bafkreiea6w4olux6nyaapncwkge42aot7mnf4rtgypr4qjzxnobcbkl33y), but a brand mark hotlinked through a public gateway inherits that gateway's availability, and two major gateways answered HTTP 429 during the upload's own verification. A missing asset is a build refusal, the copy is byte-checked by test (POS4), and B4/B4b now assert the image — not the emoji — trails the name in both header and footer. Suite: 148 → 150 cases.


15. Amendment, 2026-09-23 — the Build-with journey and the published artifacts

Operator-authorized after a third-party-reuse investigation whose finding was that the external developer journey fails not for missing material but for unpublished material: the manifest and API reference answer every discovery question, privately. Two additions, both presentation over existing gate-checked data:

The brand guard did its job during implementation: the first draft of the page said "for tooling and agents" and B7b refused the build; the shipped copy says "for tooling". Suite: 161 → 186 cases. The site remains localhost-only; deployment is a separate decision.


16. Amendment, 2026-09-23 — Get started merged into Build

Operator-directed navigation cleanup: the nav is now Overview · Applications · Packages · Build · Verification · Airdrop / Faucet. The former Get started page's one piece of unique content — the live-realm call guidance with gnoweb's generated gnokey maketx call builder — is now step 7 of the Build journey (the import and verify-before-integrating guidance already lived there); the start/ route is removed from the build and pruned from the output, with every internal link retargeted (nothing external links it: the site has never been deployed). All audited Build content is unchanged. Tests: B5 reordered; BW9–BW12 pin exactly-one-Build, the merged call guidance, and the route's absence. Suite: 187 → 193.


17. Amendment, 2026-09-23 — production hosting: Cloudflare Pages, private source

Operator-authorized end-to-end. The architecture, replacing the torn-down GitHub Pages deployment of the same day:

private repo → gen_site.py build → 194-test suite + exposure audit
            → wrangler pages deploy (Direct Upload) → Cloudflare Pages
            → cosmicbull.net (zone on Cloudflare, registrar GoDaddy)

Chosen because Direct Upload gives the cleanest possible privacy model — no git host is involved in hosting; the host receives only the built, audited artifact — and because Cloudflare's Universal SSL removes the certificate-queue failure mode that stalled the GitHub deployment (its Let's Encrypt cert never issued in over an hour with every health check green). The zone's nameservers moved to Cloudflare (the apex needs CNAME flattening GoDaddy DNS cannot do); GoDaddy remains registrar. The _dmarc TXT and _domainconnect CNAME records were carried over verbatim; the pre-migration GoDaddy zone is backed up.

Pipeline changes: deploy_site.sh's publish half is a wrangler Direct Upload gated on the full suite (credentials are runtime-only environment variables the script refuses to run without); the GitHub-Pages markers (CNAME, .nojekyll) are gone — the foreign-file guard flagged their stale copies during removal, as designed — and the one artifact the new host reads, _redirects, is emitted by the build (POS3c) carrying the canonical www → apex 301, so the deployed tree remains a pure function of the generator. HTTP→HTTPS is zone-level (Always Use HTTPS). Both hostnames are attached to the Pages project; DNS is two proxied CNAMEs to cosmicbull.pages.dev.

Verified at deploy time: all 86 servable files byte-identical on the Pages origin. Production verification on cosmicbull.net follows nameserver delegation and is recorded with the deployment.