Standalone mode
Two storage roots, not one migration. Either works end to end without the other.
A run can live under a wiki root (the legacy path, with a wiki-level paper pool) or under a repo root (self-contained, with a canonical registry). wiki-manager is never a runtime dependency of the repo path. The wiki bridges in both directions exist for interoperability and are never a required step.
| Wiki root | Repo root | |
|---|---|---|
| Runs live at | <wiki>/outputs/deep-research/<slug>/ | <repo>/runs/<slug>/ |
| Paper store | <wiki>/assets/papers/pool.jsonl | <repo>/data/papers/registry.jsonl |
| Intake + PDF index | library.py --wiki | registry.py --repo (add-pdf, import-folder) |
| Reuse across runs | pool.py --wiki | pool.py --repo, against the registry |
| Snapshot store | Per-run, isolated | Per-run plus a shared global store |
| Publishing | okf.py promote → <wiki>/research/reviews/<slug>.md | outputs/ + projects/<slug>/manuscript.qmd |
--wiki and --repo are mutually exclusive per invocation, not per script — most commands accept either. Everything about the pipeline's shape is identical between them; only the root and which script handles pool/PDF intake differ.
Layout
Exactly what research.py init <path> creates, plus the directories that appear on first use. Sketch first, exact tree below it:
data/'s three stores. Files inside each directory are listed in the tree below.projects/, runs/, and exports/ — three different jobs
These three sit at the same level and are easy to lump together as "output," but they hold fundamentally different things: one is the deliverable you're writing, one is scratch state from getting there, and one is a reserved landing spot nothing currently uses.
projects/<slug>/ | runs/<slug>/ | exports/ | |
|---|---|---|---|
| What it is | A manuscript question — the write-up itself | One execution's working state — a full review, or a lightweight summarize/extract call | Scaffolded subfolders (wiki, bib, html, docx, pdf) with nothing in them |
| Durable or scratch? | Durable. This is what you're producing; nothing else in the repo holds it | Scratch. Reusable bits (extractions, appraisals) get promoted out into data/papers/; once that's done the run dir is a redundant copy | Unused. Reserved — see below |
| Written by | project create scaffolds it; you and the Stage 7 synthesis step write synthesis.md/manuscript.qmd; paper.py copies a summary here when --project is given | Every stage script during that one run (corpus.py, fulltext.py, the extract/appraise subagents, assemble.py, verify.py…) | Nothing today |
| Cleaned up how | By hand — it's your manuscript, not disposable | /deep-research:gc-runs once its paper data is promoted (lightweight profile only; a full review run is never auto-deleted — see below) | N/A — nothing accumulates here to clean up |
One paper's worth of registry data (its extraction, its appraisal) can be produced by any number of runs over time — a review, a one-paper summary, an extract-only call — and each promotes into the same data/papers/ store. A project, by contrast, is the one write-up for one question: its manuscript.qmd is not derived from anything and is not reproduced by re-running a command. exports/ looks like it should be the third leg of that story (rendered deliverables) but isn't yet — every rendering path that exists today (render.py, html_report.py, ReadCube's export, bib-export) writes to a run's own outputs/, or to an explicit --out/--destination path you name, never into this scaffolded folder automatically.
Full directory tree
<repo>/
data/
refmgr/ # created on first PDF intake or `registry.py reindex`
library.sqlite3 # paper/identifier/asset/attachment store, the derived
# search indexes (papers_fts, chunks_fts, paper_terms),
# and saved searches (durable, not derived)
assets/sha256/<xx>/ # content-addressed PDFs
sources/
snapshots/ # write-once JSON text snapshots: src-<64 hex>.json
assets/ # pre-cutover PDF location — still created, no longer written
events.jsonl # append-only retrieval log, global
papers/
registry.jsonl # canonical registry — one record per evidence_id
pool.jsonl # GENERATED view of the registry. Never hand-edited,
# never the source of truth (`registry.py pool` rebuilds)
extractions/ # <evidence-id-slug>.json, e.g. pmid-12345678.json
appraisals/<project>/ # project-scoped — a paper's appraisal is per-question
annotations.jsonl # your tags / star ratings / notes
embeddings.jsonl # cached embedding vector per evidence_id
export_ledger.jsonl # per-destination fingerprints for `export --incremental`
projects/<slug>/ # `research.py project create` scaffolds this; DURABLE
protocol.md # your question/PICO-PECO framing — hand-written, never overwritten
synthesis.md # cross-paper synthesis prose — Stage 7, or you
manuscript.qmd # the actual manuscript; quarto front matter pre-filled
refs.bib # "generated by pool.py bib/render.py; do not hand-edit"
corpus.lock.jsonl # touched empty at creation — reserved, nothing populates it yet
figures/ # empty; your figure assets for the manuscript
tables/ # empty; your table assets for the manuscript
paper-summaries/<slug>.md # only if summarize --project copied one here
paper-summary-sets/<slug>.json # only if summarize-set --project copied one here
runs/<slug>/ # ONE execution's state — SCRATCH, gc-runs candidate once promoted
config.json # profile, stage pointer, budgets, connectors — bumped as stages complete
taskboard.jsonl # one append-only record per unit of work; never hand-edited
corpus.jsonl # one record per paper this run touched, every stage's view of it
workspace/ # per-stage working records, reserved shape per subdirectory:
search/<query_id>.json # full pipeline only
screening/<screener>/ # full pipeline only
extractions/<slug>.json # both profiles — Stage 5 / paper.py extract
appraisals/<slug>.json # both profiles — Stage 6 / paper.py appraise
summaries/<slug>.json # lightweight profile only — the §14 record
outputs/
report.md # full pipeline
digest.md # full pipeline
result.json # full pipeline — assemble.py's evidence ledger
verification.json # both profiles
single-paper-summary.{md,html} # lightweight profile only
# a `summarize-set` also leaves a second, sibling run dir with no config.json at all —
# just the set's own manifest, one level up from each constituent paper's own run dir:
runs/paper-set-<label>/
summary-set.json # §15 record: included/failed evidence_ids, overview, provenance
templates/ # starter files `project create` copies from — durable, static
exports/{wiki,bib,html,docx,pdf}/ # scaffolded at init; empty — see above
data/sources/ looks like a run directory. It is one, deliberately. Every write-once/hash/verify function in store.py is a pure function of a run-directory-shaped path, so the global store is just data/sources passed in as that path — zero duplication and zero risk to the audited evidence-kernel code. Snapshots are written to snapshots/; global reads fall back to the legacy sources/ dirname for repos written before that rename, and writes never use it.
runs/ detail beyond this page. The full Stage 0-8 shape (workspace/search/, screening/<screener>/, PRISMA, taskboard mechanics) is documented in full in Full pipeline. The lightweight single-paper-summary/paper-summary-set shape — same runs/ root, far fewer files — is documented in Paper summaries. This page only distinguishes the directory's role from its siblings; it does not re-document either shape's stage-by-stage behavior.
Source Of Truth vs. Derived
Knowing which is which tells you what a backup must contain and what a corruption costs you.
| File | Status | If lost |
|---|---|---|
data/papers/registry.jsonl | Source of truth | Unrecoverable. This is the library |
data/sources/snapshots/, events.jsonl | Source of truth | Unrecoverable; every span stops verifying |
data/papers/extractions/, appraisals/ | Source of truth | Unrecoverable; re-extraction means re-running subagents |
data/refmgr/assets/ (PDF bytes) | Source of truth | Unrecoverable from the registry — restore from backup or re-import |
data/papers/annotations.jsonl | Source of truth (your opinions) | Unrecoverable |
Saved searches in library.sqlite3 | Source of truth | Unrecoverable — the one durable thing inside an otherwise derived database |
data/papers/pool.jsonl | Derived | registry.py pool |
papers_fts, chunks_fts, paper_terms | Derived | registry.py reindex |
data/papers/embeddings.jsonl | Derived (cache) | embeddings.py index |
runs/<slug>/workspace/ | Run-local working state | Resume from the stage pointer |
A lost or corrupt SQLite database is repaired, not restored. If the index is missing or unusable, search --q falls back to scanning snapshot bodies — slower, same answers.
Registry Lifecycle Fields
Beyond the bibliographic fields it shares with a corpus record, every registry record carries:
| Field | Values | Set by |
|---|---|---|
status | registered / screening / included / excluded | Nothing — available for a project to set by hand |
metadata_status | pending / partial / complete | Derived from which fields are present |
asset_status | missing / available | add-pdf / import-folder |
extraction_status | not_started / in_progress / extracted | promote |
appraisal_status | not_appraised / in_progress / appraised | appraise-promote |
asset | {refmgr_paper_id, attachment_id, sha256, bytes, pages, mime_type, added_at} | Present once the asset is available; the bytes live in the refmgr attachment pool |
refmgr_paper_id | refmgr paper id | Duplicated top-level for lookup without unpacking asset |
extraction_path | Repo-relative path | promote |
appraisals | {project: repo-relative path} | appraise-promote — a map, not a field |
The Reuse Loop
This is the point of the whole storage model: work done once for any question is available to every later question that touches the same paper.
Stage 2 pool.py seed --run-dir <run> --repo <path> --query "..."
→ scores the registry against this run's question and upserts likely papers
into corpus.jsonl as candidates (source: pool). They still get screened.
Stage 5 pool.py reuse --run-dir <run> --repo <path> --pmid <pmid>
→ matched: true + non-null extraction ⇒ the canonical extraction is copied
into workspace/extractions/, tagged "reused_from", and the subagent is skipped.
Stage 5 registry.py promote --repo <path> --run-dir <run>
exit → copies this run's fresh extractions into the canonical store and repoints
the corpus record at them. Span-verified by default.
Stage 6 pool.py reuse ... --project <slug> # same gate, now checking appraisal_path
Stage 6 registry.py appraise-promote --repo <path> --run-dir <run> --project <slug>
exit
Why repo mode needs no snapshot-carrying step
Store(run_dir, repo_root=<path>) writes new snapshots to the global store by default and falls back run-local → global on read. So a canonical extraction promoted from any run resolves its spans from any other run constructed with the same repo_root, byte-identically, with nothing to copy.
Wiki mode cannot do that — wiki runs have isolated per-run stores — so pool.py reuse there re-registers every snapshot a reused extraction's spans cite into the destination run's own store. Same source_id, content-addressed, so it resolves identically. Either way, a reused snapshot carries a register event rather than a fetch, so C-FRESH-FETCH correctly reports it as not retrieved in this run while C-SNAPSHOT and C-SPAN pass.
Appraisal Is Project-Scoped, On Purpose
The registry stores appraisals in a map keyed by project, not in a single field. Risk of bias and certainty are a function of the question a paper is being used to answer, not an intrinsic property of the paper.
"appraisals": {
"adolescent-cbt": "data/papers/appraisals/adolescent-cbt/pmid-12345678--cdi2-12wk.json",
"digital-therapies": "data/papers/appraisals/digital-therapies/pmid-12345678--adherence.json"
}
Consequences worth knowing before they surprise you:
- A lookup without
--projectnever returns an appraisal. - The same paper can be
appraisedfor one manuscript andnot_appraisedfor another at the same moment, and both are correct. - Extractions are not project-scoped — a paper's data is its data. Only the judgement about it is scoped.
Commands
# repo / project lifecycle
python3 scripts/research.py init <path> [--from-wiki <wiki-root>]
python3 scripts/research.py project create <slug> --repo <path>
python3 scripts/research.py project list --repo <path>
python3 scripts/research.py export wiki --repo <path> --wiki <root> [--project <slug>]
# intake — all paths converge on Registry.register
python3 scripts/registry.py add --repo <path> --pmid <pmid> | --doi <doi>
python3 scripts/registry.py add-pdf --repo <path> --file <pdf> [--pmid/--doi/--pmcid/--title]
python3 scripts/registry.py import-bib --repo <path> --file refs.bib
python3 scripts/registry.py import-folder --repo <path> --dir <folder> [--recursive]
python3 scripts/registry.py lookup --repo <path> (--pmid|--doi|--pmcid|--evidence-id)
python3 scripts/registry.py list --repo <path>
python3 scripts/registry.py pool --repo <path> # regenerate the derived view
# promotion
python3 scripts/registry.py promote --repo <path> --run-dir <run>
python3 scripts/registry.py appraise-promote --repo <path> --run-dir <run> --project <slug>
# reuse
python3 scripts/pool.py seed --run-dir <run> --repo <path> --query "..." [--include-metadata-only]
python3 scripts/pool.py lookup --repo <path> (--pmid|--doi|--pmcid) [--project <slug>]
python3 scripts/pool.py reuse --run-dir <run> --repo <path> (--pmid|--doi|--pmcid) [--project <slug>]
# legacy bridge, one-way, never required
python3 scripts/pool.py migrate --from-wiki <wiki-root> --repo <path> [--project <slug>]
Migrating from a wiki pool
For every record in the legacy pool: register its bibliographic fields into the registry, then copy its freshest pointer that still resolves on disk for extraction and appraisal into the canonical store, carrying every snapshot those spans cite into the global source store so they keep verifying with no wiki involved. A pointer whose file has moved or vanished is reported under stale_pointers in the command's JSON — never silently dropped.
Who Else Reads This Store
There is no separate summary-only store and no separate reference-manager store. paper.py's summary profiles and the search/annotate/ask layer read and write the same registry, extractions, and appraisals the full pipeline does. A paper registered and extracted by a review is reused as-is by a later one-paper summary, and vice versa.
What Is Not Wired
SKILL.md's Stage 2–8 narrative still documents the wiki-backed path only. Running a project under a repo root today means using theresearch.py/registry.pycommands directly rather than the stage prose.statusandscreeningon registry records are not set by any command. They exist for a project to use, not because anything writes them.okf.py promotehas no repo-mode equivalent.research.py export wikiis a plain file copy — a courtesy bundle, not a replacement for the validated promotion path.--source refmgrin the ReadCube export cannot take--run-dir: a run's evidence ids use the registry's scheme, and there is no defined cross-resolution against refmgr identifiers yet.