Skip to content

Expose bounty finalization evidence in public rows#849

Open
Floofy6 wants to merge 1 commit into
ramimbo:mainfrom
Floofy6:codex/preflight-799-774-20260603T1617Z
Open

Expose bounty finalization evidence in public rows#849
Floofy6 wants to merge 1 commit into
ramimbo:mainfrom
Floofy6:codex/preflight-799-774-20260603T1617Z

Conversation

@Floofy6
Copy link
Copy Markdown

@Floofy6 Floofy6 commented Jun 4, 2026

Related bounty and source report

Refs #799
Source report: #774

Summary

  • expose optional finalization_evidence on public bounty row serialization when an executed create_bounty proposal has stored public GitHub finalization evidence
  • include the create-bounty proposal API URL, public bounty URL, claims-open GitHub comment URL, finalization status, label, and issue-body status when known
  • omit the object for older rows or skipped/failed finalization results without public evidence
  • document the field as lifecycle evidence only, not payment, acceptance, cash-out, price, bridge, exchange, or off-ramp evidence

Evidence

Source report #774 identifies that public bounty rows expose status and capacity but not the public evidence that made a row live: the executed create-bounty proposal and claims-open Reserved on MergeWork comment.

The implementation reuses data already persisted in executed treasury proposal result.github_issue_finalization. It does not crawl GitHub on read, mutate GitHub, change claim acceptance, change payout behavior, change treasury execution, or add private/admin state.

Tests

  • /Users/dom/bounty-work/mergework/.venv/bin/python -m pytest tests/test_bounty_api_routes.py tests/test_serializers.py tests/test_docs_public_urls.py -q -> 70 passed, 1 warning
  • /Users/dom/bounty-work/mergework/.venv/bin/python -m pytest -q -> 794 passed, 1 warning
  • /Users/dom/bounty-work/mergework/.venv/bin/ruff check . -> all checks passed
  • /Users/dom/bounty-work/mergework/.venv/bin/ruff format --check . -> 111 files already formatted
  • /Users/dom/bounty-work/mergework/.venv/bin/python scripts/docs_smoke.py -> docs smoke ok
  • /Users/dom/bounty-work/mergework/.venv/bin/mypy app -> success, no issues found in 42 source files
  • git diff --check -> clean

Out of scope

  • No payment, proof, claim acceptance, wallet, bridge, exchange, liquidity, price, cash-out, or investment behavior changes.
  • No GitHub comment posting or issue mutation from public read routes.
  • No private security details, secrets, credentials, or private user data.

Summary by CodeRabbit

Release Notes

  • New Features

    • Bounties now expose optional finalization evidence in API responses, including treasury proposal metadata and GitHub issue details when finalization has been recorded.
  • Documentation

    • API documentation updated with finalization evidence examples and clarification on when evidence is present.
  • Tests

    • Added tests verifying finalization evidence exposure and backward compatibility.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds end-to-end support for serializing finalization evidence on bounties. It extends the bounty serializers and attempt-summary helpers to accept and thread finalization evidence extracted from executed treasury proposals, exposes the evidence in API responses and documentation, and validates the behavior through route and unit tests.

Changes

Finalization Evidence Serialization

Layer / File(s) Summary
Finalization evidence extraction from treasury proposals
app/serializers.py
Adds FinalizationEvidenceByBountyId type alias, extraction helpers that parse executed treasury result_json for github-issue finalization fields, and SQL filter to constrain result-row scanning by bounty id.
Single bounty serializer with finalization evidence
app/serializers.py, app/bounty_attempts.py
Updates bounty_to_dict signature to accept optional finalization evidence, auto-computes evidence from database when session available, includes evidence in returned payload, and passes it to attempt-summary helpers.
List bounties preloading and serialization
app/serializers.py
Extends bounties_to_dict to preload finalization evidence for all bounties in bulk and thread per-bounty evidence through individual serializer and attempt-summary calls.
Attempt summary threading of finalization evidence
app/bounty_attempts.py
Extends _bounty_attempt_warnings_for_count, bounty_attempt_summary_from_count, and bounty_attempt_summary with keyword-only finalization_evidence parameters and forwards evidence through the call chain.
API route tests for finalization evidence exposure
tests/test_bounty_api_routes.py
Tests that finalization evidence appears in bounty detail and list responses when a treasury proposal has been executed with recorded github-issue finalization, and is omitted when no result field is recorded.
Serializer unit tests with treasury evidence preloading
tests/test_serializers.py
Adds test for bounties_to_dict preloading and exposing finalization evidence; tightens existing pending-proposal test to verify both payload_json and result_json are queried.
API documentation and docs validation
docs/api-examples.md, tests/test_docs_public_urls.py
Updates bounty JSON example to include finalization_evidence block with proposal URLs and GitHub claim-open evidence; adds explanatory text on when evidence is present/absent; validates documentation includes finalization evidence.

