Skip to content

fix(ledger): say WHY the anchor-key list is empty, not just that it is - #9835

Merged
loopover-orb[bot] merged 1 commit into
mainfrom
fix/anchor-key-diagnosis
Jul 29, 2026
Merged

fix(ledger): say WHY the anchor-key list is empty, not just that it is#9835
loopover-orb[bot] merged 1 commit into
mainfrom
fix/anchor-key-diagnosis

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Closes #9834

What I was actually trying to find out

You asked me to investigate why /v1/public/decision-ledger/anchor-key returns nothing on loopover.ai before touching the docs. The answer is that it cannot be determined — and that is the bug.

Six causes, one response. Verified by running the parser over each:

Cause Response today
secret never set {"keys":[],"currentKeyId":null}
unparseable JSON {"keys":[],"currentKeyId":null}
an object instead of an array {"keys":[],"currentKeyId":null}
every entry typo'd (keyid vs keyId) {"keys":[],"currentKeyId":null}
valid keys, all expired currentKeyId: null
more than one current key currentKeyId: null

parseAnchorPublicKeys returns [] from four separate paths with no diagnostic; currentAnchorKey folds the last two together, returning null both when no key is current and when several are — which are different operator problems (publish a successor vs. retire one of two).

So #9719 is unverifiable. It provisioned the anchor keypair and was closed, but whether that took effect on the hosted Worker is invisible — not just to an anonymous reader, but to the operator, because a typo in the secret looks exactly like an unset secret.

The existing status field can't answer it either. publicAnchorStatus checks tipSeq === 0 before hasSigningKey, so with an empty ledger it reports empty_ledger regardless:

status with empty ledger + NO key : empty_ledger
status with empty ledger + key    : empty_ledger

That ordering is deliberate — its comment says it mirrors runScheduledLedgerAnchor's own so a reader never gets "a second opinion" — so it is left unchanged.

The precedent this restores

PublicAnchorStatus exists in the same file for exactly this reason. Its own doc:

Without this, "never configured", "no ledger to anchor", and "anchoring is healthy but has not run yet" are all indistinguishable from outside — every one of them renders as {"anchors":[]}, which reads as a healthy empty state.

Applied to /anchors; never applied to /anchor-key.

Design notes worth reviewing

  • Additive. parseAnchorPublicKeys keeps its exact signature and behaviour — the scheduler's guard order depends on it, and there's an invariant test asserting the two agree across every input. keys and currentKeyId keep their shape for existing consumers.
  • droppedEntries is reported even when status is ok, so one typo'd key among three is visible rather than silently discarded into a healthy-looking response.
  • An empty array is unconfigured, not no_valid_entries. "Configured to publish nothing" is the same actionable state as never setting it, and deliberately distinct from "entries were present and every one was rejected".
  • The raw value is never echoed back, under any status. This endpoint is unauthenticated. An operator who mis-pastes LOOPOVER_LEDGER_ANCHOR_PRIVATE_KEY into the public LOOPOVER_LEDGER_ANCHOR_KEYS var would otherwise have the private half published by the diagnostic meant to help them. There are two tests for this, with the probe assembled from fragments so neither file contains a credential-shaped literal.

Two existing assertions were pinning the ambiguity

public-anchor-key-route.test.ts had a test titled "…distinguishable from a key that exists" asserting toEqual({ keys: [], currentKeyId: null }). The empty half was true; the distinguishable half was not. Both that and the malformed-config case are updated, with the correction noted inline rather than quietly rewritten.

Validation

tsc --noEmit · ui:openapi:check · dead-source-files:check · import-specifiers:check · docs:drift-check — clean. OpenAPI description updated and apps/loopover-ui/public/openapi.json regenerated in the same commit. 28 tests pass across the anchor-key and decision-ledger route suites, 27 in the unit suite.

Coverage: every branch and statement I added is covered — verified against the coverage JSON restricted to the added line range, not just the whole-file percentage. The file's remaining uncovered lines (294–295) are pre-existing and untouched.

Noted, not fixed here

contract:api-schemas:check fails on main today — a pending regeneration adding ignoredCheckRuns, left over from #9813 alongside the three problems #9829 already fixed. I confirmed it reproduces with my changes stashed, so it is not from this PR; keeping it out to stay focused, and doing it separately.

