From 895c02191d28c8d88b8381ea68797ff6db599728 Mon Sep 17 00:00:00 2001 From: Ayobami Adegoke Date: Mon, 5 Oct 2026 18:27:43 +0100 Subject: [PATCH] fix(portal-clis): emit the contract's `id` field from jobnet and jobdanmark search results (#518) /add-portal's contract says every search result carries at least id, title, company, location, date, url, and the four other shipped CLIs emit id. When #340 added the company/location/date/url aliases to these two, id was left out: jobnet exposed the value only as jobAdId, jobdanmark only as slug, so a consumer reading every portal's JSON through one shape had to special-case both to call detail. Purely additive: id equals jobAdId on jobnet and slug on jobdanmark, the native keys stay, nothing is renamed. Each SKILL.md notes the alias. The additive-contract test in each CLI now asserts id and its equality with the native key; both fail on master. --- .agents/skills/jobdanmark-search/SKILL.md | 2 +- .../jobdanmark-search/cli/src/commands/search.ts | 4 ++++ .../cli/tests/search-normalization.test.ts | 5 ++++- .agents/skills/jobnet-search/SKILL.md | 2 +- .../skills/jobnet-search/cli/src/commands/search.ts | 4 ++++ .../cli/tests/search-normalization.test.ts | 6 +++++- CHANGELOG.md | 11 +++++++++++ 7 files changed, 30 insertions(+), 4 deletions(-) diff --git a/.agents/skills/jobdanmark-search/SKILL.md b/.agents/skills/jobdanmark-search/SKILL.md index fec0025c..6163ebd7 100644 --- a/.agents/skills/jobdanmark-search/SKILL.md +++ b/.agents/skills/jobdanmark-search/SKILL.md @@ -224,6 +224,6 @@ All errors are written to **stderr** as `{ "error": "...", "code": "..." }` and - All data is from the public Jobdanmark.dk API — no credentials required. - Pagination is 1-indexed (`--page 1` is the first page). 30 items per page, server-enforced. - The `detail` command fetches the HTML job page and parses embedded JSON-LD when available, with a rendered-HTML fallback for pages that omit structured data. It does not use a separate JSON API. -- `slug` in search results is extracted from the API's relative `url` field (the path after `/job/`). +- `slug` in search results is extracted from the API's relative `url` field (the path after `/job/`); it is also emitted as `id`, the portal-contract name every search CLI shares. - `applicationDeadline` in search results can be `null` (no deadline set). - Job type values for filters: `fuldtid`, `deltid`, `fleksjob`, `elev`, `studiejob`, `praktik`. diff --git a/.agents/skills/jobdanmark-search/cli/src/commands/search.ts b/.agents/skills/jobdanmark-search/cli/src/commands/search.ts index 07a1f333..7b4a4f73 100644 --- a/.agents/skills/jobdanmark-search/cli/src/commands/search.ts +++ b/.agents/skills/jobdanmark-search/cli/src/commands/search.ts @@ -74,6 +74,10 @@ export function normalizeItem(item: ApiSearchItem): Record { applicationDeadline: item.applicationDeadline ?? null, url: fullUrl, slug, + // /scrape contract aliases (add-portal.md: every result carries at least + // id, title, company, location, date, url). `id` is the slug - the value + // `detail` takes - under the name every other portal CLI uses. + id: slug, company: item.companyName, location: extractCity(item.companyAddress), date: toContractDate(item.publishedDate), diff --git a/.agents/skills/jobdanmark-search/cli/tests/search-normalization.test.ts b/.agents/skills/jobdanmark-search/cli/tests/search-normalization.test.ts index f52b538f..d5f0dd4a 100644 --- a/.agents/skills/jobdanmark-search/cli/tests/search-normalization.test.ts +++ b/.agents/skills/jobdanmark-search/cli/tests/search-normalization.test.ts @@ -20,16 +20,19 @@ function item(): ApiSearchItem { } describe("Jobdanmark search normalization", () => { - test("additively emits the /scrape contract fields (company, location, date, deadline)", () => { + test("additively emits the /scrape contract fields (id, company, location, date, deadline, url)", () => { const result = normalizeItem(item()); expect(result).toMatchObject({ + id: "softwareudvikler-til-statens-it", company: "Statens It", location: "Ballerup", date: "2026-07-27", deadline: "2026-08-17", url: "https://jobdanmark.dk/job/softwareudvikler-til-statens-it", }); + // `id` is the contract name for the value `detail ` consumes. + expect(result.id).toBe(result.slug); }); test("maps a missing address zip and a null deadline to null", () => { diff --git a/.agents/skills/jobnet-search/SKILL.md b/.agents/skills/jobnet-search/SKILL.md index 473d1507..695b9de2 100644 --- a/.agents/skills/jobnet-search/SKILL.md +++ b/.agents/skills/jobnet-search/SKILL.md @@ -69,7 +69,7 @@ Key flags: bun run .agents/skills/jobnet-search/cli/src/cli.ts detail [--format json|plain] ``` -`jobAdId` is the UUID from `search` results (the `jobAdId` field). Returns the complete job +`jobAdId` is the UUID from `search` results (the `jobAdId` field, also emitted as `id`, the portal-contract name every search CLI shares). Returns the complete job description, contact persons, application deadline, employer details, and direct application URL. ### Search occupation types diff --git a/.agents/skills/jobnet-search/cli/src/commands/search.ts b/.agents/skills/jobnet-search/cli/src/commands/search.ts index 9f978cde..6625722e 100644 --- a/.agents/skills/jobnet-search/cli/src/commands/search.ts +++ b/.agents/skills/jobnet-search/cli/src/commands/search.ts @@ -104,6 +104,10 @@ export function createSearchOutput(data: SearchApiResponse, flags: SearchFlags) workPlaceAddress: job.workPlaceAddress ?? "", isSeen: job.isSeen, isFavorite: job.isFavorite, + // /scrape contract aliases (add-portal.md: every result carries at least + // id, title, company, location, date, url). `id` is the jobAdId - the + // value `detail` takes - under the name every other portal CLI uses. + id: job.jobAdId, company: job.hiringOrgName, location: job.postalDistrictName ?? job.municipality ?? null, date: job.publicationDate ? job.publicationDate.slice(0, 10) : null, 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 e2c7d7eb..0df2dd21 100644 --- a/.agents/skills/jobnet-search/cli/tests/search-normalization.test.ts +++ b/.agents/skills/jobnet-search/cli/tests/search-normalization.test.ts @@ -128,11 +128,12 @@ describe("Jobnet search normalization", () => { expect("description" in output.results[0]).toBe(false); }); - test("additively emits the /scrape contract fields (company, location, date, deadline, url)", () => { + test("additively emits the /scrape contract fields (id, company, location, date, deadline, url)", () => { const output = createSearchOutput(apiResponse(), { ...flags, limit: undefined }); expect(output.results).toHaveLength(2); expect(output.results[0]).toMatchObject({ + id: "job-1", company: "Acme", location: null, date: "2026-07-01", @@ -140,6 +141,7 @@ describe("Jobnet search normalization", () => { url: "https://jobnet.dk/find-job/job-1", }); expect(output.results[1]).toMatchObject({ + id: "job-2", company: "Example Co", location: "København Ø", date: "2026-07-02", @@ -148,6 +150,8 @@ describe("Jobnet search normalization", () => { }); expect(output.results[0].hiringOrgName).toBe("Acme"); expect(output.results[1].applicationDeadline).toBe("2026-08-01T23:59:00+02:00"); + // `id` is the contract name for the value `detail ` consumes. + expect(output.results[0].id).toBe(output.results[0].jobAdId); }); test("maps Jobnet's undisclosed-deadline sentinel (1900-01-01) to null", () => { diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a3deffd..38485dfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,17 @@ per-file diff commands. ### Fixed +- **`jobnet-search` and `jobdanmark-search` search results now carry the contract's `id` + field** (`search.ts` and `search-normalization.test.ts` in both CLIs, both `SKILL.md`s) - + `/add-portal`'s contract says every search result has at least `id`, `title`, `company`, + `location`, `date`, `url`, and the four other shipped CLIs emit `id`. When #340 added the + `company`/`location`/`date`/`url` aliases to these two, `id` was left out: jobnet exposed the + value only as `jobAdId`, jobdanmark only as `slug`, so a consumer reading every portal's + JSON through one shape had to special-case both to call `detail`. Purely additive: `id` + equals `jobAdId` on jobnet and `slug` on jobdanmark, the native keys stay, nothing is + renamed. The additive-contract test in each CLI now asserts `id` and its equality with the + native key; both fail on master. + - **`jobdanmark-search detail` renders a JSON-LD description as text instead of passing the markup through** (`.agents/skills/jobdanmark-search/cli/src/commands/detail.ts`, `tests/detail-jsonld.test.ts`) - the JSON-LD branch emitted `jobPosting.description`