Methodology · 方法论 · 方法論
This document is the contract between the repository and its readers. If the pipeline changes, this file changes in the same pull request.
1. Sources
| Kind | Endpoint | Evidence tag | Why it is trusted at this level |
|---|---|---|---|
| arXiv API | https://export.arxiv.org/api/query, six cs.RO queries |
R |
primary author text, but self-reported and often not peer reviewed |
| arXiv category feeds | https://rss.arxiv.org/rss/<category>, read only when the API returns nothing |
R |
the same papers, announced listings instead of search |
| Official vendor blogs | AWS Physical AI, NVIDIA Technical Blog / NVIDIA Blog, Google DeepMind, Hugging Face | O |
first-party statements about their own products |
| Lab research pages | Toyota Research Institute | R |
a lab’s own page for each paper: the same standing as the paper |
| Trade press | IEEE Spectrum Robotics, The Robot Report; 雷峰网 Leiphone (Chinese), MONOist (Japanese) | M |
professional reporting, still secondary |
| Standards watch | ISO, EUR-Lex, Council of the EU | O |
authoritative status and dates |
Rules:
- only declared machine-readable endpoints (Atom / RSS / documented API);
- no HTML scraping of article bodies, no paywall circumvention;
- every request is timeout-bounded and retried once before the source is given up on. A
4xx refusal is final and is not retried; 429 waits for
Retry-After, at most 30 seconds; - arXiv calls are spaced about three seconds apart, as its API guidance asks;
- a failing source is skipped with a log line and never fails the run — but if no source answers, the run exits non-zero and nothing is committed, because a radar rebuilt from the curated baseline alone must not look like a quiet news day;
radar/latest.jsoncarries afetchblock (items, sources attempted / answered, per-source counts, names of sources that did not answer) so partial degradation is visible without reading the workflow log;- every run also records that day’s unanswered sources in
radar/history.json, andpython3 -m pairadar.healthturns the last fourteen days into a verdict: a source that missed three or more days and also missed the latest run is reported as struggling. When the arXiv API refuses and the category feeds serve the day instead, the run recordsarxivunderfallback: visible in the verdict, but not a failure. The daily run prints the verdict and stays green;--fail-on-strugglingexits 1 for automation that should only act when there is something to act on; - a feed contributes at most its
filters.max_entries_per_feednewest entries (default 100). Some feeds publish their whole archive (the Hugging Face blog serves about 900 posts); without the cap one source floods the pool; - a feed description longer than 2000 characters is cut at a word boundary. Some feeds carry the full article, and a full article matches every lane;
- adding a source requires a weight and an evidence tag in
data/sources.json. A source whose every entry is about robots is also marked"topical": true(see section 2).
2. Classification
data/taxonomy.json defines eight lanes. Each lane has a keyword list and a weight.
An item is matched case-insensitively against title + summary, as whole terms: a
keyword counts only where it is not preceded or followed by a letter or digit, with an
optional plural s / es. So ppo no longer fires inside “support”, thor inside
“author” or droid inside “Android”. The lane with the highest hits × lane_weight wins. On a tie, any lane with hits beats
foundation: nearly every robot-learning paper names the VLA or world model it builds
on, and one that also names what it does with it belongs to that lane. Other ties
resolve to the first lane in file order, which makes classification deterministic.
Keywords and anchors include Chinese and Japanese terms for the zh and ja feeds; those
items are quoted in their own language on all three pages. Their descriptions are cut
at a third of the English length, which holds about as much text, so they do not
collect three times the keywords.
A keyword that is just as common outside its lane (“encoder”, “memory”, “standard”)
is listed as a phrase (“rotary encoder”, “episodic memory”, “safety standard”).
A curated entry in data/baseline.json that declares a lane keeps it: the lane is an
editorial decision, and the classifier is not allowed to overrule it. Such an entry is
scored on its own lane’s keywords rather than the best-matching lane’s.
Before any lane counts, an item must be Physical AI at all. Items from a topical
source (a "topical": true feed, or a paper listed in cs.RO) are on-topic by
construction. Everything else, which means general company blogs and papers found only
through other arXiv categories, must name at least one term from anchors in
data/taxonomy.json: robot, humanoid, embodied, manipulation, VLA, world model, ROS 2,
Isaac, LeRobot, Jetson and similar. Words like “agent”, “benchmark” or “inference” are
lane keywords but not anchors, because a coding-agent post uses them just as much.
Signals are orthogonal flags: closed_loop, real_robot, open_release, latency,
numbers. They are shown as badges and feed the score.
3. Scoring
score = min(lane_hits, 6) × 0.35 × lane_weight
+ evidence_bonus # O 0.60 · R 0.35 · M 0.10
+ Σ signal_bonus # closed_loop 0.50 · real_robot 0.40 · numbers 0.30
# open_release 0.25 · latency 0.20
+ recency_bonus # same day 1.0 · 1 day 0.8 · ≤3 days 0.5 · ≤7 days 0.25
+ (source_weight − 1.0)
Selection then applies min_score = 0.9, a per-lane cap of 2 and a global limit of 8,
so a single hot topic cannot take over the page. Two soft caps sit next to it:
filters.per_source (3) and filters.per_evidence (4). Selection makes two passes in
rank order. The first pass honours every cap. The second pass fills whatever slots are
still empty with the lane cap only. A soft cap therefore reserves room for other sources
and evidence kinds but never leaves the page short. Without them, a day when the arXiv
API refused and the category feeds answered came back as eight [R] preprints. The
shortlist is always shown in rank order. Three further gates apply:
-
on-topic (the anchor rule in section 2);
- at least one lane keyword. Evidence and same-day recency alone clear
min_score, so without this an off-topic hit from the broad arXiv queries would be published under the fallback lane, labelled as if it belonged there. Curated entries are exempt, since a person chose their lane. - not published in the last
filters.repeat_daysdays (30). Each run records the normalised URLs it published inradar/history.json; the arXiv window looks back four days and feeds keep entries for thirty, so without this a strong item would headline again the next day. The guard has to cover both horizons: at seven days, a post still in the thirty-day pool was picked again on the eighth. Matching is on a normalised URL, not id, so it survives a feed reassigning ids. The normal form drops query, fragment and trailing slash, lowercases, and upgradeshttptohttps. arXiv links are reduced tohttps://arxiv.org/abs/<id>without the version, because the API answershttp://arxiv.org/abs/<id>v1and the feeds answerhttps://arxiv.org/abs/<id>: they are the same paper. Every term is visible and tunable; no learned model sits in this path. This is a bias-by-design choice: the radar prefers items with real-robot or closed-loop evidence over pure benchmark deltas.
A fourth gate keeps one story to one slot. A vendor post and a trade outlet’s retelling have different URLs, so the repeat guard lets both through: on 2026-09-22 NVIDIA’s Isaac ROS 5.0 post and The Robot Report’s account of it took two of the eight places. Two titles tell the same story when they share at least three distinctive words and those make up at least half of the shorter title. Function words, press verbs and the names that appear in half the pool (NVIDIA, Google, robot, AI…) do not count, so two unrelated Isaac Sim posts or two Video Friday issues stay apart. The higher-ranked telling keeps the slot, and a story told on an earlier day inside the repeat window is not told again. Papers are exempt on both sides: two preprints with similar titles are two papers.
A day may publish fewer than eight picks, or none. When no live item meets these rules, the daily page says so and then lists the curated baseline under its own heading. The radar block in each README does the same: directly under the day’s heading and window line it quotes the same “no items” sentence, then a bold baseline label, then the table of curated entries. Before this, the README put the baseline under today’s heading with ranks and reasons, as if it were the day’s selection. Days with picks render unchanged.
4. Quantitative claim extraction
distill.extract_numbers matches percentages, multipliers, ms / Hz / fps,
parameter counts, hours / episodes / demonstrations / trajectories / tasks, and
GB / TB / W / kg / DoF. Up to four are shown per item, in source order, unmodified.
Numbers are quoted, not recomputed — if the source is wrong, the radar is wrong in
the same way, and the link is there to check.
A bare 48.1% · 68.9% does not say what was measured, so each figure on a page is
followed by the few words around it in the source (distill.number_context): up to four
before and three after, cut at the clause. The metric is not named by guessing from
nearby words; in “raises success rate and tracking accuracy by at least 24% and 47%” the
nearest metric to 24% is the wrong one, and a quote lets the reader see that. A figure is
quoted where it stands alone, so 17.6% is not quoted from inside 12.1-17.6% when the
text also gives it on its own.
Feed furniture is removed before anything is quoted: the WordPress footer “The post … appeared first on …” and the fixed Video Friday preamble.
Published figures can be reviewed against their sources (roadmap RA-01) with
python3 -m pairadar.claims [--store PATH] [--days N] [--limit N]. The tool is offline and
read-only. It reads radar/feed.json and prints a Markdown worksheet with one row per figure
published in the --days (1–30, default 7) before the newest entry date. It never uses the
wall clock. Each row gives the day, evidence tag, source link, a status, a quoted context and an
empty review column. The header and a whole-window status line count every figure, even when
--limit (default 30, no upper bound) caps the table. A truncation line then says how many
unlocated and no-excerpt figures are hidden and gives the --limit value that lists them all.
The statuses come from number_context over what the store kept:
- quoted: the figure is in the stored excerpt, and the context is the quoted clause;
- title: the figure is only in the title;
- no-excerpt: the entry has no excerpt, as with baseline entries;
- unlocated: the figure is in neither. The store keeps only a one- or two-sentence excerpt, while figures are extracted from the full summary. Unlocated therefore means “check the original source first”, not “unsupported”.
No status verifies a date, unit, population or result. Items that are not objects, items
without a _radar object, and entries whose date is not a canonical YYYY-MM-DD string are
skipped and counted in the header. Non-string text fields are shown as empty. An unreadable
store, invalid JSON, the wrong JSON Feed version, a non-list items or no dated entries exits
2 with one claims: line.
5. Translation policy
This is the part most repositories get wrong, so it is explicit here:
- Titles are never translated. They stay citable and searchable.
- The English abstract excerpt is an excerpt, rendered as a block quote, shown only for live (non-curated) items.
-
The analytical layer is authored, not translated. Lane names, evidence labels, signal badges and the “why it matters” line come from
data/glossary.json(ui,why_parts) and from hand-written trilingual fields indata/baseline.json.Without a curated or drafted line, the line is composed from what the item’s text shows (
render.composed_why): what its evidence tag means, which of the checks expected for that tag it gives or leaves out (real-robot results, closed-loop results, figures for a preprint; figures for a release or a report), and what to watch in its lane. Hardware and safety items are not asked for a closed loop. A single sentence per lane used to be the whole line: the week of 2026-09-19 published 48 lines of which 8 were distinct, and a README opened with two identical ones. - No machine translation is invoked anywhere in the pipeline. Titles, numbers and evidence tags are never produced or altered by a model.
- Drafted analysis is opt-in, per item, and labelled. The “why it matters” line may
come from a draft in
data/notes/<date>.jsoninstead of the composed line. Such a file is written outside the pipeline byscripts/draft_daily_notes.sh, which runs on a maintainer’s machine on a schedule and never in CI. The pull request is merged automatically once every deterministic check passes and a reviewer agent approves it; no human reads it first, and the page says so. Precedence is fixed: a human-curated line beats a draft, a draft beats the composed line. Every drafted line carries a visible label next to its heading, the page names the agent and model that drafted it, and each language is drafted separately rather than translated. A missing language falls back to the composed line, and a malformed or foreign notes file is ignored with a log line: notes can never break a run or silently replace authored text.
6. Charts
Charts are emitted as hand-written SVG (pairadar/charts.py): lane distribution,
daily cadence and evidence mix. No plotting dependency, no binary diffs, readable in a
pull request. One set is written per language, plus an unsuffixed English set because
already-published pages link to those filenames.
The lane and evidence-mix charts count the picks published in the last seven days
(cli.CHART_DAYS), read from the feed store, and their titles say so. They used to add
the 29 curated baseline entries to the day’s eight picks, so the bars mostly showed the
baseline. Seven days, not thirty, because the store keeps at most 100 entries and eight
picks a day would overflow a thirty-day count. The charts are embedded only in the
READMEs, which always show the current day. A daily page is frozen when it is published,
so it states its lane counts, source mix and recent cadence as text instead of linking a
chart file that the next run overwrites.
Bundling a font was never necessary: these are SVG text, so the glyphs come from the reader’s browser and the bytes this repository commits are identical either way.
Each lane may declare a short chart_label per language in data/taxonomy.json. The
full English names run 43–53 characters and, drawn at 12px from x=20, overran the bars
that start at x=250. Without a font library the renderer estimates advance width per
character — East Asian wide and fullwidth forms at one em, everything else at a
deliberately generous 6.8px — and trims anything still over budget with an ellipsis.
The estimate is per character because a single average is wrong for CJK by a factor of two. Chinese lane names fit the gutter unshortened; Japanese ones did not, which was visible only after rendering the charts and looking at them: five of eight labels came back cut. Short Japanese forms fixed that, and a test now asserts that no lane label in any language needs trimming at all — a cut label is a content problem to fix in the taxonomy, not a rendering detail to tolerate.
7. The site
The Pages site opens on a landing page per language (index.html in Chinese, en/,
ja/), written by pairadar/site.py from the same context as the day’s pages, so the
two cannot disagree. It shows the day’s picks as cards (lane, evidence tag, the lead
figure with its quote, the “why it matters” line) and the eight lanes as a radar: the
filled shape is the last seven days of picks, and each of today’s picks is a dot on its
lane’s spoke that links to its card.
The landing pages are plain HTML without front matter, so Jekyll copies them unchanged
and no fetched text reaches Liquid; every fetched string is escaped. The daily, weekly
and archive pages are still Markdown, rendered with _layouts/default.html, which takes
each page’s language from its file name so the page says which language it is in.
A shared link is previewed from its Open Graph tags, and previewers read neither scripts
nor SVG, so each language has a 1200×630 PNG card, assets/og.<lang>.png. It is drawn
from the site stylesheet by python3 -m pairadar.site --og, which needs a local Chrome,
and committed; the daily run never starts a browser. A share button links the day’s
frozen page rather than the landing page, which changes every morning. CI builds the site
with the same builder as Pages and checks each page’s language, stylesheet and card.
8. Reproducibility
- Python 3.10+, standard library only.
python3 -m pairadar --offlinereproduces a full render from repository data alone. It writes in place – the radar block in all three READMEs,radar/andassets/– because that is what the daily job does.python3 -m pairadar --offline --out DIRwrites the same output underDIRinstead, leaving the checkout untouched, for readers who want to inspect before committing to a rewrite. The run log is still read from the repository so the repeat window applies.python3 -m pairadar --date 2026-09-19pins the run date.- The window line on each page states both horizons: papers from
date − lookback_daysand feed posts fromdate − max_age_days, plus the repeat window. An earlier version printed only the paper horizon, which suggested a month-old blog post was new. radar/history.jsonkeeps the cadence series plus the URLs published inside the repeat window; older entries keep their counts and drop their URL list, so the log does not grow without bound.radar/latest.jsonis the machine-readable snapshot for downstream consumers.python3 -m pairadar --rerenderrebuilds the most recent published day fromradar/latest.json– same picks, same counts, no network. A day is rendered once at publish time, so this is how anything that arrives later (drafted notes, a wording fix) reaches that day’s pages without re-running selection against a moved pool. The snapshot therefore stores the source excerpt each page quotes;--rerenderreads the snapshot and never rewrites it.radar/feed.json(JSON Feed 1.1),radar/feed.xml,radar/feed.zh.xmlandradar/feed.ja.xml(Atom, RFC 4287) carry the picks of the last thirty days, at most 100 entries.feed.jsonis also the store: each item keeps its language-neutral record under a_radarextension, and a run replaces its own day’s entries, so re-running a day never duplicates it. A link already delivered on an earlier day is not delivered again. Entry ids are tag URIs built from the item id, so they are stable across runs. The feeds carry the title, lane, evidence tag, numbers, signals and source excerpt, and link to the day’s page for the “why it matters” line. That line stays off the feeds so a drafted line can never appear without its label, and so--rerendernever has to rewrite them.radar/weekly/<ISO week>.<lang>.mdgroups the week’s picks by lane. Each run rewrites the current week from the feed store; earlier weeks are left as published.python3 -m pairadar.feeds --backfillrebuilds the store from theradar/latest.jsonof each day in the git history, then rewrites the feeds and every week present. The store began on 2026-09-25, so the W39 page counted 8 picks where 32 had been published. Days already in the store are kept as they are; a day’s last commit wins; days replay oldest first, so a link repeated on a later day stays on its first. It needs a full clone: a shallow checkout has no history to read and backfills nothing.data/notes/<date>.jsonis an optional input. Without it the render is unchanged, so a reproduction from repository data alone stays deterministic.- Item ids are content-derived (
arxiv:<id>,<feed>:<sha1(link)[:12]>) and therefore stable across processes and days. - The daily workflow commits only when the working tree actually changed.
9. Known limits
- Extractive summaries can miss the real contribution of a paper; the lane and signals are a triage aid, not a review.
-
Keyword classification will misfile interdisciplinary work; corrections are welcome as PRs against
data/taxonomy.json. To see whether a taxonomy edit helps, score the classifier against a file of hand-labeled items before and after the edit:python3 -m pairadar.lanes --labels my-labels.json --margin 0.35The file is UTF-8 JSON,
{"items": [{"id": "a1", "title": "...", "summary": "...", "lane": "data"}]}.titleandlaneare required non-empty strings, andlanemust be a lane id fromdata/taxonomy.json.idandsummaryare optional, but when present they must be strings. Each entry is scored ontitle. summary, the same text the pipeline classifies, and the file is only read. The report gives the file’s basename (labels),labeled,margin,agreementanddisagreements.agreementhasn,agreeandratefor three groups:allentries;confidententries, which carry no thin or ambiguous flag; andflaggedentries, which do. If the flags work,flaggedagreement should be lower thanconfident.rateisnullwhen a group is empty.disagreementslists the misfiles in input order, with the label, the predicted lane, the flag reasons and the top two lane scores. An entry with no id showsid: null. A misfile withreasons: []is one the flags missed, and those are the cases a keyword edit most needs to address. The command exits 0 whenever it prints a report. It exits 2 with a singlelanes:line on stderr, and prints nothing on stdout, when the file is unreadable, is not UTF-8 JSON, has noitemslist, contains a malformed entry (the message names the entry’s number) or uses an unknown lane (the message lists the valid ones). It also exits 2 when--labelsis combined with--latestor--fail-on-suspicious. A small hand-labeled set shows agreement on those items only; it is not general accuracy or calibration. Keep it separate from the examples used to tune keywords, otherwise the number only confirms the edit that was fitted to it. [M]items frequently disagree on shipment and market numbers. When two credible outlets conflict, the curated baseline says so instead of picking a winner.- Nothing here is a safety argument. Standards entries report status and dates only.