Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,14 @@ jobs:
python-version: "3.12"
- run: pip install pytest
- run: make validate
# The wikilink leg of `make validate` is a ratchet: it fails only on an
# unresolved target that is not in scripts/wikilink_baseline.txt, because
# a link with no page yet is legal (wiki/CLAUDE.md, "Cross-references").
# This step re-runs it with --list so the log always carries the current
# wanted-page backlog — and, when the ratchet trips, shows how often the
# offending slug is referenced. `if: always()` so a failure above still
# gets the listing.
- name: Wikilink report
if: always()
run: python scripts/validate_wikilinks.py --list
- run: make test
3 changes: 1 addition & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ The repo has exactly **two content homes**, enforced by
[`wiki/CLAUDE.md`](wiki/CLAUDE.md). New sub-wikis are added beside the
existing ones, never at the repo root.
- **`bibliography/`** — the *only* place BibTeX lives. One canonical file
(`pyautomemory.bib`) plus `bibkey_aliases.yaml`; never add loose `.bib`
files anywhere else.
(`pyautomemory.bib`); never add loose `.bib` files anywhere else.

**Source PDFs live off-repo.** Never commit a paper (PDF/HTML, with or
without a file extension) — read it, stub it in the right
Expand Down
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
.PHONY: validate validate-literature-citations validate-structure test board
.PHONY: validate validate-literature-citations validate-structure validate-wikilinks test board

validate: validate-literature-citations validate-structure
validate: validate-literature-citations validate-structure validate-wikilinks

validate-literature-citations:
python scripts/validate_literature_citations.py

validate-structure:
python scripts/validate_structure.py

validate-wikilinks:
python scripts/validate_wikilinks.py

test:
python -m pytest tests/ -q

Expand Down
28 changes: 19 additions & 9 deletions arxiv-interests.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,28 @@ Format:
into — these papers span domains, so unlike the strong-lensing inbox there is
no single destination. A topic naming no real section falls back to
`## Interests` (`FALLBACK_SECTION` in `scripts/interests_actions.py`).
- **A backlog, not a timer.** Nothing here lapses. The Dashboard shows the
**oldest un-cleared batch only**, and its 🧹 *clear* button drops that whole
day and reveals the next — so a fortnight away is a fortnight of batches to
cycle through, one tap each, not a fortnight of lost recommendations. This is
the one way this list differs from `arxiv-inbox.md`, which lapses after seven
days.
- **A backlog on the inbox's timer.** The Dashboard shows the **oldest
un-cleared batch only**, and its 🧹 *clear* button drops that whole day and
reveals the next — but a batch nobody clears **lapses after 7 days**
(`INBOX_WINDOW_DAYS` in `scripts/inbox_actions.py`, reused by
`interests_actions.py sweep`) and is swept by the nightly job. The batch is
the unit: the whole day goes, never part of one.

