diff --git a/.agents/skills/jobindex-search/cli/README.md b/.agents/skills/jobindex-search/cli/README.md index 4316dd8..a13b69a 100644 --- a/.agents/skills/jobindex-search/cli/README.md +++ b/.agents/skills/jobindex-search/cli/README.md @@ -169,7 +169,7 @@ bun run src/cli.ts detail h1647303 --format plain ``` **Field notes:** -- `deadline` — application deadline date string; `null` if not listed. +- `deadline` — application deadline date string (`YYYY-MM-DD`); `null` if not listed. Postings flagged "ASAP" by the portal carry no fixed deadline and also map to `null`. - `employmentType` — e.g. `"Fastansættelse"`, `"Midlertidig ansættelse"`; `null` if not listed. - `hours` — e.g. `"Fuldtid"`, `"Deltid"`; `null` if not listed. - `applyUrl` — the external application URL (resolved from the Jobindex redirect link `/c?t=...`); `null` if not available. diff --git a/.agents/skills/jobindex-search/cli/src/helpers.ts b/.agents/skills/jobindex-search/cli/src/helpers.ts index 3852921..e6db4aa 100644 --- a/.agents/skills/jobindex-search/cli/src/helpers.ts +++ b/.agents/skills/jobindex-search/cli/src/helpers.ts @@ -160,7 +160,11 @@ export function parseSearchPage(html: string): SearchPageResult { } } let deadline: string | null = null - if (r.apply_deadline_asap) deadline = "ASAP" + // apply_deadline_asap means the posting states no fixed deadline ("apply + // now"). The /scrape contract represents that as null, and consumers do + // date arithmetic on this field - so the flag maps to null, and wins over + // any date field that happens to be present. + if (r.apply_deadline_asap) deadline = null else if (typeof r.apply_deadline === "string") deadline = r.apply_deadline.slice(0, 10) else if (typeof r.lastdate === "string") deadline = r.lastdate diff --git a/.agents/skills/jobindex-search/cli/tests/search-page.test.ts b/.agents/skills/jobindex-search/cli/tests/search-page.test.ts index b9a2414..6891db3 100644 --- a/.agents/skills/jobindex-search/cli/tests/search-page.test.ts +++ b/.agents/skills/jobindex-search/cli/tests/search-page.test.ts @@ -45,6 +45,21 @@ describe("parseSearchPage", () => { }); }); + test("maps an ASAP posting's deadline to null (no stated deadline)", () => { + // apply_deadline_asap means "no fixed deadline, apply now". The /scrape + // schema defines null as exactly that, and every consumer (rank's expiry + // sweep, notion-sync's typed date column) does date arithmetic on this + // field - a bare "ASAP" string broke all of them on half of live results + // (review finding F12, 2026-08-19). lastdate present too: the flag wins. + const [job] = parseSearchPage( + stashPage({ + hitcount: 1, + results: [{ ...RESULT, apply_deadline: undefined, apply_deadline_asap: true, lastdate: "2026-09-30" }], + }), + ).results; + expect(job.deadline).toBeNull(); + }); + test("falls back to lastdate when apply_deadline is absent", () => { const [job] = parseSearchPage( stashPage({ hitcount: 1, results: [{ ...RESULT, apply_deadline: undefined, lastdate: "2026-09-30" }] }), diff --git a/CHANGELOG.md b/CHANGELOG.md index daae35f..ffd16c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,13 @@ per-file diff commands. ### Fixed +- **`jobindex-search` maps ASAP postings' deadline to `null`** - the portal's + `apply_deadline_asap` flag was emitted as the literal string `"ASAP"` on roughly half + of live results, contradicting the CLI's own README ("date string; null if not + listed") and the `/scrape` schema, and breaking every consumer that does date + arithmetic (`/rank`'s urgency and expiry sweep, `/outcome`'s deadline check, + `/notion-sync`'s typed date column). ASAP means "no stated deadline", which the + contract already represents as `null`. Pinned in `tests/search-page.test.ts`. - **`/gmail-sync` no longer restricts its search to the Inbox** - the query used `in:inbox` to "skip sent/drafts", but that operator also excludes every archived message, and self-defeatingly the mail matched by the very job-search label Step 3.1