Possibly related issues

  • ramimbo/mergework#774: Implements the proposal to surface finalization-evidence URLs (proposal IDs, public bounty URLs, GitHub claims-open evidence) on public bounty rows through API serializers.

Possibly related PRs

  • ramimbo/mergework#330: Both PRs build on the same bounty_to_dict serializer extraction entrypoint; main PR extends it to accept and include finalization_evidence.
  • ramimbo/mergework#657: Both PRs modify bounty_to_dict/bounties_to_dict serialization paths using treasury proposal data via SQLAlchemy session; main PR adds finalization_evidence payload.
  • ramimbo/mergework#703: Both PRs modify the same attempt-summary/warning pipeline in app/bounty_attempts.py and shared bounty_to_dict in app/serializers.py; main PR adds finalization_evidence while retrieved PR adds active_attempt_* fields.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Bounty Pr Focus ⚠️ Warning All four review comments remain unresolved: test uses 1 bounty (N+1 not proven), missing executed-proposal omission test, O(n) unbounded preload cost, unused finalization auto-compute in warnings. Fix test coverage gaps, scope preload to listed bounty ids, short-circuit unused finalization_evidence auto-compute in warnings path.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately names the main change: exposing bounty finalization evidence in public row serialization.
Description check ✅ Passed The description covers all required sections with clear context, evidence, test results, and out-of-scope boundaries.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Mergework Public Artifact Hygiene ✅ Passed docs/api-examples.md properly disclaims finalization_evidence as lifecycle evidence only, not payment/cash-out/price/bridge/exchange guarantees. No investment or financial claims found.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 65fcfeec-548d-4b21-8002-58916b4c4ef6

📥 Commits

Reviewing files that changed from the base of the PR and between d4d0e48 and aab6f79.

📒 Files selected for processing (6)
  • app/bounty_attempts.py
  • app/serializers.py
  • docs/api-examples.md
  • tests/test_bounty_api_routes.py
  • tests/test_docs_public_urls.py
  • tests/test_serializers.py

