Skip to content

refactor(queue): put the six PR-command handlers behind one shared prologue (#9541) - #9580

Merged
JSONbored merged 1 commit into
mainfrom
refactor/pr-command-prologue-9541
Jul 28, 2026
Merged

refactor(queue): put the six PR-command handlers behind one shared prologue (#9541)#9580
JSONbored merged 1 commit into
mainfrom
refactor/pr-command-prologue-9541

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Partially addresses #9541deliverable 1 of 3.

Summary

Six handlers — resolve, review, pause, resume, explain, generate-tests — opened with a byte-identical eleven-step sequence: parse → name guard → classify → skip-if-unclassifiable → target key → redelivery guard → load PR + settings → skip-if-no-PR → authorize → record-and-stop-if-denied. Copy-pasted six times, 30 to 300 lines apart inside a 16,000-line file.

I confirmed they were identical by extracting each one's step order and diffing, not by eye:

maybeProcessResolveCommand         parse -> name-guard -> classify -> req-ok -> targetKey -> redelivery -> load-pr-settings -> pr-missing -> authorize -> denied
maybeProcessReviewCommand          parse -> name-guard -> classify -> req-ok -> targetKey -> redelivery -> load-pr-settings -> pr-missing -> authorize -> denied
...all six identical

That distance is the whole defect mechanism, and it has fired twice in the last week:

src/queue/pr-command-prologue.ts now owns the sequence once, with its IO injected so the seam is directly testable without a webhook or a database.

Behaviour-preserving, as requirement 1 demands

The full suite passes 23,919 tests with no test modified. That is the property the requirement asks for, and it is the main evidence here — a refactor that needed its tests edited would not be one.

Two orderings are load-bearing and preserved exactly:

  • the redelivery guard runs before the loads, so a replay costs no database reads (guarding after still suppresses the duplicate write, but pays for the reads on every retry of a storm)
  • targetKey is derived from the classified request, because the unclassifiable path reports against req.targetKey, which may legitimately be null

What it deliberately does not own

The response to each step. Every handler still supplies its own audit event names and skip/denied recorders, because those strings are its public contract — operators query github_app.finding_resolved_skipped, and tests assert on it. Centralising them would be a behaviour change wearing a refactor's clothes.

One real divergence, made explicit rather than smoothed over

generate-tests carried an extra pr.state !== "open" step. That is a named policy, not an accident: a command that spends AI generation and attempts a branch commit must not run on a closed PR, and both PR-panel twins carry the identical guard (their own comments say so).

It becomes requireOpenPr, opt-in — so pause/resume/explain keep working on a closed PR exactly as before — and it runs before authorization, so a closed PR costs no miner lookup.

notMine vs handled

Kept as distinct outcomes on purpose. A handler returns false on the first (keep dispatching to siblings) and true on the second (this was ours, it is finished). Collapsing them into one falsy result is exactly how a command silently stops reaching its siblings — so the type makes that impossible to write by accident.

The checker now steers toward the seam

scripts/check-command-redelivery-guards.ts (added in #9567) accepts delegation to the prologue as satisfying the guard. A handler that delegates cannot skip it, because the sequence is no longer the handler's to get wrong — so the cheapest way to pass the check is also the structurally correct one.

Testing

14 direct tests on the seam, 100% statement and branch coverage, including:

  • both distinct-outcome arms (notMine vs handled)
  • the guard-before-loads ordering, asserted by spying that neither loader is called on a replay
  • a classifier result with every field absent (missing_repo_pr_installation_or_actor is precisely that case) still recording a well-formed skip with explicit nulls
  • requireOpenPr's opt-in behaviour and its pre-authorization placement
  • needsMinerDetection threaded verbatim — flipping it silently denies confirmed miners, since no other role could match them

Remaining in #9541

Deliverable 2 shipped as #9557. Deliverable 3 — the plan-and-execute pass into its own module with a required, typed decision-pass context — is not in this PR; it is a separate extraction with its own risk profile, and requirement 2 is explicit that structural changes land apart from anything behavioural.

…ologue (#9541)

Deliverable 1 of #9541. Behaviour-preserving: the full suite passes 23,919 tests with
NO test modified, which is the property requirement 1 asks for.

Six handlers — resolve, review, pause, resume, explain, generate-tests — opened with a
byte-identical eleven-step sequence: parse, name guard, classify, skip-if-unclassifiable,
target key, redelivery guard, load PR + settings, skip-if-no-PR, authorize,
record-and-stop-if-denied. Copy-pasted six times, 30 to 300 lines apart inside a
16,000-line file. Confirmed identical by extracting each one's step order first and
diffing them, not by eye.

That distance is the whole defect mechanism, and it has fired twice in a week:
 - #9312 added the redelivery guard to five of the six and missed `resolve`, which then
   wrote a SECOND permanent review-memory suppression row per finding on every queue
   retry until #9561 caught it.
 - #9562 found the two PR-panel twins missing the same guard, for a paid model call.

src/queue/pr-command-prologue.ts now owns the sequence once, with the IO injected so
the seam is directly testable without a webhook or a database.

WHAT IT DELIBERATELY DOES NOT OWN

The response to each step. Every handler still supplies its own audit event names and
skip/denied recorders, because those strings are its public contract — operators query
`github_app.finding_resolved_skipped` and tests assert on it. Centralising them would
be a behaviour change wearing a refactor's clothes.

Two orderings are load-bearing and preserved exactly: the redelivery guard runs BEFORE
the loads (a replay costs no database reads), and targetKey is derived from the
classified request, since the unclassifiable path reports against req.targetKey, which
may legitimately be null.

ONE REAL DIVERGENCE, made explicit rather than smoothed over

generate-tests carried an extra `pr.state !== "open"` step. That is a named policy, not
an accident — a command that spends AI generation and attempts a branch commit must not
run on a closed PR, and both PR-panel twins carry the identical guard (their comments
say so). It becomes `requireOpenPr`, opt-in, so read-only commands (pause/resume/explain)
keep working on a closed PR exactly as before. It runs before authorization, so a closed
PR costs no miner lookup.

`notMine` and `handled` are separate outcomes on purpose: a handler returns false on the
first (keep dispatching to siblings) and true on the second (this was ours, it is done).
Collapsing them into one falsy result is how a command silently stops reaching its
siblings.

scripts/check-command-redelivery-guards.ts now accepts delegation to the prologue as
satisfying the guard, so the cheapest way to pass the check is also the structurally
correct one.

14 direct tests on the seam: 100% statement and branch coverage, including both
distinct-outcome arms, the guard-before-loads ordering, the all-fields-absent classifier
result, and requireOpenPr's opt-in and pre-authorization placement.
@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-28 12:06:11 UTC

4 files · 1 AI reviewer · 2 blockers · CI green · clean

⏸️ Suggested Action - Manual Review

Review summary
This PR extracts the byte-identical eleven-step prologue shared by six PR-command handlers (resolve, review, pause, resume, explain, generate-tests) into `src/queue/pr-command-prologue.ts`, with IO injected via a `deps` object for direct unit testing. I traced the new `runPrCommandPrologue` against each of the six original inline bodies line-by-line — the step order (parse → classify → redelivery guard before loads → PR/settings load → optional open-PR check → authorize) and the exact audit-event/skip-reason payloads are preserved per handler, and each handler still owns its own event-type strings via the injected `recordSkip`/`recordDenied` callbacks, consistent with the PR's stated 'behaviour-preserving' requirement. The redelivery-guard lint script is correctly updated to also accept `runPrCommandPrologueForEnv` as satisfying the guard requirement, matching the new delegation pattern, and a dedicated test file exercises the prologue's invariants (ordering, notMine/handled distinction, requireOpenPr opt-in, needsMinerDetection threading) directly against the injected deps.

Nits — 4 non-blocking
  • src/queue/processors.ts's `runPrCommandPrologueForEnv` re-does a dynamic `import("../github/pr-command-request")` for `classifyPrCommandRequest` even though the file already has a static top-level import of the same symbol (visible in the full file content) — the comment justifies this as a deliberate lazy-load carryover, but it's worth double-checking the static import is actually used elsewhere and this isn't accidental duplication.
  • In the unclassified-request skip path, the new generic `recordSkip` passes `ctx.targetKey`/`ctx.repoFullName`/`ctx.actor` coerced to `?? null`, whereas the original inline code passed `req.targetKey`/`req.repoFullName`/`req.actor` (possibly `undefined`) directly into the same metadata fields — functionally equivalent for JSON serialization but a small representational change worth a mention in the PR description if it wasn't already covered by 'behaviour-preserving'.
  • Since this is deliverable 1 of 3 for maintainability: split processors.ts (16.2k lines, 437 touches) — the structural cause of this audit's drift and determinism gaps #9541, it'd help reviewers of the next two deliverables to know upfront which handlers/files are targeted next, so the scope of the eventual full migration is clear.
  • Consider adding a short comment or test asserting that `spec.completedEventType` for each of the six call sites in processors.ts actually matches the literal string previously used inline (a copy-paste of the event-type string with a typo would be a silent, hard-to-catch drift now that the sequence is centralized).

Concerns raised — review before merging

  • No linked issue detected: No closing reference or linked issue number was found in the PR metadata/body. — If this PR is intended to solve an issue, link it explicitly in the PR body.
  • Maintainer requires a linked issue: This repo's maintainer focus manifest requires every PR to reference a tracked issue. — Link the relevant issue (for example Closes #123) before opening the PR.
📋 Copy for AI agents — paste into your coding agent
Fix the following blocker(s) from this PR review:

1. No linked issue detected: No closing reference or linked issue number was found in the PR metadata/body. — If this PR is intended to solve an issue, link it explicitly in the PR body.

2. Maintainer requires a linked issue: This repo's maintainer focus manifest requires every PR to reference a tracked issue. — Link the relevant issue (for example `Closes #123`) before opening the PR.

Decision drivers

  • ❌ Code review — 2 blockers (1 reviewer)
  • ❌ Gate result — Blocking (Repo-configured hard blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ⚠️ Missing No linked issue or no-issue rationale found.
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 (no linked issue context).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 14 registered-repo PR(s), 13 merged, 309 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 14 PR(s), 309 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: Python, TypeScript, Ruby, Go, MDX, Shell, Solidity, JavaScript
  • Official Gittensor activity: 14 PR(s), 309 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 2 steps 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.

Decision record
  • action: hold · clause: missing_linked_issue
  • config: 6f15502c02efe422b5e71a4dc8b4323ca5dec01d6e7c87a8d5bd89832bc27eb4 · pack: oss-anti-slop · ci: passed
  • record: bdfd2b29d181e55ebbddb0b22361b80b68bb2d8b931c2436cfe8101b6603117e (schema v5, head 9d4a36c)

🟩 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

@superagent-security

Copy link
Copy Markdown
Contributor

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

@JSONbored JSONbored self-assigned this Jul 28, 2026
@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.62%. Comparing base (3bffb87) to head (9d4a36c).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9580      +/-   ##
==========================================
- Coverage   89.62%   89.62%   -0.01%     
==========================================
  Files         868      869       +1     
  Lines      110876   110835      -41     
  Branches    26362    26349      -13     
==========================================
- Hits        99374    99333      -41     
  Misses      10237    10237              
  Partials     1265     1265              
Flag Coverage Δ
backend 95.33% <100.00%> (-0.01%) ⬇️

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

Files with missing lines Coverage Δ
src/queue/pr-command-prologue.ts 100.00% <100.00%> (ø)
src/queue/processors.ts 94.81% <100.00%> (-0.10%) ⬇️

@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 28, 2026
@JSONbored
JSONbored merged commit 09a5628 into main Jul 28, 2026
7 checks passed
@JSONbored
JSONbored deleted the refactor/pr-command-prologue-9541 branch July 28, 2026 17:49
JSONbored added a commit that referenced this pull request Jul 28, 2026
Rebase onto main after #9579 and #9580 landed. The newly-enabled noUnusedLocals
immediately flagged twelve dead symbols in code merged since this PR opened -- including
two I left in #9580 myself:

  - src/queue/processors.ts: PrCommandPrologueOutcome imported but unused (the adapter's
    annotated return type was dropped in favour of inference), plus eight unused bindings
    in the prologue destructures -- handlers that do not need `pr`, `settings`,
    `authorization` or `command` were still pulling them out.
  - src/queue/pr-command-prologue.ts: LoopOverMentionCommandName, superseded by
    LoopOverActionCommandName once the spec narrowed to action verbs.
  - src/mcp/dispatch-telemetry-sink.ts: an unused McpToolCallTelemetry import from #9579.

Which is the point of the PR: the flags catch dead code at the commit that introduces it
rather than at the next audit. Zero behaviour change -- every removal is a binding or an
import TypeScript proved unreferenced, and the suite passes 24,014 tests.

The rebase conflict itself was in processors.ts's import block: main added the
pr-command-prologue import on the same lines this PR removed the unused runRetentionPrune
one. Both intents kept.
JSONbored added a commit that referenced this pull request Jul 28, 2026
…#9553, #9570, #9571, #9572) (#9573)

* build(typescript): enable noUnusedLocals/noUnusedParameters repo-wide (#9553)

Dead code is the substrate every drift bug in the 2026-07-27 audit grew on: a stale
import or an orphaned constant reads exactly like a live wire, so the next person
greps, finds it, and reasons about a code path that no longer runs.

Enabling the flags made the compiler enumerate all 515 instances. Every one in
src/** and packages/** was traced to its replacement before deletion -- all 82 were
genuine supersession leftovers, no behaviour bug hiding among them -- but the triage
turned up three real problems that were invisible under the noise:

1. src/queue/processors.ts -- sweepRepoBacklogConvergence accepted `requestedBy`
   ("schedule" | "api" | "test") and dropped it. Every sibling sweep stamps it into
   recordAuditEvent's metadata; both agent.sweep.backlog_convergence events here
   omitted it, so those records could not be attributed to a schedule vs a manual API
   trigger. Now wired into both. (Note the mechanical fix would have been to rename it
   `_requestedBy`, which cements the gap instead of closing it.)

2. test/unit/openapi.test.ts -- the #9302 REST<->MCP parity guard asserted against
   src/mcp/server.ts's gatePrecisionOutputSchema and maintainerMeasurementReportOutputSchema,
   which the tools stopped registering when #9518 moved their outputs to
   @loopover/contract. The shapes are still identical, so nothing had drifted YET --
   but the guard was watching objects no runtime reads and would not have caught a
   future contract change. Re-anchored onto GetGatePrecisionOutput.shape /
   GetOutcomeCalibrationOutput.shape, which is what the tools actually register, and
   what that file's own header comment already claimed it did.

3. src/github/resolve-command.ts was reachable only from its own test, because
   src/review/review-memory-wire.ts carried a SECOND byte-identical copy of
   normalizeResolveFindingRef (regex included) and that copy was the one production
   used. Two independent implementations of the same public-safety validation, free to
   drift. Deduped onto the original via re-export; dead-source-files:check now passes
   on a file it was about to start failing on.

Also corrects packages/loopover-engine/src/scoring/preview.ts's header, which claimed
a ReDoS guarantee via a hasUnsafeWildcardCount import that had been dead since
625e236 deduped its label matching onto label-match.ts. The guarantee is real and
unchanged; it now arrives through labelMatchesPattern, and the comment says so.

Pre-existing import-specifier violations fixed in the same pass, since the tree has to
be green for the flags to mean anything:
- scripts/actionlint.ts imported a `.ts` specifier (TS5097)
- test/unit/contract-registry.test.ts had three `.js` specifiers in a Bundler zone
- check-dead-source-files-script.test.ts tripped check-import-specifiers on its own
  string FIXTURES, the same self-referential false positive that checker's
  ALLOWED_FILENAMES already documents for its own test

Mechanics: unused parameters are renamed with a leading underscore, never deleted --
they are positional, so removing one silently re-binds every later argument. Everything
else was removed by its real TypeScript AST node span (a regex pass was tried first and
mis-bounded declarations badly enough to produce unparseable files).

Full suite green: 23,894 passed, 0 failed. tsc clean with the flags on.

* chore(engine): bump to 3.15.4 for the dead type-import removal in gate-advisory.ts

check-engine-parity holds the two hand-duplicated gate-decision twins
(packages/loopover-engine/src/advisory/gate-advisory.ts and src/rules/advisory.ts)
in lockstep: touching one without the other requires an engine version bump. That is
the mechanism, and it is doing its job here.

the engine twin. They are genuinely dead there and NOT in the host: the engine copy is
a deliberately slimmed re-implementation (#4881) that omits buildIssueAdvisory /
addIssueFindings / collisionClustersForPull, which are what use those types on the host
side. So there is no matching host edit to make -- the asymmetry is correct, and the
version bump is the sanctioned way to record it.

No behaviour change: type-only imports are erased at compile time. The bump exists so
the parity contract stays enforceable, not because the gate decides anything
differently. packages/loopover-miner/expected-engine.version moves in lockstep, as its
own check requires.

* fix(scripts,mcp): restore actionlint's `.ts` specifier and drop a dead shape #9565 added

Two rebase follow-ups after #9565 and #9574 landed.

1. scripts/actionlint.ts gets its `.ts` extension back. This PR had removed it to satisfy
   check-import-specifiers, which broke the script outright -- it runs under
   `node --experimental-strip-types`, whose ESM resolver does no extension resolution, so
   the process dies at startup with ERR_MODULE_NOT_FOUND. #9565 independently reached the
   same conclusion and added TYPE_STRIPPED_ENTRYPOINTS to the checker for exactly this
   file, so the extension is now permitted where it is required. Verified by running
   `npm run actionlint`, which fails before this change and passes after.

   #9565's version of the checker is taken wholesale over this PR's: it solves the same
   two problems (that entrypoint set, plus allowlisting check-dead-source-files-script.test.ts
   for its string fixtures), and re-litigating a file main just rewrote buys nothing.

2. src/mcp/server.ts's `loginRepoPullShape` is removed -- dead on arrival in #9565, and
   the first thing the newly-enabled noUnusedLocals caught on main. Which is the point of
   this PR: dead code now surfaces at the commit that introduces it rather than at the next
   audit.

The engine bump lands at 3.16.1 (main released 3.16.0 while this was open). It is required
by check-engine-parity: this PR removes two dead TYPE-only imports from
packages/loopover-engine/src/advisory/gate-advisory.ts, and the parity contract holds that
file in lockstep with its host twin src/rules/advisory.ts. There is no matching host edit to
make -- the engine copy is a deliberately slimmed re-implementation (#4881) omitting the
functions that use those types -- so the version bump is the sanctioned way to record a
one-sided change. No behaviour change: type-only imports are erased at compile time.
packages/loopover-miner/expected-engine.version moves with it, as its own check requires.

* chore(release): sync .release-please-manifest.json to the 3.16.1 engine bump

The manifest is a generated artifact that must move with any package.json version, and
release-manifest:sync:check fails CI when it drifts. Regenerated with the repo's own
`npm run release-manifest:sync` rather than hand-edited.

* chore: prune the dead symbols the new flags caught in newly-merged code

Rebase onto main after #9579 and #9580 landed. The newly-enabled noUnusedLocals
immediately flagged twelve dead symbols in code merged since this PR opened -- including
two I left in #9580 myself:

  - src/queue/processors.ts: PrCommandPrologueOutcome imported but unused (the adapter's
    annotated return type was dropped in favour of inference), plus eight unused bindings
    in the prologue destructures -- handlers that do not need `pr`, `settings`,
    `authorization` or `command` were still pulling them out.
  - src/queue/pr-command-prologue.ts: LoopOverMentionCommandName, superseded by
    LoopOverActionCommandName once the spec narrowed to action verbs.
  - src/mcp/dispatch-telemetry-sink.ts: an unused McpToolCallTelemetry import from #9579.

Which is the point of the PR: the flags catch dead code at the commit that introduces it
rather than at the next audit. Zero behaviour change -- every removal is a binding or an
import TypeScript proved unreferenced, and the suite passes 24,014 tests.

The rebase conflict itself was in processors.ts's import block: main added the
pr-command-prologue import on the same lines this PR removed the unused runRetentionPrune
one. Both intents kept.
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

Development

Successfully merging this pull request may close these issues.

1 participant