the manual

How OKUSOKO works

OKUSOKO runs an investigation the way a careful researcher would, only relentlessly and with the receipts stapled on. It keeps a queue of open questions, spends each cycle on exactly one of them, reads what it finds, and refuses to record anything it cannot quote from bytes it has stored. Every so often it stops looking at claims and looks at the shape of what it has collected instead — and that second, slower reading is what generates its own next questions.

Two loops, one store

a fast loop that gathers ground truth, a slow loop that reads the landscape

FRONTIERopen questions, rankedGROUND LOOP10 stages · one question · every cyclesearch → read → quote → verifyEVIDENCE STOREsnapshots · chunks · claims · evidence · entitiesPATTERN LOOPreads patterns, not claims · every 3rd cyclesurvey → emit → interrogateNEW QUESTIONSpriority 0.85 — above routine workonewritesre-reads the whole storefeeds back
The ground loop — every cycle

Takes one question, searches, reads three sources, extracts claims, verifies every quote against stored bytes, and decides whether the question is answered. This is where facts enter the system.

The pattern loop — every third cycle

Ignores individual claims and reads the whole store for structure: things that co-occur, clusters, years that bulge, contradictions, and gaps. What it notices becomes new questions, ranked above routine work — so the system's own observations steer the next searches.

One cycle, stage by stage

click any stage — the same ten run every cycle, in this order

ONE CYCLEone question inverified claims + metrics out1SELECT2SEARCH3TRIAGE4ACQUIRE5EXTRACT6VERIFY7INDEX8RESOLVE9CORROB10REFLECT

1. SELECT

Pick the single question this cycle will chase.swap point S1

A ResearchSpace is, underneath, a queue of open questions — the frontier. Each cycle takes exactly one. Highest priority wins; ties go to the older question.

Priority comes from where the question was born: the primary question you wrote is 1.0; a secondary question you attached — another information vector the loop must keep in view — is 0.95; a question the system raised itself after noticing a pattern is 0.85; a routine breakdown of a bigger question is 0.7; a “nobody has asked about this entity yet” gap question is 0.6.

One deliberate twist: if the last question worked was pattern-born, the next pick is forced to be something else. Otherwise the system's own theorising crowds out plain coverage of the ground.

The first time a primary or secondary question is selected, the director splits it into sub-questions and puts them all on the frontier.

A cycle can work more than one question (cycle_width, default 1): the picks alternate pattern-born and routine, discovery runs for each in parallel, and extraction, indexing and reflection follow on one thread — breadth per cycle, without racing the store.

in · every open question + what was worked lastout · one active questionrsx/frontier.py · select_question()
stages 1–3 are the search pipeline · 4–6 turn pages into verifiable claims · 7–9 build structure · 10 decides what happens next

The search pipeline, in detail

one question becomes three queries becomes three documents — everything else is discarded on purpose

1 questionselected from the frontier3 queriesprecise · broad · primary-source≤ 24 results8 per query from Braveunseen onlyknown URLs and skip-listed domains removedranked + droppeddirector sorts primary-first, marks junknovelty within bandsleast-like-the-corpus first, three at a time3 acquiredscraper → http → readability → browserquality gatetoo short · link density · boilerplatesnapshotsha256 content-addressed, kept forever≤ 40 chunksmost question-like chunks, ~1,200 chars
Why three queries

A single query finds a single kind of page. Splitting into precise, broad and archival means one search reaches the specific record, the overview, and the primary document — three different layers of the web at once.

Why rank before fetching

Fetching is the expensive step, in time and in tokens. Ranking with the director seeing all candidates side by side is far cheaper than reading three bad pages, and it can drop obvious junk outright.

Why novelty only breaks ties

Pure novelty chases weirdness and quality collapses. Pure quality re-reads the same encyclopaedia forever. Bands of three give quality the first word and novelty the second.

Why a claim can be trusted

every claim points at a byte range in a hashed snapshot — checked in code, not by judgement

SNAPSHOT · sha256:4f9c…a12bytes 18,204 – 18,377CLAIMValdez investigated the first reported case in April 1976.factcorroboratedEVIDENCE“…Officer Gabe Valdez was called to the Manuel Gomez ranch…”✓ verified — quote found in snapshot bytes
The epistemic ladder

Every claim is labelled fact, inference, hypothesis or speculation, and the database enforces it. The point is not to filter speculation out — it's to stop speculation from silently becoming fact as it gets passed along.

