Cosmic Bull

Rendered from docs/CATALOG.md at commit f191063fd89d in the project repository. The committed file is the source of truth; this page is a rendering of it.

The portfolio manifest and its verifier

How Cosmic Bull's inventory of deployed packages is made machine-readable, and how every claim in it is proved against the chain and the repository.

Companion documents: catalog/DISCOVERY.md is the discovery record and the architecture decision; this file is the standard the decision produced. docs/DEPENDENCY_CLOSURE.md is the parallel standard for imports, and the two are deliberately separate — see §6.


1. The problem this exists to solve

Cosmic Bull's catalog is prose. Prose drifts. The repo-wide audit of 2026-09-22 found stale counts across thirteen documents, and catalog/README.md carried the sentence "nothing is deployed that this catalog does not record" — a true sentence, established by hand, in a conversation, with nothing that could re-establish it tomorrow.

The failure mode is not that someone lies. It is that the inventory grows, a record is written once, the chain moves, and the two quietly stop matching. Prose drift scales with the portfolio; hand-checking does not.

So the inventory gets a machine-readable form, and every claim that form makes gets checked against something that cannot be edited to agree with it.

2. The three artifacts

PathWhat it is
catalog/portfolio.jsonThe manifest. One entry per deployed package, per chain.
tools/verify_catalog.pyThe verifier. Exit 0 VERIFIED, 1 DIVERGENT, 2 could not complete.
tools/test_verify_catalog.shThe verifier's tests. 89 cases; each negative one breaks exactly one check and greps for that finding's own text.

Nothing new was deployed to produce them. The on-chain half of this capability is four RegisterService calls into the already-live service_registry realm — reuse, not construction. The refusal to build an on-chain catalog realm, and the four reasons for it, are recorded in catalog/DISCOVERY.md §5.

3. Three-way agreement

The manifest is not the source of truth for anything. It is a set of claims, and each one is held against two independent authorities:

                    catalog/portfolio.json
                       (claims)
                      /           \
                     /             \
         live pearl-1               committed repository
    (heights, bytes, txs,           (source files, prose
     imports, enumeration)           records, hashes)

A claim that only the manifest asserts is not verified — it is recorded. The verifier says which is which, and its own output states the boundary:

Scope: chain-attested provenance, deployed bytes, imports, committed source and namespace completeness. NOT checked: the curated prose fields, the upstream GitHub SHAs, and dependency-closure equivalence.

That the curated prose is out of scope is asserted by a test, not merely written down: POS2 rewrites a summary to something deliberately false and requires the run to still pass. A documented blind spot that no test pins is a blind spot that will quietly close or quietly widen.

4. The checks

FamilyWhat it proves
C0The package is live on the chain — by status, not by "no error"
C1Chain-attested identity: creator and [addpkg] height
C2The deploy transaction: hash, height, success, framed type URL, gas, storage
C3The deployed bytes: vm/qfile content hashed and matched
C4The file set the chain holds matches the file set claimed
C5The committed source matches the deployed bytes
C6Declared divergences from the working tree carry a written reason
C7consumed_by is recomputed from the chain's own declared imports
C8Kind (/r/ vs /p/) agrees with the path and with the chain
C9Every entry cites a record, and the record carries the transaction hash
C10Namespace completeness — the chain's enumeration equals the manifest set
C11Each service_registry registration, resolved live and traced to its tx
C12The registered set, compared in both directions

C10 and C12 are the two-sided ones

Most checks ask "is this entry true?" C10 and C12 ask the question a one-sided checker cannot: "is anything true that this manifest does not know about?" An undocumented deploy and a forgotten registration are both states where every entry present is perfectly correct and the catalog is still wrong. Those two checks are the reason the manifest can carry a completeness claim at all.

Both are gated on an explicit boolean — namespace_complete and service_registry.complete. A missing key is an accident and a false is a decision, so only an explicit false may stand the check down, and the run prints a line saying so. A truthy 1 is refused outright.

C11 exists because the task created a new drift surface

Registering four primitives produced a third artifact that can disagree with the other two — precisely the failure mode this capability exists to fight. So a registration is checked the way a deployment is: resolved live through TryResolve, traced to the transaction that created it, and matched on the event attributes, the storage deposit, the gas, and the tx's presence in a cited record.

TryResolve rather than Resolve is deliberate. An unregistered name makes Resolve panic, which arrives as an ABCI error and would be reported as "could not complete" — but a name the manifest claims is registered and is not is a finding about the world, not a broken read.

5. The tests, and why there are 89 of them

Seven of the 89 are positive or fixture-faithfulness cases (PRE, POS1, POS2, A0, A0', R0, R0'). The rest assert a specific failure.

CLAUDE.md §7a: "The checker is code, so it gets audited and tested like code." and "A tool that only ever reports PASS is worse than no tool."

Every check is exercised against a manifest mutated to break exactly that check, and every negative assertion greps for the finding's own text. An assertion that accepts any failure tests nothing, because any failure whatsoever produces DIVERGENT.

Three structural defences, each of which caught a vacuous test in the verify_depclosure.py suite audit that preceded this one:

What the proxy caught

