Skip to content

fix(miner): discover --dry-run must not create/migrate/prune the event ledger (#9679) - #9905

Merged
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
kai392:fix/9679-dryrun-no-ledger-write
Jul 29, 2026
Merged

fix(miner): discover --dry-run must not create/migrate/prune the event ledger (#9679)#9905
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
kai392:fix/9679-dryrun-no-ledger-write

Conversation

@kai392

@kai392 kai392 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

What

runDiscover's --dry-run contract (discover-cli.ts:228, :537-540) is "fetches + ranks exactly as a real run, but makes zero writes". The min-rank-override block that sits above the dry-run branch called initEventLedger unconditionally, and initEventLedger creates the ledger file, applies schema migrations, and runs pruneLedgerByRetention (which can delete rows when LOOPOVER_MINER_LEDGER_RETENTION_DAYS / _MAX_ROWS are set). That is three classes of write on a command whose entire point is to make none — and the event ledger was the only store the dry-run path still touched.

How

The min-rank value is genuinely wanted on the dry-run preview (so it shows the exact skip set a real run would produce), so read it without creating the store: guard the initEventLedger(...) call with existsSync(resolveEventLedgerDbPath(env)) when parsed.dryRun is true, falling back to AMS_MIN_RANK_SHIPPED when the file is absent — exactly the value an empty/new ledger would yield. Same "skip a file that doesn't exist yet" discipline as migrate-cli.ts:104 / store-maintenance.ts:117. The non-dry-run path keeps its unconditional initEventLedger, and the existing try/catch/finally fail-open shape is preserved.

Tests

Three named cases in test/unit/miner-discover-cli.test.ts:

  1. --dry-run against a config dir with no event ledger leaves no ledger file afterwards (fails against the current code).
  2. --dry-run against an existing ledger carrying a min-rank override still consumes it (minRankScore = 0.2), proving the preview isn't silently degraded.
  3. The non-dry-run path still opens (and creates) the ledger.

Together they cover all three arms of the new !parsed.dryRun || existsSync(...) condition. The consumer test file miner-discover-cli-eligibility-metadata.test.ts still passes.

Closes #9679

@kai392
kai392 requested a review from JSONbored as a code owner July 29, 2026 21:14
@superagent-security

Copy link
Copy Markdown
Contributor

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

@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 21:19:36 UTC

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

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
This PR narrows the min-rank override read in discover-cli.ts:519-533 so that on `--dry-run` it only calls `initEventLedger` (which creates/migrates/prunes the ledger) when the ledger file already exists via `existsSync`, otherwise falling back to `AMS_MIN_RANK_SHIPPED`. The non-dry-run path is unchanged. The three tests added directly exercise the real failure mode (ledger file created on a dry run) and the two guarded arms (existing ledger still consumed; real run still creates it), and correctly reflect the actual pre-fix behavior traced through `initEventLedger`.

Nits — 4 non-blocking
  • The `existsSync` check followed by `initEventLedger` opening the same path is a classic TOCTOU race (file could be created/removed between the check and the open by a concurrent process), though the impact here is minor since it only affects whether the dry-run preview reads a stale/missing override.
  • discover-cli.ts is now ~533 lines per the size-smell note; consider whether the min-rank-override block (lines ~510-535) could be extracted into its own helper for readability, independent of this fix.
  • Consider extracting the min-rank-override resolution block into a small helper function (e.g. `resolveMinRankScore(env, dryRun)`) to keep runDiscover's body from growing further, per the file-size note.
  • The inline comment at discover-cli.ts:519-527 is thorough; consider trimming it slightly since the PR description already captures the same rationale.

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 #9679
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: 95 registered-repo PR(s), 50 merged, 7 issue(s).
Contributor context ✅ Confirmed Gittensor contributor kai392; Gittensor profile; 95 PR(s), 7 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: moderate
Linked issue satisfaction

Addressed
The diff adds the required existsSync guard around initEventLedger scoped to the dry-run branch, preserves the unconditional non-dry-run behavior and the try/catch/finally fail-open shape, and includes three named regression tests covering all arms (dry-run without ledger file, dry-run with existing ledger/override, and non-dry-run) exactly as the issue specifies.

Review context
  • Author: kai392
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: Python, Cuda, JavaScript, Kotlin, MDX, Perl, Ruby, TypeScript
  • Official Gittensor activity: 95 PR(s), 7 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Keep the PR focused and include validation evidence before maintainer review.
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.

🟩 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

…t ledger (JSONbored#9679)

`runDiscover`'s `--dry-run` contract (discover-cli.ts:228, :537-540) is
"zero writes", but the min-rank-override block that sits above the dry-run
branch called `initEventLedger` unconditionally. initEventLedger creates
the ledger file, applies schema migrations, AND runs retention pruning
(which can delete rows) -- three classes of write on a command whose whole
point is to make none, and the only store the dry-run path still touched.

The min-rank value is genuinely wanted on the dry-run preview, so read it
WITHOUT creating the store: guard the initEventLedger call with
`existsSync(resolveEventLedgerDbPath(env))` when parsed.dryRun is true,
falling back to AMS_MIN_RANK_SHIPPED when the file is absent -- exactly the
value an empty/new ledger would yield. Same "skip a file that doesn't
exist yet" discipline as migrate-cli.ts / store-maintenance.ts. The
non-dry-run path is unchanged (unconditional initEventLedger). The existing
try/catch/finally fail-open shape is preserved.

Three named cases: --dry-run against a config dir with no ledger leaves no
ledger file (fails against current code); --dry-run against an EXISTING
ledger still consumes the earned min-rank override (0.2), proving the
preview isn't degraded; the non-dry-run path still creates the ledger.

Closes JSONbored#9679

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 29, 2026
@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 79.36%. Comparing base (25dcdd8) to head (940fbf2).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9905      +/-   ##
==========================================
+ Coverage   79.29%   79.36%   +0.06%     
==========================================
  Files         281      282       +1     
  Lines       58566    58817     +251     
  Branches     6785     6874      +89     
==========================================
+ Hits        46440    46678     +238     
- Misses      11843    11844       +1     
- Partials      283      295      +12     
Flag Coverage Δ
backend 94.82% <100.00%> (?)

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

Files with missing lines Coverage Δ
packages/loopover-miner/lib/discover-cli.ts 94.82% <100.00%> (ø)

@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 8b44697 into JSONbored:main Jul 29, 2026
8 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 30, 2026
7 tasks
JSONbored added a commit that referenced this pull request Jul 30, 2026
Refs #9948.

release-please DROPS a commit whose title it cannot parse -- no version bump, no
changelog entry -- and the job still reports success. Two shipped fix(miner)
commits were invisible to the release when this was found, and the only evidence
was buried in the run's logs.

The shape is a double reference suffix, '(#9679) (#9905)', which this repo
produces whenever a PR title already carries the issue number and the merge
appends the PR number.

Checked BEFORE release-please runs rather than by scraping its logs afterwards: a
title is a fact available at commit time, and a guard on the input is one nobody
has to remember to read.

Scoped to the commits a branch adds over its base, NOT to everything since the
last release tag. Git history is immutable and main already carries two titles of
this shape, so a check that failed on those would be permanently red, would block
every unrelated PR, and would be switched off within a day. Guarding new commits
stops the class going forward, which is the only thing a pre-merge gate can
honestly do about titles that already shipped.

The pattern is deliberately narrow -- it matches the one shape observed to break
the parser rather than reimplementing the conventional-commits grammar, which
would drift from the real parser and start rejecting titles release-please
handles fine.
JSONbored added a commit that referenced this pull request Jul 30, 2026
Refs #9948.

release-please DROPS a commit it cannot parse -- it contributes to neither the
version bump nor the changelog -- and the run still reports success. Two shipped
fix(miner) commits were invisible to the release when this was found.

The first cut of this guard matched the double issue/PR suffix in the subject,
'(#9679) (#9905)', because that is what the failing commits visibly had in
common. Driving release-please's actual parser proves that shape parses fine.
The real trigger is NESTED PARENTHESES IN THE BODY, and it is position-sensitive
-- the parser reports a column, and the same nested text parses when it sits
further along the line. That guard would have caught nothing while reading as
protection, which is worse than no guard.

So this matches nothing. It runs the parser release-please itself uses and
asserts every commit on the branch survives it, with release-please added as a
devDependency and its own exported type used for the signature so an upgrade that
changes the API is a build error rather than a silently-wrong cast.

Scoped to the branch's own commits: history is immutable, and a check that failed
on what already shipped would be permanently red and switched off within a day.

A fixture holds the real dropped message, with a regression test asserting it
STILL fails to parse -- so if an upgrade ever fixes the parser, that test goes red
and says the guard can be relaxed, rather than silently protecting nothing.
JSONbored added a commit that referenced this pull request Jul 30, 2026
… provision the anchor key without displaying it (#9957)

* ci(release): fail on a commit title release-please cannot parse

Refs #9948.

release-please DROPS a commit whose title it cannot parse -- no version bump, no
changelog entry -- and the job still reports success. Two shipped fix(miner)
commits were invisible to the release when this was found, and the only evidence
was buried in the run's logs.

The shape is a double reference suffix, '(#9679) (#9905)', which this repo
produces whenever a PR title already carries the issue number and the merge
appends the PR number.

Checked BEFORE release-please runs rather than by scraping its logs afterwards: a
title is a fact available at commit time, and a guard on the input is one nobody
has to remember to read.

Scoped to the commits a branch adds over its base, NOT to everything since the
last release tag. Git history is immutable and main already carries two titles of
this shape, so a check that failed on those would be permanently red, would block
every unrelated PR, and would be switched off within a day. Guarding new commits
stops the class going forward, which is the only thing a pre-merge gate can
honestly do about titles that already shipped.

The pattern is deliberately narrow -- it matches the one shape observed to break
the parser rather than reimplementing the conventional-commits grammar, which
would drift from the real parser and start rejecting titles release-please
handles fine.

* feat(ledger): provision the anchor key without ever displaying it

Refs #9940.

The keygen printed the private key to stdout, so provisioning meant a human
copying a signing key out of a terminal -- where it lands in scrollback, in shell
history if piped, and in whatever captured the session. One of ours had to be
discarded for exactly that reason.

`--secret-file <path>` writes the private half directly at 0600 and prints only
the public half, which is meant to be published. Nothing to copy, nothing to
leak. Point LOOPOVER_LEDGER_ANCHOR_PRIVATE_KEY_FILE at the path -- the generic
<NAME>_FILE mechanism every self-host secret already supports -- and recreate the
container.

Refuses to overwrite an existing file: replacing a live signing key strands every
anchor published under it, which is the same reason a retired key is closed
rather than removed.

The printing mode stays for reading the shape before provisioning.

* ci(release): fail on a commit release-please would silently drop

Refs #9948.

release-please DROPS a commit it cannot parse -- it contributes to neither the
version bump nor the changelog -- and the run still reports success. Two shipped
fix(miner) commits were invisible to the release when this was found.

The first cut of this guard matched the double issue/PR suffix in the subject,
'(#9679) (#9905)', because that is what the failing commits visibly had in
common. Driving release-please's actual parser proves that shape parses fine.
The real trigger is NESTED PARENTHESES IN THE BODY, and it is position-sensitive
-- the parser reports a column, and the same nested text parses when it sits
further along the line. That guard would have caught nothing while reading as
protection, which is worse than no guard.

So this matches nothing. It runs the parser release-please itself uses and
asserts every commit on the branch survives it, with release-please added as a
devDependency and its own exported type used for the signature so an upgrade that
changes the API is a build error rather than a silently-wrong cast.

Scoped to the branch's own commits: history is immutable, and a check that failed
on what already shipped would be permanently red and switched off within a day.

A fixture holds the real dropped message, with a regression test asserting it
STILL fails to parse -- so if an upgrade ever fixes the parser, that test goes red
and says the guard can be relaxed, rather than silently protecting nothing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

miner(discover): --dry-run opens, migrates and prunes the event ledger

2 participants