← back to ShortList

Stack

What's actually running.

The substrate behind ShortList — deliberately small. One Python process, one SQLite file, a model reached through the CLI, and a set of rules kept boring on purpose so the judgement could be spent where it matters.

The stack

Python 3.12 · standard library only · no framework
SQLite — one file: postings · verdicts · actions · documents · messages
http.server — a single-file page, no build step
Claude (Sonnet) via the CLI — score · confirm · summarise · tailor · chat
Seven boards: Greenhouse · Ashby · Remotive · RemoteOK · Himalayas · Jobicy · Arbeitnow
Board discovery — probes company slugs, keeps what answers
Deterministic filter layer — every hidden posting names its rule
Paced scorer — chunk · gap · stop on a failing chunk · backoff to four hours
Mutation-verified tests · AST scan for the cover-letter rule
File-size ratchet as a pre-commit hook
A negative result kept in the repo, reproducible with one flag

Rules that show their evidence

The filter's job is not to decide what is good — it removes what is impossible, so the expensive judgement is spent on postings that could actually be taken. Every rule returns the match it saw, not a boolean, so a hidden posting can say "hidden by salary_floor: $95,000" rather than just "hidden". A rule that cannot show its evidence cannot be argued with; these can. The page re-runs any subset live, and the count beside each rule is what it is currently costing.

Two axes, one bar, no percentile

Fit is whether the skills match what was asked for. Thrive is whether the work would actually succeed. The list is sorted by their mean, because sorting by fit alone buried the jobs the candidate would be best at — and a percentile cut for the shortlist once produced 1,998 "strong" matches, so the bar is absolute: combined ≥ 0.70. The fit rubric originally had only subtractive clauses and could not score above 0.75; adding a top-of-scale anchor was checked on a sample that could disagree — 19 of 60 scores went down — before it shipped.

Confirm: median of three

The top slice of the list — the top 8% by fit, which in practice is 11% because ties are included rather than broken arbitrarily — is re-read twice more, and the median of the three readings is kept. Median, not mean, on purpose: one wild reading should not be able to drag a good posting down, and averaging lets it.

A scorer that notices its own failures

One unbounded scoring run once returned 6,044 unparsed responses in a row — the calls were hitting a plan limit and coming back as error strings, and the loop could not tell "the model disagreed" from "every call is failing". So the scorer now runs in paced chunks with a gap between them, treats a chunk that mostly fails as a signal to stop, backs off on a ladder from five minutes to four hours, and probes the CLI to tell a usage limit from a credential problem before deciding which. Progress is durable; it can be killed and restarted at any point.

A negative result, kept

A cheap Python pre-ranker was written to save model calls. Measured against 8,353 postings the model had already scored, taking the top 800 by that ranking would have found 39% of the genuinely strong matches, and the top 2,000 — a quarter of the corpus — only 58%. It buried an "Internal Applications Engineer" posting the model had read as a 0.88, because no phrase list knows that internal tooling is where an analyst who builds would thrive. Refitting the weights from the measured lift made it worse. The file stays in the repo with a --measure flag, because a negative result that is deleted gets rediscovered.

Tests that bite, and one rule enforced by reading the source

Every test was mutation-verified: revert the fix, the test must go red, put the fix back. The rule that there are no cover letters is enforced three ways — the endpoint refuses the kind with a 400, omitting the kind means résumé rather than the old default, and an AST scan of every module fails if any kind parameter or lookup defaults to "cover". The live-server fixture refuses to run if something else is already answering on its port, because a stray process once passed the readiness check and the suite tested the wrong server.

Résumé: reordered, never added to

One profile file is the single source of truth. A generated résumé may reorder, re-emphasise and re-word what is there, and may never add a fact that is not — if a posting asks for something absent, the honest move is to leave it out rather than write a plausible sentence the candidate then has to defend in a room. Documents are stored per posting, append-only: before an interview you need the exact text that company received, not a fresh generation that says something slightly different.

A ratchet instead of a reviewer

When a model writes most of the implementation, the human pain signal that normally forces a refactor never fires — nobody is scrolling a 4,500-line module at 2am, so nothing ever feels bad enough to split. A pre-commit hook restores that signal as a number: a new file may not exceed 600 lines, and an existing oversized file may never grow. Nothing has to be fixed today; nothing gets worse.