[Atlas] Per-hypothesis history viewer with full attribution timeline done

← Atlas
Per-hypothesis event timeline with backfill from audit_chain, live event-bus subscribers, LLM-composed story-arc narrative.

Completion Notes

Auto-completed by supervisor after successful deploy to main

Git Commits (1)

Squash merge: orchestra/task/3cca8708-per-hypothesis-history-viewer-with-full (2 commits) (#825)2026-04-27
Spec File

Effort: thorough

Goal

A hypothesis on SciDEX is not a static document — its statement,
composite_score, mechanism prose, supporting PMIDs, debate
verdicts, market price, and Elo rank all change over weeks and
months. Today this history is invisible: /hypothesis/{id}
renders only the current state. A researcher cannot answer
"how did this hypothesis evolve?" or "when did it lose support?"

Build a per-hypothesis History Viewer: a timeline of every
change to the hypothesis with attribution (which agent or human
made the change), the diff, and the trigger event (debate,
market settlement, paper ingest, manual edit). The timeline is
the audit trail and the story arc.

Acceptance Criteria

☑ New table hypothesis_history_events with (id, hyp_id,
occurred_at, event_kind, actor_id, actor_kind: Literal[
'agent', 'human', 'system'], before JSONB, after JSONB,
trigger_artifact_id, narrative_md)
and an index on
(hyp_id, occurred_at DESC).
☑ Backfill migration migrations/<n>_hypothesis_history_backfill.py:
reconstructs history events from existing audit sources —
audit_chain rows referencing the hypothesis,
debate_sessions linked via hypothesis_id,
price_history for the linked market,
composite_score_revisions (search the codebase for the
table; if absent, reconstruct from score_history JSONB on
the hypothesis row), hypothesis_papers insert timestamps
paper_added. Goal: ≥10 events per top-50 hypothesis.
☑ New module scidex/atlas/hypothesis_history.py:
- record_event(hyp_id, event_kind, actor_id, actor_kind,
before, after, trigger_artifact_id, narrative_md)
— the
forward write path; called by the live event-bus
listeners.
- get_timeline(hyp_id, limit=200, since: datetime = None)
-> list[dict]
— query helper.
- compose_history_narrative(hyp_id) -> str — LLM-
composed 200-word "story so far" summarizing the
timeline; cached, regenerated on new event.
☑ Live wiring: subscribe record_event to events emitted by
scidex/agora/synthesis_engine.py (verdict update),
scidex/exchange/market_dynamics.py (price settlement),
scidex/atlas/citation_extraction.py (new PMID added).
Any composite_score recompute also writes an event.
☑ UI: new tab on /hypothesis/{id} titled "History" rendering
a vertical timeline with one event per row showing
timestamp, actor avatar, event kind icon, 1-line summary,
expandable diff. Header of the History tab shows the
LLM-composed "Story so far" narrative.
GET /api/hypothesis/{id}/history JSON endpoint with
pagination + since query param.
☑ Performance: timeline render uses cursor pagination (no
LIMIT/OFFSET large scans); since query param indexed
via the (hyp_id, occurred_at DESC) partial.
☑ Pytest: seed a hypothesis with 5 backfilled events;
assert timeline returns them ordered correctly; record a
new event via the live API; assert the narrative cache
invalidates.

Approach

  • Backfill is a one-time job — keep it idempotent (use an
  • event_dedup_key UNIQUE column to allow re-running).
  • The forward path uses the existing event_bus
  • (scidex/core/event_bus.py) — subscribe handlers, don't
    couple writers.
  • Diff rendering is JSON-diff in the UI (vanilla, no
  • library) — the API returns before/after JSONB and the
    client computes the diff display.
  • Narrative LLM call uses the same wrapper as
  • brief_writer.py; prompt: "Given this list of events,
    write a 200-word story arc explaining how this hypothesis
    evolved."
  • Timeline is also exposed as an embedded mini-chart on the
  • hypothesis hero showing composite_score over time.

    Dependencies

    • scidex.core.event_bus — subscriber substrate.
    • scidex.exchange.market_dynamics — price-settle events.
    • scidex.agora.synthesis_engine — verdict events.
    • q-time-field-time-series (sibling) — uses the same event
    ledger as a feed.

    Work Log

    2026-04-27 — Implementation [task:3cca8708-367b-4cf8-997b-ff9f8142141a]

    Completed all acceptance criteria:

  • Table createdhypothesis_history_events with (id, hyp_id, occurred_at,
  • event_kind, actor_id, actor_kind CHECK('agent','human','system'), before_state JSONB,
    after_state JSONB, trigger_artifact_id, narrative_md, event_dedup_key UNIQUE)
    .
    Indexes on (hyp_id, occurred_at DESC), event_kind, occurred_at DESC.
    Migration: migrations/138_hypothesis_history_events.py.

  • Backfillmigrations/139_hypothesis_history_backfill.py reconstructs events
  • from hypothesis_score_history, price_history, hypothesis_papers,
    debate_sessions/hypothesis_debates, audit_chain, and hypothesis creation
    timestamps. Ran successfully: 102,069 events inserted, 49/50 top hypotheses
    with ≥10 events.

  • Modulescidex/atlas/hypothesis_history.py with:
  • - record_event(...) — idempotent forward write via ON CONFLICT DO NOTHING on
    event_dedup_key; invalidates narrative cache on insert.
    - get_timeline(hyp_id, limit, since, before_cursor) — cursor-based pagination,
    lazily calls poll_event_bus() before querying.
    - compose_history_narrative(hyp_id) — LLM-composed story arc cached in a
    narrative_cache event row; regenerated on cache invalidation.
    - poll_event_bus(hyp_id=None) — consumes relevant events from
    scidex.core.event_bus for hypothesis_scored, debate_round_completed,
    analysis_completed, and 3 new event types.

  • Event bus — Added hypothesis_score_updated, hypothesis_paper_added,
  • hypothesis_verdict_updated to EVENT_TYPES in scidex/core/event_bus.py.
    Subscription via poll pattern (get_events(unconsumed_by='hypothesis_history')).

  • UI — History tab added to /hypothesis/{id} page: tab button with event count
  • badge, async-loaded timeline panel with per-row icons, summaries, expandable
    JSON diffs, and LLM narrative header. Vanilla JS cursor pagination via "Load more".

  • APIGET /api/hypothesis/{id}/history with limit, since, before_cursor
  • query params. Returns {events, count, next_cursor, narrative}.

  • Teststests/atlas/test_hypothesis_history.py: 11 tests covering
  • record_event, dedup, timeline ordering, since filter, cursor pagination,
    narrative cache invalidation, narrative caching, and end-to-end record+retrieve.
    All 11 pass.

    Sibling Tasks in Quest (Atlas) ↗