Claude Skill

metagraphed

Use when writing, validating, or preparing ANY contribution or pull request to the JSONbored/metagraphed repo — adding/enriching a subnet's public surfaces (the most common contribution), a code/schema change to the Worker API or build scripts, picking an issue, running the local

LLM Mart · 0 points · 17 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download JSONbored-metagraphed-.claude_skills_metagraphed-706603c.zip · 35 KB
Part of jsonbored/metagraphed — 6 skills

Install

skills CLI npx skills add https://github.com/JSONbored/metagraphed/tree/main/.claude/skills/metagraphed
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jsonbored-metagraphed@llmmart
Git git clone https://github.com/JSONbored/metagraphed.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole jsonbored/metagraphed collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Contributing to metagraphed — the one-shot PR playbook

metagraphed is the Bittensor subnet integration registry — every subnet, metagraphed. The repo is a Cloudflare Worker API + Node build scripts; JSON Schema is the canonical contract (→ OpenAPI → typed clients), and everything under public/metagraph/ is a generated projection of reviewed source, never hand-authored truth.

It merges through an automated, one-shot review: the Gittensory Gate (a GitHub App that posts Gittensory Gate + Gittensory Context checks and a single verdict) plus a strict CI suite (Validate). There is no human ping-pong and no "fix it in review" — the PR must be right before you push. This skill is the end-to-end procedure to make that happen with AI tools (Claude Code / Codex).

Work through the phases in order for your contribution type. If you cannot get the local gate green, do not push — an incomplete PR is auto-closed or held, not coached.

reference.md (next to this file) has the exhaustive tables — every CI check, the surface schema, the kind enum, the gate disposition, the validator list, the commit/PR rubric. Read it when a phase says to.

