Skip to content

fix(orb): economic-integrity and miner-identity fixes (#9077, #9078, #9079, #9080)#9238

Merged
JSONbored merged 5 commits into
mainfrom
fix/9077-9078-9079-9080-economic-integrity
Jul 27, 2026
Merged

fix(orb): economic-integrity and miner-identity fixes (#9077, #9078, #9079, #9080)#9238
JSONbored merged 5 commits into
mainfrom
fix/9077-9078-9079-9080-economic-integrity

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

Four ORB economic-integrity / miner-identity issues, one PR per the usual bundling convention.

#9079 — miner identity matched on renameable login instead of stable githubId

fetchOfficialGittensorMiner now matches on the upstream /miners list's immutable githubId first whenever the caller has one (threaded through via PullRequestRecord/IssueRecord.authorGithubId, captured since #9125), falling back to username-only matching only when no id is known yet — a strict narrowing of the old always-username behavior, never a widening.

#9078 — decision-ledger verify endpoint never reconciled record_json against record_digest

verifyDecisionLedger now joins decision_records for the window under verification and recomputes contentDigest(JSON.parse(record_json)) against what the chain committed to, reporting two new break kinds: content_mismatch and missing_record. A failed ledger append is now its own dedicated console.error alarm instead of falling into the same swallowed warn as every other persist failure.

#9080 — bounty mirror import: audit gap + permanent-wedge bug

  • Each bounty's lifecycle event is now written in the same step as its own upsert (individually try/caught), so one bad row can no longer drop the whole batch's audit trail or take down the rest of the import.
  • upsertBounty now chains a second onConflictDoUpdate on (repo_full_name, issue_number) (SQLite's native multi-target upsert), so upstream re-issuing a bounty on the same issue under a new id reconciles instead of 500ing and permanently wedging every subsequent import.
  • classifyBountyLifecycle's status regexes are now anchored on a leading word boundary (unclaimed/not_completed/avoid/renewed no longer false-positive as completed/cancelled/active via unanchored substring matches).
  • fundingStatus now parses the amount to a number and requires it finite and positive, instead of comparing against exactly one hardcoded zero-string literal ("0"/"0.0"/"0.00" no longer read as funded).

#9077 — reward multiplier chosen by contributor-authored PR title text

Two independent, judgment-scoped fixes:

  1. Satisfaction-verdict gate on reward-label maintainer trust (full fix for the SUSPECTED vector). A trustMaintainerAuthoredIssueForReward-flagged label unlocked via the maintainer-authored-issue relaxation now additionally requires a published "addressed" linked-issue-satisfaction verdict for the exact (repo, PR, issue) triple, read via the existing linked_issue_satisfaction_cache rather than spending a fresh model call. Before this, "the linked issue's author happens to be a maintainer" was the entire evidence bar — a contributor could get gittensor:priority applied by citing any open, maintainer-authored, reward-labeled issue whether or not their PR touched it. This is a deliberate coupling: a repo that hasn't opted into linkedIssueSatisfactionGateMode will never see this reward relaxation fire (fail-safe degrade, not a bypass).

  2. Partial mitigation for the CONFIRMED title-derivation vector. resolvePrTypeLabel no longer falls back to deriveKindFromTitle when a repo has linkedIssueLabelPropagation.enabled but the linked issue's labels matched none of the configured mappings — a new "propagation_unmatched" outcome applies no label at all instead (clearing any stale prior label too). This only changes behavior for a repo that has explicitly opted into propagation.

    Judgment call, flagged explicitly: this does not remove title-derived bug/feature classification for the shipped default (propagation disabled), which is what the issue's own concrete "feat: add support for X" example is about. Doing so would silently stop applying any type label at all for the overwhelming majority of self-hosted installs that haven't configured propagation mappings covering bug/feature — a bigger default-behavior change than seemed safe to ship unilaterally in this PR without a broader rollout/design discussion (the issue's own milestone is "ORB - Long Term Features & Improvements II"). The other suggested alternative — classifying from diff/changed-file evidence — isn't cheaply available at the type-label decision's call site today (no file-change data is fetched there); wiring that in is a larger, separate change. Flagging this so it can be tracked as an explicit follow-up rather than silently left half-done.

Test plan

  • npx tsc --noEmit -p tsconfig.json --incremental false (app) — clean
  • npm run build in packages/loopover-engine — clean
  • Full packages/loopover-engine own test suite (npm test) — 727/734 pass; the 7 failures are in an unrelated iterate-loop/self-review module and reproduce identically on origin/main before this branch's changes (confirmed via git stash)
  • Targeted vitest run across every file touched or exercising the changed code: pr-type-label{,-engine}.test.ts, linked-issue-label-propagation-fetch.test.ts, linked-issue-label-propagation{,-engine}.test.ts, queue.test.ts, queue-5.test.ts, decision-record.test.ts, gittensor-api.test.ts, signals{,-edge-cases,-v2}.test.ts, db-persistence.test.ts, integration/routes-errors.test.ts, integration/api.test.ts850/850 pass
  • Added regression tests for every fix: word-boundary bounty-status classification, funding-status numeric parsing, bounty upsert id-change reconciliation, bounty-import partial-batch isolation, decision-ledger content/missing-record reconciliation, githubId-first miner matching, satisfaction-verdict-gated reward propagation (both "no verdict" and "unaddressed/partial verdict" cases), and propagation-unmatched no-guess behavior
  • Rebased onto latest origin/main, rebuilt the engine package, re-ran the full targeted suite post-rebase

Closes #9077
Closes #9078
Closes #9079
Closes #9080

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

Copy link
Copy Markdown
Contributor

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

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

❌ 16 Tests Failed:

Tests completed Failed Passed Skipped
22882 16 22866 21
View the top 3 failed test(s) by shortest run time
test/unit/selfhost-pg-retention.test.ts > runRetentionPrune + processJob on the Postgres backend (#977) > processJob prune-retention deletes eligible rows and records a success audit event on Postgres
Stack Traces | 0.00561s run time
AssertionError: expected 3 to be +0 // Object.is equality

- Expected
+ Received

- 0
+ 3

 ❯ test/unit/selfhost-pg-retention.test.ts:119:44
test/unit/salvageability.test.ts > resolveAiReviewSalvageableHold > defaults: no configured floor uses the gate default, and a confidence-less blocker counts as certainty (at/above floor)
Stack Traces | 0.00745s run time
AssertionError: expected undefined to be defined
 ❯ test/unit/salvageability.test.ts:88:18
test/unit/selfhost-pg-retention.test.ts > pruneExpiredRecords on the Postgres backend (#977) > deletes across multiple bounded batches and stops at the per-table cap, same as the SQLite path
Stack Traces | 0.0134s run time
AssertionError: expected +0 to be 4 // Object.is equality

- Expected
+ Received

- 4
+ 0

 ❯ test/unit/selfhost-pg-retention.test.ts:76:33
test/unit/engine-parity-fixtures.test.ts > predicted-gate engine parity fixtures > keeps the 'merge-readiness-composite-preserves-e…' fixture parseable and aligned with its documented surface
Stack Traces | 0.0149s run time
AssertionError: expected 'success' to be 'failure' // Object.is equality

Expected: "failure"
Received: "success"

 ❯ test/unit/engine-parity-fixtures.test.ts:29:31
test/unit/reputation-wiring.test.ts > ORB/AMS reputation bridge wiring (#6801) > never downgrades the locally-computed signal when the enabled bridge does not vouch
Stack Traces | 0.0165s run time
AssertionError: expected "vi.fn()" to be called once, but got 2 times
 ❯ test/unit/reputation-wiring.test.ts:441:25
test/unit/reputation-wiring.test.ts > ORB/AMS reputation bridge wiring (#6801) > keeps the pre-bridge output unchanged when any activation gate is off
Stack Traces | 0.0167s run time
AssertionError: expected "vi.fn()" to not be called at all, but actually been called 1 times

Received:

  1st vi.fn() call:

    Array [
      "https://api.gittensor.io/miners",
      Object {
        "headers": Object {
          "accept": "application/json",
          "user-agent": "loopover/0.1",
        },
        "signal": AbortSignal {
          Symbol(kEvents): Map {},
          Symbol(events.maxEventTargetListeners): 0,
          Symbol(events.maxEventTargetListenersWarned): false,
          Symbol(kHandlers): Map {},
          Symbol(kAborted): false,
          Symbol(kReason): undefined,
          Symbol(kComposite): false,
          Symbol(kTimeout): true,
        },
      },
    ]


Number of calls: 1

 ❯ test/unit/reputation-wiring.test.ts:419:31
test/unit/reputation-wiring.test.ts > getEffectiveSubmitterReputation (#4513, install-wide for a confirmed miner) > skips the miner-identity lookup entirely when the per-repo signal already justifies downgrading
Stack Traces | 0.0168s run time
AssertionError: expected 'neutral' to be 'low' // Object.is equality

Expected: "low"
Received: "neutral"

 ❯ test/unit/reputation-wiring.test.ts:520:26
test/contract/engine-parity.test.ts > predicted-gate engine parity (#2286) > 'merge-readiness-composite-preserves-e…' matches the committed golden output
Stack Traces | 0.017s run time
AssertionError: expected { predicted: true, …(11) } to deeply equal { predicted: true, …(10) }

- Expected
+ Received

  {
    "basis": "public_config",
-   "blockers": [
+   "blockers": [],
+   "conclusion": "success",
+   "confirmedContributor": undefined,
+   "funnel": null,
+   "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring).",
+   "pack": "gittensor",
+   "predicted": true,
+   "readinessScore": 80,
+   "summary": "No configured hard blocker was found. Advisory findings, if any, stay advisory.",
+   "title": "LoopOver Orb Review Agent passed",
+   "warnings": [
      {
        "action": "If this PR is intended to solve an issue, link it explicitly in the PR body.",
        "code": "missing_linked_issue",
        "detail": "No closing reference or linked issue number was found in the PR metadata/body.",
        "title": "No linked issue detected",
      },
    ],
-   "conclusion": "failure",
-   "funnel": null,
-   "note": "Predicted from the repo's public .loopover.yml gate config + safe defaults. The maintainer may have private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only evaluated on a real PR. The slop score is NOT evaluated pre-submission (it needs the diff content) and may still fail the real gate. Provide the PR's changed paths to also predict the focus-manifest path policy, the size/guardrail hold, and any pre-merge check scoped to changed paths; without them only path-independent title/description/label pre-merge checks are predicted. Every author is gated the same: a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only on-chain scoring).",
-   "pack": "gittensor",
-   "predicted": true,
-   "readinessScore": 80,
-   "summary": "No linked issue detected — If this PR is intended to solve an issue, link it explicitly in the PR body.",
-   "title": "LoopOver Orb Review Agent: No linked issue detected",
-   "warnings": [],
  }

 ❯ test/contract/engine-parity.test.ts:46:21
test/unit/reputation-wiring.test.ts > getEffectiveSubmitterReputation (#4513, install-wide for a confirmed miner) > widens to the install-wide signal for a CONFIRMED miner when the per-repo signal alone stays neutral
Stack Traces | 0.0189s run time
AssertionError: expected 'neutral' to be 'low' // Object.is equality

Expected: "low"
Received: "neutral"

 ❯ test/unit/reputation-wiring.test.ts:482:26
test/unit/linked-issue-label-propagation-fetch.test.ts > fetchLinkedIssueLabelsForPropagation (#priority-linked-issue-gate) > reward-label gate on direct author/assignee match (#9161) > propagates the reward label to its own issue's author when that author genuinely IS a repo maintainer (opt-in + maintainer check both satisfied)
Stack Traces | 0.0238s run time
AssertionError: expected { labels: [ 'gittensor:bug' ], …(1) } to deeply equal { …(2) }

- Expected
+ Received

  {
    "inconclusive": false,
    "labels": [
      "gittensor:bug",
-     "gittensor:priority",
    ],
  }

 ❯ expectPropagation test/unit/linked-issue-label-propagation-fetch.test.ts:40:18
 ❯ test/unit/linked-issue-label-propagation-fetch.test.ts:1087:7
test/unit/predicted-gate-engine.test.ts > predicted-gate engine branch coverage (#2283) > exercises gate-advisory gateMode and blocker policy branches
Stack Traces | 0.0318s run time
AssertionError: expected 'block' to be 'advisory' // Object.is equality

Expected: "advisory"
Received: "block"

 ❯ test/unit/predicted-gate-engine.test.ts:1558:55
test/unit/reputation-wiring.test.ts > ORB/AMS reputation bridge wiring (#6801) > REGRESSION (#6801): feature on upgrades a low local signal and changes the end-to-end gate decision
Stack Traces | 0.0403s run time
AssertionError: expected "vi.fn()" to be called 2 times, but got 3 times
 ❯ test/unit/reputation-wiring.test.ts:393:25
test/unit/config-templates.test.ts > config/examples review templates (#1682) > lints loopover.full.yml with zero warnings, including no retired top-level fields
Stack Traces | 0.258s run time
AssertionError: expected [ Array(1) ] to deeply equal []

- Expected
+ Received

- []
+ [
+   "gate.mergeReadiness (\"off\") is set alongside an explicitly-authored mode for gate.linkedIssue, gate.duplicates, gate.slop.mode. The composite only fills in a sub-gate mode left unset -- it never overrides an explicitly-configured one, so those fields stay exactly as authored regardless of gate.mergeReadiness.",
+ ]

 ❯ test/unit/config-templates.test.ts:64:29
test/unit/queue-5.test.ts > queue processors > type label decoupling (#label-decoupling) > REGRESSION (#4528, PR #4494 shape): keeps the propagated labels on the PR's own merge-closed webhook, instead of falling back to the title guess
Stack Traces | 0.266s run time
AssertionError: expected [ 'gittensor:feature' ] to deeply equal [ 'gittensor:feature', …(1) ]

- Expected
+ Received

  [
    "gittensor:feature",
-   "gittensor:priority",
  ]

 ❯ test/unit/queue-5.test.ts:6660:34
test/unit/worker-entry-boundary.test.ts > worker entry boundary > does not reference pixelmatch, pngjs, visual-diff, gifenc, or sharp in worker-reachable source
Stack Traces | 0.58s run time
AssertionError: worker-reachable files must not mention Node-only visual diff/GIF/image deps: .../review/visual/visual-findings.ts: expected [ Array(1) ] to deeply equal []

- Expected
+ Received

- []
+ [
+   ".../review/visual/visual-findings.ts",
+ ]

 ❯ test/unit/worker-entry-boundary.test.ts:82:118
test/unit/check-migrations-script.test.ts > check-migrations script > reports every grandfathered duplicate migration number in the success summary
Stack Traces | 0.929s run time
Error: Command failed: .../loopover/node_modules/.bin/tsx scripts/check-migrations.ts
check-migrations: duplicate migration number 0191: "0191_linked_issue_claims.sql", "0191_retention_column_indexes.sql". Two PRs grabbed the same number — renumber the newest to the next free number (0193).

 ❯ test/unit/check-migrations-script.test.ts:38:20

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { status: 1, signal: null, output: [ null, '', 'check-migrations: duplicate migration number 0191: "0191_linked_issue_claims.sql", "0191_retention_column_indexes.sql". Two PRs grabbed the same number — renumber the newest to the next free number (0193).\n' ], pid: 18141, stdout: '', stderr: 'check-migrations: duplicate migration number 0191: "0191_linked_issue_claims.sql", "0191_retention_column_indexes.sql". Two PRs grabbed the same number — renumber the newest to the next free number (0193).\n' }

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 27, 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 Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui c5d38aa Commit Preview URL

Branch Preview URL
Jul 27 2026, 09:15 AM

@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 27, 2026
@loopover-orb

loopover-orb Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Caution

🛑 LoopOver review result - fixes required

Review updated: 2026-07-27 08:56:08 UTC

19 files · 1 AI reviewer · no blockers · CI failing · unstable

🛑 Suggested Action - Manual Review

Review summary
The AI review returned non-blocking notes for this change but did not include a separate narrative summary. Review the nits below before deciding this PR.

Nits — 3 non-blocking
  • src/api/routes.ts / src/review/decision-record.ts: the external brief's flagged magic numbers (160-char truncation, `orb(trust): the decision ledger has no external anchor and verify returns no hash — wholesale re-chaining and tail truncation both pass clean #9122` in a comment) are consistent with this file's existing `errorMessage(error).slice(0, 160)` convention elsewhere, so not worth a named constant.
  • packages/loopover-engine/src/settings/pr-type-label.ts:135 — the numbered list item `2.`/`3.` restructuring in the docstring is fine but slightly buries the behavior-change note; consider a one-line summary at the top of the function doc calling out the new `propagation_unmatched` source explicitly.
  • src/db/repositories.ts upsertBounty's chained `.onConflictDoUpdate(...).onConflictDoUpdate(...)` relies on drizzle emitting SQLite's native multi-target `ON CONFLICT` syntax verbatim — worth double-checking the generated SQL once CI is green again, since this is exactly the kind of ORM-chaining behavior that can silently drop the second clause on a version bump.

CI checks failing

  • validate
  • validate-tests
  • validate-code

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 #9077, #9078, #9079, #9080
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 (4 linked issues).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 13 registered-repo PR(s), 13 merged, 331 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 13 PR(s), 331 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: 13 PR(s), 331 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
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: success
  • config: 03a7f8b529a9 · pack: oss-anti-slop
  • record: ae0de5fc9da2 (schema v3, head 2502400)

🟩 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

@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 27, 2026
@JSONbored
JSONbored force-pushed the fix/9077-9078-9079-9080-economic-integrity branch from 2502400 to 2b31b38 Compare July 27, 2026 09:05
…9078)

verifyDecisionLedger walked decision_ledger's hash chain but never joined
decision_records to recompute contentDigest(JSON.parse(record_json)) against
the digest each row committed to -- the reconciliation its own doc comment
promised. A rewrite of record_json (or a hand-deletion of the row) passed
verification clean as long as the chain itself stayed self-consistent.

Adds two distinct break kinds: content_mismatch (record_json no longer
hashes to what the chain committed to) and missing_record (the chain
references a decision_records id that no longer exists). Both batch-fetch
against the ledger window already being verified, so this stays a single
extra query rather than N+1.

Also stops persistDecisionRecord from conflating "the record failed to
persist at all" with "the record persisted but its ledger append failed" --
the latter is now its own dedicated console.error alarm (forwarded to
Sentry like other operator-facing anomaly signals) instead of falling
through to the generic swallowed warn.
…ogin (#9079)

fetchOfficialGittensorMiner matched a PR/issue author against the upstream
/miners list purely by lowercased githubUsername. A GitHub login is
renameable and a freed one is reclaimable by anyone, so if the upstream
list still carries a miner's old username (its refresh cadence is
upstream's, not ours), whoever claims that freed login inherits
confirmed_miner -- a genuinely elevated role (command self-retrigger, the
miner-scoped open-PR cap, the unlinked-issue-guardrail velocity exception).

fetchOfficialGittensorMiner/fetchGittensorContributorSnapshot now accept an
optional githubId and match on it FIRST when the caller has one, never
falling back to a username match once an id was supplied. Threaded through
getCachedOfficialMinerDetection's callers via PullRequestRecord/
IssueRecord.authorGithubId (captured from the webhook's user.id since
#9125) and unlinked-issue-guardrail's/miner-detection-cache's own
isConfirmedOfficialMiner. Callers with no stored id yet keep the prior
login-only behavior unchanged (a strict narrowing, never a widening, of
the old always-username match).

Left the bulk fetchOfficialGittensorMinerLogins cohort-classification read
and a couple of advisory-only lookups (reputation-wire's install-wide
reputation widening, the MCP/decision-pack snapshot tools that only ever
receive a login as their input) on login-only matching -- none of them
grant an elevated role, and re-scoping every caller's own input contract
was out of scope for this fix.
…9080)

The bounty mirror import route collected every lifecycle event into one
array and persisted them all only after the whole loop finished, so a
mid-loop failure left every bounty upserted so far already advanced with
zero lifecycle rows recorded for any of them -- unrecoverable once the
next import's diff runs against the now-updated rows. Each item is now
upserted and its own lifecycle event written in the same step, wrapped in
its own try/catch, so one bad row can no longer take the rest of the
batch down with it.

upsertBounty's insert only handled a conflict on the id primary key, but
the table also has a UNIQUE(repo_full_name, issue_number) index. Upstream
re-issuing a bounty on the same issue under a new id hit that second,
unhandled unique index and threw, 500ing the import and wedging every
subsequent import on the same row permanently. A second chained
onConflictDoUpdate (SQLite's native multi-target upsert syntax) resolves
both conflicts to an update; the surviving row always keeps whichever id
it already has, so lifecycle events already keyed to it are never
orphaned by a later re-issue.

classifyBountyLifecycle's status regexes were unanchored substring
matches, so a base word buried mid-token false-positived: "unclaimed" and
"not_completed" both classified as completed, "avoid" classified as
cancelled, "renewed" classified as active. Anchoring each alternative on a
leading word boundary keeps every genuine prefix match (cancelled,
completed, rewarded, ...) while no longer matching those buried substrings.

fundingStatus compared the amount against exactly one hardcoded
zero-string literal ("0.0000"), so any other zero-ish spelling upstream
might send ("0", "0.0", "0.00") reported as funded. Parsing to a number
and requiring it to be finite and positive treats every zero-ish spelling
the same, string or numeric, and degrades a malformed amount to
target-only/unknown instead of reporting funded.

Closes #9080
…atisfaction verdict (#9077)

isLinkedIssueTrustworthy trusts every OPEN linked issue unconditionally, and
trustMaintainerAuthoredIssueForReward's entire unlock was "the linked issue's
author independently checks out as a repo maintainer" -- nothing verified the
PR actually addressed the issue it cited. A contributor could get a reward
label (e.g. gittensor:priority) applied by citing any open, maintainer-
authored, reward-labeled issue, whether or not their PR touched it.

A label reached only via that relaxed maintainer-authored path is now
additionally required to have a PUBLISHED "addressed" linked-issue-
satisfaction verdict for the exact (repo, PR, issue) triple, read via
getLatestPublishedLinkedIssueSatisfaction rather than spending a fresh model
call. This doesn't touch the direct author/assignee path (already strict) or
a non-reward trustMaintainerAuthoredIssue mapping (bug/feature mirroring,
lower stakes). A repo that hasn't opted into linkedIssueSatisfactionGateMode
never persists a verdict at all, so the reward relaxation simply never fires
for it -- a fail-safe degrade, not a silent bypass.

Closes #9077
…text once propagation is enabled (#9077)

resolvePrTypeLabel fell through to deriveKindFromTitle (pure PR-title regex
matching, entirely author-controlled) whenever linked-issue label propagation
was enabled but produced no exclusive/additive match for a pass -- silently
reintroducing the exact author-controlled-free-text classification a repo
opted OUT of by turning propagation on. A contributor could pick their own
gittensor:bug/gittensor:feature multiplier by title wording alone, on any
repo running propagation, whenever the linked issue simply hadn't been
triaged yet.

resolvePrTypeLabel now has a distinct "propagation_unmatched" outcome for
that case: no label is applied, and removeLabels still clears every
configured category (so a stale prior label doesn't look like a confirmed
classification). This only changes behavior for a repo that has explicitly
enabled linkedIssueLabelPropagation -- the shipped default (propagation
disabled, the overwhelming majority of installs) is completely unchanged,
still title-derived exactly as before.

This does NOT eliminate title-derived classification for the shipped
default itself -- doing so would silently disable type labels for every
repo that hasn't configured propagation with mappings covering bug/feature,
which is a larger design decision (a real diff/changed-path classifier, or
a default-behavior migration) better suited to its own follow-up given this
issue's own "Long Term Features" milestone. See the PR description for the
full disposition.
@JSONbored
JSONbored force-pushed the fix/9077-9078-9079-9080-economic-integrity branch from 2b31b38 to c5d38aa Compare July 27, 2026 09:13
@JSONbored
JSONbored merged commit 600b538 into main Jul 27, 2026
2 of 3 checks passed
@JSONbored
JSONbored deleted the fix/9077-9078-9079-9080-economic-integrity branch July 27, 2026 09:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment