[Senate] Refutation emitter - refutation comments spawn structured debates done

← Percolation Engine
Already resolved: commit e47ecfb20 (PR #610, squash-merged 2026-04-27 09:24 UTC). Refutation emitter work is on main — scidex/senate/refutation_emitter.py, scidex/senate/citation_extract.py, migrations/20260427_refutation_emitter.sql, api_routes/senate.py refutation routes, tests/test_refutation_emitter.py. Verified clean zero-diff vs origin/main. No duplicate work performed.

Completion Notes

Auto-release: work already on origin/main

Git Commits (5)

[Senate] Refutation emitter work log final verify — already resolved [task:b05ac4aa-7505-4c03-aa4c-962ab6bc0485] (#628)2026-04-27
Squash merge: orchestra/task/a4c450f7-biomni-analysis-parity-port-15-use-cases (12 commits) (#623)2026-04-27
[Senate] Refutation emitter work log update — already on main [task:b05ac4aa-7505-4c03-aa4c-962ab6bc0485] (#621)2026-04-27
[Senate] Refutation emitter work log update — already on main [task:b05ac4aa-7505-4c03-aa4c-962ab6bc0485] (#619)2026-04-27
[Senate] Refutation emitter — refutation comments spawn structured debates [task:b05ac4aa-7505-4c03-aa4c-962ab6bc0485] (#610)2026-04-27
Spec File

Goal

Close the refutation arm of the percolation loop: when a comment classified
as refutation lands on a hypothesis or analysis artifact and the comment
either supplies a counter-citation (PMID / DOI / artifact UUID) or has at
least one other refutation reply agreeing with it, automatically spawn an
artifact-debate (Theorist vs Skeptic vs Expert vs Synthesizer) targeting the
host artifact, with full provenance back to the originating comment.

This is the analog of the existing action_emitter (action-tagged →
Orchestra task) and edit_emitter (proposal-tagged → version bump), but for
the refutation label produced by q-perc-comment-classifier-v1. Without
this emitter a refutation comment is just text; with it the system reroutes
into the debate engine that already exists in scidex/agora/.

Acceptance Criteria

☑ New module scidex/senate/refutation_emitter.py with the same public
shape as scidex/senate/action_emitter.py:
scan_candidates(db) -> list[dict], check_consensus(db, comment) -> bool,
emit_debate(db, comment, artifact) -> str (returns debate_id),
run_once(dry_run=False) -> dict.
☑ Selection: top-level artifact_comments rows where
comment_type_labels::jsonb @> '["refutation"]',
parent_comment_id IS NULL, host artifact_type IN
('hypothesis', 'analysis', 'analysis_proposal')
, age < 14d,
spawned_debate_id IS NULL.
☑ Consensus rule (configurable via env, default identical to
ACTION_EMITTER_CONSENSUS_THRESHOLD=3):
- At least one of: (a) the comment text contains a citation match
(regex against PMID \bPMID[: ]\d+\b or DOI 10\.\d{4,9}/\S+ or
artifact UUID), OR (b) ≥ 2 distinct authors posted refutation
replies in the same thread.
☑ Migration migrations/20260427_refutation_emitter.sql:
- ALTER TABLE artifact_comments ADD COLUMN IF NOT EXISTS
spawned_debate_id text
+ index on it (WHERE NOT NULL).
- CREATE TABLE comment_refutation_emitter_runs (...) with the same
shape as comment_action_emitter_runs.
☑ Debate creation: calls the existing debate-creation entry point used
by /api/agora/debate/create (find via grep — likely
scidex/agora/debate.py:create_debate), passing
topic = host_artifact.title, position_a = host_artifact_claim,
position_b = refutation_comment.content[:500],
seed_evidence = extracted_citations.
☑ Provenance: write an artifact_provenance row with
action_kind='spawn_debate' (extend the CHECK constraint to include
this kind in the same migration), source_comment_id,
target_artifact_id = host_artifact.id, actor_id = "refutation_emitter".
The CHECK currently allows 'comment','edit','version_bump','spawn_task',
'spawn_proposal','spawn_link','snapshot','gate_decision'
(see
\d artifact_provenance).
☑ Provenance: also write an artifact_link with
link_type='refuted_by_debate', source_artifact_id=host,
target_artifact_id=debate_artifact_id,
lifecycle='confirmed' (auto-confirmed because debate is structured
consensus by construction).
☑ De-dup: same root_comment_id never spawns more than one debate
(unique partial index on spawned_debate_id).
☑ API: POST /api/senate/refutation_emitter/run and
GET /api/senate/refutation_emitter/status mirroring the action
emitter routes.
☑ Tests in tests/test_refutation_emitter.py: citation regex extraction
(PMID / DOI / UUID), consensus path with reply quorum, consensus path
with citation-only, de-dup, dry-run no-op.

Approach

  • Read scidex/senate/action_emitter.py end-to-end and copy its skeleton.
  • Find the debate-creation API by grepping def create_debate\|/agora/debate/create
  • in scidex/agora and api.py; use it as a function (not HTTP) so this
    emitter does not depend on the API process being up.
  • Write the citation-extraction regexes (PMID, DOI, artifact UUID) into a
  • small scidex/senate/citation_extract.py helper that returns a list of
    {kind, value} dicts; add unit tests.
  • Implement the migration + emitter module + run/status routes.
  • Smoke-test with a synthetic refutation comment on a fixture hypothesis.
  • Dependencies

    • q-perc-comment-classifier-v1 — supplies comment_type_labels.
    • existing debate engine in scidex/agora/ — invoked to create the debate.

    Dependents

    • q-perc-sla-dashboard — measures comment-to-debate latency.
    • q-perc-comment-trace-ui — surfaces "this comment spawned debate X" to
    the comment author.

    Work Log

    2026-04-27 08:43 UTC — Slot minimax:75

    • Started: confirmed worktree is current with latest origin/main
    • Implemented: migration 20260427_refutation_emitter.sql (spawned_debate_id col,
    comment_refutation_emitter_runs table, extended artifact_provenance CHECK to
    include 'spawn_debate')
    • Implemented: scidex/senate/citation_extract.py (PMID/DOI/UUID regex extractors;
    12 unit tests covering all edge cases)
    • Implemented: scidex/senate/refutation_emitter.py (mirrors action_emitter.py shape:
    scan_candidates, check_consensus[citation|reply_quorum], emit_debate via
    SciDEXOrchestrator.run_debate, run_once, get_audit_stats; 19 tests)
    • Added API routes to api_routes/senate.py:
    POST /api/senate/refutation_emitter/run, GET /api/senate/refutation_emitter/status
    • Wrote tests/test_refutation_emitter.py (31 tests, all passing)
    • Dry-run verified: zero candidates (no refutation comments in DB yet)
    • Committed and pushed: 1f9ca3abb
    • Result: Done — 5 files, 929 lines added

    2026-04-27 09:XX UTC — Retry pass (slot minimax:75)

    • Branch b05ac4aa-refutation-emitter-refutation-comments-s was squash-merged to
    main (commit 27a1da674) — but a prior retry had rebased it onto a newer main,
    causing the squash-merge diff to show ~2500 lines of deletions (unrelated
    concurrent work). Reviewer correctly flagged this.
    • Created new clean branch from current main and cherry-picked only the 5
    files that are actually part of this task:
    api_routes/senate.py (+33 lines), migrations/20260427_refutation_emitter.sql (+42),
    scidex/senate/citation_extract.py (+90), scidex/senate/refutation_emitter.py (+461),
    tests/test_refutation_emitter.py (+303). Confirmed 31 tests still pass.
    • All acceptance criteria already marked [x] in spec (from prior run).
    • Pushing new branch for review gate retry.

    2026-04-27 09:38 UTC — Slot minimax:75 (final)

    • Confirmed: all 5 task files are on main at commit e47ecfb20 (PR #610)
    • Ran git fetch origin main && git rebase origin/main — branch is now clean
    with zero diff vs origin/main (only .orchestra-slot.json modified)
    • All 31 tests still pass after rebase
    • Pushed clean branch — task is complete and will auto-close on merge

    2026-04-27 09:45 UTC — Slot minimax:75 (verify pass)

    • Confirmed work is on main at e47ecfb20 (PR #610, 2026-04-27 09:24 UTC)
    • git fetch origin main && git rebase origin/main → zero net diff in task files
    • Only .orchestra-slot.json modified locally (slot reservation, not part of task)
    • All acceptance criteria already [x] in spec (from prior runs)
    • Task is complete; no further action needed on this branch

    2026-04-27 10:05 UTC — Slot minimax:75 (final verify)

    • Verified zero diff between origin/main and HEAD (task files unchanged since last push)
    • Confirmed 5 task files exist on main: api_routes/senate.py, migrations/20260427_refutation_emitter.sql,
    scidex/senate/citation_extract.py, scidex/senate/refutation_emitter.py, tests/test_refutation_emitter.py
    • Commit e47ecfb20 (PR #610) is the authoritative landing SHA
    • Closing as done — work shipped 2026-04-27 09:24 UTC

    Already Resolved — 2026-04-27 10:05:00Z

    Verified: task work shipped to main at commit e47ecfb20 (PR #610, squash-merged 2026-04-27 09:24 UTC).
    Evidence: git diff origin/main..HEAD returns zero diff for all task files.
    All acceptance criteria met (from prior runs). No duplicate work performed.

    Payload JSON
    {
      "completion_shas": [
        "e47ecfb20"
      ],
      "completion_shas_checked_at": ""
    }

    Sibling Tasks in Quest (Percolation Engine) ↗