v1.6.0

Friedrich-M/UniMatev1.6.0Aug 19, 2026by MadsLorentzen

Upgrading?

Full Release Notes

Two themes this release: **things that used to fail silently now fail loudly**, and **the text you wrote actually survives into the documents you send**. Most of it comes from a deep empirical review of the whole template ([#347](https://github.com/MadsLorentzen/ai-job-search/pull/347) - 25 fixes, each carrying the test that would have caught it), alongside a portal-contract series and an onboarding privacy fix from the community.

## Highlights

### All six portal CLIs reject unknown flags (**breaking**, #347)
A silently discarded flag is worse than an error: on one portal, a plausible-but-wrong flag name returned the **entire database** - 13,862 postings, none matching - with exit 0 and empty stderr, indistinguishable from a real result set. Every CLI now exits 1 with `{"error", "code": "UNKNOWN_FLAG"}` on a flag it doesn't know, which is the bar `/add-portal` always set for contributed portals. If a script of yours passes a stray flag, it will now hear about it - that's the point.

### Your CV's text now survives into the PDF (#347)
Three silent text-loss modes, all with clean compiles and green CI: bullets written as `\item [text]` were parsed as LaTeX optional labels and rendered clipped off the page edge (the shipped example CV lost every experience bullet this way); an unescaped `%` in a quantified achievement ("cut latency by 40% and saved DKK 2M") silently deleted the rest of the line; and the documented ATS extraction command mis-decoded every non-ASCII character on Xpdf-based `pdftotext` builds, failing correct CVs. All fixed, a "LaTeX Special Characters" guide added, and CI now asserts body text survives into the PDF text layer.

### Every portal speaks the same contract now (#339, #340, #342, #344, #347)
The `/scrape` search contract (`title`, `company`, `location`, `date`, `url`) is now emitted by all six portals (thanks @oscarbol09 for the jobnet/jobdanmark/jobbank series), and deadlines are `YYYY-MM-DD` or `null` everywhere: jobbank's `DD.MM.YYYY` converts (**breaking** if you parsed the old form), jobindex's `"ASAP"` maps to `null` (it means "no stated deadline", which is what `null` is for), jobnet's `1900-01-01` sentinel no longer leaks from `detail`, and jobdanmark's HTML fallback stops emitting raw page text and the literal `"Løbende"` as dates. A repo-level contract pin (@oscarbol09, #344) derives the field list from the spec sentence itself and fails CI if any portal drifts - and `/rank` now treats any non-ISO deadline already stored in your data as absent rather than guessing.

### The jobindex `detail` command works again (#347)
Jobindex redesigned its pages; every selector the parser used was gone, so `detail` returned CSS-comment text as the deadline, an external ATS URL as its own id, and null everything - exit 0, on 4 of 5 live postings. Rewritten against both current page shapes; 5/5 live postings now return full descriptions and correct fields.

### Onboarding warns about public forks before it's too late (#345, #348)
The quick start walked a new user into `gh repo fork` - forks of public repos are **always public** - and two steps later `/setup` wrote their name, contact details, and salary expectations into tracked files, with the only complete warning buried in the updates section. Reported by @basilevs with a full reproduction after hitting it himself. The warning now sits next to both fork commands, and `/setup` checks your origin's visibility **before** writing anything, not after.

## Also in this release

- **`seen_jobs.json` records whether each job came from a CLI or the WebSearch fallback** (#338) - ghost-job reports are now diagnosable from stored state. Thanks @yshraj, with an empirical community review by @oscarbol09.
- **Job matching reframed around function, not title** - `/setup` and the evaluation framework now build priority categories around the kind of problem a role solves, with title variants as query inputs rather than the bet itself.
- **A `/` in a company or role name no longer nests the application archive one level too deep** (#329) - the canonical single-path-component rule now reaches all six derivation sites. Thanks @ayobamiseun, building on @jakob1379's independent spec.
- **`convert_salary_excel.py` no longer corrupts US-formatted numbers 1000×** - locale is decided by which separator appears last, and Danish compound headers pair correctly.
- **`/html-report`'s funnel counts stages from history** - an application that interviewed and was later rejected now counts as having interviewed, and declined offers stop inflating the rejection rate.
- **`/gmail-sync` searches beyond the Inbox** - `-in:sent -in:drafts` replaces `in:inbox`, so archived and label-filtered mail stays in scope.
- **`/rank` persists its location verdict as `location_verdict`** - the bare `location` key meant both a place and a PASS/FAIL/FLAG verdict in the same store; legacy entries are read compatibly.
- **`/reset documents` now clears `documents/postings/`**, `/upskill` handles rows with no fit rating, `/scrape` gets a client-side recency fallback for portals without a date filter, and `freehire-search` gains `--no-description` for cheap discovery passes (~83% smaller payloads).
- **The guards are finally guarded** - `check_framework_version.py` (the fork-safety CI gate) had zero tests and could be disabled by one line; it, `lint_skills.py`'s real checks, `robots_check.py`'s tie-break and browser-UA fallback, and CI's placeholder sentinels (which watched a comment, not the data) are all now covered by discriminating, mutation-verified tests.
- **CONTRIBUTING: invited PRs are reserved for the invitee.**

## Updating a personalized fork

Versioned files touched since v1.5.0:

| File | v1.5.0 | v1.6.0 |
|---|---|---|
| `job-application-assistant/SKILL.md` | 1.3.2 | 1.3.4 |
| `04-job-evaluation.md` | 1.2.2 | 1.2.4 |
| `05-cv-templates.md` | 1.4.0 | 1.4.2 |
| `06-cover-letter-templates.md` | 1.0.1 | 1.0.2 |

Run `python3 tools/check_upstream_updates.py` for per-file diffs, or `python3 tools/upstream_triage.py` for the commit-level view.

Five things to check after pulling:

1. **Scripts passing stray flags to portal CLIs now get exit 1** (`UNKNOWN_FLAG`) instead of silently unfiltered results. Fix the flag name; the CLI's `--help` and SKILL.md list the real ones.
2. **jobbank deadlines are now `YYYY-MM-DD`.** Anything parsing the old `DD.MM.YYYY` search output must update.
3. **Expect a small merge conflict in `cv/main_example.tex`** if you personalized it: upstream braced the placeholder bullet lines you replaced. Resolution: keep your own bullets - real prose was never affected by the bug.
4. **Stored `seen_jobs.json` data needs no migration.** Old `"ASAP"`/`DD.MM.YYYY` deadlines are treated as absent by `/rank`'s sweep rather than compared; a legacy PASS/FAIL/FLAG value in `location` is read as the verdict until re-ranking moves it to `location_verdict`.
5. **If your fork is public and personalized, read #345.** The new onboarding warnings are for the next user; if your own profile data already reached a public fork, GitHub's sensitive-data-removal process is the reliable path - force-pushing doesn't purge fork networks.

Related Repositories