Case study
Vedetta
A self-hosted monitor for employers' own job boards: once a day it reads the careers listings you choose, works out what is genuinely new, labels each posting with the facts that decide whether it is worth your time, and sends one email. The engineering is not the reading. It is that everything in it follows from one asymmetry.
Premise
A missed posting is silent; an irrelevant one costs two seconds
Asymmetry
The project exists because of one specific failure: a filtered search of a single careers portal passed over an eligible vacancy, which a second, unfiltered pass found an hour later. The filter was not reported as having decided anything. It simply returned a shorter list.
So nothing is ever hidden. The labels decide the order of the digest, never what appears in it. A posting the ranking dislikes is still there, further down, with the reasons shown so the reader can disagree with them in a second.
That is the whole design, not a preference. A filter that wrongly drops a posting fails silently and nothing ever tells you it happened. An irrelevant posting costs two seconds of reading. When one error is invisible and unbounded and the other is visible and trivial, the system should be built to make only the second kind.
Python, Flask, SQLite, Docker, schema.org, self-hosted
Consequence
Silence is never allowed to look like "nothing new"
The same reasoning decides what happens when a part of the system breaks. An absent result and an empty result are different facts, and a monitor that confuses them is worse than no monitor, because it is quietly trusted.
- Every source is polled and every poll is recorded, failures and empty results included. A source that breaks appears in the digest rather than disappearing from it.
- An employer with no working route is listed as unwatched, instead of simply not being there.
- Several routes to the same employer are polled where they exist. If one goes quiet while another still answers, that is a detected fault, not a closed job.
- A posting that might be a duplicate is shown twice and flagged, never merged away. Merging wrongly hides something; a duplicate costs a glance.
- The email arrives even on a day when nothing is new, so a missing email always means a fault rather than a quiet day.
Verification
Three automated methods each confidently bound the wrong company
A source is only polled once a person has looked at real postings from it and confirmed they belong to the employer that was asked for. That requirement is recorded as a verification date, and it is not ceremony.
- While the project was being built, three completely independent automated discovery methods each returned a different company than the one requested: a road-paving contractor in Florida offered as an asset manager, a Brazilian business school as a technology firm, and a vendor's demo board whose single posting was titled "Test Position".
- All three looked like success. HTTP 200, valid JSON, plausible row counts. There was no error to catch and no exception to log.
- An employer silently bound to the wrong board produces a permanently empty feed that is indistinguishable from a quiet employer - the exact failure the whole project was built to prevent, reintroduced through the back door by the convenience of automatic setup.
So automatic discovery may propose; a human enrols. It is the one place where the system deliberately refuses to be fully automatic, because it is the one place where being wrong is undetectable afterwards.
Rules
Three corrections the first real run forced
Data
A rule is a few lines of configuration carrying a human-readable explanation, and that explanation is what appears beside the posting in the digest. If a one-line reason a person would accept cannot be written, the rule is not ready. Running it against real postings changed the shape of the rule language three times.
A rule needs an escape clause. A language listed as
required should disqualify a posting; the same language
listed as a strong asset should not. Without that
distinction a language rule is wrong about half the time, so every
rule carries an unless condition alongside its match.
A rule needs to say what it reads. The first real run labelled a "Senior Backend Engineer" as early-career, because the word "graduate" sits in the boilerplate of almost every long job description. Seniority and role rules now read the title; disqualifying rules read everything.
A rule needs a weight. Without one, being in a target role outweighed nothing in particular, and an "Associate Renewals Manager" ranked alongside a "Cloud Engineer" - which the first run did in fact do.
Ranking is a plain sum over the labels: deliberately arithmetic over visible facts rather than a learned similarity, so the order can be explained in one line and argued with.
Coverage
One adapter per platform, and one path that ignores the platform
Employers do not run their own job boards; they buy them. So an adapter is written once per hiring platform and serves every employer on it, which makes coverage grow per platform rather than per employer. Four implemented paths currently reach 36 confirmed sources across 40 employers on the author's own watchlist.
- The platform adapters answer unauthenticated requests with clean JSON carrying stable posting identifiers and publication dates, so no headless browser is needed anywhere in the system.
- The path that needs no platform at all: many careers sites publish a sitemap listing every job URL, and their detail pages carry schema.org job markup because search engines reward it. That combination needs no API, no tenant and no identifier - one request gives the whole set, and detail pages are fetched only for URLs that are new. It is the only route that does not care what software the employer bought.
- Its catch, found within an hour of testing: the pattern separating job pages from content pages has to be configured per source. A single global pattern was wrong in both directions - loose enough to match a careers blog, strict enough to lose an employer whose postings live under opaque identifiers.
Crawling is deliberately quiet: the robots file is read before a host is touched, requests identify themselves, each source is polled once a day and conditionally where the platform supports it. On a day with no new postings an employer costs a single small request.
Operation
The watch is kept apart from the thing that shows it
Runtime
One image, two roles. The daily run is a throwaway container started
by a cron schedule, with a read-only root filesystem, every Linux
capability dropped, no new privileges, memory, CPU and process
limits, a noexec temporary filesystem and its
configuration mounted read-only. It runs as a non-root user and
touches nothing it does not need: it reads job boards and writes one
database and one outbox.
The interface is a separate service, bound to loopback and published only over a private network, never to the LAN or the internet. That separation is the point: if the run lived inside the interface, a stopped interface would mean no digest - and no digest is exactly what this project treats as an alarm rather than as silence.
Delivery assumes it will fail. The application writes digests to an outbox and a delivery step sends them through the server's own mail configuration, so no third-party mail credential exists to leak. An undelivered digest stays queued and is retried; a failed run sends an alert with the last lines of its log. Its data lives on a dataset with a daily snapshot task, off the server's single-disk boot pool.
Changes are predicted before they are saved. The test suite runs with no network access, in CI against the oldest supported Python and the version the container runs. On top of that, editing the rules judges every open posting under both the old and the new configuration first and lists what would stop being workable - which, run against the live database, found five defects the tests had passed, one of them wide enough to let a whole continent through.
Boundary
It signals; it does not decide
Scope
The name is the Italian word for a lookout: the one who keeps watch and signals, and does not decide what the signal means. It does not apply to anything, does not write or submit applications, does not scrape aggregators or social networks, and exposes nothing to the internet. It is a scheduled job that sends mail.
It does not judge fit. It extracts and labels facts and leaves the verdict to the reader, because a label can be argued with and a verdict can only be trusted. A model may optionally be pointed at pages that resist deterministic parsing, for extraction and labelling only; it is off by default, the program is fully functional without it, and it is never allowed to produce a verdict.
Mechanism in the code, the case in the configuration. There is nothing about any particular person or job market in the source, which is why it could be published without being taken apart first: where you may work, which employers to watch and what counts as a blocker all live in configuration files. The design record - seventeen decisions, each with the reasoning that produced it - is published with it.