Verification is deterministic

The quote is hunted in the snapshot bytes: exact, then punctuation-tolerant, then normalised alignment, then fuzzy at 0.9. Anything that can't be located is flagged. In the Dulce trial run, 96% of 676 claims verified — and the 4% are visible, not hidden.

Your own documents

a PDF you upload skips search and triage — everything after that is identical

Read page by page

The PDF becomes one snapshot (so quotes verify the ordinary way) and each page is recorded as a span over it. Pages with a thin or missing text layer — scans, photocopies, handwriting — are rendered and transcribed from the scan first (the OCR rung; such pages are marked). Every chunk is then extracted — no per-cycle budget, because you asked for this document.

Digested twice, timed throughout

A summarizer role writes a 2–4 sentence digest, tags and dates for each page, then a document-level digest with type, author, date and the period it covers. Each page shows how many claims it contributed, and the document keeps per-stage timings (read, OCR, extract, summarize, advance) — the Documents view shows the live stage and progress while a job runs.

Then the space advances

Index → resolve → skeptic → corroborate → survey → interrogate, exactly as a ground cycle would. New entities join the graph, dated claims join the Chronoscope, and any patterns become frontier questions.

rsx/ingest.py · ingest_document() · workspace → Documents · cli: rsx ingest <space> <file.pdf>

Where the agents fit

six roles inside Okusoko, each with one job

director

Splits the seed question, writes the three search queries, ranks candidates, and decides when a question is answered. The only role that sets direction.

extractor

Reads a chunk, returns claims with epistemic labels, quotes and entities. A parse failure returns nothing rather than stub claims, so what is counted is what was read.

skeptic

Re-reviews live claims every cycle and moves them between provisional, corroborated, contested and retracted.

judge

Answers one narrow question over and over: do these two claims assert the same fact? Gates both corroboration and contradiction.

summarizer

Digests each uploaded page (summary, tags, dates) and then the whole document (type, author, date, period). Read-only: it never creates claims.

engineer

Writes a small scraper for a domain the ladder keeps failing on. The scraper is validated, stored in the space, and becomes rung 1 for that domain forever after.

embedder

Turns text into vectors for novelty, retrieval and clustering — runs inside the worker, so indexing never waits on a remote endpoint.

no agent writes to the store directly — every role returns structured JSON that plain Python validates, stores, and can reject. the roles propose; the code disposes.

The slow loop: reading the landscape

five detectors, run over everything, every third cycle

co-occurrence

Entity pairs that keep turning up in the same passage across different sources — not dominated by one document, and only the strongest sixty kept, so the view stays readable.

clusters

Greedy cosine grouping over claim vectors — a knot of claims all saying related things is itself a finding at a higher scale.

bridges

Entities that connect otherwise-separate communities of the corroborated entity graph — high betweenness relative to their few links. Informationally valuable, never evidence of anything by itself.

temporal

Years that attract disproportionate claim mass. Where the corpus bulges in time.

contradictions

Claims that share a resolved entity but assert conflicting attributes — dates, counts, money — plus anything with refuting evidence. Cross-source only, and judge-gated, or the view fills with noise.

gaps

Entities the corpus keeps mentioning that no open question addresses. Absence, detected.

unheld

Documents the corpus cites but does not hold — a deposition, memo, or case file named by two or more independent sources with no matching source in the space. The most actionable kind: the question it births is 'go get it'.

suspect

Entities whose identity needs confirming: a name seen once in OCR'd handwriting ('Agino' for Aquino), two spellings one edit apart, or one label pooling several people's surnames. The question it births is 'who is this, really?'

missing links

Two entities that never share a corroborated passage yet keep sharing associates, organisations, places and years — an undocumented relationship may sit between them. Structural inference, scored against the within-source shuffle, never a node; interrogation turns it into the search that would document it.

possible matches

Two entities that may be one — a nickname, a one-edit surname, a shared identifier, shared neighbours beyond chance — with the evidence for and against on the card. Nothing merges until you rule; a ruling holds through every resolve after it.

→ interrogate

Strong patterns are handed back to the director, which writes new frontier questions about them — deduped against what's already open, and entered at priority 0.85 so they get worked before routine decomposition.

Surfaces: the terrain read along one axis

the store is the terrain; a surface is a continuous reading of it — four built in, more to come

What a surface is

