# Architecture overview

The real pipeline, stage by stage, and who owns each stage — the agent under a rubric, or the deterministic CLI.

A scan is twelve steps. Some are judgements made by a model under a written
rubric; the rest are deterministic stages the CLI performs. Knowing which is which
is most of understanding the system.

import PipelineDiagram from '@components/PipelineDiagram.astro';

<PipelineDiagram />

The main repo renders the same chain in one line:

```
brief ─▶ [plan queries] ─▶ retrieve ─▶ [screen 0–3] ─▶ expand ─▶ [screen] ─▶ coverage
      ─▶ [gap round, if coverage is thin] ─▶ shortlist ─▶ [rerank] ─▶ verify ─▶ emit
```

Bracketed steps are the agent's; the rest are CLI stages.

## The stages

| Stage | Owner | What it does |
|---|---|---|
| `init` | CLI | Creates the run directory and `brief.md`, and prints the `RunInfo` the planning agent must respect. `--profile` belongs here and is recorded in the manifest. |
| `plan` | Agent | Writes `queries.json` — 6–8 queries and 3–6 sub-criteria, derived from the brief's purpose. |
| `retrieve` | CLI | Queries every routed source, then dedups, filters, caps and writes the screening batches. |
| `screen` | Agent | Scores every candidate 0–3 against the sub-criteria. |
| `expand` | CLI | Walks the citation graph out from every paper screening kept. |
| `screen` | Agent | Scores the expansion items the same way. |
| `coverage` | CLI | Counts how well each sub-criterion is covered, so the gap round has something to aim at. |
| *gap round* | Both | At most once. The agent writes queries against the thinnest criteria; `retrieve --round 2` and `expand --round 2` add what they find. |
| `shortlist` | CLI | Checks that every candidate was screened, then orders and cuts for the reranker. |
| `rerank` | Agent | Scores the shortlist per criterion and overall, and writes the argument for each paper. |
| `verify` | CLI | Checks every ranked paper against the live record and records what did not match. |
| `emit` | CLI | Applies the selection rules and renders the deliverable. |

There is no `screen` or `rerank` command in the CLI. Those stages belong to the
agent, and the CLI has no faculty for performing them.

Every stage is idempotent and re-runnable: change `queries.json` or pass a flag
and re-run from the stage it affects.

## Sources and routing

| Source | Used for | Status |
|---|---|---|
| **OpenAlex** | Primary search, metadata, retraction flag, graph fallback | Implemented. Requires a key. |
| **Semantic Scholar** | Search, and citation-graph expansion over references, citations and recommendations | Implemented. Key optional, throttled without one. |
| **arXiv** | Extra source for `cs`, and for `general` when a query is a method query | Implemented. |
| **PubMed** | Routed for `biomed` | **Routed but not built yet.** A biomed scan runs on OpenAlex + Semantic Scholar and records `unavailable: true` in `manifest.json`. |
| **Crossref** | DOI verification only; never a retrieval source | Implemented. Verification degrades to OpenAlex if it is unreachable. |

`--domain` picks the routing: `behavioral` and `general` use OpenAlex and Semantic
Scholar; `cs` adds arXiv; `biomed` adds PubMed. `--sources` overrides the map
outright.

That PubMed row is worth reading twice. A routed source that is not built is
recorded as unavailable in the manifest rather than dropped quietly — the same
rule that governs every other kind of loss in the pipeline.

## The gap round

The gap round runs at most once, and on `standard` only when `coverage` reports
that criteria are unevenly covered or that a query came back nearly empty.
Otherwise it is skipped and the report says so. On `quick` it never runs; on
`deep` it always does.

The mechanism: `coverage` counts, per sub-criterion, how many papers screening
kept. The agent writes one or two queries against the criteria that came back
thinnest. `retrieve --round 2` and `expand --round 2` add what they find.

**A second round adds; it never subtracts.** Round one's papers and their scores
are never discarded — the pool only grows. Coverage takes one snapshot per round,
so what the gap round recovered is visible as a delta.

## What bounds a run

Two dials, both recorded in the manifest.

**Purpose** — `build`, `research` or `orient` — decides which sub-criteria the plan
derives, what screening counts as relevant, and what `why_it_matters` has to
argue.

**Profile** — `quick`, `standard` or `deep` — sets per-query depth, the pool cap,
the out-of-window total and whether the gap round runs.

| Profile | Per query | Pool cap | Out-of-window total | Gap round |
|---|---|---|---|---|
| `quick` | 20 | 250 | 12 | never |
| `standard` (default) | 40 | 450 | 20 | when coverage is uneven |
| `deep` | 40 | 450 × sources ÷ 2 | 30 | always |

An explicit flag still overrides the profile: `--per-query 60` means 60 whatever
the profile says. The out-of-window total is a budget for the whole run, not a
per-stage allowance — **caps are totals, not allowances per stage.**

## Where the time goes

Wall clock tracks the candidate pool, because screening is the long pole — it is
the agent reading the pool.

| Pool | Screening batches | Wall clock |
|---|---|---|
| ~675 candidates — `cs`, 3 sources at the default cap | ~27 | **37 min** |
| ~250 candidates — 2 sources at the default cap | ~10 | **22 min** |
| ~120 candidates — `--max-candidates 120` | 5 | **19 min** |

OpenAlex bills roughly $0.008 either way; pool size costs almost nothing in API
money. **Agent tokens are the real cost**, and they do not fall as fast as the
pool does: the 120-candidate run still cost about $5.70 in frontier-model tokens
and ran only three minutes quicker than the 250-candidate one, because reranking
and fixed stage overhead dominate once screening is small.

`--max-candidates` is the right dial for a runaway `cs` pool, not a way to make a
scan cheap.

## Read on

- [The CLI engine](/architecture/cli-engine/) — how the deterministic stages behave.
- [The agent layer](/architecture/agent-layer/) — the rubrics and the screening scale.
- [Verification](/architecture/verification/) — the last stage before emit.
