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 rootRepo 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 indexlibrary.py --wikiregistry.py --repo (add-pdf, import-folder)
Reuse across runspool.py --wikipool.py --repo, against the registry
Snapshot storePer-run, isolatedPer-run plus a shared global store
Publishingokf.py promote → <wiki>/research/reviews/<slug>.mdoutputs/ + 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:

Repo root folder structure, hand-drawn diagram <repo>/ data/ registry · snapshots · refmgr index projects/<slug>/ the manuscript itself — durable runs/<slug>/ one execution's scratch state — promote out, then gc templates/ starter files project create copies from exports/ scaffolded, reserved — nothing writes here yet papers/ registry.jsonl · extractions/ · appraisals/<project>/ · annotations.jsonl sources/ snapshots/ (write-once) · events.jsonl (retrieval log) refmgr/ library.sqlite3 · assets/sha256/<xx>/ (content-addressed PDFs)
Root → top-level directories → 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 isA manuscript question — the write-up itselfOne execution's working state — a full review, or a lightweight summarize/extract callScaffolded 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 itScratch. Reusable bits (extractions, appraisals) get promoted out into data/papers/; once that's done the run dir is a redundant copyUnused. Reserved — see below
Written byproject create scaffolds it; you and the Stage 7 synthesis step write synthesis.md/manuscript.qmd; paper.py copies a summary here when --project is givenEvery stage script during that one run (corpus.py, fulltext.py, the extract/appraise subagents, assemble.py, verify.py…)Nothing today
Cleaned up howBy 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
Why 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.

FileStatusIf lost
data/papers/registry.jsonlSource of truthUnrecoverable. This is the library
data/sources/snapshots/, events.jsonlSource of truthUnrecoverable; every span stops verifying
data/papers/extractions/, appraisals/Source of truthUnrecoverable; re-extraction means re-running subagents
data/refmgr/assets/ (PDF bytes)Source of truthUnrecoverable from the registry — restore from backup or re-import
data/papers/annotations.jsonlSource of truth (your opinions)Unrecoverable
Saved searches in library.sqlite3Source of truthUnrecoverable — the one durable thing inside an otherwise derived database
data/papers/pool.jsonlDerivedregistry.py pool
papers_fts, chunks_fts, paper_termsDerivedregistry.py reindex
data/papers/embeddings.jsonlDerived (cache)embeddings.py index
runs/<slug>/workspace/Run-local working stateResume 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:

FieldValuesSet by
statusregistered / screening / included / excludedNothing — available for a project to set by hand
metadata_statuspending / partial / completeDerived from which fields are present
asset_statusmissing / availableadd-pdf / import-folder
extraction_statusnot_started / in_progress / extractedpromote
appraisal_statusnot_appraised / in_progress / appraisedappraise-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_idrefmgr paper idDuplicated top-level for lookup without unpacking asset
extraction_pathRepo-relative pathpromote
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:

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