You have a folder. Screenshots, half-written notes, saved links, a photo of a whiteboard. Somewhere in there are the things that belong to projects you are working on, and the things somebody is waiting on you for.
shelf reads the folder, pulls text out of whatever it can, scores that text against a project list you wrote, and writes a board. It runs on your machine, needs no key, and installs nothing.
- 📦 Repo (MIT): https://github.com/dev48v/shelf
- 👉 Walkthrough, runs in your browser: https://dev48.in/agentlab/vol5-01-shelf.html
$ python shelf.py build --pile demo/pile --projects demo/projects.md
6 items -> board.md
note: tesseract not found; images used sidecars or filenames
## Replies you owe (1)
| item | read via | why |
|---|---|---|
| `email-accountant.txt` | file contents | the text reads as someone waiting on a reply |
## Kitchen rebuild (2)
| `note-quote.txt` | file contents | matched 83% of the terms for Kitchen rebuild |
| `whiteboard.png` | sidecar .txt | matched 50% of the terms for Kitchen rebuild |
## The partition
1 Replies you owe + 2 Kitchen rebuild + 1 Cycling trip + 2 Unfiled = 6 of 6
The one rule
Every item lands on exactly one shelf, and the board says why.
Not mostly one. A triage tool that can silently drop an item, or quietly file it twice, is worse than no triage tool — because you stop checking it, and then it is just another pile.
def check_partition(items, projects):
valid = {p.name for p in projects} | {UNFILED, OWED}
for item in items:
if item.shelf not in valid:
raise AssertionError(f"{item.name} landed on unknown shelf {item.shelf!r}")
if not item.reason:
raise AssertionError(f"{item.name} was filed with no reason given")
shelved = {}
for item in items:
shelved.setdefault(item.shelf, []).append(item.name)
if sum(len(v) for v in shelved.values()) != len(items):
raise AssertionError("the partition does not account for every item")
It runs on every build and the run fails if it does not hold. The board prints the sum at the bottom so you can see that it did.
🔴 The bug that invariant exists to catch
A .txt sitting next to an image of the same stem is that image's caption — evidence about the image, not an item of its own. Count it as an item and the board claims 7 where the folder holds 6.
It is a one-line mistake, it produces a board that looks entirely reasonable, and the only thing standing between you and it is a count that has to add up.
Term overlap, not similarity
OCR output is garbled. Half a word is missing, 0 is O, a line is nonsense. Counting how many of your terms survive is robust to exactly that damage. An edit distance or an embedding needs clean text to behave, and clean text is the one thing a pile of screenshots does not have.
w1dget !!! spr0cket ... widget ~~~ sprocket → still scores 100%
Inject noise every eleventh character of a real note and it still files correctly.
The categories are yours
# Kitchen rebuild
quote, builder, worktop, plumber, tiles, splashback
Headings are shelves, the lines under them are terms. Nothing invents a category or infers one from the pile. A classifier that writes its own labels is answering a question you did not ask. Multi-word terms must appear in order — self assessment matches "my self assessment form" and not "assessment of self".
Provenance, on every row
A score built on a filename is worth far less than one built on contents, and the board says which: file contents, sidecar .txt, OCR, or filename only.
🔴 And a filename alone never counts as somebody waiting on you. can-you-reply-to-this.png is a note to self, not a person.
🔴 How the walkthrough page is checked
The page ports the rules to JavaScript so you can run them live. A port that drifts from its original is the specific failure no JavaScript-only test can see — so the verifier runs the Python and demands the two agree item for item, shelf for shelf, reason for reason and provenance for provenance.
That check found its own bug: execFileSync returns stdout only, unittest writes its summary to stderr, and "the suite ran" was passing on the CLI's print output. spawnSync reading both streams fixed it.
Python 3.10+, standard library only. 51 tests, 124 cross-language verifier asserts, 29 in-page checks, 0 failures.
Top comments (0)