From 04186b9a37505654de97e29bd6e14dbba2511a75 Mon Sep 17 00:00:00 2001 From: Oscar Madera <80536682+oscarbol09@users.noreply.github.com> Date: Wed, 19 Aug 2026 01:20:23 -0500 Subject: [PATCH] fix(jobnet-search): emit the /scrape contract fields in search output (#339) Adds additive normalization so jobnet search output carries the cross-portal contract fields (company, location, date, deadline with the 1900-01-01 NotDisclosed sentinel mapped to null, and url). Emits the public /find-job/{jobAdId} route and corrects the skill's own stale /job/ documentation, which redirects anonymous visitors into the MitID login flow. Co-authored-by: oscarbol09 <80536682+oscarbol09@users.noreply.github.com> --- .agents/skills/jobnet-search/SKILL.md | 2 +- .agents/skills/jobnet-search/cli/README.md | 16 +++++++-- .../jobnet-search/cli/src/commands/search.ts | 7 ++++ .../cli/tests/search-normalization.test.ts | 33 +++++++++++++++++++ CHANGELOG.md | 8 +++++ 5 files changed, 63 insertions(+), 3 deletions(-) diff --git a/.agents/skills/jobnet-search/SKILL.md b/.agents/skills/jobnet-search/SKILL.md index 5270ec5..473d150 100644 --- a/.agents/skills/jobnet-search/SKILL.md +++ b/.agents/skills/jobnet-search/SKILL.md @@ -202,5 +202,5 @@ All errors are written to **stderr** as `{ "error": "...", "code": "..." }` and - Pagination is 1-indexed (`--page 1` is the first page). - `search` results omit the HTML job description — use `detail` to get it. - `detail --format plain` strips HTML tags for readable text output. -- Job ad detail pages on jobnet.dk: `https://jobnet.dk/job/{jobAdId}` +- Job ad detail pages on jobnet.dk: `https://jobnet.dk/find-job/{jobAdId}` - `suggestions` is tuned for Danish job titles — English terms may return empty results. diff --git a/.agents/skills/jobnet-search/cli/README.md b/.agents/skills/jobnet-search/cli/README.md index 3578c27..634b913 100644 --- a/.agents/skills/jobnet-search/cli/README.md +++ b/.agents/skills/jobnet-search/cli/README.md @@ -147,7 +147,12 @@ bun run src/cli.ts search \ "workPlaceAddress": "", "conceptUriDa": "http://data.star.dk/esco/occupation/426e017f-ebe5-4bea-b1eb-7d2d5ab3c6db", "isSeen": false, - "isFavorite": false + "isFavorite": false, + "company": "Region Midtjylland", + "location": "Viborg", + "date": "2026-03-13", + "deadline": "2026-04-05", + "url": "https://jobnet.dk/find-job/9ef43bce-d82b-4ea1-a098-7ff6520f99be" } ] } @@ -155,6 +160,8 @@ bun run src/cli.ts search \ > **Note**: The `description` field (raw HTML) is intentionally omitted from `search` results for brevity. Use `detail` to retrieve the full job description. +> **Note**: Every result also carries the cross-portal contract fields `company`, `location`, `date`, `deadline` and `url` — derived respectively from `hiringOrgName`, `postalDistrictName`/`municipality`, and the jobnet detail page URL. `/scrape` Step 2 expects search output to include title, company, location, date, and URL, and dates follow the `YYYY-MM-DD` convention of the other portal CLIs. The API's `1900-01-01` deadline sentinel (deadline not disclosed) maps to `null`. The native fields above are preserved unchanged. + > **Note**: `resultsPerPage` and `pageNumber` must always be provided — omitting them while also providing `searchString` causes the API to return error 1014 ("Fejl i formatering af inputs"). --- @@ -350,9 +357,14 @@ All errors are written to **stderr** in JSON format and exit with code `1`: Job ad detail pages on jobnet.dk: ``` -https://jobnet.dk/job/{jobAdId} +https://jobnet.dk/find-job/{jobAdId} ``` +The legacy `https://jobnet.dk/job/{jobAdId}` route redirects anonymous visitors into the +MitID login flow, so it is never emitted. External ads (`isExternal: true`, jobAdIds with an +`E` prefix) 404 on `/find-job/` and hit the login wall on `/job/` - neither route serves them +anonymously; `/find-job/` is still strictly better and external ads are left as-is. + Company logo images (prefix relative logoUrl from API): ``` diff --git a/.agents/skills/jobnet-search/cli/src/commands/search.ts b/.agents/skills/jobnet-search/cli/src/commands/search.ts index e06c6ed..6b6232d 100644 --- a/.agents/skills/jobnet-search/cli/src/commands/search.ts +++ b/.agents/skills/jobnet-search/cli/src/commands/search.ts @@ -100,6 +100,13 @@ export function createSearchOutput(data: SearchApiResponse, flags: SearchFlags) workPlaceAddress: job.workPlaceAddress ?? "", isSeen: job.isSeen, isFavorite: job.isFavorite, + company: job.hiringOrgName, + location: job.postalDistrictName ?? job.municipality ?? null, + date: job.publicationDate.slice(0, 10), + deadline: job.applicationDeadline && !job.applicationDeadline.startsWith("1900-01-01") + ? job.applicationDeadline.slice(0, 10) + : null, + url: `https://jobnet.dk/find-job/${job.jobAdId}`, })) if (flags.limit !== undefined) { diff --git a/.agents/skills/jobnet-search/cli/tests/search-normalization.test.ts b/.agents/skills/jobnet-search/cli/tests/search-normalization.test.ts index 8ae00fa..84ba9a5 100644 --- a/.agents/skills/jobnet-search/cli/tests/search-normalization.test.ts +++ b/.agents/skills/jobnet-search/cli/tests/search-normalization.test.ts @@ -127,4 +127,37 @@ describe("Jobnet search normalization", () => { }); expect("description" in output.results[0]).toBe(false); }); + + test("additively emits the /scrape contract fields (company, location, date, deadline, url)", () => { + const output = createSearchOutput(apiResponse(), { ...flags, limit: undefined }); + + expect(output.results).toHaveLength(2); + expect(output.results[0]).toMatchObject({ + company: "Acme", + location: null, + date: "2026-07-01", + deadline: null, + url: "https://jobnet.dk/find-job/job-1", + }); + expect(output.results[1]).toMatchObject({ + company: "Example Co", + location: "København Ø", + date: "2026-07-02", + deadline: "2026-08-01", + url: "https://jobnet.dk/find-job/job-2", + }); + expect(output.results[0].hiringOrgName).toBe("Acme"); + expect(output.results[1].applicationDeadline).toBe("2026-08-01T23:59:00+02:00"); + }); + + test("maps Jobnet's undisclosed-deadline sentinel (1900-01-01) to null", () => { + const response = apiResponse(); + response.jobAds[0].applicationDeadline = "1900-01-01T00:00:00+01:00"; + response.jobAds[0].applicationDeadlineStatus = "NotDisclosed"; + + const output = createSearchOutput(response, { ...flags, limit: undefined }); + + expect(output.results[0].deadline).toBeNull(); + expect(output.results[1].deadline).toBe("2026-08-01"); + }); }); diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ae2a47..5695672 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,14 @@ per-file diff commands. ### Fixed +- **`jobnet-search` search output now carries the `/scrape` contract fields** - the CLI emitted + the raw Jobnet API schema (`jobAdId`, `hiringOrgName`, `publicationDate`, …) with no + `company`, `location`, `date` or `url`, so every `/scrape` run flagged jobnet as degraded + forever (CI stayed green), the `seen_jobs.json` dedupe fell back to company+title, and `/rank` + lost the posting link. Search results now additively emit `company`, `location`, `date`, + `deadline` and `url` (`https://jobnet.dk/find-job/{jobAdId}` - the `/job/` route is + login-walled); the API's `1900-01-01` "deadline not disclosed" sentinel maps to `null`. + - **A `/` in a company or role name no longer nests the application archive one level too deep** (jakob1379/ai-job-search#22). `Novo Nordisk A/S` derived `documents/applications/novo_nordisk_a/s_data_scientist/` - written and found by every command