A surface is the whole store aggregated along one axis into cells, each carrying claim mass (density), verified share (coverage), distinct sources, status mix and a gap score. Time cells are years; place cells are place entities; actor cells are people and organisations; document cells are sources. Every axis also has a summary: how many claims could be placed on it at all, how many cells, the loudest gap.

Why the slow loop reads them

Gaps in surfaces are a better thing to interrogate than raw entity counts: a hole in the time surface between two busy years, an actor only one source speaks for, a place that carries claims but has no location, a source that was acquired and yielded nothing. These are emitted as gap patterns and become frontier questions — the system asking about the shape of what it knows, not just its contents.

rsx/surfaces.py · materialize() runs every cycle and after every upload (“what moved” is reported per axis) · workspace → Surfaces, and the strip on Overview · cli: rsx surfaces <space>

Dynamics: momentum and entropy of a thread

every question lineage and every entity is a trajectory with a clock — the loop measures how each one moves

Momentum

Information is mass arriving over time: verified claims weigh 1, unverified .4, corroborated ×1.5. Per cycle a thread has velocity (mass gained, smoothed), acceleration, and momentum = velocity × √mass × a bonus for independent sources. Coherence is the cosine between successive cycle centroids — speeding up in the same direction is acceleration with high coherence; speed with low coherence is churn.

Entropy

Shannon bits over two distributions: which sources a thread's claims come from (low = a monoculture, one document speaks for it) and which entities they name (low = focused, high = diffuse). Per claim, surprise = −log₂ of how often its entity combination occurs; a bridge is a high-surprise claim joining two well-established entities that almost never co-occur — something between surfaces.

Nothing is hidden for being slow

Flags are additive, not a ranking: accelerating, dormant, focused, diffuse, coherent, monoculture, woke up, bridging. The quiet focused pocket is listed because it is quiet. Momentum tells you where the information is running; entropy tells you how wide the road is.

rsx/dynamics.py · measure() every cycle, after uploads and surveys · workspace → Dynamics, tile on Overview, surprise on each claim · cli: rsx dynamics <space>

When it stops

three measured conditions — none of them an opinion

frontier empty

Every question closed. Nothing left to ask.

budget spent

The cycle cap you set, or max_cycles (12 by default).

information gain floor

Three cycles running where each brought fewer than 0.5 new claims per source AND mean novelty under 0.15. Diminishing returns, measured.

The workspace, tab by tab

the tray inside an investigation — every view reads the same store

Two things are always present: the Inspector — click any claim anywhere and its full evidence chain opens in the right rail — and the floating ask and note pair at the foot of the stage: one steps into the conversation with the record, the other opens a note already listening. Slash commands in the Ask composer drive the worker.

orientation
Workbench

The instruments at your fingertips — one card per view with what it tells you, what it currently sees in this corpus, the latest break it found, and a mark when something changed since you last looked. Breaks (the moments what is known actually changed) name the instrument that found them.

Overview

Count tiles, the verified-evidence ring, epistemic and status mix, per-cycle information-gain sparklines — and “what the investigation noticed”: the questions the pattern loop asked on its own.

Narrative

Prose written by the writer role from cited claims only. Every citation marker renders as a chip that opens the Inspector on the exact quote. Versioned, refreshed after runs and uploads.

Surfaces

The coverage instrument: density, coverage and gap per cell along the four axes. Click a cell to jump to the matching view.

Dynamics

The momentum-vs-entropy scatter per thread — which inquiries are hot, which churn without converging, which went quiet while focused.

Notebook

Your margin, all in one place — a quick note on the whole investigation, and every note inked on a claim, a source, an entity or a break. Dictate with the mic; Whisper settles the text.

the record
Frontier

The question queue, grouped by state, each with its origin — why is the system asking this? Attach your own secondary questions at priority 0.95.

Findings

Every claim, filterable by status, epistemic type and text. Horizon-aware.

Sources

The bibliography, with a date-coverage strip and click-to-correct publication dates — the column the time machine stands on. Horizon-aware.

Documents

Ingested files, expandable to page-by-page digests with OCR, rotation and handwriting flags.

Lineage

The source-to-source graph: solid arrows are explicit citations, dashed links are kinship (shared phrasing); hulls group lineages. Horizon-aware.

bringing evidence in
Upload

Drag-drop PDFs and text. Duplicates are caught before upload; a live queue shows each ingestion stage as it runs.