Comment thread app/bounty_attempts.py
Comment on lines +90 to 97
lookup_session = None if pending_proposals is not None else session
bounty_data = bounty_to_dict(
bounty,
session=session,
session=lookup_session,
pending_proposals=pending_proposals,
attempt_summary={},
finalization_evidence=finalization_evidence,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Warnings path triggers a redundant finalization-evidence scan.

_bounty_attempt_warnings_for_count never reads finalization_evidence (it only consumes effective_awards_remaining, status, availability_state, availability_note). But via bounty_attempt_warnings (Line 139-144) lookup_session is the session and finalization_evidence is None, so bounty_to_dict auto-computes _bounty_finalization_evidence — a LIKE/contains scan over treasury_proposals — whose result is thrown away. api_create_bounty_attempt calls bounty_attempt_warnings on every response path (Lines 316, 334, 370, 378), so this adds one wasted scan per call on a request thread.

Short-circuit the auto-compute since the value is unused here:

Proposed fix
     bounty_data = bounty_to_dict(
         bounty,
         session=lookup_session,
         pending_proposals=pending_proposals,
         attempt_summary={},
-        finalization_evidence=finalization_evidence,
+        finalization_evidence=finalization_evidence if finalization_evidence is not None else {},
     )

Comment thread app/serializers.py
return [bounty_to_dict(bounty) for bounty in bounties]

pending_by_bounty = _pending_bounty_proposals_by_bounty_id(session)
finalization_by_bounty = _create_bounty_finalization_evidence_by_bounty_id(session)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Bulk preload scans every executed create_bounty proposal, not just the listed bounties.

_create_bounty_finalization_evidence_by_bounty_id(session) ignores the bounties being serialized and loads/parses all executed create_bounty results, then discards entries for bounties not in the page. On a public list route this grows O(total bounties ever created) per request, independent of page size.

Consider scoping the preload to the bounty ids in bounties (e.g. an or_ of _result_bounty_id_filter(id) markers, or filtering after fetch) so list serialization cost tracks the page, not lifetime bounty count.

Comment on lines +115 to +136
def test_bounty_api_omits_finalization_evidence_when_not_recorded(sqlite_url: str) -> None:
create_schema(sqlite_url)
with session_scope(sqlite_url) as session:
ensure_genesis(session)
bounty = create_bounty(
session,
repo="ramimbo/mergework",
issue_number=75,
issue_url="https://github.com/ramimbo/mergework/issues/75",
title="Older bounty without finalization result",
reward_mrwk="25",
acceptance="Older rows should stay compatible.",
)
bounty_id = bounty.id

client = TestClient(create_app(database_url=sqlite_url, webhook_secret="secret"))

detail = client.get(f"/api/v1/bounties/{bounty_id}").json()
listed = client.get("/api/v1/bounties?repo=ramimbo%2Fmergework&issue_number=75").json()[0]

assert "finalization_evidence" not in detail
assert "finalization_evidence" not in listed
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Exercise the executed-proposal omission path.

This test creates the bounty directly, so it never reaches the serializer branch that omits finalization_evidence for an executed create_bounty proposal with missing/non-public github_issue_finalization. A regression in app/serializers.py:265-292 would still pass here. Please add a case that executes a proposal and then records either no finalization field or a field without public URLs, and assert the key stays absent.

As per coding guidelines, tests/**/*.py: Focus on whether tests prove the changed behavior and include negative, replay, boundary, or regression cases where relevant.

Comment thread tests/test_serializers.py
Comment on lines +256 to +329
def test_bounties_to_dict_exposes_preloaded_finalization_evidence(sqlite_url: str) -> None:
create_schema(sqlite_url)
with session_scope(sqlite_url) as session:
ensure_genesis(session)
proposal = propose_treasury_action(
session,
action="create_bounty",
payload={
"repo": "ramimbo/mergework",
"issue_number": 324,
"issue_url": "https://github.com/ramimbo/mergework/issues/324",
"title": "Finalization evidence serializer",
"reward_mrwk": "25",
"max_awards": 1,
"acceptance": "Serializer should expose stored finalization evidence.",
},
proposed_by="maintainer",
)
proposal_id = proposal.id
proposal.executes_after = utc_now() - timedelta(seconds=1)
session.flush()
executed = execute_treasury_proposal(
session, proposal_id=proposal_id, executed_by="maintainer"
)
bounty_id = int(json.loads(executed.result_json)["bounty"]["id"])
record_proposal_result_field(
session,
proposal_id=proposal_id,
field="github_issue_finalization",
value={
"status": "updated",
"label": "mrwk:bounty",
"bounty_url": "https://api.mrwk.online/bounties/324",
"comment_url": ("https://github.com/ramimbo/mergework/issues/324#issuecomment-1"),
"issue_body_status": "updated",
},
)
bounty = session.get(Bounty, bounty_id)
assert bounty is not None
treasury_selects: list[str] = []

def count_treasury_selects(
conn,
cursor,
statement,
parameters,
context,
executemany,
) -> None:
del conn, cursor, parameters, context, executemany
if "treasury_proposals" in statement.lower() and statement.lstrip().upper().startswith(
"SELECT"
):
treasury_selects.append(statement)

bind = session.get_bind()
event.listen(bind, "before_cursor_execute", count_treasury_selects)
try:
serialized = bounties_to_dict([bounty], session=session)
finally:
event.remove(bind, "before_cursor_execute", count_treasury_selects)

assert serialized[0]["finalization_evidence"] == {
"create_bounty_proposal_id": proposal_id,
"create_bounty_proposal_url": f"/api/v1/treasury/proposals/{proposal_id}",
"status": "updated",
"public_bounty_url": "https://api.mrwk.online/bounties/324",
"claims_open_comment_url": (
"https://github.com/ramimbo/mergework/issues/324#issuecomment-1"
),
"label": "mrwk:bounty",
"issue_body_status": "updated",
}
assert len(treasury_selects) == 2
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use more than one bounty to prove list preloading.

bounties_to_dict([bounty], session=session) does not distinguish bulk preload from per-bounty lookup; an N+1 regression would still pass with a single-element list. Add at least one more bounty and keep len(treasury_selects) == 2 so this test actually locks in the preload behavior from app/serializers.py:122-150.

As per coding guidelines, tests/**/*.py: Focus on whether tests prove the changed behavior and include negative, replay, boundary, or regression cases where relevant.

Copy link
Copy Markdown

@xiefuzheng713-alt xiefuzheng713-alt left a comment

Choose a reason for hiding this comment

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

Bounty #654 current-head review for PR #849.

Reviewed head aab6f79e43cc563f4662dec746bb831f8ef05c51 against base d4d0e4860cfa3e9c338b1349001277c148afaf6d as a non-author. I inspected app/serializers.py, app/bounty_attempts.py, tests/test_serializers.py, tests/test_bounty_api_routes.py, tests/test_docs_public_urls.py, and docs/api-examples.md.

Blocking finding: bounties_to_dict() now calls _create_bounty_finalization_evidence_by_bounty_id(session) for every list serialization, but that helper selects every executed create_bounty proposal without restricting the scan to the bounty rows being serialized. A filtered list/page with one bounty therefore still fetches and parses all historical create-bounty proposal results, so public bounty list cost grows with lifetime proposal history instead of with the returned page. The current preload test only asserts that there are two treasury SELECTs; it does not catch that the finalization SELECT is unbounded.

Current-head reproduction:

  • Created 3 executed create_bounty proposals with finalization evidence in a temporary SQLite DB.
  • Serialized only the first bounty via bounties_to_dict([listed], session=session).
  • Observed parsed_proposal_ids_during_single_row_serialize = [1, 2, 3] even though listed_bounty_id = 1.
  • The finalization SELECT was: WHERE treasury_proposals.status = ? AND treasury_proposals.action = ? ORDER BY treasury_proposals.id ASC, params ('executed', 'create_bounty'), with no result_json bounty-id filter.

Suggested fix: pass the listed bounty ids into the finalization preload helper and apply _result_bounty_id_filter()/equivalent filtering for those ids before fetching rows, then add a regression with at least one unrelated executed create_bounty proposal that is not fetched/parsed for the serialized list.

Validation on the reviewed head:

  • uv run --extra dev pytest tests/test_serializers.py::test_bounties_to_dict_exposes_preloaded_finalization_evidence tests/test_bounty_api_routes.py::test_bounty_api_exposes_create_bounty_finalization_evidence -q -> 2 passed, 1 existing Starlette/httpx warning.
  • uv run --extra dev pytest tests/test_serializers.py tests/test_bounty_api_routes.py tests/test_docs_public_urls.py -q -> 70 passed, 1 existing warning.
  • uv run --extra dev ruff check app/serializers.py app/bounty_attempts.py tests/test_serializers.py tests/test_bounty_api_routes.py docs/api-examples.md tests/test_docs_public_urls.py -> passed.
  • uv run --extra dev ruff format --check app/serializers.py app/bounty_attempts.py tests/test_serializers.py tests/test_bounty_api_routes.py -> passed.
  • uv run --extra dev mypy app -> success for 42 source files.
  • uv run --extra dev python scripts/docs_smoke.py -> docs smoke ok.
  • git diff --check origin/main...HEAD -> clean.
  • git merge-tree --write-tree origin/main HEAD -> clean tree 9f9a2ce605d6ffc661550851446cafab47f03f76.

No private data, secrets, wallet material, payout execution, treasury mutation, ledger mutation, exchange, bridge, cash-out, price behavior, or fabricated payout claim was used.

Copy link
Copy Markdown
Contributor

@aglichandrap aglichandrap left a comment

Choose a reason for hiding this comment

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

Review evidence (head SHA: aab6f79e43cc):

  • Scope: Passes finalization_evidence through bounty_attempt_warnings_for_count and bounty_attempt_summary_from_count.
  • Code quality: Adds optional finalization_evidence param with None default — backwards compatible.
  • Session optimization: lookup_session = None if pending_proposals is not None else session — avoids redundant DB lookups when data is pre-fetched.
  • Files: 6 files, +335/-6 — larger PR but well-structured across serializers, bounty_attempts, routes, tests, docs.
  • Mergeable: state=clean.

No blockers. Approve.

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.

3 participants