¶ case file 4 of 7 · applied · filed 2026-02 · last verified 2026-09
status: shipped · live in invite-only beta; a restricted gmail scope caps the app at 100 test users, and the hosted classifier runs layer 1 only
↳ consigned at applied, ¶ 04 · 08:47 · waybill: sorted mail → manifest
Applied
A job tracker that reads the search out of the inbox. Connect Gmail and Applied fetches your mail, names each message, and turns the noise into a pipeline of real applications you can act on. It runs as a hosted web app, live in invite-only beta. The demo and the on-device import need no invite and no account.
[ problem ] · § as found
The status of a job search scatters across Gmail, employer systems, and one-off messages. A spreadsheet can’t keep up: updates get missed, rows get retyped, and the record drifts from the truth. The first answer was a desktop app, which meant the record only existed on one machine.
constraints
- Ask for the least Gmail access that can work: read-only, and keep the message body out of the database.
- Classify noisy inbox messages into useful application states.
- Make row isolation the database’s job, not the query writer’s.
- Fit the whole classifier into a serverless slot, or be honest about which layers didn’t fit.
- Never let a routine sync delete an application the current scan happened to miss.
fig. 1: the sorting line, settled: rules ⟶ e5 similarity ⟶ gated setfit. a held mark keeps its category and is only flagged; a person reviews it at 0.70 and up, and below that the hosted pipeline sets it aside.
a drawn plate, at rest, not a screenshot. the gates are the repository’s real three-layer path; the marks are illustrative and carry no counts. the measured numbers live in the receipts below.
[ architecture ] · § fig. 2, inked
Gmail hands over the message, the classifier names it and the body goes no further, Postgres files the verdict under an identity the database itself checks, and a Next.js dashboard reads it back. One backend package serves the whole path; the two heavier classifier layers are not deployed anywhere a reader can open, because they do not fit the serverless slot. They ran in the int8 browser export until its weights were withdrawn on 2026-08-15, and they run in the evaluation harness today.
Gmail
gmail.readonly, nothing wider
read-only
Message fetch
Subject, From, Date, snippet, and a body it never stores
subject, snippet, body
Classifier
Rules on the hosted path; e5 + SetFit in evaluation only
verdicts, scoped by user
Postgres
RLS on, FORCE’d, per-transaction JWT claims
rows the role may see
FastAPI
One package, deployed as a serverless function
pipeline state, over the caller’s jwt
Next.js
Pipeline board, review queue, stat tiles
no identity bound, no rows: the guc is unset and rls denies
[ decisions ] · § as filed
d1: fetch gmail metadata, never message bodies · superseded
Filed 2026-07-26: the subject line, the sender, and Gmail’s own snippet are enough to name an application email.1
1 tradeoff: A body-blind classifier gives up signal on ambiguous mail, and buys a privacy boundary that holds without being trusted.
d2: run the hosted classifier on the rules layer alone · accepted
torch, sentence-transformers, and SetFit do not fit a serverless function slot: not the size limit, and not the cold start.2
2 tradeoff: The hosted verdict is weaker than the full cascade. The alternative was pretending otherwise, so the limit is written into the code, the tests, and the boundary rows below.
d3: enforce row isolation in postgres, not in the handlers · accepted
Application-level WHERE user_id = … is one forgotten clause away from a leak.3
3 tradeoff: Every transaction pays a set_config round trip, and the app must run as a role that cannot bypass its own policies.
d4: make routine sync additive; keep destruction behind a button · accepted
A bounded scan of a large inbox will miss applications it already found, and re-syncing wiped them.4
4 tradeoff: Stale rows survive until an explicit rebuild, which is the cheaper failure.
d5: read the message body in flight, and store none of it · accepted
Gmail’s own snippet averages 186 characters and an ATS rejection spends them on its preamble, so the sentence carrying the decision falls off the end. Of four real rejections, three could not be decided from the snippet at all, and one of those has no snippet whatsoever. The classifier had never once filed a rejection without a human.5
5 tradeoff: The privacy boundary stops being a property of what is requested and becomes a property of the code: the object every persist path receives has no body field, bodies travel beside it and fall out of scope when the request ends, and a test drives a real scan with a sentinel in every body and asserts it reaches no column of any table, no log record, and no response of any endpoint the scan touches. What is stored is unchanged, which is the claim that had to survive: Gmail’s own snippet, the sender, the subject, the date, and the verdict.
[ validation ] · § the receipts
validation
walks the receipts below, top to bottom, and marks each row this page can verify: a check where a pinned artifact resolves, a ring where the trail ends in an on-page capture, a dash where a claim is described only. walked once, the result settles here and stays.-
01claim: Connecting Gmail asks for gmail.readonly and nothing wider, and the consent step deliberately does not merge previously granted scopes.
-
02claim: The classifier is three layers (rules, e5 embeddings, SetFit), and SetFit stays off until its training gates are met.
[public]
-
03claim: The hosted fetch reads metadata only: Subject, From, Date, and Gmail’s own snippet. Full message bodies are never downloaded on the web path.
[public]
-
04claim: The backend suite runs at 0285675, the head this receipt is pinned to: 3,747 tests passed, 0 skipped, and 13 expected failures, under the test/null-keyring environment. It read 305 passed and 0 skipped at 71b74f8 until 2026-09-23. The Postgres RLS module of 26 tests sits inside that total, and the rls-postgres job runs the same module again on its own, so the two figures are one set of tests counted twice and are never added together.
- artifact: applied @ 0285675 · backend/tests ↗
[public]
-
05claim: The rules v3 gate and the deterministic hybrid v3 gate both pass at the pinned commit on the v3 set of 96 messages: macro-F1 0.9896, 1 message misclassified out of 96. The deterministic profile disables the learned layers, so both files are measuring the rules stage: the stage of 220 rules at this pin (129 strong, 31 weak and 60 negative patterns; the 48 vetoes are not counted). Printed to three decimals the figure is 0.990.
[public]
-
06claim: The macOS Debug target built locally with xcodebuild against the JobTracker scheme, and the desktop app was in the repository at the pinned commit. It was de-scoped on 2026-08-12 and deleted; this pinned tree is the record of it, not a description of the repository today.
- artifact: applied @ 36a2f54 · apps/macos ↗
[local, verified on request]
-
07claim: The dashboard renders real applications: it reads the summary and application endpoints server-side and draws a pipeline board, a stage funnel, and a review queue from what comes back.
-
08claim: Row isolation is enforced by Postgres, not by the handlers: the app runs as a role that cannot bypass RLS, every transaction sets request.jwt.claims locally, and user_credentials is FORCE’d so even the table owner is held to the policies.
-
09claim: The hosted classifier runs the rules layer alone. On the serverless path it returns after layer 1 even when the rules were unsure, and embeddings and SetFit are never imported there.
outcomes
-
10claim: Job updates land in a trackable pipeline instead of a spreadsheet, and now in a browser instead of on one Mac.
- artifact: getapplied.vercel.app ↗
[public]
-
11claim: In-browser inference was built for all three classifier layers: an int8 ONNX export of the same model, 90 to 23 MB, served by transformers.js. That build was separate from the hosted web app, and its verdict was never the one getapplied.vercel.app returns. Its weights were withdrawn on 2026-08-15 because they were fitted partly on a real mailbox, so the size is an attested figure rather than a reproducible one. The export script still ships.
[public]
rows marked [local, verified on request] were run by me on personal or demo data · ci rows link the public run · repo pins are the exact commits verified 2026-09.
what i’m NOT claiming
- I’m not claiming the hosted app runs the full three-layer classifier. On Vercel it runs the rules layer only, deliberately, because the model stack does not fit the function slot. Embeddings and SetFit run in the evaluation harness only. They ran in the int8 browser export until its weights were withdrawn on 2026-08-15, and on the desktop client until that client was de-scoped and deleted on 2026-08-12.
- I’m not claiming Applied is generally available. Connecting your own Gmail is invite-only, and the reason is not positioning: gmail.readonly is a Google restricted scope, so until the app clears Google’s OAuth verification and an independent security assessment it may authorise at most 100 test users, each added by address on the consent screen. What needs no invite and no account is the demo, which is the whole interface over synthetic mail, and the import path, which classifies a Google Takeout export on the reader’s own device.
- This bullet used to say CI could not prove the RLS policies enforce, because the Postgres suite skipped unless a live database URL was supplied and no workflow supplied one. That stopped being true on 2026-07-31 and the disclaimer outlived it. backend-ci.yml now runs an rls-postgres job against a postgres:16 service, sets JOBTRACKER_TEST_PG_ADMIN_URL, and fails if that URL is missing rather than letting the module skip quietly, so all ten tests execute on every push. A stale disclaimer is the same broken receipt as a stale boast, and the harder one to catch, because nobody audits a claim that costs its author something.
- I’m not citing the repository’s README or docs/WEB_ARCHITECTURE.md as evidence for the web app. At the commit this file pins, both described apps/web as an unwired scaffold with a placeholder dashboard. They were behind the code, and this file cites the code. The README has since been rewritten as the product’s own record; it is not back-cited here, because these receipts are pinned and a pin is not re-read to suit a later document.
- No production email-volume or user numbers are claimed. Source, migrations, and test runs are shown publicly; private email and application records are not shown.
[ corrections ] · § the register
erratum · 2026-07-26
Until today this file described a native macOS app whose “web beta is a scaffold”, and pinned every receipt at 3225eb4. That stopped being true: Applied ships as a hosted web app at getapplied.vercel.app. The caveat is retired because it is false now, not because it was wrong then — and the receipts are re-pinned to 36a2f54, the public head of the branch that carries the web app. The scaffold claim’s own successor is receipt 07.
erratum · 2026-07-26
The backend-suite row read 182 tests, audited at the old pin. Re-run at 36a2f54 on 2026-07-26 it is 271 passed and 10 skipped. Nothing was retracted — the tree grew, and the number moved with it. The 10 skips are named in the row rather than folded into the total.
note · 2026-07-26
The repository was renamed yadava5/jobtracker ⟶ yadava5/applied. GitHub still redirects the old links, so nothing this file ever published is broken; the pins name the current repository instead of relying on a redirect. The case-file route stays /projects/jobtracker/ for the same reason.
erratum · 2026-08-02
The backend-suite row read 271 passed and 10 skipped. Re-run on the provenance audit at the current head 0f2b63f: 278 passed, 10 skipped. Same movement as the 182 ⟶ 271 correction above and for the same reason — the tree grew seven tests, nothing was retracted, and the skips did not move. The row is re-pinned to the commit the new count was taken at; every other Applied receipt keeps 36a2f54, which is still where those source audits were done.
note · 2026-08-02
Provenance audit: the classifier claims were re-derived by running the code rather than reading about it. The rules gate passed live at 96 samples, macro-F1 0.9791, 2 misclassified — and the deterministic hybrid gate returned the identical numbers, which is the direct evidence for the attribution correction recorded earlier: the file named “hybrid” measures the regexes alone. The 201-rule figure was recomputed by importing jobtracker.classifier.rules and summing the pattern lists across all seven categories — 106 strong, 26 weak, 69 negative, plus 14 ATS domains. The eval set was counted from its own JSONL: 96 samples, 8 classes, 12 each. Every one of those figures matched what this file already said.
erratum · 2026-08-15
This file described a desktop app that no longer exists. The macOS client was de-scoped on 2026-08-12 and deleted from the repository — apps/macos went, and with it a second, unmounted set of FastAPI routers — so five present-tense claims here were false: the role line, fig. 2’s SwiftUI branch and the edge feeding it, the architecture summary’s “the desktop branch is where the two heavier classifier layers still live”, the rules-layer trade-off, and the boundary row that sent embeddings and SetFit “to the desktop path”. All five are corrected: layers 2 and 3 run in the int8 browser export, and nowhere else. Nothing is retracted and nothing is re-pinned — receipt 06 still cites the macOS build, because it was true at 36a2f54 and that tree still resolves. What changed is the tense: a pinned receipt speaks for its commit, and prose speaks for today.
note · 2026-08-15
Applied is a product now, not a study, and this file leads with it: the run’s rail sends a reader to the live app and the System Card before the case file. Two disclosures come with that. It is licensed proprietary, all rights reserved — the repository stays public so the privacy and isolation claims above can be read against the code, not so the code can be reused. And access is an invite-only beta, for the reason the access boundary row gives rather than a reason anyone chose.
erratum · 2026-08-15
Four sentences here said Applied fetches Gmail metadata and never message bodies: the access constraint, the architecture summary, fig. 2’s fetch node, and decision d1. They stopped being true on 2026-08-15, and so did the edge feeding the classifier, which said the classifier is handed a subject and a snippet. The decision was real and it was reversed on evidence rather than on preference. Gmail’s own snippet averages 186 characters, an ATS rejection spends them on its polite preamble, and of four real rejections three could not be decided from the snippet at all; one has no snippet whatsoever, and the classifier had never once filed a rejection without a human correcting it. Applied now fetches the body, classifies it in flight, and drops it. What is stored did not change: Gmail’s own snippet, the sender, the subject, the date, and the verdict. Bodies stay out by the shape of the code rather than by a promise, and tests/test_body_is_never_persisted.py drives a real scan with a sentinel in every body, then asserts the sentinel reaches no column of any table, no log record, and no response of any endpoint the scan touches; it also asserts the stored snippet equals Gmail’s, because a sentinel search alone passes for a body prefix that stops short of it. On the day of the change the classifier filed two real rejections by itself, reading its own confidence at 0.90 and 0.95, which it had never once managed before. Those two figures are the model’s own outputs on 2026-08-15 and not a count of anybody’s mail: the boundary row that refuses production email volumes and user numbers stands, and neither message is shown. d1 keeps its argument and is marked superseded, because that section is filed as filed; the date it was taken is added at the head of it, so the row cannot be read as a claim about today, and d5 states what runs instead. Nothing is retracted and nothing is re-pinned: receipt 03 still says the hosted fetch reads metadata only, because that was true at 36a2f54 and that tree still resolves. What changed is the tense, and a pinned receipt speaks for its commit while prose speaks for today.
note · 2026-08-15
The Hugging Face Space that hosted the full three-layer classifier went private today, so every link to it returns 401. The references are gone from this file rather than left to fail in a reader’s browser, and the prose no longer offers a hosted demo it cannot deliver. What stands in its place is the receipt the claim always rested on: the browser build itself, ml/browser/site at the pinned commit, where index.html, app.js and the 22.8 MB int8 weights sit together and can be read. Nothing about the export is retracted; the place it was hosted stopped being public, and that is a smaller fact than the one the row asserts.
erratum · 2026-09-23
The rule count read 201. Recounted at Applied’s public main head 0285675 by walking the AST of PATTERNS in backend/jobtracker/classifier/rules.py, the same way Applied’s own scripts/readme_facts.py counts it: 129 strong, 31 weak, 60 negative, 220 scored patterns across seven categories. The 48 veto patterns are excluded, because a veto scores nothing and instead caps a category at zero, and folding them in would stop the breakdown summing to the total. 201 was true at 36a2f54, where the same walk gives 106 strong, 26 weak and 69 negative; the 2026-08-02 note above recorded it correctly and stays exactly as written. Rules were added through #523 on 2026-09-09. Nothing is retracted: the tree grew, and the number moves with the commit it was counted at.
erratum · 2026-09-23
Receipt 05 read “Rules and deterministic hybrid v3 gates both passed on 96 samples with macro-F1 0.9791”, with 2 of 96 misclassified, pinned at 36a2f54. It was true there. The baseline was re-recorded on 2026-09-07 and the gate now reads macro-F1 0.9896 with 1 misclassified out of 96, which prints as 0.990 at three decimals. Re-run at 0285675 on 2026-09-22 it exits 0 against its own 0.001 tolerance, and public backend-ci run 34429395505 prints the same line twice, once for the rules gate and once for the deterministic hybrid gate. The receipt is re-pinned IN PLACE rather than appended, so the anchor #v-jobtracker-5 that the home page and the evidence manifest point at does not move; the old value is quoted here instead of being deleted there. The receipt takes its own pin, 0285675, while receipts 01 to 03 and 06 to 09 keep 36a2f54: those are source audits nobody re-read, and moving one pin would restate them as if they had been. The artifact also changes from baseline_hybrid_v3.json to baseline_rules_v3.json, which is the file that decides the figure; the deterministic hybrid file repeats it, because that profile disables the learned layers.
erratum · 2026-09-23
Receipt 04 read 305 passed with 0 skipped, measured at 71b74f8 in backend-ci run 31152038153. True there. Re-read at 0285675 off backend-ci run 34429395505 of 2026-09-10: 3,747 passed, 0 skipped, 13 expected failures. The zero holds, which is still the part worth reading, and the 13 xfails are named beside the total rather than folded into it, because an expected failure is not a pass. The Postgres row-level-security module is 26 tests now and is inside that total; the separate rls-postgres job runs the same module again, so the two counts are one set of tests seen twice and are not summed. The receipt is re-pinned IN PLACE, keeping #v-jobtracker-4, and its pin moves to 0285675 with the number, as it has at every earlier step: 182 ⟶ 271 ⟶ 278 ⟶ 305 ⟶ 3,747, each true at the commit beside it. The row also drops its breakdown of the old +27, which described a move between two commits neither of which is the pin any more.
erratum · 2026-09-23
The evidence page put Applied’s line coverage at 53 percent, from 8,210 statements with 3,865 missed at 71b74f8. At 0285675, in backend-ci run 34429395505, the same
pytest tests -q --cov=jobtrackerreports 9,786 statements, 3,054 missed, 69 percent. Every per-package sub-figure moved with it and every one of them is restated rather than left standing beside the new total: cloud 93.0, auth 89.6, database 80.8, classifier 78.0, credentials 67.4, email_clients 42.7, scripts 40.0 over 2,434 statements, tracking 28.9, and the package root 92.2. The old row named cloud at 82.2, auth at 80.5, database at 76.6 and jobtracker/scripts at 2,240 statements and 33.7 percent; those were true of the 53 percent run and are kept here as its record. The under-claim is the interesting part: this file had been publishing a figure 16 points BELOW the work, which no gate could catch, because nothing goes red when a number is too modest.erratum · 2026-09-23
The evidence manifest’s 3-layer classifier row said all three layers “run on the desktop app and in the browser Space”. Both halves were false when read today. The desktop client was de-scoped on 2026-08-12 and deleted, which this register recorded at the time without the evidence row moving with it, and the Hugging Face Space went private on 2026-08-15, which the note above records. The row now says what is true: the hosted web app runs the rules layer alone, and the learned layers are not deployed on any surface a reader can open. This is the second time a desktop claim outlived the desktop; the first was caught on this file, and the copy of it on /evidence/ went unfixed until today because no gate reads across the two.
erratum · 2026-09-23
Four places described the int8 browser export in the present tense: the architecture summary, fig. 2’s classifier node, the boundary row about embeddings and SetFit, and outcome 02. Nothing public has run that export since its weights were withdrawn on 2026-08-15, so the present tense was claiming a live surface. All four move to the past tense the résumé uses: in-browser inference was built, an int8 ONNX export of 90 to 23 MB served by transformers.js, measured on 2026-08-03. The sizes are unchanged and not retracted; Applied pins them as literals in scripts/readme_facts.py at 90,362,391 bytes fp32 and 22,843,695 bytes int8, and its own README says the figure is attested rather than reproducible and should be described that way wherever it is cited. That is what this file now does. Today the two heavier layers run in the evaluation harness and nowhere else.
erratum · 2026-09-23
The home page’s Applied lede said the app reads your mail “on a device you control”. That is false for the product this file documents. The Gmail connected app classifies on Vercel, by design: classifier/hybrid.py short-circuits to the rules layer whenever deployment is cloud, which receipt 09 argues. In your own browser is the /import and /demo path, which runs the layer 1 rules port and nothing else, and the desktop client that made the phrase true was deleted on 2026-08-12. The same sentence said Applied “defers to you when it isn’t sure”, which is only part of the behaviour: the hosted pipeline queues a verdict between 0.70 and 0.85 for a person and sets aside anything below 0.70, except an ATS-relayed verdict and a message retracting an offer already on the board. Both clauses are wrong as written, and the replacement has to name the 0.70 floor rather than imply a person sees everything.
erratum · 2026-09-23
Outcome 02 linked ml/browser/site at 36a2f54, and the 22.8 MB model.onnx in that tree still downloads today. Applied withdrew those weights on 2026-08-15 because they were fitted partly on a real mailbox, and removing them at its own head stops redistribution going forward. This file was undoing that: a pinned link is a permalink, so the row was re-publishing the exact artifact the upstream project had pulled, and the 2026-08-15 note above sends a reader to it in as many words. The row’s artifact is now ml/browser/export_onnx.py at 0285675, a commit after the deletion, where
git cat-file -e 0285675c:ml/browser/site/model/onnx/model.onnxfails and the export script resolves. The note above is NOT edited, because this register does not rewrite its own history; what it describes was true, and this entry is where the consequence is recorded. Scrubbing the weights from Applied’s git history is the upstream project’s decision and is not claimed here.
[ appendix ] · § artifacts, indexed
artifact index
- README, the desktop-era record ↗[repo] · yadava5/applied @ 36a2f54
- Architecture docs ↗[repo] · yadava5/applied @ 36a2f54
- ML strategy and evaluation gates ↗[benchmark] · yadava5/applied @ 36a2f54
- Backend test suite ↗[repo] · yadava5/applied @ 36a2f54
- Web app source ↗[repo] · yadava5/applied @ 36a2f54