`GET /v1/public/decision-ledger/anchor-key` answered
`{"keys":[],"currentKeyId":null}` to six different causes: the secret unset,
unparseable JSON, a non-array, every entry failing validation, all keys expired,
and an ambiguous rotation. parseAnchorPublicKeys returns [] from four separate
paths with no diagnostic, and currentAnchorKey folds two more together by
returning null both when NO key is current and when more than one is.

The consequence is concrete rather than cosmetic. #9719 provisioned the anchor
keypair and was closed, but whether that took effect on the hosted deployment
could not be determined -- from outside OR by the operator -- because a typo in
the secret is byte-identical to an unset secret. An empty response reads as a
healthy empty state.

This is the same defect PublicAnchorStatus was introduced to fix for the sibling
anchors listing ("never configured", "no ledger to anchor" and "healthy but has
not run yet" all rendering as {"anchors":[]}), never applied here. It could not
be answered by that status either: publicAnchorStatus checks tipSeq === 0 before
hasSigningKey, so on an empty ledger it reports empty_ledger whether or not a key
is configured -- deliberate, since it mirrors the scheduler's own guard order,
and left unchanged.

diagnoseAnchorPublicKeys is additive and pure. parseAnchorPublicKeys keeps its
exact signature and behaviour (the scheduler depends on it, asserted), and
`keys`/`currentKeyId` keep their shape for existing consumers. droppedEntries is
reported even when the status is ok, so one typo'd key among several is visible
instead of silently discarded into a healthy-looking response.

The raw value is NEVER echoed back under any status. This endpoint is
unauthenticated, and an operator who mis-pastes
LOOPOVER_LEDGER_ANCHOR_PRIVATE_KEY into the public var would otherwise have the
private half published by the very diagnostic meant to help them.

Closes #9834
@loopover-orb

loopover-orb Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Tip

✅ LoopOver review result - approve/merge recommended

Review updated: 2026-07-29 13:14:52 UTC

6 files · 1 AI reviewer · no blockers · readiness 98/100 · CI green · clean

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
This PR replaces the ambiguous `{keys: [], currentKeyId: null}` empty response with a `diagnoseAnchorPublicKeys` function that classifies six previously-indistinguishable failure causes (unconfigured, malformed, no_valid_entries, expired, ambiguous_rotation, ok) into an explicit `status` field plus a `droppedEntries` count, wired through the `/v1/public/decision-ledger/anchor-key` route, OpenAPI docs, and both unit/integration tests. The change is additive (existing `keys`/`currentKeyId` shape preserved), correctly leaves `publicAnchorStatus`'s deliberate `tipSeq === 0` ordering untouched per its own stated reasoning, and includes a well-targeted security test verifying the raw configured value (which could contain a mis-pasted private key) is never echoed back under any status. Trace of `parseAnchorPublicKeys`/`currentAnchorKey` confirms the six-cause collapse claim is accurate given the existing code shown in reference context.

Nits — 6 non-blocking
  • src/openapi/spec.ts and openapi.json duplicate the same long description string; consider deriving one from the other or sharing a constant to avoid drift (nit, pre-existing pattern in this file though).
  • The magic issue number 9834 is scattered through comments/type names rather than centralized, though this matches the codebase's existing convention of inline issue-number comments.
  • No test appears to directly assert `droppedEntries` counts multiple dropped entries (e.g., two typo'd keys among three), only the single-entry case — minor coverage gap.
  • Consider adding a test case with more than one dropped entry to fully exercise `droppedEntries` arithmetic beyond the `parsed.length - keys.length` single-off-by-one case.
  • The `empty` object literal in `diagnoseAnchorPublicKeys` is spread into three return sites (src/review/ledger-anchor.ts) — fine as-is, but a small named return type/constant could reduce repetition if it grows further.
  • 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.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9834
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 (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 14 registered-repo PR(s), 13 merged, 345 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 14 PR(s), 345 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: significant
Linked issue satisfaction

Addressed
The PR adds a pure diagnoseAnchorPublicKeys function distinguishing all six required states (unconfigured, malformed, no_valid_entries, expired, ambiguous_rotation, ok) without changing parseAnchorPublicKeys, wires it into the route while preserving keys/currentKeyId shape, adds droppedEntries counting, ensures raw values are never echoed, updates OpenAPI specs, and includes tests for each status,

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), 345 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 <question> answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat <question> 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.

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 29, 2026
@superagent-security

Copy link
Copy Markdown
Contributor

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

@cloudflare-workers-and-pages

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 Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui b975521 Commit Preview URL

Branch Preview URL
Jul 29 2026, 12:56 PM

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 844 bytes (0.01%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.9MB 844 bytes (0.01%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-BSPnTYjU.js (New) 2.16MB 2.16MB 100.0% 🚀
assets/tanstack-vendor-BSfui9Ry.js (New) 929.59kB 929.59kB 100.0% 🚀
openapi.json 422 bytes 747.96kB 0.06%
assets/docs.fumadocs-spike-api-reference-PEOA5eSX.js (New) 443.45kB 443.45kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-DSb-a9zj.js (New) 201.7kB 201.7kB 100.0% 🚀
assets/modal-B0PABJnF.js (New) 184.5kB 184.5kB 100.0% 🚀
assets/client-Vn4u2jya.js (New) 151.47kB 151.47kB 100.0% 🚀
assets/maintainer-panel-B_OywHlT.js (New) 78.99kB 78.99kB 100.0% 🚀
assets/routes-BJ5dZvL6.js (New) 35.96kB 35.96kB 100.0% 🚀
assets/owner-panel-XwBRI6EE.js (New) 27.99kB 27.99kB 100.0% 🚀
assets/app-oW9SoyCK.js (New) 25.78kB 25.78kB 100.0% 🚀
assets/ui-vendor-DJr9t1ia.js (New) 24.57kB 24.57kB 100.0% 🚀
assets/miner-panel-D28oBVsG.js (New) 20.24kB 20.24kB 100.0% 🚀
assets/app.runs-BBUw0Jy7.js (New) 20.22kB 20.22kB 100.0% 🚀
assets/api._op-nH36pdBa.js (New) 17.57kB 17.57kB 100.0% 🚀
assets/self-hosting-docs-audit-DAewGL-F.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs._slug-EnD-dtAp.js (New) 15.52kB 15.52kB 100.0% 🚀
assets/playground-panel-ByASbOi5.js (New) 14.42kB 14.42kB 100.0% 🚀
assets/fairness-BxxDu4ne.js (New) 13.98kB 13.98kB 100.0% 🚀
assets/app.audit-BZX-F7R9.js (New) 10.08kB 10.08kB 100.0% 🚀
assets/app.config-generator-D2QtU-1j.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-1kdw3a8z.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-B8Y4qB1N.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-DuRIBvje.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/commands-panel-DHKrkVHD.js (New) 6.65kB 6.65kB 100.0% 🚀
assets/maintainer-workflow-CV_0wYQJ.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/digest-panel-CozGcaup.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-ClOqNqn9.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs-nav-B47PLe_j.js (New) 6.01kB 6.01kB 100.0% 🚀
assets/docs.index-BUu0bCHw.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/api.index-DPPFg3Fg.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-BETvgXpZ.js (New) 2.7kB 2.7kB 100.0% 🚀
assets/api-DiOaQ19w.js (New) 2.69kB 2.69kB 100.0% 🚀
assets/docs-page-C3Fhc46Z.js (New) 2.1kB 2.1kB 100.0% 🚀
assets/table-Bv1338s_.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-CZnNaiHu.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-CYJ3N9FE.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-vet5si0G.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-Cmu1ocjr.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-2oHsLwk6.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/app.maintainer-BwNHuVKL.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-CSfsqlov.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-BtOP0jPS.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-BzYn0Xwd.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-BPBk3bod.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-D7TQmknz.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-DFWYwQHn.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-FW94CuGr.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-Bf47khQa.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-DaJULD9c.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/trash-2-BPDBn-jV.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/save-DGDZI0bb.js (New) 327 bytes 327 bytes 100.0% 🚀
assets/git-pull-request-arrow-B4fNJL5x.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-D4E68ExU.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-BvyqHKPd.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-CANcsOs3.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-DR1pJk_4.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-hYiPwyK8.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/rotate-cw-Cgmrz7Hb.js (New) 201 bytes 201 bytes 100.0% 🚀
assets/play-C4XH4baf.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check--NMvNaEc.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/search-gpTP6R1a.js (New) 174 bytes 174 bytes 100.0% 🚀
assets/add-scalar-classes-CpckAJw7.js (Deleted) -2.16MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-jSG0qOwH.js (Deleted) -929.17kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-CAJ9nwHj.js (Deleted) -443.45kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-BVG1BGnC.js (Deleted) -201.7kB 0 bytes -100.0% 🗑️
assets/modal-CI3aTtIm.js (Deleted) -184.5kB 0 bytes -100.0% 🗑️
assets/client-Cfx7r9or.js (Deleted) -151.47kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-WCenTlfY.js (Deleted) -78.99kB 0 bytes -100.0% 🗑️
assets/routes-CNYuSS6E.js (Deleted) -35.96kB 0 bytes -100.0% 🗑️
assets/owner-panel-7icsvLKN.js (Deleted) -27.99kB 0 bytes -100.0% 🗑️
assets/app-BBOF3N-i.js (Deleted) -25.78kB 0 bytes -100.0% 🗑️
assets/ui-vendor-Bf_DjUlJ.js (Deleted) -24.57kB 0 bytes -100.0% 🗑️
assets/miner-panel-B_5vhoZZ.js (Deleted) -20.24kB 0 bytes -100.0% 🗑️
assets/app.runs-Dk6f334u.js (Deleted) -20.22kB 0 bytes -100.0% 🗑️
assets/api._op-CAUogDC4.js (Deleted) -17.57kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-DWvIKAF-.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs._slug-CbLcuCbS.js (Deleted) -15.52kB 0 bytes -100.0% 🗑️
assets/playground-panel-DIodF9--.js (Deleted) -14.42kB 0 bytes -100.0% 🗑️
assets/fairness-DEtlsZg5.js (Deleted) -13.98kB 0 bytes -100.0% 🗑️
assets/app.audit-LvJiqdWC.js (Deleted) -10.08kB 0 bytes -100.0% 🗑️
assets/app.config-generator-HvuleBJc.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-BWAxNIdu.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-BmyDrhsk.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-C4r26Vy8.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/commands-panel-H45BMY7j.js (Deleted) -6.65kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-Jajy4AWK.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/digest-panel-CB0jWd4O.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-Do5Ouyq3.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs-nav-DqKPIPBZ.js (Deleted) -6.01kB 0 bytes -100.0% 🗑️
assets/docs.index-_EHrdDYO.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/api.index-BrfDtSDi.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-BTjKzJne.js (Deleted) -2.7kB 0 bytes -100.0% 🗑️
assets/api-BKRI__-Q.js (Deleted) -2.69kB 0 bytes -100.0% 🗑️
assets/docs-page-Cn43ifm4.js (Deleted) -2.1kB 0 bytes -100.0% 🗑️
assets/table-Bc3HSbuf.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-BSROBbfz.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-BX8qkAz4.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-CP2UyF46.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-BqVLAGYv.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-D2Rd_ZC4.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-CRc5kNCH.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-_8xr3eil.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-DN52M4_4.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-CpCbnlQp.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-DdJPT2wo.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-CT_kvd9D.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-D8SbS7zy.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-C7yc7W4M.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-DYhQr0ag.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-DI76Rvwk.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/trash-2-DiCArEGg.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/save-Dim3-rnd.js (Deleted) -327 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-WE2uLd3c.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-CgEt3psO.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-BjNCaKK8.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-DuummLVJ.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-Ck1a9Ocz.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-DTgZIQSq.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/rotate-cw-Bn2srQKR.js (Deleted) -201 bytes 0 bytes -100.0% 🗑️
assets/play-BFlmSNLI.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-GbDdJEL5.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️
assets/search-C_CtyCf_.js (Deleted) -174 bytes 0 bytes -100.0% 🗑️

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.76%. Comparing base (1b3ca31) to head (b975521).
⚠️ Report is 8 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9835      +/-   ##
==========================================
- Coverage   91.59%   90.76%   -0.83%     
==========================================
  Files         915      915              
  Lines      112692   112704      +12     
  Branches    27078    27082       +4     
==========================================
- Hits       103220   102297     -923     
- Misses       8144     9314    +1170     
+ Partials     1328     1093     -235     
Flag Coverage Δ
backend 94.10% <100.00%> (-1.49%) ⬇️

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

Files with missing lines Coverage Δ
src/api/routes.ts 95.61% <100.00%> (ø)
src/openapi/spec.ts 99.27% <ø> (ø)
src/review/ledger-anchor.ts 100.00% <100.00%> (ø)

... and 3 files with indirect coverage changes

@loopover-orb loopover-orb Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoopOver approves — the gate is satisfied and CI is green.

@loopover-orb
loopover-orb Bot merged commit 2b2c98a into main Jul 29, 2026
12 checks passed
@loopover-orb
loopover-orb Bot deleted the fix/anchor-key-diagnosis branch July 29, 2026 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

api(ledger): /anchor-key collapses five distinct causes into one empty response

1 participant