The proxy is not decorative. A1/RED-1 reproduces, end to end against the live chain, a pre-fix read of base.get("Data") or "" in which an ABCI success carrying no Data field decoded to b"". Against a manifest entry declaring the empty hash, the pre-fix tool printed:

VERIFIED -- 38 checks, 0 findings.   (exit 0)

C3 was comparing sha256(b"") against a declared e3b0c442… — nothing against nothing. The hardened tool exits 2 on the same input.

R21 uses the same mechanism for a reading the live chain will not produce: ListByType answering with its "none" empty-result sentinel while the manifest declares four registrations. Reading that as "both sets are empty, so they agree" is the same false pass in a different check.

A check family that does not run is a finding

require_families asserts that each expected family actually ran for each scope. Without it, a future edit that drops a family — or an early return that quietly skips one — reads exactly like a clean run. A12 asserts this directly against the Report class.

6. What this does NOT prove

State the limits, because a verifier trusted past its scope is worse than one not trusted at all.

7. Running it

/usr/bin/python3 tools/verify_catalog.py                    # full portfolio
/usr/bin/python3 tools/verify_catalog.py --json out.json    # machine-readable
/usr/bin/python3 tools/verify_catalog.py --chain pearl-1    # one chain
bash tools/test_verify_catalog.sh                           # the verifier's tests

Both hit live pearl-1 and take a few minutes. The tests must be green before a verdict from the verifier is quoted anywhere.

Unlike verify_depclosure.py, this tool needs no GNOROOT/GNOHOME pin — it reads the chain and the repository directly and never resolves a dependency, so there is no cache whose provenance could be wrong.

8. Toward cosmicbull.net

The manifest is the shape a site can consume. That is the point of the JSON: portfolio.json plus a verified-at timestamp and a run's --json output is enough to render a package index, a dependency graph, and a per-package provenance page without a human writing any of it a second time.

Nothing here builds that site, and nothing here should be read as a commitment to a particular one. What is established is the invariant a site would need: every published claim is re-derivable from the chain and the repository, by a command anyone can run.

A site was considered directly, on 2026-09-22, and deferred with a stated condition rather than built — see catalog/DISCOVERY_APIDOCS.md §6. The short version: gnoweb already serves this portfolio live, and a site had no content it alone could carry. §9 is the content.

Amendment, later the same day: the condition was met and the site was built. §9's generated API reference existed and was verified, which is exactly what the deferral was waiting for. tools/gen_site.py (with tools/sitemd.py and tools/test_gen_site.sh, 193 cases) builds cosmicbull.net into site/ — 61 pages over 21 packages, 33 rendered records and 372 exported symbols, plus the manifest and API reference published verbatim as machine-readable artifacts. A same-day public-exposure hardening pass made the output publishable without the (private) repository — the full categorization is catalog/DISCOVERY_SITE.md §9 — and a third pass evolved the site into the public identity of the ecosystem: the Gno Application Factory homepage, separate Applications and Packages indexes, community links, and an honest future-facing SHAMS page (catalog/DISCOVERY_SITE.md §10). The build is offline and hermetic: it reads portfolio.json, catalog/api/*.md and the committed records, opens no socket, and links to gnoweb for anything live, so it inherits this invariant rather than adding a second source of truth. site/ is a gitignored build output, not a committed artifact. The discovery record with the four refused alternatives and the measurements behind them is catalog/DISCOVERY_SITE.md. The site is not deployed — no DNS, no host; localhost only, which is what was authorized.


9. The second generated artifact — the API reference

tools/gen_api_docs.py extends the same invariant from provenance to comprehension. The manifest proves the bytes are what the repository says. It does nothing to tell a developer how to call them.

PathWhat it is
tools/gen_api_docs.pyThe generator. Exit 0 generated/matches, 1 DRIFT, 2 could not complete.
catalog/api/*.mdThe committed output — one page per package plus an index.
tools/test_gen_api_docs.shIts tests. 36 cases.

Source of truth is vm/qdoc, which the node derives from the deployed bytes. That is the same authority as the vm/qfile byte-matches in C3: a doc page generated from it cannot disagree with the code the way a hand-written one can.

Why it is a separate tool

For the same reason verify_catalog.py and verify_depclosure.py are separate (§6): one artifact, one question. And for one more, specific to this case — the API surface is deliberately not recorded in portfolio.json. Writing it there would create a third copy of a fact the chain already holds, which is precisely the drift surface C11 was added to fight. --check compares the committed page against the chain, never against the manifest.

What the sabotage runs caught

§5's standard applies here too: a generator whose --check cannot fail is indistinguishable from no --check. Two guards were removed and the suite re-run, to prove the tests bite rather than merely pass:

Guard removedResult
The package_path equality checkA2 fails. The run exits 0 and writes feeledger.md containing service_registry's entire API — a false pass in the one shape documentation cannot recover from, because the page looks perfectly plausible
The exported() filterE4, E5, E6 and U1 fail. vm/qdoc returns unexported declarations and the chain will not filter them; unfiltered, the pages publish checkedAdd, feeFor and rejectStraySend as though they were API

A2 is reachable only through the degraded-node proxy — no manifest mutation can make a healthy node describe the wrong package.

What it does NOT prove