Web Research

Paste URLs for the acquisition ladder, or issue a search that fans out into batched acquisition jobs — one digest email when the batch lands.

the shape of the corpus
Entities

The constellation: a force graph sized by mentions, colored by kind, with community hulls, per-entity dossiers, an ego-focus mode and a time scrubber.

Patterns

What the slow loop found — clusters, co-occurrences, temporal bunching, gaps — each with its supporting members.

Contradictions

Judge-confirmed conflicts, laid out side by side.

Chronoscope

The zoomable evidence timeline: swimlanes by source, entity or epistemic type; green arcs corroborate, red arcs contradict; a density strip finds the core period. Horizon-aware.

Drift

One account retold over the years, with a strength score per telling — flags stories that got louder without gaining a single new independent lineage.

Map

Geocoded entities on a basemap, co-mention arcs, place dossiers, time scrubber.

time machine & operations
Horizon Diff

Then versus now — the comparison the horizon slider exists for. Detailed in the next section.

Activity

Every phase the worker executed, newest first, with token costs.

Jobs

The queue: queued, running, done, failed.

Team

Roles (admin / editor / viewer), invites, visibility, and an append-only event feed of who did what.

/run n [width] · /survey · /reverify · /add url [title] · /ask q · /q text · /kill qid · /reopen qid · /promote · /contest · /retract claim_id · /open claim_id · /view tab — bare text is an ask

The Research Time Machine

what did the record look like as of a date — or as known to one investigator?

The horizon system stands on one load-bearing column — published_at on every source — and a hard rule set designed to keep hindsight from leaking. Views that cannot honestly filter show an explicit full-knowledge banner instead of pretending to: filtering some numbers while others stay full-corpus is hindsight leaking through the arithmetic.

The record as of a date

Toggle the horizon strip and Findings, Sources, Lineage and Chronoscope filter to sources published inside it. Findings recomputes each claim's source count within the horizon and suppresses stored status chips — a “corroborated” badge earned in 2005 must not decorate a claim viewed from 1991. Undated sources are never silently included or excluded: they are quarantined, and counted, visibly.

An investigator's horizon

Name an investigator in the strip and mark sources “known to” them from the Sources view, with an optional reason logged to the event feed. A marked source is in-horizon regardless of publication date — including undated ones, which escape quarantine. The horizon stops being a calendar and becomes a person's knowledge state.

Horizon Diff — then vs now