Zero-setup environment: if you're operating in a devcontainer-aware tool, open the repo there — .devcontainer/devcontainer.json pins Node 22 and preinstalls Playwright's Chromium (needed for Phase C2's screenshot contract), so npm install is the only remaining step. Otherwise .nvmrc at the repo root pins Node 22 for nvm use.


Three kinds of contribution — pick your path

You are… Path Files you touch
Adding or enriching a subnet's public surfaces (API, OpenAPI, docs, repo, dashboard, SDK, data artifact…) Path A — Surface contribution (Phases A0–A5) exactly one registry/subnets/<slug>.json
Changing code, schemas, or build scripts (Worker API, schemas/, scripts/, workflows) Path B — Code/schema PR (Phases B0–B5) src/, workers/, schemas/, scripts/, .github/, + regenerated artifacts
Fixing a bug or shipping a feature in the web app (block explorer, docs pages, dev tools) Path C — Frontend PR (below) apps/ui/** only

Most contributions are Path A. Do not mix any of the three in one PR.


What the gate does to your PR — it merges and closes, automatically

The Gittensory Gate is not advisory. Once your checks settle, for a contributor PR (you are not the repo owner or an automation bot) it takes a one-shot disposition:

Situation Gate action
Content verified (owner-matched, fresh, grounded) + both AI reviewers confidently approve (≥0.9) + CI green + mergeable-clean auto-approve → MERGE
A deterministic fail — duplicate surface, placeholder, private/localhost URL, secret, dead source_url CLOSE (one-shot)
Every reviewer returns a clear reject CLOSE (one-shot)
Any CI check failed CLOSE (cites the failing check)
Legitimate but uncertain — a reviewer wanted merge but under 0.9, a reviewer said manual, reviewers split, owner-mismatch, stale repo, unfetchable evidence MANUAL (held, not closed)
CI still pending / unverified fork run no action — waits

So a flawed contributor PR is closed, not coached — recovery means fixing the problem and opening a fresh PR. Verified + green ⇒ merged; a clear adverse signal ⇒ closed; genuine uncertainty ⇒ held for a human. (Owner / automation-bot PRs are exempt from auto-close — but assume you are a contributor.)


The non-negotiables (read once, hold throughout)

  1. One subnet = one file = one PR. A surface contribution edits exactly one registry/subnets/<slug>.json and nothing else (no generated artifacts, no scripts, no other subnet). You may add several surfaces for that one subnet in the same diff — that is one merge, the way it should be. Never split a subnet's surfaces across multiple PRs and never re-title the same surface as a different kind to make it look new: the gate dedups within the file and closes redundant/near-duplicate PRs. (This is exactly the farming the single-file model exists to stop.)
  2. Prove the claim. Every surface needs a public url and a source_urls entry that independently proves the subnet/operator actually publishes it (an official repo README, the provider's own site, on-chain identity). A source_url that 404s or doesn't back the claim → closed.
  3. Don't invent surfaces. Only register what a subnet actually exposes. Schema-valid ≠ accepted.
  4. Health is probe-derived only. Never hand-set health, uptime, latency, incidents, or verification — the build's prober owns those. You set identity (url, kind, provider, source_urls) and review.state: community-submitted; the gate and build do the rest.
  5. Public-safe only. No secrets, PATs, wallet/hotkey/coldkey paths, private/localhost URLs, or validator-local data anywhere — in files, commits, or PR text. auth fields are placeholders (Bearer <token>), never real credentials.
  6. Link an open issue — required. Every PR must reference an issue (Closes #<n> / Refs #<n>) in the PR body, and that issue must be open/unclosed at submission time — the gate verifies the PR against that issue's intent, clause by clause. No linked issue, or a linked issue that's already closed, is an automatic close on its own, before content is even scored. For surface work, the per-subnet enrichment issues under epic #427 are the natural home to link — pick one that's still open before you start.
  7. Schema is the contract — regenerate + commit (Path B). Editing schemas/ means npm run build then committing openapi.json + types/clients in the same PR, or validate:contract-drift fails CI.
  8. Conventional Commits, no AI attribution. Lowercase scope, specific subject, no trailing period; no AI/Claude/agent mention anywhere in commits or PR text. Frontend/UI work lives in this repo at apps/ui/ — see Path C below; it is not Path A or Path B.

Path A — Surface contribution (the common case)

Phase A0 — Bootstrap

# External contributor? Fork JSONbored/metagraphed, then clone YOUR fork:
git clone https://github.com/<you>/metagraphed && cd metagraphed
git remote add upstream https://github.com/JSONbored/metagraphed
nvm use            # Node 22 (engines: >=22.23.0)
npm install        # required before any validator runs

Phase A1 — Pick the subnet + find a real surface

  • Search first. Check open issues AND open PRs for the same subnet/surface — a duplicate is a close-worthy signal. Browse good first issue / help wanted; the per-subnet enrichment issues (#427) each name the exact gap.
  • Find the gap. npm run curation:brief lists profile-light subnets (directory-only, no website / source repo, public APIs with no OpenAPI yet). See docs/curation-playbook.md.
  • Confirm the surface is real and public. A safe public url you can fetch, plus a source_url that proves the subnet publishes it. Pick the right kind (full enum in reference.md): contributor kinds are docs, website, source-repo, openapi, subnet-api, dashboard, sse, data-artifact, sdk, example, repo-registry — all auto-reviewable; authed/paid APIs + unknown providers are higher-trust (airtight ownership proof). Base-layer chain endpoints (subtensor-rpc/wss, archive) are maintainer-curated infra (the endpoint lane), not contributor surfaces. Prefer high-value callable kinds (openapi, subnet-api, sse, data-artifact, sdk) — source-repo and website are auto-promoted from on-chain identity data, and validate:surface rejects them if the machine already has the URL (CI fails → gate closes). See reference.md §5.

Phase A2 — Edit the ONE subnet file

A surface contribution adds entries to the surfaces[] array of registry/subnets/<slug>.json. Use the helper so the id/shape are correct:

# Find the provider slug for the team behind the surface.
npm run providers:list

# Append a community surface to the subnet file (writes into registry/subnets/<slug>.json):
npm run surface:add -- \
  --netuid 43 --kind subnet-api \
  --url https://api.example.com/v1 \
  --source-url https://github.com/example/project/blob/main/README.md \
  --provider <provider-slug> --submitted-by <github-login> --write
  # Debut provider (slug not registered)? Add the team identity and surface:add scaffolds
  # registry/providers/<slug>.json (flat — trust is the authority field) in the SAME PR.
  # --provider-url is the provider's website_url and MUST be a public URL (validate
  # rejects private/localhost), as must any logo/docs/github/team/contact/social URL:
  #   --provider-name "Example Team" --provider-url https://example.com

Each added surface must carry authority: "community" and a review block — the helper sets these:

{
  "id": "sn-43-example-subnet-api",
  "name": "Example subnet API",
  "kind": "subnet-api",
  "url": "https://api.example.com/v1",
  "provider": "example",
  "authority": "community", // existing enum value — community-submitted, not official truth
  "auth_required": false,
  "public_safe": true,
  "source_urls": ["https://github.com/example/project/blob/main/README.md"],
  "review": {
    "state": "community-submitted",
    "submitted_by": "<github-login>",
  },
  "notes": "One line on what it is / why it's the right surface.",
}

You set identity + proof + review.state: community-submitted only. For an existing subnet manifest, do not add verification, health, or curation changes, and do not touch other surfaces or top-level fields in the file — a community PR that edits anything beyond appending its own community surface(s) is out-of-shape and gets routed to full review or closed. A missing subnet manifest is the exception: subnet:new creates the required top-level scaffold fields, then surface:add appends the community surface in that same new file. review.state is the human-governance axis: a maintainer flips it → maintainer-reviewed (or rejected) in place; machine verification + freshness is the separate probe overlay (the build's prober fills verification/health).

New subnet not yet in registry/subnets/? Scaffold it with npm run subnet:new -- --netuid <n> first (one file), then add your surface to it in the same PR.

Phase A3 — Validate locally

npm run validate:surface -- registry/subnets/<slug>.json   # schema + provider-slug + review-shape
npm run scan:public-safety                                  # no secrets / private URLs

Fix every finding. (CI runs the full validate suite; these two are the fast local pre-checks for the submission lane.)

Phase A4 — Commit + open the PR

  • One subnet file changed, nothing else. git diff --stat should show a single registry/subnets/<slug>.json.
  • Commit (Conventional): feat(registry): add SN43 Example subnet-api surface (#<issue>).
  • PR body: fill .github/pull_request_template.md honestly — a real Summary, the url + source_url proof, the validation commands you ran, and Closes #<issue> — required, and the issue must still be open. No AI attribution.

Phase A5 — Let the gate adjudicate

Watch Validate and Gittensory Gate go green. Verified + green → merged. A deterministic fail (dup / dead source / private URL) or a clear reject → closed; fix and open a fresh PR. Genuine uncertainty → held for a human — don't open a duplicate.


Path B — Code / schema PR

Phase B0 — Bootstrap + scope

npm install (Node 22). Open an issue first for anything risky (public behavior, schema/contract changes, new routes, workflows, deps). Keep the PR narrow — one coherent change. Anchor on existing code: find ≥2 analogues in the repo, cite them file:line, trace the closest end-to-end, and match its structure, naming, and comment density. Build for the class, not the one case.

Phase B1 — Implement (match the house style)

  • The Worker entry/router is workers/api.ts; serving/overlay/health logic lives in src/*.ts; the contract lives in schemas-src/ (Zod, the single source) and src/contracts.ts. schemas/*.json is a different thing — the INPUT schemas that validate hand-written registry files. A published component declared anywhere but schemas-src/ fails validate:single-schema-source.
  • All new code/script/test files must be .ts — never .mjs/.js. The TypeScript migration (metagraphed#7510) is complete, and the validate:no-hand-written-mjs CI gate fails any PR that adds a .mjs/.js file under src/, workers/, scripts/, or tests/ (metagraphed#7521).
  • Schema-first rule: never hand-edit the generated contract. Edit the Zod schema under schemas-src/ (and register it in schemas-src/openapi-registry.ts if it is new) → npm run build → commit openapi.json + generated types/clients in the same PR.
  • A new /api/v1 route or artifact trips hidden contract gates — see the new-route checklist in reference.md before adding one.

Phase B2 — Test

Tests are vitest under tests/. Add coverage for new branches and fallback paths, and a regression test for every bug fix. Codecov is the coverage gate — codecov/patch enforces 99% patch coverage, branch-counted, with zero threshold slack (target: 99%, threshold: 0% in codecov.yml), scoped to src/** + workers/** runtime code. Run it unsharded locally: npm run test:coverage. Reader tests serve R2-only artifacts that only exist after a build, so npm run build before the suite if a test reads served artifacts. Running a single reader test file in isolation (e.g. npx vitest run tests/some-route.test.ts) needs the same precondition — those artifacts live under gitignored dist/metagraph-r2/metagraph/, populated as a side effect of tests/artifacts.test.ts / tests/discovery-artifacts.test.ts when the full suite runs first (alphabetically), but absent on a fresh checkout or a filtered single-file run. For just the fixture tree, without the rest of npm run build's slower steps (type/client/GraphQL generation) and without ever touching public/metagraph/, run npm run artifacts:prepare-local first.

Phase B3 — Regenerate what you invalidated (then commit it)

You changed… Run Commit
schemas-src/ (any published schema) npm run build openapi.json, generated types, contracts.json, api-index
A new/edited /api/v1 route or artifact npm run build the derived public/metagraph/* it produces
A canonical registry/providers/<slug>.json npm run build regenerated artifacts (commit only the provider file + its artifacts)
MCP tools in src/mcp-server.ts — nothing — the server card is worker-computed, not a committed artifact
(any of the above) npm run build never public/metagraph/r2-manifest.json / public/metagraph/schemas/index.json — see below

Stale committed artifacts fail the derived-artifact freshness + contract-drift gates.

Never commit public/metagraph/r2-manifest.json or public/metagraph/schemas/index.json. npm run build always rewrites both to reflect a full local/CI build, but neither committed copy is yours to move: r2-manifest.json is the publish lockfile, read from its committed path at publish time; schemas/index.json is a live network-capture cache whose current copy lives in the generated/schemas-index.json R2 store, written daily by a Worker cron (src/schema-snapshots-sync.ts) — the committed file is only the fallback seed a credential-less build reads when it cannot reach that store. Both are excluded from the "Verify committed derived artifacts are fresh" step in .github/workflows/validate.yml for this reason. A contributor build will always show them as changed for reasons unrelated to your change.

public/metagraph/operational-surfaces.json is the same shape of thing one rung down: also a fallback seed (for the health prober's cold start), also written live by an hourly Worker cron (src/operational-surfaces-sync.ts → generated/operational-surfaces.json), also excluded from that gate. It is not on the never-commit list because a normal build reproduces it deterministically — but if it shows up dirty in a PR that did not intend to change the surface set, revert it the same way.

Both npm run build and a standalone node scripts/build-artifacts.ts / npm run build:artifacts already auto-revert whichever of these actually went dirty, back against your base remote (upstream/main if you forked per Phase A0, origin/main otherwise) — git status should already be clean after either. Treat that as a safety net, not a guarantee: it silently degrades to a printed warning (not a failure) if the revert itself can't reach your base remote (e.g. no network, or upstream/origin isn't fetched). So still check git status before staging, and if either file shows modified, revert it by hand:

git checkout "$(git remote | grep -qx upstream && echo upstream || echo origin)/main" -- \
  public/metagraph/r2-manifest.json public/metagraph/schemas/index.json

before staging/committing.

Client SDK version: do not bump packages/client/package.json in your PR. The sync-client-version workflow auto-opens a chore/sync-client-version PR after a contract-changing merge. validate:client-sdk-sync now emits a notice (not a failure) when the version wasn't bumped in the contributor PR.

Phase B4 — Run the gates locally (must be green)

git diff --check
npm run lint && npm run format:check        # NOTE: main isn't fully prettier-clean — never reformat whole files you didn't change
npm run typecheck                           # tsc --noEmit, whole in-scope tree (src/workers/scripts/tests)
npm run validate                            # registry + API + OpenAPI checks
npm test                                    # or: npm run test:coverage for the coverage gate
# Then the focused validators for what you touched (full list in reference.md), e.g.:
npm run validate:contract-drift  npm run validate:schemas  npm run validate:api  npm run validate:openapi

For a faithful full local run, npm run pipeline:check — but only trust it in isolation after a clean npm run build (see the build-gotchas note in reference.md).

Phase B5 — Commit + PR

Conventional Commit (no AI attribution); Closes #<issue> — required, and the issue must still be open; fill the PR template with the validation commands you actually ran. Sync with main if it moved (git fetch upstream && git rebase upstream/main) — a base conflict closes a contributor PR.


Path C — Frontend PR (apps/ui/)

apps/ui/ is the TanStack Start + Vite + React web app at metagraph.sh — folded into this repo as an npm workspace via the monorepo consolidation. It has its own ui CI job (lint + typecheck + test + a responsive-overflow e2e check + build + bundle-budget, see reference.md §2) and its own review contract, distinct from Path A/B.

Phase C0 — Bootstrap + pick an issue

npm install            # root install wires the apps/ui workspace too (Node 22)

Pick a gittensor:bug / gittensor:feature issue scoped to apps/ui/ (Wave 3 milestone). Keep the PR narrow — aim for ≤10 files / ≤1000 LOC; if an issue looks bigger than that once you're in the code, ship the smallest coherent slice and leave a follow-up note rather than bundling everything into one PR.

Phase C1 — Implement (match the house style)

  • Reuse existing shared components and the design tokens in packages/ui-kit/src/styles.css (the v2 system — warm bone/paper background, deep ink text, mint accent used sparingly, flat surfaces with hairline borders, no shadows or gradients) instead of inventing new one-off styles.
  • Anchor on an existing analogous page/component before writing a new one — this codebase already has shared primitives (table-controls, chart primitives, copy/share buttons, entity hover-cards, freshness badges) that most issues should compose rather than reimplement.
  • Creative additions beyond an issue's stated scope are welcome but held to a higher bar — expect extra scrutiny, and call out explicitly in the PR body anything you added beyond the issue.

Phase C2 — Screenshot contract (external-contributor visual changes)

Non-negotiable for an external-contributor PR that changes rendered output. Without an explicit maintainer override, a PR without this evidence is auto-closed. A real PR (#3757) shipped 10 of its 12 screenshots at 115,000–142,000px tall (a full-page capture bug, not a display issue) and sat unreviewable until recaptured. Don't repeat that.

Maintainer-directed override. When a repository maintainer explicitly waives screenshots for the current task, skip this phase—including static and animated evidence—and do not request the waived evidence again. Never infer the waiver from branch ownership or repository access. Continue to run proportional responsive/interaction validation and every required code/CI gate.

Recommended: automated capture (#3769). apps/ui/tests/e2e/capture-pr-screenshots.ts automates everything below — the two-worktree orchestration, fixed-viewport-only capture, explicit theme toggling, the 12-image matrix, and (with --push) hosting + the ready-to-paste markdown table:

npm run screenshots --workspace=apps/ui -- --route /subnets/1 --section volume-24h --prefix 5483-volume --push

Add --section <id> for a below-the-fold section anchor (omit to capture the page top), and --fallback-section <id> when before doesn't have that anchor yet (the common case for a new section — point it at the existing anchor the new one attaches after). Already have two dev servers running (e.g. mid-session in an AI coding tool)? Skip the orchestration and point at them directly: --before-url http://localhost:8081 --after-url http://localhost:8080. Run npm run screenshots --workspace=apps/ui -- --help for the full flag list.

If the tool doesn't fit your case (a capture step needs manual intervention, or you're debugging the tool itself), the equivalent manual steps are below — same contract, same output.

1. Two dev servers — one for before, one for after. Don't reuse a single server for both; run the before state from a separate worktree so nothing needs stashing/restoring mid-capture:

git worktree add ../metagraphed-before $(git merge-base main HEAD)
cd ../metagraphed-before && npm install && npm run dev --workspace=apps/ui   # note the printed Local URL — this is "before"
cd -                                                                          # back to your feature branch
npm run dev --workspace=apps/ui                                              # note this Local URL — this is "after"

2. Fixed viewport sizes only — never a full-page / fullPage: true capture. A full-scroll-height capture is exactly what produced #3757's broken screenshots. Use these three sizes — chosen to straddle this app's actual Tailwind breakpoints (md=768px, lg=1024px, the two most-used responsive prefixes in apps/ui/src):

Viewport Size (px)
Mobile 375 × 812
Tablet 768 × 1024
Desktop 1280 × 800

Capture exactly that viewport, nothing more. If the changed content is below the fold, scroll to it first — don't reach for a full-page capture to get there.

Also never locator.screenshot() / elementHandle.screenshot() (e.g. page.locator(".hero").screenshot(...)) as a shortcut to "just the changed part" — confirmed on #6928/#6933, both shipped with a locator-scoped capture instead of the fixed viewport. It crops to that element's own rendered box, not the fixed viewport frame this contract requires, so a reviewer can't see the change in its real page context (surrounding layout, whether anything else shifted). The only call to make is page.screenshot({ path }) — no fullPage, no locator, after resizing the viewport and scrolling to the changed content if needed.

3. Force each theme explicitly — never rely on system/prefers-color-scheme (it varies by capture environment, so it isn't reproducible run to run). In the page, before capturing:

localStorage.setItem("mg-theme", "dark"); // or "light"
location.reload();

mg-theme is THEME_STORAGE_KEY in apps/ui/src/lib/theme.ts — the only supported mechanism. Reload after setting it so the pre-hydration bootstrap script applies it with no flash-of-wrong-theme.

4. 3 viewports × 2 themes × {before, after} = 12 images, for a page/feature-level change. Skip a combo only if you state in one sentence why it's provably unaffected (e.g. a change gated behind a desktop-only code path).

5. Host the 12 files on a dedicated branch in your own fork — never drag-and-drop, never commit them to your feature branch. Drag-and-drop into the GitHub web editor requires a human browser session, which an AI coding tool cannot do end-to-end; a pushed branch is fully scriptable and keeps binary images out of your feature branch's diff entirely. Do this from a throwaway worktree, not your feature branch's working directory:

git worktree add ../metagraphed-screenshots main
cd ../metagraphed-screenshots
git checkout --orphan screenshots       # first time; if you already have a `screenshots` branch from a
git rm -rf . 2>/dev/null                # prior PR, just `git checkout screenshots` instead and skip these two lines
cp /path/to/your/12/*.png .
git add *.png && git commit -m "screenshots for PR"
git push origin screenshots
cd -    # your feature branch's working directory was never touched

Reference each file as https://raw.githubusercontent.com/<your-fork-owner>/metagraphed/screenshots/<file>.png.

6. Table format — one row per viewport+theme, thumbnail + caption in each cell, both before and after:

| Viewport · Theme | Before                                                                | After                                                              |
| ---------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Desktop · Light  | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |
| Desktop · Dark   | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |
| Tablet · Light   | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |
| Tablet · Dark    | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |
| Mobile · Light   | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |
| Mobile · Dark    | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |

Screenshots go inside the table only — never pasted loose in the PR body, never committed to the feature branch itself.

A PR confined to apps/ui/src/lib/** / apps/ui/src/hooks/** / test files, with no visual change, skips this entirely — it isn't rendering anything different.

The devcontainer (.devcontainer/devcontainer.json) preinstalls Node 22 + Playwright's Chromium, so setup for the steps above (manual or via the automated tool) is zero-config there.

Animated evidence (#4825) — for effects no static screenshot can show. Required whenever the changed behavior is only visible in motion: a hover-triggered popover, a scroll-linked effect, a CSS transition/animation, a drag interaction, or anything else where "before" and "after" aren't just two different static layouts. This is additional to the static table above, not a replacement for it — a real PR (#4814) shipped both: the static viewport × theme matrix for the at-rest layout, plus a before/after GIF table for the hover behavior itself, because a still image genuinely cannot show what happens on hover.

  1. Record the interaction, don't screenshot it. Use your OS's screen recording (macOS Cmd+Shift+5 or screencapture -V; Linux wf-recorder/ffmpeg -f x11grab) or a Playwright video/trace, scoped tightly to the interactive element — not the full viewport, and not a long clip. A few seconds showing the cursor entering, the effect triggering, and the resulting state is enough.
  2. Convert to a GIF — a .mov/.webm file won't render inline in a GitHub-hosted <img> tag the way a .gif does:
    ffmpeg -i recording.mov -vf "fps=12,scale=480:-1:flags=lanczos" -loop 0 hover-before.gif
    
    Keep it small (a few seconds, ~12fps, ≤480px wide) — an oversized GIF is as unreviewable as #3757's full-page screenshot bug was.
  3. Same hosting mechanism as step 5 above — the dedicated screenshots branch on your own fork. Push the .gif files alongside your PNGs in the same orphan-branch commit; reference them the same way: https://raw.githubusercontent.com/<your-fork-owner>/metagraphed/screenshots/<file>.gif.
  4. Table format — one row per interaction target (not per viewport/theme; a hover/scroll/transition effect is rarely breakpoint- or theme-dependent, so don't multiply it out the way the static matrix does unless the interaction genuinely differs by breakpoint):
    ### Hover interaction (animated)
    
    Static images can't show the pointer-driven [behavior] — here's the actual interaction.
    
    | Target                                                          | Before                                                   | After                                                  |
    | --------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------ |
    | <describe the interactive element, e.g. "Blocks · author cell"> | [<img src="BEFORE_GIF_URL" width="380">](BEFORE_GIF_URL) | [<img src="AFTER_GIF_URL" width="380">](AFTER_GIF_URL) |
    
    One row per distinct interactive element the PR changes.
  5. A PURELY interaction-only change (identical at-rest state, only the triggered behavior changed) can skip the static matrix for that specific view — state why in one sentence, the same "provably unaffected" exception already allowed for a static viewport/theme combo.

Phase C3 — Test + gates locally

The ui CI job runs lint, typecheck, test, build, a responsive-overflow e2e check, and a bundle-size-budget check, in that order — run the same locally before pushing:

npm run lint --workspace=apps/ui && npm run format:check --workspace=apps/ui
npm run typecheck --workspace=apps/ui  # auto-builds packages/client first (pretypecheck) -- no separate step needed
npm test --workspace=apps/ui
npm run build:worker --workspace=apps/ui  # MUST precede test:e2e since #8928 — see below
npm run test:e2e --workspace=apps/ui   # needs a Chromium browser: npx playwright install --with-deps chromium (once)

Build now comes before test:e2e (#8928). The e2e check serves the PRODUCTION build via wrangler dev rather than npm run dev: the sweep loads 26 routes x 4 viewports, and Vite's dev server compiled each route on first hit, making that step ~48% of the whole ui job. It also means the check now exercises the bundle that actually ships, so prod-only breakage the dev server hid is in scope.

Use build:worker, not build. A plain npm run build emits .output/ with no Worker entry; the cloudflare-module preset (which CI and production use) emits dist/. test:e2e serves dist/server/wrangler.json and fails without it.

This does NOT change Phase C2's screenshot workflow below, which still uses npm run dev on purpose: those are contributor-facing before/after captures, not a CI gate.

The responsive-overflow e2e check replays recorded API traffic (tests/e2e/har/*.har) instead of live production data, so it's deterministic regardless of live chain state. If your PR adds a new API call on one of the checked routes (/, /subnets/1, /endpoints, /status, /settings, /explorer), re-record: npm run test:e2e:record-har --workspace=apps/ui against a running dev server.

CI also gzip-measures the initial client JS for a cold / visit against a budget (currently 400 KB, .github/workflows/validate.yml's "Bundle size budget" step) — keep new dependencies/imports lean; if a real feature legitimately grows it, raise the budget deliberately in the same PR. If your PR also touches packages/client or packages/ui-kit, CI rebuilds each fresh and diffs against its committed dist (packages/client/dist / packages/ui-kit/dist) — run npm run build --workspace=packages/client (or --workspace=packages/ui-kit) and commit the result if you changed packages/client/src (or packages/ui-kit/src). packages/ui-kit also gets its own npm run typecheck --workspace=packages/ui-kit step in the ui CI job.

Phase C4 — Commit + PR

Conventional Commit (e.g. feat(ui): add validator directory table), no AI attribution, Closes #<issue> — required, and the issue must still be open. External-contributor visual changes must fill the screenshot table unless a repository maintainer explicitly waived it for the current task.

Phase C5 — Review disposition

An external-contributor visual PR touching apps/ui/ is held for manual review regardless of AI-review confidence. For maintainer-directed work, an explicit authorization to merge after required CI is green replaces that manual-review wait; merge when green without asking for the same approval again. A non-visual apps/ui/ PR (data/hooks/tests only) follows the normal auto-merge/auto-close gate like Path A/B.


Final pre-push checklist

Path A (surface):

  • Exactly one registry/subnets/<slug>.json changed; existing manifests only append community surface(s), while missing manifests may include the required subnet:new scaffold plus the community surface(s); no other file.
  • Each surface: real public url + a proving source_url; right kind; authority: community; review.state: community-submitted; public_safe: true; no health/verification/secrets set by hand.
  • Not a duplicate of an existing surface or an open PR; not the same surface re-titled by kind.
  • npm run validate:surface + npm run scan:public-safety clean.
  • If you ran npm run build locally out of caution (not normally required for Path A), your diff still touches only your one subnet file — see the Path B note below on public/metagraph/r2-manifest.json / public/metagraph/schemas/index.json; the Gittensory Gate's registry-review lane rejects a PR that bundles either in with your surface change.
  • Conventional Commit (no AI attribution); PR template filled; Closes #<issue> — required, referencing an issue that's still open.

Path B (code/schema):

  • In scope, narrow, anchored on ≥2 analogues; general not special-cased.
  • Regenerated + committed: npm run build artifacts (OpenAPI/types/contracts) as applicable. MCP tool additions do NOT require server-card regen (worker-computed). Client version bump NOT required (auto-sync workflow handles it post-merge).
  • public/metagraph/r2-manifest.json and public/metagraph/schemas/index.json are not part of your diff — both always change on a local/CI build for reasons unrelated to your PR (they're deploy/publish-pipeline-owned, not contract artifacts). npm run build (and a standalone scripts/build-artifacts.ts) already auto-revert either one if it went dirty; double-check git status and fall back to the Phase B3 command above if either still shows modified (e.g. the auto-revert couldn't reach your base remote).
  • git diff --check clean · lint + format:check + typecheck clean · npm run validate green · npm run test:coverage green · the focused validate:* for what you touched green.
  • Branch current with main; Conventional Commit (no AI attribution); PR template filled; Closes #<issue> — required, referencing an issue that's still open.

Path C (frontend):

  • Scoped to apps/ui/** only; ≤10 files / ≤1000 LOC where reasonably possible.
  • Reuses existing design tokens (apps/ui/src/styles.css) and shared components rather than one-off styling.
  • If visual and not explicitly waived by a repository maintainer: a filled before/after screenshot table (mobile + dark-mode captures where relevant) — missing/malformed external-contributor evidence is an automatic close.
  • If the change is only visible in motion (hover/scroll/transition/animation) and evidence was not explicitly waived: a before/after GIF table alongside the static one, per the "Animated evidence" step in Phase C2.
  • lint + format:check + typecheck + test + test:e2e + build all green (--workspace=apps/ui); bundle size still under budget.
  • If packages/client/src or packages/ui-kit/src changed: rebuilt and committed the respective dist (packages/client/dist / packages/ui-kit/dist).
  • Conventional Commit (no AI attribution); Closes #<issue> — required, referencing an issue that's still open.

If every box is checked, the PR has the best chance of a one-shot approve-and-merge. If any box can't be checked, keep working — don't push.


When you need the exhaustive detail behind any phase, read reference.md in this skill directory.

Files (metagraphed)
  • reference.md 58.9 KB
    # metagraphed contribution — deep reference
    
    Exhaustive tables behind the `SKILL.md` playbook. Read the section you need. All commands run from the
    repo root (Node 22, `npm install` first — or open `.devcontainer/devcontainer.json` for a preconfigured
    environment with Playwright's Chromium already installed).
    
    ---
    
    ## 0. The single-file surface model (what changed, and why)
    
    **Surfaces live in ONE file per subnet:** `registry/subnets/<slug>.json` → its `surfaces[]` array. A
    community contribution **appends a surface to that one file** with `authority: "community"` and
    `review.state: "community-submitted"`. If the subnet has no manifest on the base branch, the valid
    one-file shape is a new `subnet:new` scaffold plus the community surface in that same file. The
    Gittensory Gate flips the review state in place on merge; the build's prober fills
    `verification`/health.
    
    This **replaces** the old per-surface intake lane (`registry/candidates/community/<one-file-per-surface>.json`).
    That lane created the farm: one surface = one file = one PR = one merge, so a contributor split a single
    subnet's surfaces across several near-identical PRs (re-titled by `kind`) to multiply merges. The
    single-file model closes it: a subnet's surfaces are **one diff = one merge**, the gate sees them
    together (trivial dedup), and redundant/split PRs touching the same file are closed.
    
    **Trust is preserved per surface, not per file:** `authority` (`official` / `provider-claimed` /
    `community` / `registry-observed`) + the per-surface `review.state` tell the API and the gate how much
    to trust a surface. "community-submitted" ≠ verified truth until the gate/build promote it.
    
    **The filename is the slugified name, not the netuid.** Correct: `registry/subnets/zeus.json`. Wrong:
    `registry/subnets/sn-18.json`. The `sn-<netuid>` form is only correct as a fallback for the rare subnet
    whose name doesn't produce a usable slug — see `scripts/subnet-new.ts`, the only correct way to
    scaffold a new subnet file (`npm run subnet:new -- --netuid <n> --name "<Real Name>" --write`). Never
    hand-name a new file, including during an ad-hoc enrichment pass (e.g. a taostats identity gap-fill) run
    interactively rather than through a committed script — always scaffold through `subnet:new`. `npm run
    validate` fails CI if any subnet filename doesn't match the slugified name (the machine-owned
    `registry/subnets/generated/` directory is exempt).
    
    ---
    
    ## 1. The surface object (`schemas/subnet-manifest.schema.json` → `$defs.surface`)
    
    Required on every surface: `id, name, kind, url, provider, auth_required, authority, public_safe`.
    
    | Field                           | Type / values                                                                                                                                                                                                                 | Who sets it                                                     |
    | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
    | `id`                            | `^[a-z0-9][a-z0-9-]*$`, unique in the file (convention `sn-<netuid>-<provider>-<kind>`)                                                                                                                                       | you (helper)                                                    |
    | `name`                          | human label                                                                                                                                                                                                                   | you                                                             |
    | `kind`                          | see enum below                                                                                                                                                                                                                | you                                                             |
    | `url`                           | public URI you can fetch                                                                                                                                                                                                      | you                                                             |
    | `provider`                      | registered provider slug `^[a-z0-9][a-z0-9-]*$`                                                                                                                                                                               | you (`providers:list`; debut via `surface:add --provider-name`) |
    | `authority`                     | `official` · `provider-claimed` · **`community`** · `registry-observed`                                                                                                                                                       | you → **`community`**                                           |
    | `auth_required` / `public_safe` | boolean                                                                                                                                                                                                                       | you (`false` / `true` for auto-review kinds)                    |
    | `source_urls`                   | array of URIs that **prove** the claim                                                                                                                                                                                        | you (≥1, required in practice)                                  |
    | `review`                        | `{ state, submitted_by?, submitted_at?, confidence?, review_notes? }` — `state` ∈ `community-submitted · maintainer-reviewed · rejected` (HUMAN-governance axis only; machine verify/freshness is the separate probe overlay) | you set `community-submitted`; a maintainer promotes/rejects    |
    | `verification`                  | `{ classification, verified_at, status_code, latency_ms, confidence_score, … }`                                                                                                                                               | **build prober only — never by hand**                           |
    | `schema_url` / `schema_status`  | OpenAPI URL · `machine-readable`/`ui-only`/`not-captured`                                                                                                                                                                     | you (optional)                                                  |
    | `rate_limit`                    | `{ requests, window, burst?, scope?, cost_notes? }` (`requests`+`window` required)                                                                                                                                            | you (optional, integration-only)                                |
    | `auth`                          | `{ scheme, location?, name?, value_format?, … }` — **placeholders only, never a secret**                                                                                                                                      | you (optional)                                                  |
    | `probe`                         | `{ enabled, method, expect, timeout_ms? }` (`method` ∈ GET/HEAD/JSON-RPC/WSS-RPC)                                                                                                                                             | you (optional)                                                  |
    
    **Contributor `kind` enum (11):** `docs · website · source-repo · openapi · subnet-api · dashboard ·
    sse · sdk · example · repo-registry · data-artifact` — all auto-reviewable. Higher-trust within these
    (harder review, airtight ownership proof): authed/paid APIs and unknown providers.
    
    > **`source-repo` and `website` have a native-chain dedup gate.** The build pipeline auto-promotes
    > these kinds from SubnetIdentitiesV3 on-chain data. `validate:surface` will reject any community
    > `source-repo` or `website` surface whose `(kind, netuid, normalized-url)` triple matches a
    > machine-promoted native-chain candidate (`classification: live` or `redirected`). Focus contributor
    > effort on callable surfaces the machine cannot discover: `openapi`, `subnet-api`, `sse`,
    > `data-artifact`, `sdk`.
    
    > **Base-layer chain endpoints** (`subtensor-rpc` / `subtensor-wss` / `archive`) are NOT contributor
    > surfaces — they are maintainer-curated network infrastructure served through the endpoint lane (the
    > `/rpc` proxy + `/api/v1/rpc/*`). They stay valid in the schema (for `registry/subnets/root.json` +
    > the endpoint pipeline) but are excluded from the contributor surface template.
    
    Subnet-level fields you must **not** touch in an existing-manifest community PR: `curation` (`level` +
    `review_state`), `status`, `categories`, `baseline_excluded_*`, `social`, `contact`. Those are
    maintainer/build-owned after the manifest exists. New subnet manifests are the exception: `subnet:new`
    must create the required scaffold fields before the first surface is added.
    
    ---
    
    ## 2. CI — the `Validate` workflow (`.github/workflows/validate.yml`)
    
    **Every contributor PR runs the FULL validation — there is no reduced "ugc" fast-lane.** (It was
    retired: it skipped the safety scans and kept tripping a stale-base preflight false-positive.) A
    one-file surface PR runs the same gates as a code PR. Five parallel jobs (the two Node jobs both
    build):
    
    - **`changes`** — computes docs-fast-lane eligibility for `checks` (see below), plus narrow
      path-scoped flags (`run_workflows_validation`, `run_migrations_validation`, `run_ui_validation`)
      for validators/jobs whose entire footprint is their own directory. Pure inline `git diff`, no
      third-party action.
    - **`test`** — builds, then runs the suite in two non-overlapping passes: `test:ci` (everything
      except the two filesystem-mutating artifact writers, run in parallel, WITH coverage → the single
      Codecov upload) then `test:ci:artifacts` (those two writers, serial). Locally just use
      `npm test` / `npm run test:coverage` (full suite, serial — the config default is race-safe). Does
      **not** participate in the docs fast lane below — coverage is a repo-wide delta gate, not
      diff-scoped, and it isn't the wall-clock long pole regardless.
    - **`checks`** — builds, then lint + format + the ~20 contract/schema/safety validators (below).
    - **`python`** — runs BOTH hermetic Python suites, and is gated on
      `python/**` or `scripts/**.py` changing. Step 1 is the Python SDK's, via `uv run --extra test
    python -m unittest discover -s tests` (the `[test]` extra pulls in httpx so the async cases run);
      the same step runs in `publish-python.yml`'s unprivileged `build` job before the artifact is
      built, so a red suite blocks a PyPI publish. Step 2 is `scripts/test_*.py`, via
      `uv run --no-project python -m unittest discover -s scripts -p 'test_*.py'` — stdlib-only, run
      outside the SDK's project env because it isn't part of that project. **A new `scripts/*.py` needs
      no wiring, but a `scripts/test_*.py` whose subject is deleted now fails at import here** (#9473 —
      four of them sat unrun and broken for two weeks before this step existed). Node-independent, so
      neither adds wall-clock to the long poles.
    - **`ui`** — lint + typecheck + test + three Playwright e2e projects: `responsive-overflow` (**zero tolerance** at
      four widths — nothing may escape the viewport. It was baseline-diffed against
      `overflow-baseline.json` while the app carried the #3930/#3931/#3985 backlog; the v2 rebuild
      emptied that file completely, so #11678 deleted it, its generator and
      `test:e2e:update-baseline` — there is no longer any way to record an overflow bug as accepted), `interaction` (sticky headers, crawlability, offline, deep links) and `token-inventory` (#11605/#11628 — the design-system contract measured in the rendered page: font families (mono everywhere; Plex Sans only on the prose routes in `PROSE_ROUTES`), the seven sizes, `letter-spacing: normal`, one 4px radius, zero pills, no resting shadow, **at most seven `section.mg-section` per route**, **at most one table over 900px inside one section**, and **zero elements carrying a class the v2 purge deleted** — measured at 375 / 768 / 1280 since #11678, because a phone was previously only proven not to OVERFLOW, never to be the same design; absolute, never baseline-diffed),
      build, and bundle-size-budget for `apps/ui` (the TanStack Start/Vite frontend, folded into this
      repo as an npm workspace — #3062), plus a `packages/client/dist` drift check (rebuild fresh,
      `git diff --exit-code` against the committed runtime bundle — #3066/#3294) and, the same way, a
      `packages/ui-kit/dist` drift check plus its own `npm run typecheck --workspace=packages/ui-kit`
      step (`packages/ui-kit` is the design-system component library extracted from `apps/ui` — issue
      #4867's epic).
    
      **A new route must enter the sweep.** `token-inventory-coverage.unit.ts` reads
      `routeTree.gen.ts` and fails on any route that is neither swept
      (`apps/ui/tests/e2e/overflow-check.config.ts` + a HAR fixture), redirect-only, nor named in
      `NOT_SWEPT` with a reason. A route that fetches nothing needs no fixture — list it in
      `NO_API_ROUTES` instead. The two structural exemptions (`SPECIMEN_ROUTES`, `PROSE_ROUTES`) are
      themselves asserted to name only routes that exist and are swept, so an exemption cannot outlive
      its subject. Gated on `run_ui_validation` (`^apps/ui/` **or** `^packages/client/` **or**
      `^packages/ui-kit/` in the diff — `packages/client` and `packages/ui-kit` are both required, not
      optional: each is the only place that verifies its own committed `dist/index.js`/`index.cjs` still
      matches a fresh build, so a `packages/client`- or `packages/ui-kit`-only PR must also trip this
      job or stale/tampered committed runtime code could merge unverified) via the same per-step guard
      pattern `checks` uses for its docs fast lane — never a job-level skip. Entirely independent of the
      backend's own lint/test/build; a backend-only PR touching neither directory doesn't build or
      install `apps/ui`'s tree at all, and vice versa. Not part of the Gittensory contributor gate —
      both `apps/ui/**` and `packages/**` are `blockedPaths` entries in `.gittensory.yml`, maintainer-only.
    
    **The docs fast lane (`checks` only) — narrower than, and does not weaken, the "no reduced ugc
    fast-lane" rule above.** That rule is about _registry/community-surface_ content never getting a
    weaker gate. This is a separate, much narrower thing: when a PR's diff consists entirely of paths
    matching the glob `**/*.md` or `.claude/skills/**/*.md` (pure contributor-facing prose — cannot
    touch registry data, schemas, code, or CI config; a non-`.md` file anywhere, including a hypothetical
    future non-`.md` file under `.claude/skills/`, disqualifies the whole PR), the `changes` job sets
    `docs_only=true` and `checks` skips only its build/contract/registry/deploy-dry-run steps via a
    **per-step** `if: env.DOCS_ONLY != 'true'` guard — never a job-level skip, so `checks` always reports
    a real `success`/`failure` conclusion, never `skipped`. `Lint + format`, `Typecheck` (`tsc --noEmit`,
    added by the TypeScript migration, metagraphed#7510), `validate:docs`,
    `validate:intake`, `scan:public-safety`, and `validate:private-boundary` still run
    unconditionally on every PR, docs-only or not — they're cheap (no build, no network) and are
    exactly what a stray secret, private-boundary leak, or broken doc-contract reference in a
    "docs-only" PR would trip. **Hard guardrail, no exceptions:** any diff
    touching `registry/` forces `docs_only=false` regardless of what else is in the diff — computed as an
    independent override in the same `changes` job step, before the docs-pattern check even runs. This
    exists because the retired "ugc" lane above was scoped to registry/community-surface PRs specifically
    and caused a real stale-base preflight false-positive; registry-touching diffs get zero special
    treatment here. The filter is a plain `git diff --name-only` + `grep` in the trusted workflow — **not**
    `dorny/paths-filter` or any other third-party action: this repo's Actions allowlist
    (`repos/JSONbored/metagraphed/actions/permissions/selected-actions`) only allows GitHub-owned +
    verified-creator actions plus one explicit `peter-evans/create-pull-request` pattern, and
    `dorny/paths-filter` is published by an individual GitHub user (not a GitHub-verified-creator org) —
    using it as-is would hit a `startup_failure`. If a future change wants a real path-filter action, it
    needs an explicit allowlist pattern added via Settings → Actions → General first (a live settings
    change, not something a PR can do).
    
    **Two further narrow, independent skips in the same `changes` job — unrelated to `docs_only`.**
    `Validate workflows` (`npm run validate:workflows`) reads only `.github/workflows/*.yml`/`.yaml`, and
    `Validate migration sequence` (`npm run validate:migrations`) reads only `migrations/neon/*.sql` — each
    verified by reading its script's full source, neither imports anything outside its own directory. The
    `changes` job sets `run_workflows_validation`/`run_migrations_validation` to `true` only when the
    diff touches that specific path, and `checks` gates each validator step on its own flag
    (`env.RUN_WORKFLOWS_VALIDATION`/`env.RUN_MIGRATIONS_VALIDATION`), independent of `docs_only` and of
    each other — a PR can be workflow-only or migration-only without being a docs PR. These are the only
    two `checks` validators with a clean enough path boundary to skip safely; every other validator
    (`validate:schemas`/`api`/`mcp`/`ai`/`openapi`/`types`/`client-sdk-sync`) transitively imports most of
    `src/`+`workers/**` via `workers/api.ts`, so no path glob short of "almost the whole repo" would
    safely exclude them — see the "new artifact/route checklist" in §8 for why a route/handler change can
    trip a contract gate with no lexical hint in the diff. Per-area **test** splitting (e.g. skip
    MCP-specific tests when `src/mcp-server.ts` wasn't touched) was evaluated and rejected: the suite has
    no per-subject directory structure (all 154 files sit flat in `tests/`), a third of it imports
    `workers/api.ts`'s shared router directly, and `vitest.config.ts`'s `fileParallelism: false` exists
    for a filesystem-race reason (see below) unrelated to subject area — splitting by area would need a
    real test-tree/module-boundary refactor, not a CI config change.
    
    **Gates (all must pass):** `lint` · `format:check` · `typecheck` (`tsc --noEmit`, whole-tree, not
    diff-scoped — reads `tsconfig.json`'s own `include`/`exclude`) · `validate:contract-drift` ·
    `validate:schema-enums` · `validate:openapi-examples` · `validate:generated-client` ·
    `validate:graphql-types-drift` (types-epic D, metagraphed#7862; #10214 — `generated/graphql/schema.ts`
    printed from the Zod-built schema, then `generated/graphql/types.ts` from `codegen.ts` against it; run
    `npm run build:graphql-types` and commit both on drift) ·
    `validate:committed-seed` · `npm run build` ·
    committed-derived-artifact freshness (working tree clean
    under `public/` after a fresh build — only CONTRACT artifacts are gated; DATA/CONTENT-derived artifacts
    are NOT: `public/datasets/` + the llms.txt catalogs are gitignored, the README catalog is refreshed
    out-of-band by `readme-catalog-refresh.yml`, and `operational-surfaces.json` is committed-but-excluded —
    adding a probe-enabled operational-kind surface (subnet-api/sse/data-artifact) regenerates the prober's
    input list, which a one-file surface PR does not commit; since #9096 that list is written live to the
    `generated/operational-surfaces.json` R2 store by an hourly Worker cron and the committed file is just
    the prober's cold-start seed) · `validate` ·
    `validate:schemas` · `validate:api` ·
    `validate:mcp` · `validate:ai` · `validate:openapi` · `validate:types` · `validate:artifact-budgets` ·
    `validate:docs` · `validate:contract-doc-sync` (PR-only, diff-scoped: fails when an artifact/route
    description changes what it claims to serve — tier, liveness, provenance — versus the merge base
    while its `docs/backend-artifact-contracts.md` bullet stays byte-identical) ·
    `validate:intake` · `validate:surface` · `validate:workflows` ·
    `validate:revenue-provenance` (relationships the surface schema cannot see: a `revenue.provenance`
    of `probe-derived` on a surface whose `probe.enabled` is not true — or which is `auth_required` —
    did not come from a probe; `operator-attested` must carry a `source_url`; and a
    `payment-collector`/`treasury`/`burn`/`multisig` entity role may **never** sit on a PROTOCOL-DERIVED
    subnet account. That last one is the trap: a subnet's own TAO reserve receives large, continuous,
    many-party inbound because that is what buying alpha looks like, so it presents exactly like a
    team's payment collector and an evidence citation for it would be real while the conclusion stays
    wrong — see metagraphed#10448) ·
    `validate:migrations` (unique, gap-free `migrations/neon` prefixes) ·
    `validate:retired-store-vocabulary` (the PUBLISHED contract may not name a store we deleted:
    zero `\bD1\b` in `public/metagraph/contracts.json` + `openapi.json`, because those strings are
    what API consumers read — they become the generated client's doc comments and 284 docs pages.
    Past-tense narration is allowed only as an EXACT phrase in `ALLOWED_PHRASES`, never a `/retired/`
    carve-out that would also pass "served live from D1 (the retired tier)"; an allowance matching
    nothing published fails too, so the exemption list cannot outlive its text) ·
    `validate:pg-json-binds` (type-directed: a SQL bind whose static type is an array or a structured
    object is REINTERPRETED by node-postgres — an array becomes a Postgres array literal `{"a","b"}`,
    which the column's own `JSON.parse` reader degrades to null on every read, the #10179 bug. Bind the
    JSON text explicitly via `jsonColumn`, or cast the parameter — `$1::text[]`/`UNNEST(...)` — if the
    statement really takes an array) ·
    `cloudflare:verify:dry-run` · r2/kv dry-runs · `worker:deploy:dry-run` · `worker:bundle:budget`
    (gzip-measures the `wrangler deploy --dry-run` Worker bundle against a budget so an over-1MiB bundle
    fails at PR time, not at the Cloudflare deploy) · `scan:public-safety` · `validate:private-boundary`.
    
    Codecov is configured in `codecov.yml`: `codecov/patch` enforces **99% patch coverage, branch-counted**
    (`target: 99%, threshold: 0%`, near-zero slack) on every changed line in `src/**`/`workers/**`;
    `codecov/project` is informational only (`target: auto`, `threshold: 1%`). Run
    `npm run test:coverage` locally for the full-suite number. CI uploads coverage once, from the
    `test:ci` pass — the two artifact writers run via child processes and contribute no in-process
    coverage, so splitting them out is coverage-neutral.
    
    ---
    
    ## 3. The Gittensory Gate — auto-MERGE / auto-CLOSE / MANUAL (not advisory)
    
    The review gate is **gittensory** (the old "reviewbot" was converged into gittensory 2026-06-22). It
    posts `Gittensory Gate` + `Gittensory Context` checks and acts on **contributor** PRs with autonomy:
    
    | Condition                                                                                                                                | Disposition                          |
    | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
    | Both AI reviewers confidently approve (**≥0.9**) verified + owner-matched + fresh + netuid-grounded content, CI green, mergeable-clean   | **auto-MERGE**                       |
    | **Deterministic fail** — duplicate surface, placeholder, private/localhost URL, secret, dead `source_url`                                | **auto-CLOSE**                       |
    | **Every** reviewer returns a clear reject                                                                                                | **auto-CLOSE**                       |
    | Any CI check failed                                                                                                                      | **CLOSE** (cites the failing check)  |
    | Legitimate but uncertain — a reviewer < 0.9, a reviewer said `manual`, reviewers split, owner-mismatch, stale repo, unfetchable evidence | **MANUAL** (held, never auto-closed) |
    | CI pending / unverified fork run                                                                                                         | no action — waits                    |
    
    **Content bar** (benchmarked strict): official/primary sources wherever possible, 100% verifiable, the
    `url` owner must match the subnet's registered identity, source repo fresh, no prompt-injection in
    fetched or submitted text. Make the `source_url` an _independent_ proof of ownership.
    
    **Linked issues are required and are a gate.** A PR with **no linked issue**, or one linked to an
    issue that's already **closed**, is auto-closed on that basis alone — before content is even scored.
    Link an **open** issue (`Closes #<n>`) and the gate verifies the PR against that issue's intent,
    clause by clause. (What the gate does with a linked issue is configured in the gittensory system,
    **not** in this repo.)
    
    The gate's private scoring rubric/thresholds must **never** appear in this repo —
    `validate:private-boundary` fails CI if they do. Keep gate heuristics in the gittensory system only.
    
    ---
    
    ## 4. npm scripts you'll actually use
    
    | Need                                      | Command                                                                                                                                                                                                                                                                                                                                                                                                                   |
    | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | Find the data gaps                        | `npm run curation:brief` (`-- --limit 20`, `-- --json`)                                                                                                                                                                                                                                                                                                                                                                   |
    | List / register providers                 | `npm run providers:list` (debut a new provider via `surface:add --provider-name`)                                                                                                                                                                                                                                                                                                                                         |
    | Add a community surface to a subnet file  | `npm run surface:add -- --netuid … --kind … --url … --source-url … --provider … --submitted-by … --write` — debut provider: add `--provider-name "…" --provider-url …` (the `website_url`, **must be a public URL**) and it scaffolds the provider stub too                                                                                                                                                               |
    | Scaffold a brand-new subnet file _(new)_  | `npm run subnet:new -- --netuid <n>`                                                                                                                                                                                                                                                                                                                                                                                      |
    | Validate a surface contribution _(new)_   | `npm run validate:surface -- registry/subnets/<slug>.json`                                                                                                                                                                                                                                                                                                                                                                |
    | Public-safety scan                        | `npm run scan:public-safety`                                                                                                                                                                                                                                                                                                                                                                                              |
    | Code/schema: regenerate the contract      | `npm run build`                                                                                                                                                                                                                                                                                                                                                                                                           |
    | Code/schema: typecheck _(new)_            | `npm run typecheck` (`tsc --noEmit`; `src/`+`workers/`+`scripts/`+`tests/` are **all `.ts`** — the migration epic metagraphed#7510 is complete, and `validate:no-hand-written-mjs` fails CI if a new `.mjs`/`.js` appears under a covered directory)                                                                                                                                                                      |
    | Code/schema: validators                   | `npm run validate` · `validate:schemas` · `validate:api` · `validate:openapi` · `validate:types` · `validate:contract-drift` · `validate:single-schema-source` · `validate:schema-opacity` · `validate:schema-vocabularies` · `validate:mcp` · `validate:ai` · `validate:docs` · `validate:contract-doc-sync` · `validate:intake` · `validate:workflows` · `validate:pg-json-binds` · `validate:retired-store-vocabulary` |
    | Tests / coverage                          | `npm test` · `npm run test:coverage`                                                                                                                                                                                                                                                                                                                                                                                      |
    | Full local pipeline (after a clean build) | `npm run pipeline:check`                                                                                                                                                                                                                                                                                                                                                                                                  |
    
    > `surface:add`, `subnet:new`, and `validate:surface` are the single-file-model commands. They fully
    > replaced the retired `candidate:new` / `validate:candidate` intake lane — and `surface:add`
    > live-verifies the URLs at add-time (probes reachability, fills openapi schema fields) and
    > auto-scaffolds a debut provider stub. Providers are flat objects in
    > `registry/providers/*.json` (trust is the `authority` field, not a directory —
    > there is no `providers/community/` subdir).
    
    ---
    
    ## 5. Anti-farming rules (why this model exists — do not work around them)
    
    - **One subnet = one file = one PR.** Add all of a subnet's new surfaces in a single diff to its one
      file. That is one merge — correct and complete.
    - **Never split** a subnet's surfaces across multiple PRs to inflate merge count. The gate dedups
      within the file and closes redundant/split PRs.
    - **Never re-title** the same surface as a different `kind`, provider, or subnet to dodge dedup. The
      gate compares the actual file diff, not the PR title.
    - **Never pad** — no docs/website surfaces invented to bulk a PR, no generated-artifact noise.
    - **Don't duplicate machine-promoted native-chain surfaces.** `validate:surface` loads
      `registry/candidates/generated/public-sources.json` + `registry/verification/promotions.json` at
      start-up and rejects any community surface whose `(kind, netuid, normalized-url)` triple matches a
      native-chain candidate already classified `live` or `redirected`. These surfaces are auto-promoted by
      `generateBaselineOverlaySet` — a community submission adds no signal and will fail CI.
    - A contribution's value is the **verified surface**, not the PR. Low-effort / bulk-generated /
      no-real-surface PRs are closed.
    
    ---
    
    ## 6. Commits & PR text
    
    **Commit (Conventional):** `type(scope): summary` — types `feat fix test docs refactor build ci chore
    revert`; lowercase specific scope (`registry api mcp schema build ci docs …`); no trailing period; not
    a bare generic word; **no AI/Claude/agent mention**. Examples:
    
    ```
    feat(registry): add SN43 Graphite subnet-api surface (#1623)
    feat(registry): enrich SN15 ORO — openapi + data-artifact surfaces (#1280)
    fix(health-serving): stamp merged RPC endpoint observed_at with sweep time (#1612)
    ```
    
    **PR body:** GitHub pre-fills `.github/pull_request_template.md`. Fill it — don't replace it: a real
    `## Summary`, the `url` + `source_url` proof (Path A) or the validation commands you ran (Path B), and
    **`Closes #<issue>`** — required, and the issue must still be open (a missing or already-closed link
    fails the PR on its own). No local paths, env dumps, or private notes.
    
    ---
    
    ## 7. What gets a PR closed / routed to manual
    
    - More than the one subnet file touched (generated artifacts, scripts, workflows, a second subnet).
    - A `source_url` that 404s or doesn't back the claim; an invented/unpublished surface.
    - A duplicate of an existing surface or an open PR; the same surface re-titled by `kind`.
    - A community `source-repo` or `website` surface whose URL the machine already promotes from
      SubnetIdentitiesV3 — `validate:surface` rejects it (CI fails → gate closes).
    - Secrets/PATs/wallet paths, private/localhost URLs, real credentials in `auth`.
    - Hand-set health/uptime/`verification` (probe-derived only).
    - An external-contributor visual `apps/ui/**` change with no before/after screenshot table in the PR
      body (or one pasted outside the table), unless a repository maintainer explicitly waived that
      evidence for the current task — see Path C in `SKILL.md`. The waiver never skips code-quality or CI
      gates and must not be inferred.
    - Editing the contract by hand without `npm run build` (contract-drift), or stale committed artifacts.
    - Committing generated artifacts — `public/datasets/*` or any `public/metagraph/*` outside the reviewed
      contract (regenerated on build/deploy; `ci-verify-submitted-artifacts` rejects them).
    - Bundling `public/metagraph/r2-manifest.json` or `public/metagraph/schemas/index.json` into the diff —
      even on a Path A surface PR. `npm run build` always rewrites both locally; they are deploy/publish-
      pipeline-owned (see §8) and the gate's registry-review lane treats their presence as "bundling other
      file changes" outside the one subnet file. Revert them before committing — see §8 for the exact
      command.
    
    ---
    
    ## 8. Code/schema gotchas (Path B)
    
    - **Schema-first:** edit the Zod schema under `schemas-src/` → `npm run build` → commit `openapi.json` +
      types/clients. `validate:contract-drift` + `validate:schema-enums` + `validate:committed-seed` guard it.
    - **`schemas-src/` is the ONLY source of published components (#9830).** There is no hand-written
      JSON Schema layer any more — `schemas/components/`, `schemas/api-components.schema.json` and
      `scripts/bundle-schemas.ts` are deleted. `schemas/*.json` still exists and is a different thing:
      the INPUT schemas that validate hand-written registry files (`subnet-manifest`, `provider`,
      `candidate-surface`, `saved-query`, `entity`, `provider-submission`, `public-artifacts`). Those are
      never published as OpenAPI components.
      - Layout: `schemas-src/routes/*.ts` (one file per REST route family, each exporting an
        `<X>ArtifactSchema`), `schemas-src/artifacts/*.ts` (build-published artifacts with a
        `contracts.json` entry but no route — surface-aliases, operational-surfaces, schema-drift,
        r2-manifest, review intake, verification, the two health artifacts),
        `schemas-src/mcp-tools/*.ts` (MCP tool input/output), and the shared leaves in `envelope.ts` /
        `shared.ts`.
      - **A component is published only if it is `register()`ed** in `schemas-src/openapi-registry.ts`.
        `scripts/generate-openapi-zod-components.ts` runs `z.toJSONSchema()` against that registry and
        `scripts/openapi-components.ts` puts the result straight into `components.schemas`.
      - **`validate:single-schema-source` is the permanent lock.** It fails if `schemas/components/`,
        `schemas/api-components.schema.json` or `scripts/bundle-schemas.ts` reappears, if ANY JSON under
        `schemas/` declares a `components.schemas` map, or if the set of names the registry declares and
        the set the published document carries are not equal. That last check is the load-bearing one: it
        catches a second source of any shape, not just the one that existed before.
      - **Register every named leaf, not just the artifact root** (PR #8054 review caught this the hard
        way): `z.toJSONSchema(..., { reused: "inline" })` only keeps a schema as its own
        `components.schemas.X` entry when X is separately registered. An unregistered leaf is silently
        inlined and its name vanishes from `packages/contract/index.d.ts` — a real public-contract
        regression. After regenerating, diff `packages/contract/index.d.ts` against `origin/main` and
        confirm no component name was lost. `validate:single-schema-source`'s set-equality check now
        catches the inverse (a registered name that never reaches the document).
    - **A vocabulary has ONE owner (#9799).** A string-literal list of three or more members may appear
      in only one file under `schemas-src/`. Export the value set once from the module that owns it and
      import it everywhere else; where the owner is a `Record`, derive the tuple from its keys so the two
      cannot disagree. `validate:schema-vocabularies` fails on a new copy and on a stale allowlist entry
      alike; the allowlist names vocabularies whose owner has not been extracted yet and may only shrink.
    - **An open object must be a REVIEWED choice, not an accident (#9797).** Every object site in
      both published contracts is one of: fully typed properties, a typed record (dynamic keys with a
      declared VALUE schema), or explicitly open with a written reason listed in
      `scripts/validate-schema-opacity.ts`. `validate:schema-opacity` fails on an unlisted open site AND
      on an allowlist entry that no longer matches one, so the list cannot rot into a blanket exemption.
      The MCP side's remaining entries carry the `NOT_YET_TYPED` reason — standing debt against #9797,
      not a decision; that list may only shrink.
      - **`validate-schema-enums.ts` reads `public/metagraph/openapi.json`**, the published document —
        the right place for anything that needs component _content_.
    - **Client SDK version: do NOT bump in your PR.** `packages/client/package.json` is versioned by the
      post-merge `sync-client-version` workflow, which auto-opens a `chore/sync-client-version` PR whenever
      a contract file lands on main. `validate:client-sdk-sync` emits a notice (not a failure) either way:
      when the version isn't bumped in a contributor PR (expected — automation handles it), and _also_ when
      a contributor bumps it themselves anyway (unnecessary — the workflow's own diff-since-last-bump check
      doesn't look for a manual bump, only for contract-file changes since its last `chore(client): bump
    SDK` commit, so a hand-bump here is redundant at best and a conflicting version at worst once the
      auto PR lands).
    - **MCP server version: do NOT bump in your PR, same as the client SDK above.** `MCP_SERVER_VERSION`
      (`src/mcp-server.ts`) and `server.json`'s `"version"` are versioned by the post-merge
      `sync-mcp-version` workflow — same shape as `sync-client-version`, watching `src/mcp-server.ts` since
      its last `chore(mcp): bump server version` commit and bumping both files together when a tool was
      added/changed. `validate:mcp` only checks _internal_ consistency (`MCP_SERVER_VERSION` ==
      `serverInfo.version` == `server.json`'s version) — it stays green regardless of what number those
      hold, so a PR that adds an MCP tool without touching either file passes CI fine; the workflow is what
      actually advances the number, entirely after the fact. A contributor hand-bumping either file is pure
      unrewarded toil (and setup for a merge conflict with the auto-opened `chore/sync-mcp-version` PR) —
      flag it in review the same way as a manual client-SDK bump.
    - **Zod-owned MCP tool schemas (types-epic E, #7863) live in `schemas-src/mcp-tools/`, one file per
      tool.** For the tools this covers (currently the pilot batch: `search_subnets`, `list_subnets`,
      `get_subnet`, `get_network_health`, `get_subnet_stake_quote`, `get_economics`), the hand-written
      `inputSchema`/`TOOL_OUTPUT_SCHEMAS` object literals in `src/mcp-server.ts` (or, for a couple of
      tools, `src/global-operational-health.ts` / `src/network-economics.ts`, wherever that tool's
      `..._MCP_TOOL`/`..._OUTPUT_SCHEMA` const actually lives) are replaced with
      `z.toJSONSchema(FooSchema, { target: "draft-2020-12" })`, computed once at module load — never a
      build step, unlike types-epic B's OpenAPI components: MCP tool schemas are served live by the
      running Worker, there's no committed artifact to regenerate. Where a tool mirrors a REST route
      already covered by `schemas-src/routes/`, reuse that module's schema for the OUTPUT shape ONLY if
      the hand-written MCP schema was ALSO that deep/strict (verify field-for-field — several "mirrors"
      tools deliberately shipped a shallower contract than their REST counterpart, e.g. `subnets:
    {type:"array", items:{type:"object"}}` with no per-row property constraints; reusing the REST
      route's own `.strict()` schema there would be a real tightening, not a wire-compatible relocation).
      `MCP_TOOLS`'s array needs an explicit `McpToolDefinition` interface annotation (already added) for
      this to typecheck at all — without it, TypeScript infers the array's element type by intersecting
      every entry's `handler` parameter (contravariant), which collapses into an unsatisfiable type the
      moment more than one entry declares a specific (non-`Row`) `args` type; the explicit interface uses
      bivariant parameter checking for the method-shorthand syntax every tool already uses instead. The
      one-time conversion audit that guarded this (`npm run diff:mcp-tool-schemas`) was **retired in
      #8639**: it compared generated schemas against a frozen, edit-forbidden transcription of the
      pre-conversion literals, so every later feature that legitimately added a parameter or corrected a
      type made it fail. `npm run validate:mcp` is the living check — it exercises all 205 tools, the
      lifecycle, and both subscribe→notify round trips against the real handler.
    - **`packages/client` is an npm workspace (#3066), with no lockfile of its own.** `apps/ui` consumes it
      as a live workspace link (`"@jsonbored/metagraphed": "*"` in `apps/ui/package.json`, resolved from
      `packages/client` directly) instead of round-tripping through the published npm package. Verified
      this does NOT silently fall back to a registry-fetched copy even when installing from directly
      inside `apps/ui` (`cd apps/ui && npm install`, no `--workspace` flag) — npm still walks up to the
      root `package.json`'s `workspaces` field and links `node_modules/@jsonbored/metagraphed` as a real
      symlink to `packages/client`, matching a root-scoped `npm ci --workspace=apps/ui` install exactly
      (confirmed by identical package counts and a real symlink check). Editing
      `packages/client/src/*` and rebuilding (`npm run build --workspace=packages/client`) is immediately
      visible to `apps/ui`, no publish needed. `packages/client`'s own `typescript` devDependency must stay
      aligned with the root/`apps/ui` range (`^5.9.3`): `tsup` (its build tool) is hoisted to the _root_
      `node_modules` and resolves `typescript` from there regardless of which workspace invokes it, so a
      workspace-local TypeScript version pin silently gets ignored by `tsup --dts` — don't reintroduce one.
      `packages/client`'s `dist/index.js` + `dist/index.cjs` (the RUNTIME bundle ONLY) are committed —
      an explicit, narrow exception to the root `.gitignore`'s blanket `dist` rule, carved out in
      `packages/client/.gitignore`. This is deliberate, not an oversight: Cloudflare Workers Builds'
      actual production deploy hit three consecutive, unreproducible-locally failures trying to build
      `packages/client` at deploy time (a `vite`-hoisting split, then `--workspace=` failing from a
      non-root cwd, then `tsup` itself not resolving via PATH in whatever cached state Cloudflare had
      restored) — each fixed in turn, but the pattern kept recurring because this repo's own environment
      could never reproduce Cloudflare's exact caching/hoisting behavior to verify a fix with full
      confidence. Committing the runtime bundle removes the entire failure class: the live deploy never
      builds `packages/client`, it just uses what's checked into git via the workspace symlink.
      **`dist/index.d.ts`/`dist/index.d.cts` (the type declarations) stay gitignored, built fresh by
      CI/local dev only** — confirmed directly that `vite build` doesn't need them at all (esbuild strips
      types without resolving them; a real build with the `.d.ts` files absent succeeds unchanged), and
      they're the ~1.1 MB majority of this package's output, growing with every new API route added
      anywhere in the backend — committing them would reintroduce the exact diff-churn-on-every-
      contract-change problem committing the runtime bundle was meant to avoid (this repo gets hundreds
      of contributor PRs; anything that churns on unrelated changes is a real ongoing cost, not a one-off
      annoyance). **After editing `packages/client/src/*`, you must run
      `npm run build --workspace=packages/client` and commit the resulting `dist/index.js`/`index.cjs`
      in the same PR** — the `ui` CI job's "Build packages/client (drift check)" step rebuilds fresh and
      fails loudly (`git diff --exit-code`) if the committed copy doesn't match; `git diff` only
      considers tracked files, so the gitignored `.d.ts` is naturally excluded from that check with no
      extra scoping needed. Neither `apps/ui`'s own scripts nor Cloudflare's Build command need to build
      `packages/client` at all anymore — do NOT reintroduce that (a
      `(cd ../../packages/client && npm run build)` step, a `prepare` lifecycle script, etc.); it only
      reintroduces the exact fragility this commits-the-artifact approach was built to eliminate.
      Deliberately NOT a package.json "prepare" script even for the drift-check purpose: that would
      auto-run on every `npm install`/`ci` repo-wide, which a security scan already flagged once as
      unnecessary install-time code execution (#3066 review).
    - **`packages/ui-kit` is an internal design-system component library extracted from `apps/ui`
      (issue #4867's epic), following the identical committed-dist pattern as `packages/client` above:**
      `dist/index.js`, `dist/index.cjs`, and `dist/index.css` are committed — the same `.gitignore`
      exception, same reasoning (`apps/ui` consumes it as a live workspace link, and Cloudflare Workers
      Builds must never depend on rebuilding a sibling workspace package) — while
      `dist/index.d.ts`/`dist/index.d.cts` stay gitignored, built fresh by CI/local dev only. **After
      editing `packages/ui-kit/src/*`, you must run `npm run build --workspace=packages/ui-kit` and
      commit the resulting `dist/index.js`/`index.cjs`/`index.css` in the same PR** — the `ui` CI job's
      "Build packages/ui-kit (drift check)" step rebuilds fresh and fails loudly (`git diff --exit-code`)
      if the committed copy doesn't match, same as the `packages/client` drift check above. The job also
      runs dedicated `npm run typecheck --workspace=packages/ui-kit` ("Typecheck packages/ui-kit") and
      `npm test --workspace=packages/ui-kit` ("Test packages/ui-kit") steps. **A separate "Lint
      packages/ui-kit (app-logic import guardrail)" step (#4865) enforces that the package stays a real,
      standalone library** — `packages/ui-kit/eslint.config.ts`'s `no-restricted-imports` rule fails on
      any import of `@tanstack/react-router`, `@tanstack/react-query`, or anything resolving into
      `apps/ui/**`. If a component genuinely needs routing/data, accept it as a prop from the caller
      instead of reaching for app infrastructure — that's the exact regression this package's extraction
      exists to prevent.
    - **`packages/contract` is a types-only npm workspace (#3067) holding the OpenAPI-derived contract
      types** — `openapi-typescript`'s output (`scripts/generate-types.ts`/`validate-types.ts`/
      `validate-contract-drift.ts` all write/check `packages/contract/index.d.ts` now, no longer
      `generated/metagraphed-api.d.ts`, which no longer exists). `packages/client` depends on it as a
      real `devDependency` (`"metagraphed-contract": "*"`) and imports `type { components, paths } from
    "metagraphed-contract"` directly — no more copying it into `packages/client/src` first (unlike
      `generated/metagraphed-client.ts`, the hand-templated SDK helper logic, which is unrelated contract
      content and still gets copied there exactly as before). **`packages/client`'s build command MUST
      keep the `--dts-resolve` flag** (`tsup ... --dts --dts-resolve ...`): without it, `tsup` leaves a
      bare `import { components, paths } from 'metagraphed-contract'` in the PUBLISHED package's own
      `dist/index.d.ts`/`index.d.cts` instead of inlining the 1.1 MB of actual type content — since
      `metagraphed-contract` is `"private": true` and never published to npm, every external SDK consumer's
      TypeScript compiler would fail outright trying to resolve that import. Verified directly: without
      `--dts-resolve`, the published output shrinks from ~1.13 MB to ~8.7 KB (the tell that nothing got
      inlined) and literally contains that import line; with it, the output is back to ~1.13 MB with zero
      occurrences of `metagraphed-contract` anywhere in it. `packages/contract` needs no build step of its
      own (no runtime code, nothing to bundle) and is a required trigger for the `ui` CI job's drift check
      same as `packages/client/**` (see the `changes` job comment above) — a schema-only PR can regenerate
      `packages/contract/index.d.ts` (caught by `checks`' `validate:contract-drift`) without anyone
      rebuilding+committing `packages/client/dist` to match, and only that job's drift check would catch it.
    - **`vite` must stay an explicit ROOT-level devDependency**, even though the backend never imports it.
      Cloudflare Workers Builds' automatic dependency-install step runs scoped to `--workspace=apps/ui`
      only (never a full monorepo install — confirmed by matching package counts against a real Cloudflare
      build log, ~470 vs. a full install's ~560), which never touches root's own devDependencies. Without
      `vite` declared at root, nothing gives npm a reason to hoist `apps/ui`'s own `vite` up to the bare
      root `node_modules` during that scoped install, so anything ALSO hoisted to root with only a _peer_
      (not direct) range on vite — e.g. `@lovable.dev/vite-tanstack-config`, which `vite.config.ts` needs —
      can't find it (`Error: Cannot find module 'vite'`, real Workers Builds failure, #3183). A worktree
      nested under the main checkout can mask this locally: Node's resolution silently falls back to a
      stray `node_modules/vite` in an ancestor directory outside the repo, so a real reproduction needs a
      genuinely isolated clone (no parent `node_modules` anywhere in its ancestry) plus the exact
      `npm ci --workspace=apps/ui` command Cloudflare runs — a plain full `npm ci`/`install` won't surface
      this class of bug at all.
    - **MCP server card is worker-computed — no committed artifact.** Adding or changing tools in
      `src/mcp-server.ts` does NOT require regenerating `public/.well-known/mcp/server-card.json` (that
      file no longer exists in git). The card is served dynamically by `mcpServerCardResponse` in
      `workers/request-handlers/discovery.ts`.
    - **New `/api/v1` route or artifact** trips hidden gates depending on whether it's committed
      (DUAL_PATTERNS), live-only D1 (R2_ONLY_PATTERNS, plus `COMPUTED_LIVE` on the route's own
      `artifact()` entry in `src/contracts.ts` when it writes no static file — `validate:schemas`
      derives its skip set from that flag and otherwise expects a file on disk), or `/.well-known`
      worker-computed. Mirror an existing route end-to-end; the build's derived-artifact freshness gate
      fails if a committed `public/metagraph/*` is stale. It also trips gates the "new route" framing
      above doesn't cover, caught live 2026-07-18 shipping two routes with none of these:
      `scripts/validate-api.ts`'s own `checks` array needs a matching `[route, assertion]` entry (it
      asserts `checks.length === API_ROUTES.length`, a **live RPC call** against real finney, not a
      stub); codecov/patch (99%, branch-counted) needs GraphQL/MCP test coverage in the _centralized_
      `tests/graphql.test.ts`/`tests/mcp-server.test.ts` files specifically, not just a per-feature
      test file — a per-feature file alone leaves the GraphQL resolver and MCP tool handler at 0% patch
      coverage even with 100% coverage on the underlying `src/*.ts` module; and the `ui` CI job's
      "Build API reference docs (drift check)" step (added 2026-07-18, mirrors the
      `packages/client`/`packages/ui-kit` drift checks just above it) fails if `apps/ui/content/docs/
    api-reference/**` wasn't regenerated — run `node scripts/generate-openapi-docs.ts` from
      `apps/ui/` and commit the result alongside the contract change. The same job runs
      the same drift check for `content/docs/catalog.mdx` (`generate-catalog-docs.ts`) and
      `content/docs/limits.mdx` (`generate-limits-docs.ts`, metagraphed#8610 — generated from the five
      live `*_TIERED_RATE_LIMIT` configs, so a rate-limit ceiling changed without regenerating fails
      CI rather than leaving callers planning against a number we no longer enforce). Two more, caught live 2026-07-18
      shipping a templated computed artifact: `src/contracts.ts` has **two separate registries** for a
      route with no static file — a `route(...)` entry (API surface metadata) AND a distinct
      `artifact(id, path, description, schemaName)` entry (the schema-ref mapping); omitting the
      `artifact()` call makes `npm run build` throw `No public artifact contract maps API artifact
    <path>` from `schemaRefForArtifactPath`, since `route()`'s own `artifactPath` argument is just a
      label, not a registration. And the local pre-push guard's `validate:docs` check requires a
      matching prose entry in `docs/backend-artifact-contracts.md` (mirror an existing bullet like the
      `subnet-burn` one) for every artifact path — easy to miss since nothing else in the schema/route
      wiring references this file. **Two more registrations, both caught live 2026-07-30 on a
      computed artifact (metagraphed#8761 shipped missing both):** pass `COMPUTED_LIVE` as
      `artifact()`'s 5th argument (metagraphed#8773 replaced `validate-schemas.ts`'s hand-maintained
      `COMPUTED_ARTIFACTS` Set with this structural flag) — a live-computed artifact without it is
      expected to exist as a file, and validate:schemas dies with a bare `ENOENT: ...
    public/metagraph/<x>.json` that names neither the cause nor the fix; and `src/artifact-storage.ts`'s `R2_ONLY_PATTERNS` — an
      artifact matching no explicit R2-only or dual pattern falls through to the default-git tier, which
      `tests/artifact-tiering-explicit.test.ts` fails as the #998 mis-tiering landmine. Adding the
      `R2_ONLY_PATTERNS` entry also flips `storage_tier` in `contracts.json`/`api-index.json`, so
      rebuild and commit those in the same PR.
    - **Reader tests** serve R2-only artifacts that only exist after `npm run build` — build before the
      suite if a test reads served artifacts.
    - **Never commit `public/metagraph/r2-manifest.json` or `public/metagraph/schemas/index.json`.**
      `npm run build` fully populates R2 staging (per ADR-0001) and rewrites both to reflect that local/CI
      build, but their committed copies on `main` reflect the last real deploy/publish — not a local build —
      for reasons unrelated to your change: `r2-manifest.json` is publish infrastructure read from its
      committed path by `scripts/kv-publish-pointer.ts` / `scripts/cloudflare-verify.ts` /
      `scripts/sync-summary.ts` during the actual publish, and its `*_artifact_size_bytes` totals are
      inherently non-deterministic build-to-build; `schemas/index.json` is a network-capture cache the build
      "reconciles in place", and since #9096 its live copy lives in the `generated/schemas-index.json` R2
      store written daily by a Worker cron (`src/schema-snapshots-sync.ts`) — the committed file is only the
      fallback seed a credential-less build reads. Both are explicitly excluded from the derived-artifact freshness gate in
      `.github/workflows/validate.yml` (see the comment above that step) — CI won't catch this, but the
      Gittensory Gate's registry-review lane will reject a PR that bundles them in. After `npm run build`,
      revert them against your **base** remote — `upstream/main` if you forked per Phase A0, or
      `origin/main` if you cloned this repo directly (no `upstream` configured):
      `git checkout "$(git remote | grep -qx upstream && echo upstream || echo origin)/main" -- public/metagraph/r2-manifest.json public/metagraph/schemas/index.json`.
      `npm run build` itself prints a non-fatal warning if either changed, with the same command.
    - **`format:check`:** `main` is not fully prettier-clean — never `prettier --write` whole files you
      didn't change; format only your own lines.
    - **`pipeline:check`** is only trustworthy in isolation after a clean `npm run build`.
    - **`validate.yml`'s `actions/setup-node` steps set `cache-dependency-path: package-lock.json`
      explicitly.** Without it, `setup-node`'s cache key hashes every `package-lock.json` in the tree
      (root only, since #9353 deleted the second), so a stray lockfile would invalidate the CI npm cache even though
      `npm ci` in `validate.yml` only ever reads the root lockfile. `packages/client` is an npm workspace
      with no lockfile of its own — its version bumps land in the root lockfile, already covered by this
      path. If you ever add a new `actions/setup-node` step to a workflow in this repo, set this
      explicitly rather than r
  • SKILL.md 41 KB
    ---
    name: metagraphed
    description: >-
      Use when writing, validating, or preparing ANY contribution or pull request to the
      JSONbored/metagraphed repo — adding/enriching a subnet's public surfaces (the most common
      contribution), a code/schema change to the Worker API or build scripts, picking an issue,
      running the local gates, and formatting the commit + PR. metagraphed reviews PRs ONE-SHOT via
      the Gittensory Gate (the GitHub App that auto-merges/auto-closes) plus a strict CI suite; there
      is no review back-and-forth, so a PR must be correct, in-scope, and green before it is pushed.
      Surfaces live in ONE file per subnet (registry/subnets/<slug>.json) — never per-surface
      candidate files, never split across multiple PRs. Also covers frontend PRs against apps/ui/
      (the web app, folded into this repo via monorepo consolidation) — external-contributor visual
      changes require a before/after screenshot table and are held for manual review; explicitly
      maintainer-directed work may use the documented override. Invoke for any "contribute to / open a
      PR against / enrich a subnet in / add a surface to / fix a bug in / add a frontend feature to
      metagraphed" task.
    ---
    
    # Contributing to metagraphed — the one-shot PR playbook
    
    metagraphed is the Bittensor subnet **integration registry** — every subnet, metagraphed. The repo
    is a Cloudflare Worker API + Node build scripts; **JSON Schema is the canonical contract** (→ OpenAPI
    → typed clients), and everything under `public/metagraph/` is a _generated projection_ of reviewed
    source, never hand-authored truth.
    
    It merges through an **automated, one-shot review**: the **Gittensory Gate** (a GitHub App that posts
    `Gittensory Gate` + `Gittensory Context` checks and a single verdict) plus a **strict CI suite**
    (`Validate`). There is no human ping-pong and no "fix it in review" — **the PR must be right before
    you push.** This skill is the end-to-end procedure to make that happen with AI tools (Claude Code /
    Codex).
    
    Work through the phases **in order** for your contribution type. If you cannot get the local gate
    green, **do not push** — an incomplete PR is auto-closed or held, not coached.
    
    `reference.md` (next to this file) has the exhaustive tables — every CI check, the surface schema,
    the `kind` enum, the gate disposition, the validator list, the commit/PR rubric. Read it when a phase
    says to.
    
    **Zero-setup environment:** if you're operating in a devcontainer-aware tool, open the repo there —
    `.devcontainer/devcontainer.json` pins Node 22 and preinstalls Playwright's Chromium (needed for
    Phase C2's screenshot contract), so `npm install` is the only remaining step. Otherwise `.nvmrc` at
    the repo root pins Node 22 for `nvm use`.
    
    ---
    
    ## Three kinds of contribution — pick your path
    
    | You are…                                                                                                      | Path                                             | Files you touch                                                                 |
    | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------- |
    | **Adding or enriching a subnet's public surfaces** (API, OpenAPI, docs, repo, dashboard, SDK, data artifact…) | **Path A — Surface contribution** (Phases A0–A5) | **exactly one** `registry/subnets/<slug>.json`                                  |
    | **Changing code, schemas, or build scripts** (Worker API, `schemas/`, `scripts/`, workflows)                  | **Path B — Code/schema PR** (Phases B0–B5)       | `src/`, `workers/`, `schemas/`, `scripts/`, `.github/`, + regenerated artifacts |
    | **Fixing a bug or shipping a feature in the web app** (block explorer, docs pages, dev tools)                 | **Path C — Frontend PR** (below)                 | `apps/ui/**` only                                                               |
    
    Most contributions are **Path A**. Do **not** mix any of the three in one PR.
    
    ---
    
    ## What the gate does to your PR — it merges and closes, automatically
    
    The Gittensory Gate is **not advisory**. Once your checks settle, for a **contributor** PR (you are
    not the repo owner or an automation bot) it takes a one-shot disposition:
    
    | Situation                                                                                                                                                         | Gate action                         |
    | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
    | Content **verified** (owner-matched, fresh, grounded) + **both** AI reviewers confidently approve (≥0.9) + CI green + mergeable-clean                             | **auto-approve → MERGE**            |
    | A **deterministic fail** — duplicate surface, placeholder, private/localhost URL, secret, dead `source_url`                                                       | **CLOSE** (one-shot)                |
    | **Every** reviewer returns a clear reject                                                                                                                         | **CLOSE** (one-shot)                |
    | Any CI check failed                                                                                                                                               | **CLOSE** (cites the failing check) |
    | Legitimate but **uncertain** — a reviewer wanted merge but under 0.9, a reviewer said `manual`, reviewers split, owner-mismatch, stale repo, unfetchable evidence | **MANUAL** (held, not closed)       |
    | CI still pending / unverified fork run                                                                                                                            | **no action** — waits               |
    
    So a flawed contributor PR is **closed, not coached** — recovery means fixing the problem and opening
    a **fresh** PR. **Verified + green ⇒ merged; a clear adverse signal ⇒ closed; genuine uncertainty ⇒
    held for a human.** (Owner / automation-bot PRs are exempt from auto-close — but assume you are a
    contributor.)
    
    ---
    
    ## The non-negotiables (read once, hold throughout)
    
    1. **One subnet = one file = one PR.** A surface contribution edits **exactly one**
       `registry/subnets/<slug>.json` and **nothing else** (no generated artifacts, no scripts, no other
       subnet). You may add **several surfaces for that one subnet in the same diff** — that is one merge,
       the way it should be. **Never** split a subnet's surfaces across multiple PRs and **never** re-title
       the same surface as a different `kind` to make it look new: the gate dedups within the file and
       **closes redundant/near-duplicate PRs**. (This is exactly the farming the single-file model exists to
       stop.)
    2. **Prove the claim.** Every surface needs a public `url` **and** a `source_urls` entry that
       _independently proves_ the subnet/operator actually publishes it (an official repo README, the
       provider's own site, on-chain identity). A `source_url` that 404s or doesn't back the claim → closed.
    3. **Don't invent surfaces.** Only register what a subnet actually exposes. Schema-valid ≠ accepted.
    4. **Health is probe-derived only.** Never hand-set health, uptime, latency, incidents, or
       `verification` — the build's prober owns those. You set identity (`url`, `kind`, `provider`,
       `source_urls`) and `review.state: community-submitted`; the gate and build do the rest.
    5. **Public-safe only.** No secrets, PATs, wallet/hotkey/coldkey paths, private/localhost URLs, or
       validator-local data anywhere — in files, commits, or PR text. `auth` fields are _placeholders_
       (`Bearer <token>`), never real credentials.
    6. **Link an open issue — required.** Every PR must reference an issue (`Closes #<n>` / `Refs #<n>`)
       in the PR body, and that issue must be **open/unclosed** at submission time — the gate verifies the
       PR against that issue's intent, clause by clause. No linked issue, or a linked issue that's already
       closed, is an automatic close on its own, before content is even scored. For surface work, the
       per-subnet enrichment issues under [epic #427](https://github.com/JSONbored/metagraphed/issues/427)
       are the natural home to link — pick one that's still open before you start.
    7. **Schema is the contract — regenerate + commit (Path B).** Editing `schemas/` means
       `npm run build` then committing `openapi.json` + types/clients in the same PR, or
       `validate:contract-drift` fails CI.
    8. **Conventional Commits, no AI attribution.** Lowercase scope, specific subject, no trailing period;
       **no AI/Claude/agent mention** anywhere in commits or PR text. Frontend/UI work lives in this repo
       at `apps/ui/` — see **Path C** below; it is not Path A or Path B.
    
    ---
    
    ## Path A — Surface contribution (the common case)
    
    ### Phase A0 — Bootstrap
    
    ```sh
    # External contributor? Fork JSONbored/metagraphed, then clone YOUR fork:
    git clone https://github.com/<you>/metagraphed && cd metagraphed
    git remote add upstream https://github.com/JSONbored/metagraphed
    nvm use            # Node 22 (engines: >=22.23.0)
    npm install        # required before any validator runs
    ```
    
    ### Phase A1 — Pick the subnet + find a real surface
    
    - **Search first.** Check open issues AND open PRs for the same subnet/surface — a duplicate is a
      close-worthy signal. Browse [`good first issue`](https://github.com/JSONbored/metagraphed/labels/good%20first%20issue)
      / [`help wanted`](https://github.com/JSONbored/metagraphed/labels/help%20wanted); the per-subnet
      enrichment issues (#427) each name the exact gap.
    - **Find the gap.** `npm run curation:brief` lists profile-light subnets (directory-only, no website /
      source repo, public APIs with no OpenAPI yet). See `docs/curation-playbook.md`.
    - **Confirm the surface is real and public.** A safe public `url` you can fetch, plus a `source_url`
      that proves the subnet publishes it. Pick the right `kind` (full enum in `reference.md`): contributor
      kinds are `docs, website, source-repo, openapi, subnet-api, dashboard, sse, data-artifact, sdk,
    example, repo-registry` — all auto-reviewable; authed/paid APIs + unknown providers are higher-trust
      (airtight ownership proof). Base-layer chain endpoints (`subtensor-rpc/wss`, `archive`) are
      maintainer-curated infra (the endpoint lane), not contributor surfaces.
      **Prefer high-value callable kinds** (`openapi`, `subnet-api`, `sse`, `data-artifact`, `sdk`) —
      `source-repo` and `website` are auto-promoted from on-chain identity data, and `validate:surface`
      rejects them if the machine already has the URL (CI fails → gate closes). See `reference.md §5`.
    
    ### Phase A2 — Edit the ONE subnet file
    
    A surface contribution adds entries to the `surfaces[]` array of `registry/subnets/<slug>.json`. Use
    the helper so the id/shape are correct:
    
    ```sh
    # Find the provider slug for the team behind the surface.
    npm run providers:list
    
    # Append a community surface to the subnet file (writes into registry/subnets/<slug>.json):
    npm run surface:add -- \
      --netuid 43 --kind subnet-api \
      --url https://api.example.com/v1 \
      --source-url https://github.com/example/project/blob/main/README.md \
      --provider <provider-slug> --submitted-by <github-login> --write
      # Debut provider (slug not registered)? Add the team identity and surface:add scaffolds
      # registry/providers/<slug>.json (flat — trust is the authority field) in the SAME PR.
      # --provider-url is the provider's website_url and MUST be a public URL (validate
      # rejects private/localhost), as must any logo/docs/github/team/contact/social URL:
      #   --provider-name "Example Team" --provider-url https://example.com
    ```
    
    Each added surface must carry `authority: "community"` and a `review` block — the helper sets these:
    
    ```jsonc
    {
      "id": "sn-43-example-subnet-api",
      "name": "Example subnet API",
      "kind": "subnet-api",
      "url": "https://api.example.com/v1",
      "provider": "example",
      "authority": "community", // existing enum value — community-submitted, not official truth
      "auth_required": false,
      "public_safe": true,
      "source_urls": ["https://github.com/example/project/blob/main/README.md"],
      "review": {
        "state": "community-submitted",
        "submitted_by": "<github-login>",
      },
      "notes": "One line on what it is / why it's the right surface.",
    }
    ```
    
    You set **identity + proof + `review.state: community-submitted`** only. For an existing subnet
    manifest, **do not** add `verification`, health, or `curation` changes, and **do not** touch other
    surfaces or top-level fields in the file — a community PR that edits anything beyond appending its own
    community surface(s) is out-of-shape and gets routed to full review or closed. A missing subnet
    manifest is the exception: `subnet:new` creates the required top-level scaffold fields, then
    `surface:add` appends the community surface in that same new file. `review.state` is the
    human-governance axis: a maintainer flips it → `maintainer-reviewed` (or `rejected`) in place; machine
    verification + freshness is the separate probe overlay (the build's prober fills
    `verification`/health).
    
    > New subnet not yet in `registry/subnets/`? Scaffold it with `npm run subnet:new -- --netuid <n>`
    > first (one file), then add your surface to it in the same PR.
    
    ### Phase A3 — Validate locally
    
    ```sh
    npm run validate:surface -- registry/subnets/<slug>.json   # schema + provider-slug + review-shape
    npm run scan:public-safety                                  # no secrets / private URLs
    ```
    
    Fix every finding. (CI runs the full `validate` suite; these two are the fast local pre-checks for the
    submission lane.)
    
    ### Phase A4 — Commit + open the PR
    
    - **One subnet file changed, nothing else.** `git diff --stat` should show a single
      `registry/subnets/<slug>.json`.
    - **Commit (Conventional):** `feat(registry): add SN43 Example subnet-api surface (#<issue>)`.
    - **PR body:** fill `.github/pull_request_template.md` honestly — a real Summary, the `url` +
      `source_url` proof, the validation commands you ran, and **`Closes #<issue>`** — required, and the
      issue must still be open. No AI attribution.
    
    ### Phase A5 — Let the gate adjudicate
    
    Watch `Validate` and `Gittensory Gate` go green. Verified + green → merged. A deterministic fail
    (dup / dead source / private URL) or a clear reject → closed; fix and open a fresh PR. Genuine
    uncertainty → held for a human — don't open a duplicate.
    
    ---
    
    ## Path B — Code / schema PR
    
    ### Phase B0 — Bootstrap + scope
    
    `npm install` (Node 22). Open an issue first for anything risky (public behavior, schema/contract
    changes, new routes, workflows, deps). Keep the PR narrow — one coherent change. **Anchor on existing
    code:** find ≥2 analogues in the repo, cite them `file:line`, trace the closest end-to-end, and match
    its structure, naming, and comment density. Build for the class, not the one case.
    
    ### Phase B1 — Implement (match the house style)
    
    - The Worker entry/router is `workers/api.ts`; serving/overlay/health logic lives in `src/*.ts`;
      the contract lives in **`schemas-src/` (Zod, the single source)** and `src/contracts.ts`.
      `schemas/*.json` is a different thing — the INPUT schemas that validate hand-written registry
      files. A published component declared anywhere but `schemas-src/` fails `validate:single-schema-source`.
    - **All new code/script/test files must be `.ts`** — never `.mjs`/`.js`. The TypeScript migration
      (metagraphed#7510) is complete, and the `validate:no-hand-written-mjs` CI gate fails any PR that
      adds a `.mjs`/`.js` file under `src/`, `workers/`, `scripts/`, or `tests/`
      (metagraphed#7521).
    - **Schema-first rule:** never hand-edit the generated contract. Edit the Zod schema under
      `schemas-src/` (and register it in `schemas-src/openapi-registry.ts` if it is new) →
      `npm run build` → commit `openapi.json` + generated types/clients in the same PR.
    - A new `/api/v1` route or artifact trips hidden contract gates — see the new-route checklist in
      `reference.md` before adding one.
    
    ### Phase B2 — Test
    
    Tests are vitest under `tests/`. Add coverage for new branches and fallback paths, and a **regression
    test for every bug fix**. **Codecov is the coverage gate** — `codecov/patch` enforces **99% patch
    coverage, branch-counted, with zero threshold slack** (`target: 99%, threshold: 0%` in `codecov.yml`),
    scoped to `src/**` + `workers/**` runtime code. Run it unsharded locally: `npm run test:coverage`.
    Reader tests serve R2-only artifacts that only exist after a build, so `npm run build` before the
    suite if a test reads served artifacts. Running a single reader test file in isolation (e.g.
    `npx vitest run tests/some-route.test.ts`) needs the same precondition — those artifacts live under
    gitignored `dist/metagraph-r2/metagraph/`, populated as a side effect of `tests/artifacts.test.ts` /
    `tests/discovery-artifacts.test.ts` when the full suite runs first (alphabetically), but absent on a
    fresh checkout or a filtered single-file run. For just the fixture tree, without the rest of
    `npm run build`'s slower steps (type/client/GraphQL generation) and without ever touching
    `public/metagraph/`, run `npm run artifacts:prepare-local` first.
    
    ### Phase B3 — Regenerate what you invalidated (then commit it)
    
    | You changed…                                 | Run             | Commit                                                                                            |
    | -------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------- |
    | `schemas-src/` (any published schema)        | `npm run build` | `openapi.json`, generated types, `contracts.json`, api-index                                      |
    | A new/edited `/api/v1` route or artifact     | `npm run build` | the derived `public/metagraph/*` it produces                                                      |
    | A canonical `registry/providers/<slug>.json` | `npm run build` | regenerated artifacts (commit only the provider file + its artifacts)                             |
    | MCP tools in `src/mcp-server.ts`             | —               | **nothing** — the server card is worker-computed, not a committed artifact                        |
    | _(any of the above)_                         | `npm run build` | **never** `public/metagraph/r2-manifest.json` / `public/metagraph/schemas/index.json` — see below |
    
    Stale committed artifacts fail the **derived-artifact freshness** + **contract-drift** gates.
    
    **Never commit `public/metagraph/r2-manifest.json` or `public/metagraph/schemas/index.json`.**
    `npm run build` always rewrites both to reflect a full local/CI build, but neither committed copy is
    yours to move: `r2-manifest.json` is the publish lockfile, read from its committed path at publish
    time; `schemas/index.json` is a live network-capture cache whose current copy lives in the
    `generated/schemas-index.json` R2 store, written daily by a Worker cron
    (`src/schema-snapshots-sync.ts`) — the committed file is only the **fallback seed** a
    credential-less build reads when it cannot reach that store. Both are excluded from the "Verify
    committed derived artifacts are fresh" step in `.github/workflows/validate.yml` for this reason. A
    contributor build will **always** show them as changed for reasons unrelated to your change.
    
    `public/metagraph/operational-surfaces.json` is the same shape of thing one rung down: also a
    fallback seed (for the health prober's cold start), also written live by an hourly Worker cron
    (`src/operational-surfaces-sync.ts` → `generated/operational-surfaces.json`), also excluded from
    that gate. It is not on the never-commit list because a normal build reproduces it deterministically
    — but if it shows up dirty in a PR that did not intend to change the surface set, revert it the same
    way.
    
    Both `npm run build` and a standalone `node scripts/build-artifacts.ts` /
    `npm run build:artifacts` already **auto-revert** whichever of these actually went dirty, back
    against your base remote (`upstream/main` if you forked per Phase A0, `origin/main` otherwise) —
    `git status` should already be clean after either. Treat that as a safety net, not a guarantee: it
    silently degrades to a printed warning (not a failure) if the revert itself can't reach your base
    remote (e.g. no network, or `upstream`/`origin` isn't fetched). So still check `git status` before
    staging, and if either file shows modified, revert it by hand:
    
    ```sh
    git checkout "$(git remote | grep -qx upstream && echo upstream || echo origin)/main" -- \
      public/metagraph/r2-manifest.json public/metagraph/schemas/index.json
    ```
    
    before staging/committing.
    
    **Client SDK version:** do **not** bump `packages/client/package.json` in your PR. The
    `sync-client-version` workflow auto-opens a `chore/sync-client-version` PR after a contract-changing
    merge. `validate:client-sdk-sync` now emits a notice (not a failure) when the version wasn't bumped
    in the contributor PR.
    
    ### Phase B4 — Run the gates locally (must be green)
    
    ```sh
    git diff --check
    npm run lint && npm run format:check        # NOTE: main isn't fully prettier-clean — never reformat whole files you didn't change
    npm run typecheck                           # tsc --noEmit, whole in-scope tree (src/workers/scripts/tests)
    npm run validate                            # registry + API + OpenAPI checks
    npm test                                    # or: npm run test:coverage for the coverage gate
    # Then the focused validators for what you touched (full list in reference.md), e.g.:
    npm run validate:contract-drift  npm run validate:schemas  npm run validate:api  npm run validate:openapi
    ```
    
    For a faithful full local run, `npm run pipeline:check` — but only trust it in isolation **after** a
    clean `npm run build` (see the build-gotchas note in `reference.md`).
    
    ### Phase B5 — Commit + PR
    
    Conventional Commit (no AI attribution); `Closes #<issue>` — required, and the issue must still be
    open; fill the PR template with the validation commands you actually ran. Sync with `main` if it moved
    (`git fetch upstream && git rebase upstream/main`) — a base conflict closes a contributor PR.
    
    ---
    
    ## Path C — Frontend PR (`apps/ui/`)
    
    `apps/ui/` is the TanStack Start + Vite + React web app at [metagraph.sh](https://metagraph.sh) —
    folded into this repo as an npm workspace via the monorepo consolidation. It has its own `ui` CI job
    (lint + typecheck + test + a responsive-overflow e2e check + build + bundle-budget, see
    `reference.md §2`) and its own review contract,
    distinct from Path A/B.
    
    ### Phase C0 — Bootstrap + pick an issue
    
    ```sh
    npm install            # root install wires the apps/ui workspace too (Node 22)
    ```
    
    Pick a `gittensor:bug` / `gittensor:feature` issue scoped to `apps/ui/` (Wave 3 milestone). Keep the
    PR **narrow — aim for ≤10 files / ≤1000 LOC**; if an issue looks bigger than that once you're in the
    code, ship the smallest coherent slice and leave a follow-up note rather than bundling everything into
    one PR.
    
    ### Phase C1 — Implement (match the house style)
    
    - Reuse existing shared components and the design tokens in `packages/ui-kit/src/styles.css` (the v2
      system — warm bone/paper background, deep ink text, mint accent used **sparingly**, flat surfaces
      with hairline borders, **no shadows or gradients**) instead of inventing new one-off styles.
    - Anchor on an existing analogous page/component before writing a new one — this codebase already has
      shared primitives (table-controls, chart primitives, copy/share buttons, entity hover-cards,
      freshness badges) that most issues should compose rather than reimplement.
    - Creative additions beyond an issue's stated scope are welcome but held to a **higher bar** — expect
      extra scrutiny, and call out explicitly in the PR body anything you added beyond the issue.
    
    ### Phase C2 — Screenshot contract (external-contributor visual changes)
    
    **Non-negotiable for an external-contributor PR that changes rendered output.** Without an explicit
    maintainer override, a PR without this evidence is auto-closed. A real PR (#3757) shipped 10 of its
    12 screenshots at 115,000–142,000px tall (a full-page capture bug, not a display issue) and sat
    unreviewable until recaptured. Don't repeat that.
    
    > **Maintainer-directed override.** When a repository maintainer explicitly waives screenshots for
    > the current task, skip this phase—including static and animated evidence—and do not request the
    > waived evidence again. Never infer the waiver from branch ownership or repository access. Continue
    > to run proportional responsive/interaction validation and every required code/CI gate.
    
    **Recommended: automated capture (#3769).** `apps/ui/tests/e2e/capture-pr-screenshots.ts`
    automates everything below — the two-worktree orchestration, fixed-viewport-only capture, explicit
    theme toggling, the 12-image matrix, and (with `--push`) hosting + the ready-to-paste markdown
    table:
    
    ```sh
    npm run screenshots --workspace=apps/ui -- --route /subnets/1 --section volume-24h --prefix 5483-volume --push
    ```
    
    Add `--section <id>` for a below-the-fold section anchor (omit to capture the page top), and
    `--fallback-section <id>` when `before` doesn't have that anchor yet (the common case for a new
    section — point it at the existing anchor the new one attaches after). Already have two dev
    servers running (e.g. mid-session in an AI coding tool)? Skip the orchestration and point at them
    directly: `--before-url http://localhost:8081 --after-url http://localhost:8080`. Run
    `npm run screenshots --workspace=apps/ui -- --help` for the full flag list.
    
    If the tool doesn't fit your case (a capture step needs manual intervention, or you're debugging
    the tool itself), the equivalent manual steps are below — same contract, same output.
    
    **1. Two dev servers — one for `before`, one for `after`.** Don't reuse a single server for both; run
    the `before` state from a separate worktree so nothing needs stashing/restoring mid-capture:
    
    ```sh
    git worktree add ../metagraphed-before $(git merge-base main HEAD)
    cd ../metagraphed-before && npm install && npm run dev --workspace=apps/ui   # note the printed Local URL — this is "before"
    cd -                                                                          # back to your feature branch
    npm run dev --workspace=apps/ui                                              # note this Local URL — this is "after"
    ```
    
    **2. Fixed viewport sizes only — never a full-page / `fullPage: true` capture.** A full-scroll-height
    capture is exactly what produced #3757's broken screenshots. Use these three sizes — chosen to straddle
    this app's actual Tailwind breakpoints (`md`=768px, `lg`=1024px, the two most-used responsive prefixes
    in `apps/ui/src`):
    
    | Viewport | Size (px)  |
    | -------- | ---------- |
    | Mobile   | 375 × 812  |
    | Tablet   | 768 × 1024 |
    | Desktop  | 1280 × 800 |
    
    Capture exactly that viewport, nothing more. If the changed content is below the fold, scroll to it
    first — don't reach for a full-page capture to get there.
    
    **Also never `locator.screenshot()` / `elementHandle.screenshot()`** (e.g.
    `page.locator(".hero").screenshot(...)`) as a shortcut to "just the changed part" — confirmed on
    #6928/#6933, both shipped with a locator-scoped capture instead of the fixed viewport. It crops to
    that element's own rendered box, not the fixed viewport frame this contract requires, so a
    reviewer can't see the change in its real page context (surrounding layout, whether anything else
    shifted). The only call to make is `page.screenshot({ path })` — no `fullPage`, no locator, after
    resizing the viewport and scrolling to the changed content if needed.
    
    **3. Force each theme explicitly — never rely on system/`prefers-color-scheme`** (it varies by capture
    environment, so it isn't reproducible run to run). In the page, before capturing:
    
    ```js
    localStorage.setItem("mg-theme", "dark"); // or "light"
    location.reload();
    ```
    
    `mg-theme` is `THEME_STORAGE_KEY` in `apps/ui/src/lib/theme.ts` — the only supported mechanism. Reload
    after setting it so the pre-hydration bootstrap script applies it with no flash-of-wrong-theme.
    
    **4. 3 viewports × 2 themes × {before, after} = 12 images**, for a page/feature-level change. Skip a
    combo only if you state in one sentence why it's provably unaffected (e.g. a change gated behind a
    desktop-only code path).
    
    **5. Host the 12 files on a dedicated branch in your own fork — never drag-and-drop, never commit them
    to your feature branch.** Drag-and-drop into the GitHub web editor requires a human browser session,
    which an AI coding tool cannot do end-to-end; a pushed branch is fully scriptable and keeps binary
    images out of your feature branch's diff entirely. Do this from a throwaway worktree, not your feature
    branch's working directory:
    
    ```sh
    git worktree add ../metagraphed-screenshots main
    cd ../metagraphed-screenshots
    git checkout --orphan screenshots       # first time; if you already have a `screenshots` branch from a
    git rm -rf . 2>/dev/null                # prior PR, just `git checkout screenshots` instead and skip these two lines
    cp /path/to/your/12/*.png .
    git add *.png && git commit -m "screenshots for PR"
    git push origin screenshots
    cd -    # your feature branch's working directory was never touched
    ```
    
    Reference each file as `https://raw.githubusercontent.com/<your-fork-owner>/metagraphed/screenshots/<file>.png`.
    
    **6. Table format — one row per viewport+theme, thumbnail + caption in each cell, both before and
    after:**
    
    ```md
    | Viewport · Theme | Before                                                                | After                                                              |
    | ---------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------ |
    | Desktop · Light  | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |
    | Desktop · Dark   | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |
    | Tablet · Light   | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |
    | Tablet · Dark    | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |
    | Mobile · Light   | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |
    | Mobile · Dark    | [<img src="BEFORE_URL" width="260">](BEFORE_URL)<br><sub>before</sub> | [<img src="AFTER_URL" width="260">](AFTER_URL)<br><sub>after</sub> |
    ```
    
    Screenshots go **inside the table only** — never pasted loose in the PR body, never committed to the
    feature branch itself.
    
    A PR confined to `apps/ui/src/lib/**` / `apps/ui/src/hooks/**` / test files, with **no** visual change,
    skips this entirely — it isn't rendering anything different.
    
    > The devcontainer (`.devcontainer/devcontainer.json`) preinstalls Node 22 + Playwright's Chromium, so
    > setup for the steps above (manual or via the automated tool) is zero-config there.
    
    **Animated evidence (#4825) — for effects no static screenshot can show.** Required whenever the
    changed behavior is only visible in motion: a hover-triggered popover, a scroll-linked effect, a CSS
    transition/animation, a drag interaction, or anything else where "before" and "after" aren't just two
    different static layouts. This is _additional_ to the static table above, not a replacement for it — a
    real PR (#4814) shipped both: the static viewport × theme matrix for the at-rest layout, plus a
    before/after GIF table for the hover behavior itself, because a still image genuinely cannot show what
    happens on hover.
    
    1. **Record the interaction, don't screenshot it.** Use your OS's screen recording (macOS `Cmd+Shift+5`
       or `screencapture -V`; Linux `wf-recorder`/`ffmpeg -f x11grab`) or a Playwright video/trace, scoped
       tightly to the interactive element — not the full viewport, and not a long clip. A few seconds
       showing the cursor entering, the effect triggering, and the resulting state is enough.
    2. **Convert to a GIF** — a `.mov`/`.webm` file won't render inline in a GitHub-hosted `<img>` tag the
       way a `.gif` does:
       ```sh
       ffmpeg -i recording.mov -vf "fps=12,scale=480:-1:flags=lanczos" -loop 0 hover-before.gif
       ```
       Keep it small (a few seconds, ~12fps, ≤480px wide) — an oversized GIF is as unreviewable as #3757's
       full-page screenshot bug was.
    3. **Same hosting mechanism as step 5 above — the dedicated `screenshots` branch on your own fork.**
       Push the `.gif` files alongside your PNGs in the same orphan-branch commit; reference them the same
       way: `https://raw.githubusercontent.com/<your-fork-owner>/metagraphed/screenshots/<file>.gif`.
    4. **Table format — one row per interaction target** (not per viewport/theme; a hover/scroll/transition
       effect is rarely breakpoint- or theme-dependent, so don't multiply it out the way the static matrix
       does unless the interaction genuinely differs by breakpoint):
       ```md
       ### Hover interaction (animated)
    
       Static images can't show the pointer-driven [behavior] — here's the actual interaction.
    
       | Target                                                          | Before                                                   | After                                                  |
       | --------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------ |
       | <describe the interactive element, e.g. "Blocks · author cell"> | [<img src="BEFORE_GIF_URL" width="380">](BEFORE_GIF_URL) | [<img src="AFTER_GIF_URL" width="380">](AFTER_GIF_URL) |
       ```
       One row per distinct interactive element the PR changes.
    5. A PURELY interaction-only change (identical at-rest state, only the triggered behavior changed) can
       skip the static matrix for that specific view — state why in one sentence, the same "provably
       unaffected" exception already allowed for a static viewport/theme combo.
    
    ### Phase C3 — Test + gates locally
    
    The `ui` CI job runs lint, typecheck, test, **build**, a responsive-overflow e2e check, and a
    bundle-size-budget check, in that order — run the same locally before pushing:
    
    ```sh
    npm run lint --workspace=apps/ui && npm run format:check --workspace=apps/ui
    npm run typecheck --workspace=apps/ui  # auto-builds packages/client first (pretypecheck) -- no separate step needed
    npm test --workspace=apps/ui
    npm run build:worker --workspace=apps/ui  # MUST precede test:e2e since #8928 — see below
    npm run test:e2e --workspace=apps/ui   # needs a Chromium browser: npx playwright install --with-deps chromium (once)
    ```
    
    **Build now comes before `test:e2e`** (#8928). The e2e check serves the PRODUCTION build via
    `wrangler dev` rather than `npm run dev`: the sweep loads 26 routes x 4 viewports, and Vite's
    dev server compiled each route on first hit, making that step ~48% of the whole `ui` job. It
    also means the check now exercises the bundle that actually ships, so prod-only breakage the
    dev server hid is in scope.
    
    Use **`build:worker`**, not `build`. A plain `npm run build` emits `.output/` with no Worker
    entry; the cloudflare-module preset (which CI and production use) emits **`dist/`**. `test:e2e`
    serves `dist/server/wrangler.json` and fails without it.
    
    This does NOT change Phase C2's screenshot workflow below, which still uses `npm run dev` on
    purpose: those are contributor-facing before/after captures, not a CI gate.
    
    The responsive-overflow e2e check replays recorded API traffic (`tests/e2e/har/*.har`)
    instead of live production data, so it's deterministic regardless of live chain state. If
    your PR adds a new API call on one of the checked routes (`/`, `/subnets/1`,
    `/endpoints`, `/status`, `/settings`, `/explorer`), re-record:
    `npm run test:e2e:record-har --workspace=apps/ui` against a running dev server.
    
    CI also gzip-measures the initial client JS for a cold `/` visit against a budget (currently 400 KB,
    `.github/workflows/validate.yml`'s "Bundle size budget" step) — keep new dependencies/imports lean; if
    a real feature legitimately grows it, raise the budget deliberately in the same PR. If your PR also
    touches `packages/client` or `packages/ui-kit`, CI rebuilds each fresh and diffs against its committed
    `dist` (`packages/client/dist` / `packages/ui-kit/dist`) — run `npm run build --workspace=packages/client`
    (or `--workspace=packages/ui-kit`) and commit the result if you changed `packages/client/src` (or
    `packages/ui-kit/src`). `packages/ui-kit` also gets its own `npm run typecheck --workspace=packages/ui-kit`
    step in the `ui` CI job.
    
    ### Phase C4 — Commit + PR
    
    Conventional Commit (e.g. `feat(ui): add validator directory table`), no AI attribution, `Closes
    #<issue>` — required, and the issue must still be open. External-contributor visual changes must fill
    the screenshot table unless a repository maintainer explicitly waived it for the current task.
    
    ### Phase C5 — Review disposition
    
    An **external-contributor** visual PR touching `apps/ui/` is held for manual review regardless of
    AI-review confidence. For maintainer-directed work, an explicit authorization to merge after required
    CI is green replaces that manual-review wait; merge when green without asking for the same approval
    again. A non-visual `apps/ui/` PR (data/hooks/tests only) follows the normal auto-merge/auto-close gate
    like Path A/B.
    
    ---
    
    ## Final pre-push checklist
    
    **Path A (surface):**
    
    - [ ] Exactly one `registry/subnets/<slug>.json` changed; existing manifests only append community
          surface(s), while missing manifests may include the required `subnet:new` scaffold plus the
          community surface(s); no other file.
    - [ ] Each surface: real public `url` + a proving `source_url`; right `kind`; `authority: community`;
          `review.state: community-submitted`; `public_safe: true`; no health/`verification`/secrets set by hand.
    - [ ] Not a duplicate of an existing surface or an open PR; not the same surface re-titled by `kind`.
    - [ ] `npm run validate:surface` + `npm run scan:public-safety` clean.
    - [ ] If you ran `npm run build` locally out of caution (not normally required for Path A), your diff
          still touches only your one subnet file — see the Path B note below on
          `public/metagraph/r2-manifest.json` / `public/metagraph/schemas/index.json`; the Gittensory Gate's
          registry-review lane rejects a PR that bundles either in with your surface change.
    - [ ] Conventional Commit (no AI attribution); PR template filled; **`Closes #<issue>`** — required, referencing an issue that's still open.
    
    **Path B (code/schema):**
    
    - [ ] In scope, narrow, anchored on ≥2 analogues; general not special-cased.
    - [ ] Regenerated + committed: `npm run build` artifacts (OpenAPI/types/contracts) as applicable. MCP
          tool additions do NOT require server-card regen (worker-computed). Client version bump NOT required
          (auto-sync workflow handles it post-merge).
    - [ ] `public/metagraph/r2-manifest.json` and `public/metagraph/schemas/index.json` are **not** part of
          your diff — both always change on a local/CI build for reasons unrelated to your PR (they're
          deploy/publish-pipeline-owned, not contract artifacts). `npm run build` (and a standalone
          `scripts/build-artifacts.ts`) already auto-revert either one if it went dirty; double-check
          `git status` and fall back to the Phase B3 command above if either still shows modified (e.g.
          the auto-revert couldn't reach your base remote).
    - [ ] `git diff --check` clean · `lint` + `format:check` + `typecheck` clean · `npm run validate` green ·
          `npm run test:coverage` green · the focused `validate:*` for what you touched green.
    - [ ] Branch current with `main`; Conventional Commit (no AI attribution); PR template filled; `Closes #<issue>` — required, referencing an issue that's still open.
    
    **Path C (frontend):**
    
    - [ ] Scoped to `apps/ui/**` only; ≤10 files / ≤1000 LOC where reasonably possible.
    - [ ] Reuses existing design tokens (`apps/ui/src/styles.css`) and shared components rather than
          one-off styling.
    - [ ] If visual and not explicitly waived by a repository maintainer: a filled before/after screenshot
          table (mobile + dark-mode captures where relevant) — missing/malformed external-contributor
          evidence is an automatic close.
    - [ ] If the change is only visible in motion (hover/scroll/transition/animation) and evidence was not
          explicitly waived: a before/after GIF table alongside the static one, per the "Animated
          evidence" step in Phase C2.
    - [ ] `lint` + `format:check` + `typecheck` + `test` + `test:e2e` + `build` all green
          (`--workspace=apps/ui`); bundle size still under budget.
    - [ ] If `packages/client/src` or `packages/ui-kit/src` changed: rebuilt and committed the respective
          `dist` (`packages/client/dist` / `packages/ui-kit/dist`).
    - [ ] Conventional Commit (no AI attribution); `Closes #<issue>` — required, referencing an issue that's still open.
    
    If every box is checked, the PR has the best chance of a one-shot approve-and-merge. If any box can't
    be checked, **keep working — don't push.**
    
    ---
    
    When you need the exhaustive detail behind any phase, read **`reference.md`** in this skill directory.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related