# Story triage — how a candidate becomes a dossier

**Version 1.0 · 2026-08-05**

A research lane can carry two or three live dossiers at once. Roughly a hundred and fifty
plausible candidates arrive every week. The expensive decision is therefore not *what is
true* — that is what the [claim rubric](../../05_graph/CONTRACT.md) is for — but *what is
worth a week*. Made by feel, that decision is unauditable and drifts toward whatever is
loudest. This is the rule it follows instead.

---

## 1 · What gets pulled

Six sources, all free, no keys. One design decision worth stating: **general news
aggregators are deliberately excluded.** A news feed delivers coverage-of-coverage, which
is precisely the material this rubric exists to reject — so ingesting it would be paying
to generate work for the filter.

| Source | What it is | Why it earns a slot |
|---|---|---|
| `arxiv:astro-ph.EP` | exoplanets, planetary science | where the observational material would land if someone did the work |
| `arxiv:astro-ph.IM` | instrumentation and methods | instrument-artifact arguments live here |
| `arxiv:physics.hist-ph` | history and philosophy of physics | the fringe-science lineage, with citations |
| `federal_register` | rulemaking, records schedules, Privacy Act notices | where an agency must say in writing what it is doing, months before anyone announces it |
| `rss:nature` | Nature current issue | a press-released result is one the institution has decided to defend in public |
| `rss:science_news` | Science news | same |

Federal Register queries are **phrase-quoted**. The API's term search is full-text, and a
bare `anomalous` returns Medicare payment rules; `"unidentified anomalous phenomena"`
returns seven documents, all real.

---

## 2 · Six axes

Four are measurable by machine. Two are not, and the system does not pretend otherwise.

| Axis | Weight | Kind | Rule |
|---|---|---|---|
| `retrievable` | 20 | machine | DOI or federal document number → 1.0 · arXiv id → 0.8 · other identifier → 0.5 · none → 0 |
| `lane_fit` | 20 | machine | overlap with the subjects this show has *actually covered*, measured from the claim corpus |
| `freshness` | 15 | machine | linear decay to zero over 60 days |
| `named_owner` | 10 | machine | public contact email → 1.0 · named authors → 0.7 · neither → 0 |
| `decisive_test` | 20 | **human** | is there an observable that settles it, runnable with the tools at hand |
| `contradiction` | 15 | **human** | visible tension: claim against its own data, source against source, market against narrative |

### `lane_fit` is measured, not asserted

A hand-written keyword list would encode what someone *imagines* the lane to be. This one
is derived at runtime from the 801 scored claims in `05_graph/claims/*.jsonl`, weighting
each topic by how central it is to the corpus — **1,012 terms** on the 2026-08-05 build.
Meta-topics that describe how a claim was handled rather than what it is about (`method`,
`provenance`, `counts`, `absence`, `testimony`) are excluded, or every candidate
containing the word "method" would look like a subject match.

The consequence is that the vocabulary updates itself when new episodes are scored. The
lane does not have to remember to tell the triage what it now covers.

---

## 3 · Two readings, and the gap between them

Identical in principle to the [lunar pit board](../../06_cases/moon-arena/): a score that
silently treats an unfilled axis as zero and a score that silently drops it are both lying,
in opposite directions. So both are computed.

- **measured** — averaged over the axes that were actually filled, rescaled to 100
- **strict** — unfilled axes score zero and still cost their full weight
- **gap** — how much of this candidate's rank is resting on judgment nobody has applied yet

A machine-only candidate carries a structural gap of about +28 to +33, because the two
human axes are worth 35 of the 100 points. That number is not noise; it is the honest
statement that nobody has read the paper.

---

## 4 · Bands name an action

| Band | Condition | What it means |
|---|---|---|
| `open` | strict ≥ 55 **and both human axes filled** | start a dossier |
| `read` | strict ≥ 35, human axes empty | worth a person's twenty minutes |
| `watch` | strict ≥ 35, read, below the open line | real but not this week; re-score on a named trigger |
| `log` | below 35 | recorded, not pursued |

**An unread candidate can never reach `open`, however good its machine score.** The four
machine axes together describe a paper that is recent, on-topic, by named authors, with a
DOI — a description of several hundred papers a week, not of a story worth a week of work.
What separates those is a decisive test and a visible contradiction, and neither is
machine-readable. The machine's job ends at handing over a reading queue.

This was not the first design. The first version let a candidate reach `open` on machine
axes alone, and on the first live run three exoplanet papers were ranked as dossiers on the
strength of having recent dates and DOIs. The band rule was changed rather than the
weights, because the weights were not the thing that was wrong.

---

## 5 · Calibration is a falsification test

The three dossiers already published were chosen by hand, months before this rubric
existed. **If the rubric scores them below its own open line, the rubric is wrong** — and
`triage.py --calibrate` exits non-zero and the daily run refuses to rank anything.

Measured 2026-08-05:

| Dossier | strict | band |
|---|---|---|
| `trump-disclosure` | 92.2 | open |
| `moon-ufo` | 83.9 | open |
| `amazon-frontier` | 78.8 | open |

All three clear the line of 55. One result is worth reading closely: **`moon-ufo` scores
only 0.21 on `lane_fit`** — the lunar-observation paper barely matches the show's measured
vocabulary — and it is carried entirely by its two human axes. A purely mechanical triage
would have filed the strongest of the three dossiers under `log`. That is the clearest
available argument for why the human axes hold 35 of the 100 points.

---

## 6 · Memory

`decisions.jsonl` records every candidate already ruled on, keyed by a stable `uid`. A
triage that re-proposes yesterday's rejects every morning trains its reader to skim it,
which is the same as not running it.

---

## 7 · Running it

```bash
python3 09_news/triage/triage.py --calibrate   # check the rubric against known-good work
python3 09_news/triage/triage.py --pull        # fetch live, score, write queue.json
python3 09_news/triage/triage.py --cached      # re-score the last pull without refetching
```

Scheduled daily by `com.lfc.ab7-triage` (launchd). Output is `queue.json`, rendered to
[`/news/triage/`](https://ab7.ai/news/triage/).

The run publishes its own denominator: how many candidates were seen, from which sources,
how many fell in each band, and which sources errored. A shortlist without a denominator
is a recommendation; with one, it is a measurement.