What was already in the record at the horizon (today's corroborated conclusions whose independent lineages all sat inside it, recomputed over in-horizon links only), what surfaced only later — ranked by the lag between event and record — which contradictions only became visible later, and what came after.

Rigorous mode — a real fork

rsx horizon materializes a fork that honestly contains only the record as of the date: out-of-horizon sources removed, every full-knowledge judgment dropped — statuses reset, patterns cleared, corroboration links and narrative deleted — then recomputed from scratch inside the horizon. The fork records what was kept, cut and quarantined, and verify passes on it.

Asking inside the horizon

With the strip active every ask is horizon-constrained automatically: retrieval is restricted to in-horizon claims and quotes — a claim citing both a 1975 and a 2005 source surfaces only its 1975 quotes — and a deterministic year rule plus a judge pass strips hindsight sentences from the answer. The Inspector shows how many were stripped, with the text on hover, and the answer always carries a leak warning: Okusoko itself still knows the future, even when its citations don't.

the #1 foreseeable mistake — the horizon gates the assertion axis (when things entered the record), not the eventaxis (when things happened). “The record as of 1991” still describes events from 1975 — it just describes them using only what had been written down by 1991. Keeping those two time dimensions distinct is the whole discipline.

rsx/horizon.py · rsx/ask.py · the strip in the workspace header · cli: rsx horizon <src> <dst> --as-of YYYY-MM [--overlay name] · rsx ask --as-of --overlay · rsx report --as-of

A worked investigation

the arc the demo space actually ran — the 1975–79 Rio Arriba mutilation wave

1Seed

One question creates the space. Config worth knowing lives in researchspace.json: chunk size, sources per cycle, cycle budget, pattern cadence.

rsx init spaces/dulce --question "…1975–79 cattle mutilation wave in Rio Arriba County, and NMSP officer Gabe Valdez's role?"
2Run the loop

The director decomposes the seed on first contact, then the ten stages cycle. When the pattern loop first fires, watch Overview's “what the investigation noticed” — questions with a pattern origin are the system asking things you didn't.

rsx run spaces/dulce --cycles 4 · workspace: /run 4
3Feed it the primary record

Web sources get you the retellings; the case lives in scanned documents. Drag the district office reports and Valdez's memos into Upload and watch the stage queue run — dedup, read, OCR, extract, summarize, document, advance. Then correct any inferred publication dates in Sources, backfill the rest, and let kinship work out who copied whom.

rsx dates spaces/dulce · rsx kinship spaces/dulce
4Read the shape

Findings filtered to contested shows where sources disagree. Lineage shows a dozen “independent” accounts are one wire story wearing costumes — which is exactly why the corroboration count didn't move. Chronoscope finds the core period; Entities puts Valdez at the center of ranchers, labs and reporters; Drift flags the tellings that grew stronger over the years with no new lineage behind them.

5Interrogate it

The Ask view does the same. The answer comes back cited, and every citation chip opens the Inspector on the exact verified quote.

rsx ask spaces/dulce "Who investigated the 1976 Gomez ranch case?"
6Travel

Flip the horizon strip to 1979-06. Findings drops to what had actually been written down mid-wave; corroboration chips vanish (they were earned later); the quarantine badge counts the undated clippings. Open Horizon Diff to see which of today's conclusions were already fully supported inside 1979 — and which only became sayable years later. Ask the Gomez question again with the strip active and read the answer a 1979 investigator could have given, hindsight sentences stripped and counted.

7Make it portable, keep it honest

The rigorous fork is a real space — runnable, forkable, verifiable — that honestly contains only the 1979 record. Hand the .rsx to a collaborator and their verify tells them, mechanically, whether to trust it.

rsx horizon spaces/dulce spaces/dulce-1979 --as-of 1979-06 · rsx verify spaces/dulce-1979 · rsx package spaces/dulce --out dulce.rsx

The CLI, grouped by intent

python3 -m rsx.cli <command> <space> — the hosted product drives the same engine

lifecycle
initcreate a space around a seed question
runthe loop, --cycles --corpus
statuscounts, cycle, verified %, spend
ledgertoken cost by phase
sources in
add-sourcea file or URL through the ladder
ingestthe document pipeline; --replace re-ingests
reextractextraction only — no re-fetch, no re-OCR
scraperlist · add · test the fixture-gated registry
analysis
surveyre-run the pattern loop
surfaces / dynamicscoverage grids · thread momentum
resolve / kinship / geocodeentities · lineage · places
datesbackfill published_at — cheap, load-bearing
narrate / search / asknarrative · semantic search · cited answers
steering
question add · killinject or retire frontier questions
claim promote · contest · retractoverride the record, on the record
github-for-research
export / packageJSONL + hashed manifest · portable .rsx
fork / mergeverifiable lineage · conflicts reported, never auto-resolved
import / verifyrebuild · re-check every hash and quote
push / pullto and from the hosted platform
report / horizonreport.md · a rigorous as-of fork
eval
evalscore extraction vs gold fixtures; a degraded run exits 1 so an outage never masquerades as a score

Where a new method plugs in

eleven swap points, each with a contract you can honour without touching anything else

Each socket below is a place where the pipeline currently makes a decision with a simple rule. The contract line is what any replacement must take and return; satisfy it and the rest of the loop neither knows nor cares what's inside. Sockets marked markov-shaped are the ones where state-transition methods fit naturally — because the thing being decided already has states and moves between them.

S1Question selectionmarkov-shapedrsx/frontier.py · select_question()

now — A priority sort with an anti-starvation interleave. No learning: a question's expected yield never updates.

contract · (open questions, history) → one question

Markov decision process over question statescontextual bandit / Thompson sampling on yieldrestless bandit (questions decay while unworked)
S2Query synthesismarkov-shapedrsx/llm.py · director_queries()

now — One call to the director returns three queries: precise, broad, archival. Nothing in the space's own graph informs them.

contract · question → string[]

random walk on the entity co-occurrence graph to pick 2-hop termsn-gram Markov model over the corpus for period-accurate phrasingquery expansion from cluster centroids
S3Candidate rankingmarkov-shapedrsx/loop.py TRIAGE

now — The director ranks candidates comparatively, then novelty reorders within bands of three. Purely local — no notion of a source's standing in a wider graph.

contract · (question, candidates[]) → order + drop set

PageRank / absorbing Markov chain over the source-citation graphdeterminantal point process for diverse shortlistsmaximal marginal relevance
S4Chunk budgetmarkov-shapedrsx/extract.py · extract_source()

now — Cosine similarity to the question, top 40. Chunks are scored independently, which ignores that relevance runs in streaks down a page.

contract · (chunks[], question) → keep set

HMM over chunk sequence (relevant / irrelevant runs)change-point detection on the similarity seriesMMR for redundancy
S5Entity resolutionmarkov-shapedrsx/entities.py · resolve()

now — Pairwise name similarity with first-name compatibility and generic-surname guards. Effectively single-link clustering.

contract · mentions[] → canonical groups

Markov clustering (MCL) — literally random walks on the mention graphlabel propagationcorrelation clustering with must/cannot-link constraints
S6Claim status & source reliabilitymarkov-shapedrsx/extract.py · skeptic_pass() · rsx/patterns.py · corroborate()

now — The skeptic plus cross-source counting. Every source is treated as equally trustworthy, and status moves are decided one claim at a time.

contract · (claims, evidence, sources) → status ∈ {provisional, corroborated, contested, retracted}

truth discovery by EM (TruthFinder / latent truth model) — source reliability as a hidden variablebelief propagation over the claim–source bipartite graphDempster–Shafer combinationthe status ladder as an explicit transition matrix rather than ad-hoc moves
S7Pattern detectionmarkov-shapedrsx/patterns.py · survey()

now — Eight hand-written detectors: co-occurrence, bridge entities (spectral modularity communities + Brandes betweenness), cosine clusters, temporal mass, entity-attribute contradictions, gaps, cited-but-unheld documents, suspect entities needing identity confirmation. Each returns a strength score.

contract · store → patterns(kind, scale, strength, signature)

Infomap / random-walk community detection on the entity graphHMM over the dated-claim timeline to segment regimesHawkes process for self-exciting event clusters (waves, flaps)motif detection on the relation-typed graph
S8Question generation from patternsrsx/patterns.py · interrogate()

now — The director reads patterns above a strength floor and writes new questions, deduped against what's already open.

contract · patterns[] → new questions[]

expected value of information scoringrank candidate questions by predicted novelty gaincounterfactual: which question, if answered, most changes the pattern set
S10Surface gap detectionmarkov-shapedrsx/surfaces.py · _gaps()

now — Hand-written rules per axis: holes between busy years, thin coverage, single-source actors, unlocated places, silent or weakly verified documents. Each returns a strength.

contract · surface cells → gap patterns(cell, description, strength)

Markov chain over time-surface cells: transition mass between years/actors, stationary distribution vs observed mass = where attention is missingkriging / spatial interpolation on the place surface to estimate unobserved densityexpected-vs-observed (null model) per cell — surprise as the gap score
S11Thread scoring (momentum / entropy)markov-shapedrsx/dynamics.py · measure()

now — Mass-weighted velocity with an EMA, acceleration as its difference, momentum = v·√mass·source bonus, Shannon entropy over sources and entities, surprise from co-occurrence frequency, coherence from embedding centroids. Additive flags, no ranking-out.

contract · thread claims by cycle → series + flags (accelerating, dormant, focused, …)

Markov chain over surface cells per cycle: transition mass vs stationary distribution = where attention flows and never reachesHawkes process intensity as momentum (self-exciting arrivals)change-point detection on the velocity series for 'woke up'transfer entropy between threads — which thread's motion predicts another's
S9The stopping rulemarkov-shapedrsx/frontier.py · should_stop()

now — Two hard floors over a three-cycle window: new-claims-per-source and mean novelty. Simple and honest, but it can only see backwards.

contract · metrics window → stop reason | null

capture–recapture / Chao1 estimate of unseen claimsabsorbing Markov chain hitting time to exhaustionBayesian posterior on remaining yield, stop at a credible-interval floor

And then it's a file

a finished investigation exports whole — claims, snapshots, lineage

A ResearchSpace exports to a .rsx package: sorted JSONL tables, the raw snapshots, and a hashed manifest. Someone else can import it, run verify to re-check every quote against the bytes, fork it — which records your space as parent lineage — and merge findings back. Conflicting claims are never auto-resolved; they land in a merge report for a human to settle. That's the part that makes this closer to version control than to a chatbot.