diff --git a/.agents/skills/jobdanmark-search/cli/README.md b/.agents/skills/jobdanmark-search/cli/README.md index 5a9b0a3..a621ca9 100644 --- a/.agents/skills/jobdanmark-search/cli/README.md +++ b/.agents/skills/jobdanmark-search/cli/README.md @@ -148,7 +148,11 @@ bun run src/cli.ts search --text "sygeplejerske" --zip 8000 --limit 10 "url": "https://jobdanmark.dk/media/idvbnt4y/rah-service-as-billede.png", "focalPoint": { "top": 0.488, "left": 0.499 } }, - "silhouetteLogo": false + "silhouetteLogo": false, + "company": "Rah Service A/S", + "location": "Ringkøbing", + "date": "2026-03-12", + "deadline": "2026-04-10" } ] } @@ -161,6 +165,7 @@ bun run src/cli.ts search --text "sygeplejerske" --zip 8000 --limit 10 > - `companyLogo` can be `null`. > - `publishedDate` format: `"DD-MM-YYYY"`. > - `coverImage` can be `null`. +> - Every result also carries the cross-portal contract fields `company`, `location`, `date` and `deadline`, derived from `companyName`, the city after the postal code in `companyAddress`, and the day-first dates converted to `YYYY-MM-DD` — `/scrape` Step 2 expects search output to include title, company, location, date, and URL. Native fields are preserved unchanged. --- diff --git a/.agents/skills/jobdanmark-search/cli/src/commands/search.ts b/.agents/skills/jobdanmark-search/cli/src/commands/search.ts index d19969a..f0faee8 100644 --- a/.agents/skills/jobdanmark-search/cli/src/commands/search.ts +++ b/.agents/skills/jobdanmark-search/cli/src/commands/search.ts @@ -2,7 +2,7 @@ import { defineCommand, option } from "@bunli/core" import { z } from "zod" import { apiPost, writeError, BASE_URL } from "../helpers.js" -interface ApiSearchItem { +export interface ApiSearchItem { title: string companyName: string companyLogo: { @@ -12,7 +12,7 @@ interface ApiSearchItem { } | null companyLogoSvgMarkup: string | null overlayColor: string | null - companyAddress: string + companyAddress: string | null jobTypes: string[] boostJob: boolean publishedDate: string @@ -34,7 +34,12 @@ interface ApiSearchResponse { totalPages: number } -function normalizeItem(item: ApiSearchItem): Record { +function toContractDate(value: string | null): string | null { + const match = value?.match(/^(\d{2})-(\d{2})-(\d{4})$/) + return match ? `${match[3]}-${match[2]}-${match[1]}` : (value ?? null) +} + +export function normalizeItem(item: ApiSearchItem): Record { const relativeUrl = item.url const fullUrl = relativeUrl.startsWith("http") ? relativeUrl @@ -77,6 +82,10 @@ function normalizeItem(item: ApiSearchItem): Record { slug, coverImage, silhouetteLogo: item.silhouetteLogo, + company: item.companyName, + location: item.companyAddress?.match(/\d{4}\s+(.+)$/)?.[1] ?? null, + date: toContractDate(item.publishedDate), + deadline: toContractDate(item.applicationDeadline), } } diff --git a/.agents/skills/jobdanmark-search/cli/tests/search-normalization.test.ts b/.agents/skills/jobdanmark-search/cli/tests/search-normalization.test.ts new file mode 100644 index 0000000..6a63111 --- /dev/null +++ b/.agents/skills/jobdanmark-search/cli/tests/search-normalization.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, test } from "bun:test"; +import { normalizeItem, type ApiSearchItem } from "../src/commands/search"; + +function item(): ApiSearchItem { + return { + title: "Softwareudvikler", + companyName: "Statens It", + companyLogo: null, + companyLogoSvgMarkup: null, + overlayColor: null, + companyAddress: "Lautruphøj 2, 2750 Ballerup", + jobTypes: ["fuldtid"], + boostJob: false, + publishedDate: "27-07-2026", + applicationDeadline: "17-08-2026", + url: "/job/softwareudvikler-til-statens-it", + coverImage: null, + silhouetteLogo: false, + }; +} + +describe("Jobdanmark search normalization", () => { + test("additively emits the /scrape contract fields (company, location, date, deadline)", () => { + const result = normalizeItem(item()); + + expect(result).toMatchObject({ + company: "Statens It", + location: "Ballerup", + date: "2026-07-27", + deadline: "2026-08-17", + url: "https://jobdanmark.dk/job/softwareudvikler-til-statens-it", + }); + }); + + test("maps a missing address zip and a null deadline to null", () => { + const result = normalizeItem({ + ...item(), + companyAddress: "Lautruphøj 2", + applicationDeadline: null, + }); + + expect(result.location).toBeNull(); + expect(result.deadline).toBeNull(); + expect(result.company).toBe("Statens It"); + }); + + test("survives a null companyAddress from the API", () => { + const result = normalizeItem({ + ...item(), + companyAddress: null, + }); + + expect(result.location).toBeNull(); + expect(result.company).toBe("Statens It"); + }); + + test("keeps native fields unchanged (additive contract)", () => { + const result = normalizeItem(item()); + + expect(result.companyName).toBe("Statens It"); + expect(result.publishedDate).toBe("27-07-2026"); + expect(result.applicationDeadline).toBe("17-08-2026"); + }); +}); \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5695672..2ea671d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,13 @@ per-file diff commands. ### Fixed +- **`jobdanmark-search` search output now carries the `/scrape` contract fields** - the CLI + exposed the API-native schema (`companyName`, `publishedDate` in `DD-MM-YYYY`, …) with no + `company`, `location`, `date` or `deadline`, so every `/scrape` run flagged jobdanmark as + degraded and the `seen_jobs.json` dedupe lost the company. Search results now additively emit + `company`, `location` (city after the postal code in `companyAddress`), and `date`/`deadline` + in the `YYYY-MM-DD` convention, with null-safe handling of a missing address. + - **`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