Skip to content

feat(mcp): generate every tool, CLI, client, and docs surface from the contract (#9521) - #9590

Merged
JSONbored merged 10 commits into
mainfrom
feat/generated-surfaces-9521
Jul 29, 2026
Merged

feat(mcp): generate every tool, CLI, client, and docs surface from the contract (#9521)#9590
JSONbored merged 10 commits into
mainfrom
feat/generated-surfaces-9521

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Closes #9521. Executes and closes #9282 (its recorded option 1, per that issue's decision record).

What changed

Every hand-maintained MCP/CLI/docs surface this issue inventoried is now generated from, derived from, or validated against one source of truth, with a --check drift gate in test:ci for each generated artifact.

Generated reference surfaces (scripts/gen-mcp-tool-reference.ts, mcp:tool-reference:check): the stdio README tool table, the miner README tool table, the typed apps/loopover-ui/src/lib/mcp-tool-reference.ts docs module, the stdio README CLI command block, and the README telemetry-property table all emit from the @loopover/contract registry between markers. check-docs-drift.ts gains an MCP surface: every loopover_* token under apps/loopover-ui/content/docs must exist in the registry.

One CLI source of truth: CLI_COMMAND_SPEC is now a typed {subcommands, usage, note} table that drives runCli dispatch (a Record<CliCommand, handler> tsc exhaustiveness-checks — replacing a 34-branch if-chain), printHelp, the three sub-command help printers, all four shell-completion builders, and the generated README block. CLI_FLAG_SPEC replaces parseOptions' two hand-listed repeatable/boolean Set literals. The regex-scraping test now imports the spec directly. Real drift this surfaced: agent start existed in the hand-written help printer but not the spec, so top-level help silently omitted it.

Typed validated API client: scripts/gen-contract-api-schemas.ts generates @loopover/contract/api-schemas from src/openapi/schemas.ts (which stays canonical — moving the schemas was tried and rejected because both post-hoc component-naming routes changed the published document; generation keeps openapi.json byte-identical by construction) plus CLI_RESPONSE_SCHEMAS, a path→schema table derived from scanning the CLI's literal api* call sites against the published document — nothing hand-listed. apiGet/apiPost are overloaded so a documented literal path returns the schema's inferred type, and apiFetch safeParses every documented response under #9519's recorded posture: warn on stderr (deduped per path) and pass the payload through untouched by default, throw under LOOPOVER_VALIDATE_RESPONSES (CI/self-host). payload: any is extinct, pinned by a grep-gate test; the unvalidated remainder is pinned as a shrink-only list for #9531. Drift this surfaced: /v1/local/branch-analysis returns predictedGate/dataQuality, which the document omitted while two predict-gate tools read them — now declared.

Miner dedup into @loopover/contract (recorded choice: the contract package, the one home both separately-installable CLIs already depend on): local-config (config path/profile/session/API-URL policy, pure with fs injected so the package stays Workers-safe) and orb-broker (the ORB_BROKER_URL validation + stored-secret exchange, also imported by src/orb/broker-client.ts). Both hand-copies had already drifted: the two CLIs disagreed on API-URL precedence past a legacy value (the miner's fall-through is correct and now shared), and the miner's local-host list kept a bare ::1 that #8334 removed. The contract tsconfig moves from "types": [] to the Workers types — the same node-builtins ban stated precisely, with the globals the Worker really has.

#9282 executed: PublicStats/PublicRulePrecision move to @loopover/contract/public-api; the UI infers via z.infer, src/openapi/schemas.ts keeps only the OpenAPI decoration (document byte-identical). The hand-typed interface had drifted five ways with zero signal (missing fleetAccuracy.basis and rules[].confirmed, wrong nullability on decidedCount and accuracyTrend counts, rulePrecision wrongly optional). check-ui-derived-types.ts (ui-derived-types:check, in test:ci) fails any hand-authored type/interface shadowing a shared name and grows automatically as shapes migrate.

Telemetry allowlist single-sourced from @loopover/contract (buildLegacyToolCallProperties, whose signature is the allowlist) consumed by both telemetry modules and the generated README section; stale 3.0.0 README samples are now version-agnostic.

Validation

Full typecheck clean (root + all packages). All MCP suites (149 files), miner token/credential suites, contract suites (100% line+branch on the new modules), UI typecheck/tests/build, ui:openapi:check, mcp:tool-reference:check, contract:api-schemas:check all green after rebasing onto main. The response-validation posture is tested both against the real compiled bin (subprocess, both modes) and in-process for coverage.

@loopover-orb

loopover-orb Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Warning

⏸️ LoopOver review result - manual review recommended

Review updated: 2026-07-29 00:16:58 UTC

47 files · 1 AI reviewer · no blockers · CI green · clean

⏸️ Suggested Action - Manual Review

  • Touches a guarded path — held for manual review: This PR changes guardrail-protected path(s): .github/workflows/ci.yml (matched .github/workflows/**), .github/workflows/ui-preview.yml (matched .github/workflows/**).

Review summary
This PR generates the MCP tool-reference docs module and a `@​loopover/contract/api-schemas` package from the contract/OpenAPI source of truth, replacing hand-maintained CLI/README/docs surfaces with generated artifacts plus `--check` drift gates wired into `test:ci`. The two generated files shown in full (`api-schemas.ts`, `mcp-tool-reference.ts`) are internally consistent, well-commented about why generation (not relocation) was chosen, and the `CLI_RESPONSE_SCHEMAS` path table plus `ApiResponse<Path>` type give the CLI actual validated typing instead of `any`. The generator scripts, CLI dispatch table, and most test files are not included in the diff/full-file content given here, so the wiring correctness of `runCli`'s `Record<CliCommand, handler>` dispatch and the `--check` drift comparisons can't be verified directly — I'm trusting the described behavior. CI has real failures (`validate`, `validate-code`, `validate-tests`, both Workers Builds) with no detail provided, which is the one concrete signal something in this large surface doesn't build/typecheck as claimed.

Nits — 8 non-blocking
  • `packages/loopover-contract/src/orb-broker.ts:20` hardcodes `api.loopover.ai` — confirm this is meant as a genuine default rather than something that should come from env/config given the rest of the codebase's config-driven URL patterns.
  • The external brief flags depth-5 nesting in `orb-broker.ts:88`, `check-docs-drift.ts:507`, `check-ui-derived-types.ts:61`, `gen-contract-api-schemas.ts:89`, and `gen-mcp-tool-reference.ts:140` — worth a quick look since these are exactly the new generator/drift-check scripts this PR's correctness hinges on, though I can't inspect the actual code to confirm severity.
  • `scripts/gen-contract-api-schemas.ts:156` reportedly returns null instead of propagating an error — for a `--check` drift gate, a silently-null result on a real failure could make the gate report false-green; worth confirming this can't mask a genuine drift/parse error.
  • Several explicit-`any` assertions are flagged in `packages/loopover-mcp/bin/loopover-mcp.ts` (lines ~5607–5717) — given the PR's stated goal of exhaustiveness-checked typed dispatch, any `any` in the new CLI response-handling path undercuts that guarantee and is worth a comment or a narrower type.
  • `packages/loopover-contract/src/api-schemas.ts` and `apps/loopover-ui/src/lib/mcp-tool-reference.ts` are ~890-line generated files; fine since they're marked do-not-edit, but confirm prettier/lint exclusions (seen for the tool-reference file) are mirrored for `api-schemas.ts` too so formatting churn doesn't fight the generator.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.
  • Possible secret-shaped assignment in the diff (generic_secret_assignment) — Verify the value is not a real credential.
  • Touches a guarded path — held for manual review — A maintainer must review and merge this change.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ⚠️ Gate result — Not blocking (Advisory; not blocking this PR.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9521, #9282
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (2 linked issues).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 14 registered-repo PR(s), 13 merged, 310 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 14 PR(s), 310 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Linked issue satisfaction

Addressed
The PR delivers a generated tool-reference module, a single typed CLI spec table driving dispatch/help/completion, a generated contract-derived API schema package for validated client responses, and generated README/UI doc surfaces with drift checks, directly matching the issue's core asks even though the diff is truncated before the miner-dedup and full telemetry sections can be fully verified.

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is registered but has no active allocation in the current snapshot.
  • Public profile languages: not available
  • Official Gittensor activity: 14 PR(s), 310 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Then work through the remaining 1 step in the Signals table above.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

Decision record
  • action: hold · clause: guardrail_hold
  • config: fb65b52e42965f92c35900934c70fd6ac7f1e7d244d0cbb393d93dfd372fccac · pack: oss-anti-slop · ci: failed
  • record: a66f438d71412b815117e5fdcd1e965918633542b880a01524e13f09a4680ff0 (schema v5, head d256d66)
Visual preview
Route Viewport Before (production) After (this PR's preview) Diff
/ desktop before /
before /
after /
after /
/ mobile before / (mobile)
before / (mobile)
after / (mobile)
after / (mobile)

Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy.

Scroll preview
Route Before (production) After (this PR's preview)
/ before / (scroll)
before / (scroll)
after / (scroll)
after / (scroll)

A short scroll-through clip (desktop) — click either thumbnail to open the full animation. Evidence for scroll-linked behavior a single screenshot can't show.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@JSONbored JSONbored self-assigned this Jul 28, 2026
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 28, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
loopover-ui a14b7d7 Jul 28 2026, 10:07 PM

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.95%. Comparing base (26071f1) to head (246a2ed).
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9590      +/-   ##
==========================================
+ Coverage   89.85%   89.95%   +0.10%     
==========================================
  Files         880      884       +4     
  Lines      111175   111247      +72     
  Branches    26473    26438      -35     
==========================================
+ Hits        99891   100074     +183     
+ Misses       9992     9843     -149     
- Partials     1292     1330      +38     
Flag Coverage Δ
backend 95.81% <100.00%> (+0.18%) ⬆️
control-plane 99.86% <ø> (ø)
engine 65.92% <ø> (ø)
rees 89.62% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/loopover-contract/src/api-schemas.ts 100.00% <100.00%> (ø)
packages/loopover-contract/src/cli-config.ts 100.00% <100.00%> (ø)
packages/loopover-contract/src/orb-broker.ts 100.00% <100.00%> (ø)
packages/loopover-contract/src/public-api.ts 100.00% <100.00%> (ø)
packages/loopover-contract/src/telemetry.ts 100.00% <100.00%> (ø)
packages/loopover-mcp/bin/loopover-mcp.ts 66.43% <100.00%> (+5.84%) ⬆️
packages/loopover-mcp/lib/telemetry.ts 100.00% <100.00%> (ø)
...ages/loopover-miner/lib/github-token-resolution.ts 100.00% <100.00%> (ø)
...loopover-miner/lib/tenant-credential-resolution.ts 100.00% <100.00%> (ø)
src/mcp/telemetry.ts 100.00% <ø> (ø)
... and 2 more

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

…ct registry

First #9521 batch. One generator (scripts/gen-mcp-tool-reference.ts, the
gen-command-reference pattern) now emits, between markers, with --check in
test:ci:

- the stdio README's tool table (which previously documented nothing and told
  users to run 'loopover-mcp tools' instead);
- the miner README's tool section, replacing a hand-kept 12-bullet prose list;
- a typed apps/loopover-ui/src/lib/mcp-tool-reference.ts module for the docs
  site;
- the stdio README's telemetry property table, generated from
  LEGACY_MCP_TELEMETRY_PROPERTY_KEYS.

That constant is new: the legacy mcp_tool_call event's four-property allowlist
existed three times (src/mcp/telemetry.ts, packages/loopover-mcp/lib/
telemetry.ts, README prose) with nothing holding them together. Both modules now
build the event through one shared builder whose SIGNATURE is the allowlist --
there is nowhere in it to put a fifth field.

check-docs-drift.ts gains the MCP surface: every loopover_* token in
apps/loopover-ui/content/docs must exist in the registry. Non-tool uses of the
prefix (a Postgres db name, a secrets filename, Prometheus metric prefixes) sit
in an allowlist with per-entry reasons and an anti-rot check; metric-suffix and
glob forms are excluded by convention, with a registry-side guard so no future
tool can take a metric-shaped name and slip out of the scan. 12 real mentions
verified; the very first run caught that the scan WOULD have flagged nothing --
every one of the 32 hand-typed names the issue counted had already drifted out
of the docs or into non-tool uses.

Stale @loopover/mcp/3.0.0 samples in the stdio README become version-agnostic.
…he CLI

The CLI surface was kept in FOUR hand-maintained copies: a flat command-to-subcommands
table, a 34-branch if-chain in runCli, a hand-written help string, and the README's
command block. They had already drifted -- printHelp silently omitted commands the
dispatch chain accepted.

CLI_COMMAND_SPEC now carries usage lines alongside subcommands and is exported, so
printHelp, all four shell-completion builders, dispatch, and the README all derive from
it. The if-chain becomes a Record keyed by CliCommand, which makes tsc reject a command
without a handler or a handler without a command; the README block is generated between
markers by gen-mcp-tool-reference.ts with --check enforcing it in CI.

The handler Record is a hoisted function rather than a const: the entrypoint awaits
runCli during module evaluation, so a const declared below it is still in its temporal
dead zone at call time. The old if-chain was accidentally safe because it called hoisted
function declarations.

The parity test imported the spec directly instead of regex-scraping the built bundle,
which could pass against a stale dist and could not see a type error at all. The handler
side is still read from source, since each run*Cli parses its subcommands inline.
…om the spec

parseOptions hand-listed its repeatable and boolean flags as two literal Sets sitting
inside the function, so adding a repeatable flag to a command only accumulated if
someone also remembered to edit them. CLI_FLAG_SPEC now declares the kind once and
parseOptions derives both sets from it.

printCacheHelp, printAgentHelp, and printProfileHelp each carried their own copy of the
usage lines the spec already held, and they had drifted: `agent start` appeared in the
printer but not in the spec, so top-level help silently omitted it. All three now render
from the spec's usage lines plus a note field, and the spec carries the full
per-subcommand usage that the printers used to own. Their output is unchanged.
…tract

Both copies said outright that they were hand-synced and why no shared home existed.
Both had already drifted.

github-token-resolution.ts re-implemented loopover-mcp's config path, profile selection,
session lookup, and API-URL precedence, because @loopover/miner and @loopover/mcp are
separately-installable CLIs. The API-URL precedence had diverged: loopover-mcp stopped at
a legacy profile apiUrl and returned the default, so a stale profile value masked a valid
top-level override, while the miner kept looking. The miner's behavior is the correct one
and is now what both get.

tenant-credential-resolution.ts duplicated the broker exchange and its ORB_BROKER_URL
safety validation, because a relative import into root src/ resolves outside the package's
rootDir and fails tsc with TS6059. Its local-host list still accepted a bare "::1" that
broker-client.ts had dropped once a WHATWG URL was confirmed to always bracket IPv6.
Harmless there, but this function decides where a bootstrap credential may be sent.

@loopover/contract is the home both sides can reach, since both already depend on it. The
new modules keep it Workers-safe: local-config.ts is pure policy with the filesystem left
to its callers, and orb-broker.ts uses only URL, fetch, and AbortSignal. The package's
tsconfig moves from "types": [] to the Workers types, which is the same rule stated more
precisely -- node builtins still cannot compile there, but the globals the Worker really
has are now available.

Both new modules are covered at 100% line and branch, and the three branches the miner's
new profile fallbacks introduced are covered in its own suite.
…rker serves (#9282)

PublicStats was a hand-authored TypeScript interface kept in sync with the backend by
whoever last touched both files, and it had drifted in five ways at once: it was missing
fleetAccuracy.basis and rulePrecision.rules[].confirmed outright, typed decidedCount as
optional where the wire says nullable, typed accuracyTrend's counts as non-null where the
wire says nullable, and marked rulePrecision optional where the wire says required. None
of that produced a single compiler or CI signal.

This lands option 1 from that issue's recorded decision. The shape moves to
@loopover/contract/public-api, which both the Worker and the UI already reach, and the UI
takes z.infer of the same object. src/openapi/schemas.ts keeps only the OpenAPI
decoration, re-wrapping through the local z.object because extendZodWithOpenApi attaches
.openapi at construction time and the contract package must not depend on zod-to-openapi.
The generated openapi.json is byte-identical.

The two component fixtures grow the fields the wire always carries. The one test that
genuinely needs a payload without rulePrecision -- an older deployed Worker, which the
current schema no longer describes -- now strips it explicitly instead of relying on a
type that quietly permitted it.

check-ui-derived-types.ts is what stops this recurring: for every type the shared module
exports, no file under apps/loopover-ui/src may declare its own type or interface of that
name, so migrating another response shape extends the check to it with no edit. It also
asserts the pilot still imports the shared module, since regressing to a hand-authored
shape would leave the shared names unused rather than duplicated.

The generated tool-reference module joins openapi.json in .prettierignore for the reason
already recorded there: prettier rewrites the emitted literal, and its drift check then
fails against a file nobody edited.
…ndary

The stdio CLI read every API response as `payload: any` and picked fields out by
optional-chaining guesswork, so a renamed Worker field degraded silently at runtime.

gen-contract-api-schemas.ts generates @loopover/contract/api-schemas from
src/openapi/schemas.ts, which stays canonical and untouched: moving the schemas was
tried first and rejected because zod-to-openapi's .openapi() exists only on schemas
constructed after extendZodWithOpenApi runs, and both post-hoc naming routes (.openapi()
clones; .meta({id}) propagates through .nullable()/.extend() differently) changed the
published document. Generation with the names stripped keeps the document byte-identical
by construction, with --check in test:ci like the other generators.

The generator also emits CLI_RESPONSE_SCHEMAS, path -> response schema, derived from a
scan of the CLI's own literal api* call sites joined against the published document --
nothing hand-listed. The scanner requires the closing delimiter: the first draft
collected a template path's truncated prefix, which for a documented base path would
have validated the wrong endpoint's schema. apiGet/apiPost are overloaded so a literal
documented path returns the schema's inferred type, and apiFetch safeParses every
documented response under #9519's recorded posture: report and pass the payload through
untouched by default (returning zod's parse would strip fields the document
under-describes), throw under LOOPOVER_VALIDATE_RESPONSES for CI and self-host. The
warning dedupes per path so a polling loop cannot flood stderr, and lives in a hoisted
accessor because the entrypoint awaits runCli before module consts this far down
initialize.

Typing the responses immediately surfaced real drift: /v1/local/branch-analysis returns
predictedGate and dataQuality, which the document omitted entirely while two predict-gate
tools read them; both are now declared (as unknown until #9531 types the verdict). The
skipped-pr-audit call site moves its query out of the path template so the scanner keeps
it validated.

`payload: any` is extinct in the package and pinned by a grep gate test, along with the
table matching the scan exactly and the unvalidated remainder as a shrink-only list. The
posture tests drive the real compiled bin against a local server in both modes.
Adding four modules to @loopover/contract produced a GREEN `@loopover/contract:build` in
CI followed five seconds later, on the same runner, by
`Cannot find module '@loopover/contract/local-config'` from the miner's build.

`tsc -p` with `incremental` (inherited from the root tsconfig) decides what to emit from
.tsbuildinfo alone -- it never checks whether the outputs that stamp describes are still
on disk. turbo caches .tsbuildinfo alongside dist/, so a run where the two come back out
of lockstep, or a --force run over a leftover stamp, makes tsc declare itself up to date
and emit nothing. Reproduced exactly by deleting dist/ with the stamp left in place.

All three packages that cache .tsbuildinfo as a turbo output have the same exposure, so
each clears the stamp before compiling; any new file in any of them could have hit this.

The UI preview workflow separately never built the contract at all -- it builds the engine
and then runs ui:openapi, which now reaches src/openapi/schemas.ts's re-export of the
public-API schemas. It builds both packages now.

package-lock.json was out of sync with two real changes: the contract's new
@cloudflare/workers-types devDependency and the UI's new @loopover/contract dependency.
…was silently dropping

The module was never committed. `local-config.*` is a common global-gitignore pattern, so
every commit silently omitted the file while the local tree built fine — CI then failed on
a module that did not exist in the repo. The clean-clone reproduction is what found it:
dist/ had orb-broker and public-api but no local-config, because the source was not there.

Renamed to cli-config, which no common pattern matches, with a note not to rename it back.
Also hoists the contract build above the drift checks, which import the tool registry.
…r does not fail it

The packaging-convention test asserted build:tsc was exactly `tsc -p tsconfig.json`, which
the incremental-emit fix breaks by prefixing the stamp clear. Matched as a suffix so the
guard still pins the compile itself.
…ead fallbacks

codecov/patch was 86.76% with 27 lines missing, all in packages/loopover-mcp/bin. The
cause is structural: nearly every CLI test spawns the real compiled bin, which is right for
testing the process boundary but reports no coverage back to vitest — the work happens in
another process. So the dispatch table, the derived help printers, and the response
renderers were exercised without being measured.

These drive the same paths in-process through the exported runCli and the in-memory MCP
transport, the pattern mcp-cli-basics and mcp-cli-list-notifications-tool already use.

The response-validation suite pins both halves of #9519's recorded posture: the default
warns on stderr and returns the payload untouched, and LOOPOVER_VALIDATE_RESPONSES opts
into throwing. Driving it needed fixture responses that genuinely violate a schema, so the
harness gains three narrow options — a body that fails validation, one that is valid JSON
of the wrong top-level type, and one that is not JSON at all on a 200. Picking a violating
path took care: several validated responses declare every field optional, so `{}` satisfies
them and the test would have passed while proving nothing.

Three fallbacks turned out to be unreachable rather than untested, so they are gone instead
of covered: printableUsage's missing-note arm (its parameter is now narrowed to the
commands that declare one), `path.split("?")[0] ?? path` (split always yields an element),
and the repeated `watch.labels ?? []` inside its own length check. The mismatch reporter's
`issues[0]` is likewise always present on a failed parse; only its path can be empty, which
is what "(root)" is for.
@JSONbored
JSONbored merged commit bd139a5 into main Jul 29, 2026
10 checks passed
@JSONbored
JSONbored deleted the feat/generated-surfaces-9521 branch July 29, 2026 00:19
JSONbored added a commit that referenced this pull request Jul 29, 2026
The hand-edited list this branch added is replaced by the generated one now that #9590 has
landed -- which is what the issue asked for: the tool tables pick new tools up with no
hand-edits. 163 registry entries across the three servers.
JSONbored added a commit that referenced this pull request Jul 29, 2026
The hand-edited list this branch added is replaced by the generated one now that #9590 has
landed -- which is what the issue asked for: the tool tables pick new tools up with no
hand-edits. 163 registry entries across the three servers.
JSONbored added a commit that referenced this pull request Jul 29, 2026
…dflare Workers build

The Workers build for loopover-ui has been failing on every PR since #9521
(merged as #9590) made src/openapi/schemas.ts import
@loopover/contract/public-api:

  Cannot find module '.../node_modules/@loopover/contract/dist/public-api.js'
    imported from /opt/buildhome/repo/src/openapi/schemas.ts

ui:build builds ui-kit and engine, then runs ui:openapi -- but never builds
the contract package, so the import resolves to a dist/ that does not
exist. CI did not catch it because the GitHub workflow has its own
separate "Build contract package" step (ci.yml:361) before the drift
checks; the Cloudflare build runs npm run build:cloudflare -> ui:build
directly and gets no such step. The two paths had silently diverged.

Add @loopover/contract to the same turbo invocation that already builds
the engine, so the one script both paths share produces everything
ui:openapi imports.

Reproduced locally by deleting packages/loopover-contract/dist and running
ui:openapi (identical ERR_MODULE_NOT_FOUND), then confirmed the fixed
chain builds the package and writes the spec with no drift.
JSONbored added a commit that referenced this pull request Jul 29, 2026
…doctor, tenant health (#9523) (#9607)

* feat(mcp): add the AMS management tool family behind the governor gate

The miner MCP exposed 11 read-only tools while its entire mutating ops surface was
CLI-only. This adds 10 tools: a dedicated doctor (split out of status so status stays
cheap), a structured metrics snapshot, and eight mutations.

Every mutation dispatches through the miner's existing governor-gated chat-action
chokepoint -- the same boundary the dashboard's own actions use. The MCP layer never
touches a store: it dispatches an action NAME, and the registry structurally refuses any
handler not produced by governorGatedHandler(), whose brand is a private symbol a raw
function cannot forge. That is what makes "an MCP caller cannot reach a write path the
dashboard could not" a property of the code rather than of review discipline, and the
structural test asserts it from both ends.

Three claims I had to correct against the real code rather than ship as written. The
migrate CLI has no dry-run, because applying a migration IS opening the store -- so the
tool has no apply flag either, instead of advertising a safety mode that does not exist.
The deny-hook store keys proposals by (repo, id), so the decide tool takes the repo rather
than scanning every repo's proposals to resolve an id. And purgeRepoAcrossStores is
extracted from runPurge so the tool runs the CLI's own purge over the CLI's own target
list, rather than a second implementation free to miss a store.

collectMinerPredictionMetrics is likewise extracted in the engine: the Prometheus text
renderer now formats those families, so the scrape and the JSON snapshot share one
aggregation and cannot disagree about what a counter means.

validate:mcp earned its keep twice here. It caught the two AMS tenant tools declared but
never registered, and it caught loopover_miner_run_migrations/_purge_repo returning fields
their output schemas did not declare -- the .shape re-wrap that drops looseObject's
catchall, the same -32602 class this epic already fixed once. Both outputs now declare
every field their handlers return.

AMS tenant create/list/destroy are deliberately absent: #9522's loopover_tenant_* tools are
product-parameterized and already serve product "ams", because the control plane's routes
are. Only health and wake -- the pair with no ORB counterpart -- are added. The catalog's
recorded exclusions (calibration floors, raw run-state set, the one-way kill switch) stay
CLI-only, pinned by a test so reversing that decision has to be deliberate.

* test(mcp): cover the miner ops actions and the full dispatch path

The store operations and the registered handlers were only reachable through the MCP tools,
so nothing measured them directly. These drive each action end to end -- action name
through the governor gate to the store call -- plus the validator guard clauses for null,
primitive, and array params, and the fail-closed paths (flag disabled, unknown action,
malformed params never reaching a store).

registerMinerOpsChatActions gains the same evaluateGate seam the dashboard's own
registerPortfolioQueueChatActions already exposes, so a test can drive the dispatch path
without the real chokepoint; production passes none and gets the real one.

paramsOf loses its `?? {}`: every action that calls it has a validator requiring an object,
and dispatchChatAction runs that validator first, so the fallback had no reachable case.

* chore(docs): let #9521's generator own the miner tool table

The hand-edited list this branch added is replaced by the generated one now that #9590 has
landed -- which is what the issue asked for: the tool tables pick new tools up with no
hand-edits. 163 registry entries across the three servers.

* test(mcp): cover the AMS tools' registration and result-shaping layers

codecov/patch was short because the new tool REGISTRATIONS were never driven in-process --
the miner bin's 169 changed lines had 54 uncovered statements, and the AMS tenant handlers
had none of their own tests at all.

These drive every new tool over the in-memory transport against injected seams: the doctor
mapping from status.js's {name, ok, detail} onto the contract's pass/warn/fail, the metrics
snapshot's shared aggregation, and each mutating tool's dispatch and result shaping --
including that a governor refusal comes back as a structured blocked result rather than a
thrown error, and that a purge with confirm absent or false is rejected by the schema
BEFORE any dispatch happens.

The AMS tenant pair gets its not-configured, healthy, and throttled paths, the last of
which is the one that must not be audited as a cycle: the schedule guard refusing a
too-soon wake is the guard working, not work that ran.

Writing the metrics test surfaced a fixture trap worth naming: toPredictionRecords reads
the LEDGER row shape (conclusion/targetId/ts), so a fixture shaped like the downstream
record yields conclusion: undefined and fails output validation. The test now uses the
ledger's own shape.

* test(engine): cover the prediction-metrics split in the engine's OWN suite

codecov reported this file at 35.84% under the `engine` flag while the root vitest suite
showed it fully covered. Both were right: @loopover/engine uploads coverage from its own
node:test suite (packages/loopover-engine/test/**), and that suite had no test for this
file at all — the vitest one lands under `backend` and does not lift the engine flag.

So the aggregation and the renderer get tests where the engine actually measures itself,
including the property the split exists for: every sample `collectMinerPredictionMetrics`
reports appears verbatim in what `renderMinerPredictionMetrics` emits, so the JSON snapshot
and the Prometheus scrape cannot report different numbers.

Also pins the escaping, which is a correctness rule rather than cosmetics: a conclusion is
DATA, so a quote, backslash, or newline inside one must not forge a second series.
JSONbored added a commit that referenced this pull request Jul 29, 2026
…#9569) (#9608)

* feat(proof): public per-repo proof summary, endpoint and README badge (#9569)

The shareable, unauthenticated twin of the in-app trust panel. One
composition serves both, so the public page and #9193's panel cannot
disagree about a figure -- which is the property the page exists to
demonstrate.

THE PRIVACY BOUNDARY IS STRUCTURAL. Every field is built by NAMING it,
never by filtering a wider object. A blocklist has to anticipate every
field a future upstream type might grow and silently leaks the one it did
not; an allowlisted shape cannot leak a field nobody wrote down. Tested by
feeding hostile records carrying hotkey/wallet/reward/trust-score/private-
rank and asserting none of it reaches the serialized page -- while the
named fields do, so the test proves allowlisting rather than an empty
object.

NEVER A BARE SCALAR. Any accuracy figure carries its coverage and a Wilson
interval; below a 20-decision floor there is no rate at all, only an
explicit insufficient_data state that still publishes the count. A perfect
record over 19 decisions must not render as 100%. Wilson rather than Wald
because a gate metric lives near p->1, exactly where Wald claims
impossible certainty.

HONEST BOUNDARY STATES. An empty ledger is `empty`, not `verified` --
different claims. A failed read is `unavailable`, not `broken`, which
would accuse the operator of tampering. A FAILED anchor attempt is not an
anchor: the public attempt log is where failures are legible, and
presenting one here would claim corroboration that does not exist. The
verification-contract boundary statement travels IN the payload, so a
screenshot or embed cannot shed it the way a footer caption can.

The badge reports the LEDGER's state rather than an accuracy percentage: a
badge is a one-glance claim, and an accuracy number without the interval
that makes it honest does not fit in one. Disabled and errored both render
a neutral SVG -- a broken image in a README is worse than an honest
"unavailable".

DECISION (requirement 6), recorded beside the code that implements it: the
page is opt-OUT per repo, default ON once the operator's fleet-wide flag
(default OFF) is on. Every figure is already publicly fetchable through
the ledger-verify / anchors / decision-record endpoints, so gating a page
over it would add friction without privacy. The per-repo switch still
exists because a page is a different artifact from an API -- discoverable,
linkable, and it markets a repo's numbers whether or not the maintainer
wants that. A repo can opt out but cannot opt IN when the operator has
not, which keeps the fleet switch a real switch.

Found and fixed while testing: `DB.prepare()` throws SYNCHRONOUSLY on a
driver-level failure, so the `.catch()` chain never ran and a D1 outage
would have 503'd the whole public page instead of degrading. Each section
is now a real try/catch, which is the difference between the
fail-safe-per-section contract being documented and being true.

Backend half of #9569; the /proof/:owner/:repo UI route renders this
payload and lands separately.

* fix(proof): actually wire the per-repo opt-out the routes only claimed to honor (#9569)

Review caught the real defect: both handlers called
isProofPageEnabledForRepo(c.env) with no second argument, so the
ProofPageRepoOverride documented at length in proof-summary.ts and in the
PR body was never loaded or passed. Every repo was effectively
opt-out-less once the fleet flag was on -- a gate that is described,
typed, and unit-tested as a pure function, but never reachable from the
surface it governs. That is the registered-but-unreachable class, and the
long comment made it worse rather than better by making it look done.

- Adds a real `publicProof:` focus-manifest block (engine parser + toJson
  + loader snapshot), mirroring `publicStats:`/`ops:`. Precedence is
  deliberately the opposite of those two: read from the TARGET repo's
  manifest rather than the operator's self-repo, because the thing being
  opted out of is that repo's own page.
- loadProofPageRepoOverride resolves it, degrading a failed manifest load
  to "no override" -- a broken manifest never takes a page DOWN, which is
  the failure direction worth accepting here and is now stated in the
  doc comment rather than left implicit.
- Both routes load the override BEFORE anything else, so a repo that
  turned its page off does not have its decision records queried to build
  a summary that will be discarded.
- Documents the block in .loopover.yml.example, including the precedence
  and the opt-out default.

Tests that would have caught it: a repo opting out in its manifest now
gets 404 from BOTH routes with the fleet flag on, while a different repo
in the same fleet still serves 200 (the opt-out is per repo, not a kill
switch); explicit opt-in and no-block-at-all both serve; and the resolver
is covered across absent/explicit/failing loads.

* fix(build): build @loopover/contract in ui:build, unbreaking the Cloudflare Workers build

The Workers build for loopover-ui has been failing on every PR since #9521
(merged as #9590) made src/openapi/schemas.ts import
@loopover/contract/public-api:

  Cannot find module '.../node_modules/@loopover/contract/dist/public-api.js'
    imported from /opt/buildhome/repo/src/openapi/schemas.ts

ui:build builds ui-kit and engine, then runs ui:openapi -- but never builds
the contract package, so the import resolves to a dist/ that does not
exist. CI did not catch it because the GitHub workflow has its own
separate "Build contract package" step (ci.yml:361) before the drift
checks; the Cloudflare build runs npm run build:cloudflare -> ui:build
directly and gets no such step. The two paths had silently diverged.

Add @loopover/contract to the same turbo invocation that already builds
the engine, so the one script both paths share produces everything
ui:openapi imports.

Reproduced locally by deleting packages/loopover-contract/dist and running
ui:openapi (identical ERR_MODULE_NOT_FOUND), then confirmed the fixed
chain builds the package and writes the spec with no drift.

* fix(manifest): register publicProof as a known top-level field, and sync the example template

Two failures from the #9569 manifest block, both mine.

1. The unknown-top-level-field validator never learned about `publicProof`,
   so every manifest carrying it warned "Manifest contains unknown
   top-level field: publicProof." That was invisible on the first pass and
   appeared on every LATER one, because the first pass parses a manifest
   with no such key while later passes reload the persisted snapshot --
   which my loader change now serializes the field into. The warning lands
   in the published review comment, so an unchanged PR got a fresh comment
   PATCH on every regate sweep: exactly the #3379 churn that test exists to
   prevent, reintroduced by a field the writer knew about and the reader
   did not.

   Found by instrumenting the test's PATCH interception to diff the two
   comment bodies rather than guessing at the cause; the added line named
   itself.

2. config/examples/loopover.full.yml must mirror .loopover.yml.example
   from "WHERE IT LIVES" onward, and I documented the block in only one of
   the two.

Verified against origin/main first to confirm both were regressions from
this branch rather than pre-existing.

* test(proof): close the patch-coverage gaps, and fix a second sync-throw the gap exposed

Codecov flagged 8 uncovered changed lines across focus-manifest.ts and
routes.ts. I had measured coverage on proof-summary.ts and proof-badge.ts
only, and never on the two files the manifest block and the routes
actually touched -- so the gap was in my own verification, not just the
tests.

Closing it turned up a real defect rather than only missing assertions:
loadProofPageRepoOverride used `.catch()` on the injected manifest loader,
so a loader throwing SYNCHRONOUSLY (a driver-level failure before it ever
returns a promise) skipped the handler entirely and would have escaped to
the route -- 503ing a public page over a manifest read that is supposed to
be optional. That is the same defect this file already had in
loadProofSummary's section reads, which I fixed there and then
reintroduced here. Now a real try/catch, with a regression test using a
synchronously-throwing loader.

Coverage:
- parsePublicProofConfig / publicProofConfigToJson: explicit on/off, a
  present-but-empty block (present-but-false, which the resolver keys on),
  absence, three non-mapping shapes warning rather than throwing, and a
  snapshot round-trip.
- A regression test asserting publicProof is a KNOWN top-level field, so
  the writer/reader split behind the #3379 regate churn cannot return.
- The two route 503 arms are unreachable today (every inner read is
  individually fail-safe), so they are excluded with the house v8 pragma
  and a note on why they are kept: a future unguarded read should degrade
  to 503 rather than 500 on an unauthenticated public route. The badge arm
  uses ignore start/stop -- `next 2` miscounts across a multi-line comment
  and left the return uncovered.

All three changed files now report zero uncovered changed lines.

* refactor(proof): one shared resolver for both surfaces, and delete the unreachable arms

Replaces the coverage pragmas with the fix they were papering over.

The gate, the read and the outcome now live in ONE resolver
(resolveProofPage) that both handlers render. That is not tidiness: the
gate previously lived inline in both route bodies and exactly one of them
was wired to the per-repo opt-out, which is the defect review caught. A
shared resolver makes "the page and the badge agree about whether this
repo is published" true by construction instead of by two call sites
remembering the same thing.

With that in place the two 503 arms were provably unreachable, because
loadProofSummary is TOTAL -- every read is wrapped per section, so a
failing ledger/anchor/record read degrades to that section's honest
neutral state and the page still composes. Rather than excluding dead
branches from coverage, the outcome is gone from the type: ProofPageResult
is `ok | disabled`. A test asserts the totality directly -- every
dependency failing at once, including a DB binding that throws on property
access, still resolves to a rendered page in its neutral states.

Same treatment for buildProofAccuracy's `!interval` guard: wilsonInterval
returns null exactly when there are no trials, which IS the
nothing-decided case, so one reachable guard covers both reasons a rate is
unpublishable instead of a dead branch behind a pragma.

Net: no `v8 ignore` pragmas anywhere in the #9569 code, and zero
uncovered changed lines or branches across proof-summary.ts, routes.ts and
focus-manifest.ts.

---------

Co-authored-by: loopover-orb[bot] <296761690+loopover-orb[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

manual-review Gittensor contributor context

Projects

None yet

1 participant