Context first: the bounty system is not a value-transfer state machine. There is no create/fund/claim/award/cancel path in src/**. The only writer is POST /v1/internal/bounties/import (src/api/routes.ts ~5030), gated by INTERNAL_JOB_TOKEN, ingesting a Gittensor snapshot; every read is advisory context. No payout webhook, no wallet/address/hotkey storage, no external call that moves money. Verified live: the bounties table is currently EMPTY. So the findings below are latent, not active — filed so they are fixed before the mirror is populated.
1. Transition events persist after all upserts (audit gap). The loop does getBounty → upsertBounty → diff-and-collect, then persists events only after the whole loop (await Promise.all(events.map(...))). Any mid-loop failure leaves bounties rows already advanced with zero lifecycle rows recorded — and because the event is derived by diffing against the now-overwritten row, the transition is unrecoverable. The same "state advanced, audit trail didn't" shape as #8997.
2. Upsert cannot survive an id change (permanent wedge). upsertBounty (src/db/repositories.ts ~4984-5008) uses onConflictDoUpdate({target: bounties.id}), but the table also has UNIQUE(repo_full_name, issue_number) (migrations/0001_initial.sql ~103). If upstream re-issues a bounty on the same issue under a new id, the insert conflicts on the unhandled index and throws. No per-item try/catch, no transaction → the route 500s, the rest of the snapshot never imports, and per (1) the whole batch's lifecycle events are lost. It repeats on every subsequent import: the mirror silently freezes until someone edits the DB by hand.
3. classifyBountyLifecycle uses unanchored substring matching (packages/loopover-engine/src/signals/engine.ts ~3832-3846). /complete|paid|resolved|rewarded|awarded|fulfil|merged|claimed|done/ → completed, so a status of unclaimed (contains claimed) classifies as completed, as do not_completed and undone. avoid matches void → cancelled; renewed matches new → active. isHistoricalBountyLifecycle then continues at ~1470, dropping the issue from contributor opportunity ranking entirely — one mis-vocabularied upstream status silently hides live funded work from every contributor.
4. fundingStatus compares against one hardcoded zero-string (~3946): amount !== 0 && amount !== "0.0000". The !== 0 arm is dead for numbers (numeric 0 is already falsy), and the only zero-string caught is that exact literal — upstream sending "0", "0.0", or "0.00" yields funded, telling contributors unfunded work is paid.
Fix
- Write the lifecycle event in the same step as the upsert; wrap each item so one bad row cannot drop the batch.
- Add
(repo_full_name, issue_number) as a second conflict target.
- Anchor the classifier to word boundaries and/or an explicit known-status enum, falling back to
ambiguous.
Number(amount) with a finite check and > 0 instead of string equality.
Context first: the bounty system is not a value-transfer state machine. There is no create/fund/claim/award/cancel path in
src/**. The only writer isPOST /v1/internal/bounties/import(src/api/routes.ts~5030), gated byINTERNAL_JOB_TOKEN, ingesting a Gittensor snapshot; every read is advisory context. No payout webhook, no wallet/address/hotkey storage, no external call that moves money. Verified live: thebountiestable is currently EMPTY. So the findings below are latent, not active — filed so they are fixed before the mirror is populated.1. Transition events persist after all upserts (audit gap). The loop does
getBounty→upsertBounty→ diff-and-collect, then persists events only after the whole loop (await Promise.all(events.map(...))). Any mid-loop failure leavesbountiesrows already advanced with zero lifecycle rows recorded — and because the event is derived by diffing against the now-overwritten row, the transition is unrecoverable. The same "state advanced, audit trail didn't" shape as #8997.2. Upsert cannot survive an id change (permanent wedge).
upsertBounty(src/db/repositories.ts~4984-5008) usesonConflictDoUpdate({target: bounties.id}), but the table also hasUNIQUE(repo_full_name, issue_number)(migrations/0001_initial.sql~103). If upstream re-issues a bounty on the same issue under a new id, the insert conflicts on the unhandled index and throws. No per-item try/catch, no transaction → the route 500s, the rest of the snapshot never imports, and per (1) the whole batch's lifecycle events are lost. It repeats on every subsequent import: the mirror silently freezes until someone edits the DB by hand.3.
classifyBountyLifecycleuses unanchored substring matching (packages/loopover-engine/src/signals/engine.ts~3832-3846)./complete|paid|resolved|rewarded|awarded|fulfil|merged|claimed|done/→ completed, so a status ofunclaimed(containsclaimed) classifies as completed, as donot_completedandundone.avoidmatchesvoid→ cancelled;renewedmatchesnew→ active.isHistoricalBountyLifecyclethencontinues at ~1470, dropping the issue from contributor opportunity ranking entirely — one mis-vocabularied upstream status silently hides live funded work from every contributor.4.
fundingStatuscompares against one hardcoded zero-string (~3946):amount !== 0 && amount !== "0.0000". The!== 0arm is dead for numbers (numeric 0 is already falsy), and the only zero-string caught is that exact literal — upstream sending"0","0.0", or"0.00"yields funded, telling contributors unfunded work is paid.Fix
(repo_full_name, issue_number)as a second conflict target.ambiguous.Number(amount)with a finite check and> 0instead of string equality.