This list was built the other way — "a backlog, not a timer; nothing here
lapses" — so that a fortnight away would be a fortnight of batches to cycle
through rather than a fortnight of lost recommendations. The measurement
said otherwise: of 123 papers appended, **2 reached the reading queue and 1
batch was cleared** — a 1.6 % clearance rate, leaving 114 entries in 11
day-batches unread. A backlog nobody walks forward through is not a backlog,
and it makes the oldest batch — the one the Dashboard shows — the least
relevant thing on the board. Seven days is how long a suggestion is worth
looking at; the same rule as `arxiv-inbox.md`, applied a day at a time.
- Each paper carries the same one-tap actions as the inbox: 📄 straight to the
PDF, ➕ add to the reading queue, 📥 intake into memory, 📑 make citeable,
✖️ dismiss.
- A cleared batch is not lost: git history holds it. Same reasoning as the
inbox sweep — the never-delete rule in `reading-queue.md` protects *reading
history*, and an un-acted suggestion is not history.
- A cleared or swept batch is not lost: git history holds it. Same reasoning as
the inbox sweep — the never-delete rule in `reading-queue.md` protects
*reading history*, and an un-acted suggestion is not history.
- One `last digest: <YYYY-MM-DD>` line, directly below the `---`, records the
last run of the nightly digest — **papers or none** — so an empty list says
*which* kind of empty it is: dated today it is a genuinely quiet day, four
Expand Down
22 changes: 14 additions & 8 deletions index.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,15 @@ the schema in [`wiki/CLAUDE.md`](wiki/CLAUDE.md).
that is **not** strong lensing: black holes, dark matter, galaxy formation,
statistics. One line per paper as `<YYYY-MM-DD> — [<Topic>] <title>[ — <ref>]`,
where the topic names the reading-queue section ➕ files it into. It differs
from the inbox in one way: it is a **day-batched backlog, not a timer**. Each
run appends that day's ten, the dashboard shows the **oldest un-cleared batch
only**, and one 🧹 *clear* button drops that whole day and reveals the next —
so a fortnight away is a fortnight of batches to cycle through, not a
fortnight of lost recommendations. Same five per-paper actions as the inbox.
Format and transitions live in `scripts/interests_actions.py`.
from the inbox in one way: it is **day-batched**. Each run appends that day's
ten, the dashboard shows the **oldest un-cleared batch only**, and one 🧹
*clear* button drops that whole day and reveals the next. The seven-day
window is the inbox's: a batch nobody clears lapses whole (`sweep` in
`scripts/interests_actions.py`, on `INBOX_WINDOW_DAYS`), because at a
measured 1.6 % clearance rate an unlapsing backlog just makes the oldest
batch — the one on the board — the least relevant thing on it. Same five
per-paper actions as the inbox. Format and transitions live in
`scripts/interests_actions.py`.
- [`reading-queue.md`](reading-queue.md) — the paper queue AND the reading
history: `## ` section headers group papers by domain, one title per line,
and a read paper is never deleted — its line gains a `DONE <date> — `
Expand All @@ -68,7 +71,7 @@ reviewed`), and source-page layout are uniform across sub-wikis.
## Citation metadata

The sub-wikis explain which claims papers support. Canonical BibTeX metadata,
key aliases, downstream-project resolution rules, and validation live in
downstream-project resolution rules, and validation live in
[`bibliography/`](bibliography/README.md).

## Provenance
Expand All @@ -77,6 +80,9 @@ The wikis were seeded in 2026-05 from a ~615-paper personal PDF library
(the PDFs themselves were deleted from the repo the same day, backed up
externally). Most legacy per-paper entries remain filename-inferred stubs:
upgrade them to compact, claim-oriented `drafted` entries only after
verifying the paper, and log the change in the relevant sub-wiki. The live
verifying the paper, and log the change in the relevant sub-wiki. The eight
pages on which *every* entry is still such a stub were split out of `sources/`
into `wiki/<domain>/seed/` — same shape, nothing citeable, promoted a paper at
a time as each is verified. The live
counts — pages, maturity, unresolved citation keys, reading queue — are on
the [dashboard](https://pyautolabs.github.io/PyAutoMemory/).
46 changes: 36 additions & 10 deletions scripts/board.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@
the same overnight machinery pointed at everything that is *not* strong
lensing — black holes, dark matter, galaxy formation, statistics — as one
day-batch of ten. It renders the same five actions per paper, plus one 🧹
*clear* button on the batch itself: the list is a backlog rather than a timer,
so the board shows the OLDEST un-cleared day only and clearing it reveals the
next. ``scripts/interests_actions.py`` owns that format and those transitions.
*clear* button on the batch itself: the board shows the OLDEST un-cleared day
only and clearing it reveals the next. That batch carries a days-left count of
its own, because it lapses on the inbox's window if nobody clears it — whole,
a day at a time. ``scripts/interests_actions.py`` owns that format and those
transitions.

Both suggestion tiers also carry a **freshness line**: the date of the last digest run,
so an empty inbox says *which* kind of empty it is — arXiv was quiet, or the
Expand Down Expand Up @@ -217,6 +219,7 @@ def collect(root: Path | None = None) -> dict:
# `interests_backlog` are how much is queued behind it.
"interests": [],
"interests_date": None,
"interests_days_left": None,
"interests_batches": 0,
"interests_backlog": 0,
"interests_last_digest": None,
Expand All @@ -233,7 +236,10 @@ def collect(root: Path | None = None) -> dict:
pages = sorted(wdir.rglob("*.md"))
stems |= {p.stem for p in pages}
statuses: dict[str, int] = {}
kinds = {"concepts": 0, "entities": 0, "sources": 0}
# `seed/` is the 2026-05 import's unverified half, split out of
# `sources/`; counted separately so the sub-wiki row does not
# read as if those pages were verified claim support.
kinds = {"concepts": 0, "entities": 0, "sources": 0, "seed": 0}
sections = 0
todo = 0
resolved: set[str] = set()
Expand Down Expand Up @@ -288,9 +294,10 @@ def _flush():
papers.append(_parse_paper(line.strip()))
_flush()

today = datetime.datetime.now(datetime.timezone.utc).date()

inbox = root / inbox_actions.INBOX_FILE
if inbox.exists():
today = datetime.datetime.now(datetime.timezone.utc).date()
inbox_text = inbox.read_text(errors="replace")
snapshot["inbox_last_digest"] = inbox_actions.last_digest(inbox_text)
for entry in inbox_actions.papers(inbox_text):
Expand All @@ -314,12 +321,19 @@ def _flush():
current = interests_actions.current_batch(text)
if current:
snapshot["interests_date"] = current[0]
# The batch is what lapses, so the days-left is the batch's, not
# each paper's — every line in it carries the same date. Kept on
# the papers too so an interests paper renders like an inbox one.
snapshot["interests_days_left"] = interests_actions.days_left(
current[0], today)
for entry in current[1]:
snapshot["interests"].append({
"title": entry["title"],
"ref": entry["ref"],
"line": entry["line"],
"added": entry["added"],
"days_left": interests_actions.days_left(entry["added"],
today),
"topic": entry["topic"],
"section": interests_actions.section_for(entry),
"done": False,
Expand Down Expand Up @@ -763,8 +777,13 @@ def _render_md(snapshot: dict) -> str:
if left else " · nothing behind it")
clear_url = _interests_clear_url(snapshot, date, len(interests))
clear = f" · [clear this day \U0001f9f9]({clear_url})" if clear_url else ""
# The batch's days-left, beside the inbox's per-line one: a batch
# nobody clears is swept whole on the same window.
dleft = snapshot.get("interests_days_left")
left_bit = f" · {dleft}d left" if dleft is not None else ""
lines += [f"_{date} · {len(interests)} "
f"paper{'s' if len(interests) != 1 else ''}{backlog}{clear}_",
f"paper{'s' if len(interests) != 1 else ''}{left_bit}"
f"{backlog}{clear}_",
""]
for p_ in interests:
label = p_["title"].replace("[", "\\[").replace("]", "\\]")
Expand Down Expand Up @@ -1264,11 +1283,16 @@ def _fresh_span(fresh: dict, now_text: str, stale_text: str,
# backlog depth matters most, and dropping it from the stale variant
# hid it twice over: on a render that is already stale, and on a fresh
# render the moment _EXTRA_JS swaps in `data-stale-text`.
# The batch's days-left rides on both texts too, for the same reason
# `behind` does: a late digest is exactly when it matters that the day
# on screen is about to be swept.
idleft = snapshot.get("interests_days_left")
ileft = f" · {idleft}d left" if idleft is not None else ""
imeta = _fresh_span(
ifresh,
f"{len(interest_items)} paper"
f"{'s' if len(interest_items) != 1 else ''}{behind}",
f"{len(interest_items)} paper(s){behind} · {istale_tmpl}")
f"{'s' if len(interest_items) != 1 else ''}{ileft}{behind}",
f"{len(interest_items)} paper(s){ileft}{behind} · {istale_tmpl}")
# The batch's date IS its name — this is a backlog of days, and which
# day you are looking at is the first thing to know.
interests_block = (
Expand Down Expand Up @@ -1321,7 +1345,8 @@ def _fresh_span(fresh: dict, now_text: str, stale_text: str,
wiki_rows.append(
f"<tr><td class='name'>{link}</td>"
f"<td>{w['pages']} pages · {w.get('concepts', 0)}c/"
f"{w.get('entities', 0)}e/{w.get('sources', 0)}s</td>"
f"{w.get('entities', 0)}e/{w.get('sources', 0)}s"
f"{('/' + str(w['seed']) + ' seed') if w.get('seed') else ''}</td>"
f"<td>{_bar(s)} <span class='meta'>{s.get('stub', 0)} stub · "
f"{s.get('drafted', 0)} drafted</span> "
f"{_copy_btn(_stub_prompt(snapshot, w['name']), 'copy: upgrade a stub')} "
Expand Down Expand Up @@ -1392,7 +1417,8 @@ def _fresh_span(fresh: dict, now_text: str, stale_text: str,
{inbox_block}
<h2>arXiv interests <span class="muted">(everything that is not strong
lensing — one day's ten at a time; \U0001f9f9 clears the day and shows the
next{interests_backlog_note})</span></h2>
next; un-cleared batches lapse after {inbox_actions.INBOX_WINDOW_DAYS} days
{interests_backlog_note})</span></h2>
{interests_block}
<h2>Reading queue <span class="muted">({trend_bits})</span>{spark}</h2>
{filter_box}
Expand Down
Loading
Loading