@cavuno/board 3.2.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +433 -0
  2. package/README.md +6 -2
  3. package/dist/{_spec-nkWAraS0.d.mts → _spec-BZDv2dc5.d.mts} +100 -68
  4. package/dist/{_spec-nkWAraS0.d.ts → _spec-BZDv2dc5.d.ts} +100 -68
  5. package/dist/bin.mjs +7 -2
  6. package/dist/{blog-iV9XEIGf.d.ts → blog-BHx6iH3Z.d.ts} +1 -1
  7. package/dist/{blog-DGBHmExV.d.mts → blog-DyDvyQrI.d.mts} +1 -1
  8. package/dist/{board-JhdUWIoq.d.ts → board-BF3Qao6J.d.mts} +7 -6
  9. package/dist/{board-BJbXd_9_.d.mts → board-BTEZ0uiI.d.ts} +7 -6
  10. package/dist/breadcrumbs-SGaWth6C.d.ts +372 -0
  11. package/dist/breadcrumbs-icxRVydh.d.mts +372 -0
  12. package/dist/chunk-BGKNXG4H.mjs +146 -0
  13. package/dist/{chunk-NXY3OWGF.mjs → chunk-T2KC6XGV.mjs} +1 -1
  14. package/dist/{chunk-STE77MAT.mjs → chunk-YEAQ3JEI.mjs} +143 -134
  15. package/dist/doctor.js +1 -1
  16. package/dist/doctor.mjs +1 -1
  17. package/dist/filters.d.mts +25 -20
  18. package/dist/filters.d.ts +25 -20
  19. package/dist/filters.js +3 -692
  20. package/dist/filters.mjs +3 -29
  21. package/dist/format.d.mts +57 -231
  22. package/dist/format.d.ts +57 -231
  23. package/dist/format.js +327 -968
  24. package/dist/format.mjs +207 -264
  25. package/dist/go.js +11 -1
  26. package/dist/go.mjs +1 -1
  27. package/dist/index.d.mts +67 -76
  28. package/dist/index.d.ts +67 -76
  29. package/dist/index.js +45 -36
  30. package/dist/index.mjs +45 -36
  31. package/dist/{jobs-BiEvL9d7.d.ts → jobs-BX6d_sdd.d.mts} +4 -4
  32. package/dist/{jobs-lVvKCBCU.d.mts → jobs-BbLJfz1z.d.ts} +4 -4
  33. package/dist/paths.d.mts +89 -1
  34. package/dist/paths.d.ts +89 -1
  35. package/dist/paths.js +62 -14
  36. package/dist/paths.mjs +11 -3
  37. package/dist/route-contract.js +23 -13
  38. package/dist/route-contract.mjs +2 -2
  39. package/dist/{search-CCyFWPi3.d.mts → search-7B-mVRNA.d.ts} +14 -3
  40. package/dist/{search-CyRvVMUp.d.ts → search-OKrdAHv8.d.mts} +14 -3
  41. package/dist/seo.d.mts +30 -29
  42. package/dist/seo.d.ts +30 -29
  43. package/dist/seo.js +197 -718
  44. package/dist/seo.mjs +26 -24
  45. package/dist/server.d.mts +5 -5
  46. package/dist/server.d.ts +5 -5
  47. package/dist/sitemap.d.mts +5 -5
  48. package/dist/sitemap.d.ts +5 -5
  49. package/dist/sitemap.js +25 -14
  50. package/dist/sitemap.mjs +3 -2
  51. package/dist/skills.js +1 -1
  52. package/dist/skills.mjs +1 -1
  53. package/dist/suggest.d.mts +2 -2
  54. package/dist/suggest.d.ts +2 -2
  55. package/dist/suggest.js +17 -3
  56. package/dist/suggest.mjs +17 -3
  57. package/dist/well-known.js +23 -13
  58. package/dist/well-known.mjs +2 -2
  59. package/package.json +2 -4
  60. package/skills/cavuno-board-api-client/SKILL.md +42 -0
  61. package/skills/cavuno-board-applications/SKILL.md +4 -1
  62. package/skills/cavuno-board-filters/SKILL.md +11 -12
  63. package/skills/cavuno-board-format/SKILL.md +68 -56
  64. package/skills/cavuno-board-i18n/SKILL.md +102 -21
  65. package/skills/cavuno-board-search-suggestions/SKILL.md +8 -3
  66. package/skills/cavuno-board-seo/BLOG.md +4 -1
  67. package/skills/cavuno-board-seo/JOB_AND_BREADCRUMBS.md +5 -2
  68. package/skills/cavuno-board-seo/LISTING.md +35 -12
  69. package/skills/cavuno-board-seo/SALARY.md +83 -18
  70. package/skills/cavuno-board-seo/SKILL.md +3 -2
  71. package/skills/manifest.json +3 -3
  72. package/dist/breadcrumbs-1hMjcsqA.d.mts +0 -171
  73. package/dist/breadcrumbs-DexU07TT.d.ts +0 -171
  74. package/dist/chunk-IJCJ26SQ.mjs +0 -596
  75. package/dist/chunk-L7ZOYWHW.mjs +0 -98
  76. package/dist/chunk-LUGJWOT7.mjs +0 -96
  77. package/dist/ui-copy-CCkz6CVF.d.mts +0 -230
  78. package/dist/ui-copy-CCkz6CVF.d.ts +0 -230
package/CHANGELOG.md CHANGED
@@ -3,6 +3,439 @@
3
3
  This changelog records changes that affect Board API compatibility, exported
4
4
  types, runtime behavior, or supported integration patterns.
5
5
 
6
+ ## 4.1.0 — 2026-08-06
7
+
8
+ - **Salary null-clears on job PATCH** (operator `PATCH /v1/jobs/:id` and
9
+ employer `PATCH /v1/boards/:identifier/me/companies/:slug/jobs/:id`):
10
+ `salaryMin`, `salaryMax`, `salaryCurrency`, and `salaryTimeframe` now
11
+ accept an explicit `null` meaning "clear the stored value" — the same
12
+ tri-state `applicationUrl` and `expiresAt` already document (value sets,
13
+ `null` clears, omitted leaves unchanged). Clearing also drops the derived
14
+ USD figures, so salary filters stop matching a job whose salary was
15
+ withdrawn.
16
+ - **Structured remote fields on `PublicJobCard`**: `remoteWorldwide`
17
+ (`boolean | null`) and `remoteWorkPermitCountryCodes` (`string[]`) beside
18
+ the pre-worded `remoteLocationLabel`, so templates can word remote
19
+ regions from their own catalogs instead of matching the board-language
20
+ `"Worldwide"` label string. Derived from the same permit selection the
21
+ label reads — the boolean and the label always agree.
22
+ - **Docs**: `Plan.name` and `Plan.description` are documented as authoring
23
+ defaults — the localized public string lives in the board template,
24
+ composed from `featureSummary`, `billingInterval`, and `talent`.
25
+ - **Marketing consent**: consent is a property of the board
26
+ user. New `board.me.marketingConsent` namespace — `retrieve()` (the
27
+ consent, or `null` when no decision exists), `grant()`, `withdraw()` —
28
+ plus the `MarketingConsent` type. The API records the decision, never
29
+ the prose: disclosure wording is authored by the board's own frontend.
30
+ - **Outbound webhooks**: webhook subscription endpoints and event types
31
+ join the OpenAPI contract and generated types.
32
+
33
+ ## 4.0.0 — 2026-08-03
34
+
35
+ - **Lossy seams:** Resolution is fine;
36
+ discarding the input is not. After every call the application must
37
+ still be able to produce a different legitimate rendering.
38
+ - `resolveCustomFieldDisplay` multi-select returns
39
+ `{ kind: 'multi_select', values: string[] }` instead of a
40
+ pre-joined `kind: 'text'` sentence. `Intl.ListFormat` (or chips /
41
+ short style / disjunction) moves to the app — same reason numbers
42
+ stay raw. Leading `language` stays for signature stability; the SDK no longer reads it for multi-select.
43
+ - `buildSalaryFaq` average entries carry `avgMin`, `avgMax`, and
44
+ `currency` alongside the compact `range` convenience. FAQ prose can
45
+ reformat with `notation: 'standard'` without re-threading the detail
46
+ object. `label` is still echoed so FAQ mapping is a pure map over
47
+ entries (the SDK never reads it for formatting).
48
+ - `formatSalaryStat` / `formatSalaryStatRange` accept optional
49
+ `notation` with the same magnitude default as `formatSalaryRange`
50
+ (`|value| ≥ 1000` → compact; smaller → standard). `$90K` vs
51
+ `$90,000` is register, not locale.
52
+ - : remove `ListFormat` from "what the SDK keeps"; add the
53
+ mechanical recoverability test and worked table; document Google
54
+ controlled vocabularies (`EDUCATION_TO_CREDENTIAL`,
55
+ `EMPLOYMENT_TYPE_TO_GOOGLE`) as wire values, not copy; correct that
56
+ timeframe units ship as the wire enum, not unit words from `Intl`.
57
+
58
+ - **Pre-publish blockers (salary format + SEO paths):** Closes the final
59
+ review that blocked publish.
60
+ - `formatSalaryStat` / `formatSalaryStatRange` null-guard currency
61
+ (`currency?.trim()`) so a runtime `null` returns `null` instead of
62
+ `TypeError` — matches `formatSalaryRange` and the skill/changelog
63
+ contract (`string | null`).
64
+ - Non-finite amounts (`NaN`, `±Infinity`) return `null` across
65
+ `formatSalaryStat`, `formatSalaryStatRange`, and `formatSalaryRange`
66
+ (they are `number` at the type level and slip past `== null`).
67
+ - Identical range ends (`min === max`) format as a fixed salary
68
+ (single amount), not ICU's `approximatelySign` form (`~$140K`). A
69
+ posted pin is not a degenerate range.
70
+ - `formatSalaryRange` defaults `notation` by **magnitude**: compact
71
+ only where ICU shortens (`|value| ≥ 1000`); smaller values stay
72
+ `standard` so hourly/day rates keep CLDR minor units (`$22.50`,
73
+ `KWD 22.567`). Explicit `notation` still wins.
74
+ - `listingJsonLd` builds job URLs via `jobDetailPath` / `boardUrl`
75
+ (drops company-less jobs rather than linking `/jobs/{slug}`); omits
76
+ `BreadcrumbList` when the trail has fewer than 2 crumbs.
77
+ `buildJobBreadcrumbs` uses `paths/` helpers instead of hand-built
78
+ strings.
79
+ - `companySalaryJsonLd` / `locationSalaryJsonLd` accept omitted
80
+ `options` and return `null` (siblings already did; these threw).
81
+
82
+ - **i18n audit (format / paths / skills):** Closes the specialist audit
83
+ against ICU/CLDR/W3C practice. Architecture (discriminant seams, no
84
+ composition, `Intl`-over-catalogs) stands; defects were concentrated in
85
+ timezone pinning, CLDR width choice, and structure-vs-support locale
86
+ validation.
87
+ - `formatMonthYear` pins `timeZone: 'UTC'` (and date-only inputs as UTC
88
+ midnight) so full ISO timestamps agree with `formatDate` under every
89
+ host zone — no more `"Mai 2023"` / `"Dec 2022"` for June / Jan UTC.
90
+ - `formatPublishedRelativeDate` uses RTF `style: 'short'` (not
91
+ `narrow`). English reads `"5 days ago"`; French / Russian / Romanian
92
+ no longer degrade to signed numbers (`-5 j`).
93
+ - `normalizeLocale` requires `Intl.NumberFormat.supportedLocalesOf` —
94
+ well-formed but unsupported tags (`xx`, `qqq`, `und`, …) return
95
+ `null` instead of silently using the host default. Contract in
96
+ `locale.ts`, `salary-range.ts`, and `skills/cavuno-board-format` is
97
+ now enforced.
98
+ - Currency fraction digits: compact notation keeps a one-digit cap
99
+ (documented); `notation: 'standard'` (optional override on
100
+ `formatSalaryRange`) lets each currency's CLDR minor units win.
101
+ Default notation is magnitude-selected (see pre-publish blockers
102
+ above), not unconditional compact.
103
+ - `resolveCustomFieldDisplay` emits `kind: 'number'` with the raw
104
+ value (not pre-stringified `kind: 'text'`), so the app formats with
105
+ `Intl.NumberFormat`.
106
+ - Path helpers percent-encode dynamic segments (non-ASCII board-language
107
+ slugs become URIs for sitemap `<loc>`, canonical, email). `:` is kept
108
+ for route-contract templates; already-encoded input is not
109
+ double-encoded.
110
+ - Filter / suggest Set keys NFC-normalize; suggest `minChars` counts
111
+ graphemes (`Intl.Segmenter`), so a single CJK character qualifies.
112
+ - `skills/cavuno-board-i18n` carries the five obligations
113
+ (plural selection, gender, bidi isolation with FSI/PDI, grapheme
114
+ truncation, locale-aware case) and extends the completion gate.
115
+ Salary-range / format skills document bidi isolation at the join
116
+ site rather than returning `{ value, dir }`.
117
+
118
+ - **Rule (word order):** The SDK never decides what goes next to what.
119
+ "No hardcoded strings" is the easy half. Word order is the half that
120
+ survives a grep, because every operand is data and only the
121
+ *arrangement* is English. A template with two data holes and a space
122
+ (or `|`, or `/`) between them is an English sentence with the words
123
+ removed. The SDK formats with `Intl`, returns structure, and leaves
124
+ composition — counters, particles, separators, prefix vs postfix — to
125
+ the application. Invalid locales prefer `null` over English-arranged
126
+ fallbacks; underscore tags (`ja_JP`) normalize to BCP-47 (`ja-JP`) so
127
+ one typo does not drop every formatter into English at once.
128
+
129
+ - **Breaking:** `listingHead` requires a caller-supplied `title` (same
130
+ ownership as `description`). Removed `heading`, `boardName`, `count`,
131
+ and `language` from `ListingHeadOptions` — the SDK no longer composes
132
+ `` `${count} ${heading} | ${boardName}` `` (Japanese wants
133
+ `1,225件の求人`, not `1,225 求人 | …`). Pass the finished title from
134
+ the board's message catalog. Also removes the only unguarded
135
+ `Intl.NumberFormat` construction in the SDK (a malformed board
136
+ language no longer crashes the listing page).
137
+
138
+ - **Breaking:** `formatSalaryRange` returns
139
+ `{ text, timeframe, bound } | null`. `text` is the `Intl`-formatted
140
+ amount/range only; `timeframe` is the **wire enum** (`per_year`,
141
+ `per_hour`, …) — never a unit word from `Intl` and never an owner
142
+ override. Applications map the enum through their own catalog and
143
+ compose amount↔timeframe order (`$90K / year` vs `年収900万〜1200万円`).
144
+ Missing currency, invalid locale, or rejected currency format →
145
+ `null` (no bare number, no English `M`/`k`/`$`).
146
+
147
+ - **Breaking:** Removed `SalaryTimeframeOverrides` from
148
+ `@cavuno/board/format`. Owner-edited unit labels are application-side
149
+ copy keyed off the wire `timeframe` enum the helper returns; the SDK
150
+ no longer accepts an override map.
151
+
152
+ - **Breaking:** `GET/POST …/me/saved-jobs` embeds a slim `job_card` (the
153
+ same shape as jobs list/search), not a full `public_job` with HTML
154
+ `description`. Fetch the job detail by slug when the full body is
155
+ needed. `remoteLocationLabel` is resolved server-side from
156
+ `remoteRules`, so region-limited remote jobs keep their real region
157
+ label instead of a client-side lossy guess.
158
+
159
+ - **Breaking:** Removed `fullJobToCard` from `@cavuno/board/format`. The
160
+ saved-jobs endpoint now serves cards; convert with the same card VM as
161
+ listings (`toJobCardVM(saved.job, …)`). That helper was the last
162
+ user-facing hardcoded English in the SDK (`Remote`, `Worldwide`,
163
+ `(hybrid)`).
164
+
165
+ - **Breaking:** `formatSalaryRange`'s board language is required — no
166
+ silent `en` default. `formatPublishedRelativeDate` under one minute uses
167
+ `Intl.RelativeTimeFormat` (`"now"` / `"jetzt"` / `"maintenant"`), not a
168
+ hardcoded English string. `resolveCustomFieldDisplay` takes the board
169
+ language as its leading parameter (signature stability). Multi-select
170
+ originally joined via `Intl.ListFormat`; that was lossy and now returns
171
+ `kind: 'multi_select'` with `values: string[]` (see Lossy seams above).
172
+
173
+ - **Breaking:** Renamed and currency-aware salary-stat formatters:
174
+ `formatUsd` / `formatRange` → `formatSalaryStat(locale, value, currency)` /
175
+ `formatSalaryStatRange(locale, min, max, currency)`. The `/format`
176
+ aliases `formatSalaryStatUsd` / `formatSalaryStatRange` are replaced by
177
+ the same names (no USD-only alias). `buildSalaryFaq` takes `currency`
178
+ as a fourth argument so FAQ ranges match the board's money. Both formatters
179
+ return `string | null` (`null` when currency is empty or `Intl` rejects
180
+ the locale/currency) instead of inventing English `M`/`k` or a bare `$`.
181
+
182
+ - **Breaking:** `formatSalaryRange` first moved from `string | null` to
183
+ `{ text; bound } | null` so open floors/ceilings are named by the app;
184
+ the timeframe is now a third field (see top of this heading) rather than
185
+ a suffix glued onto `text`.
186
+
187
+ - **Breaking:** Unit, currency, and date *words* derive from `Intl`;
188
+ `skills/` snippets show shape (catalog placeholders), not pasteable
189
+ English copy or join templates. The SDK formats with `Intl` and returns
190
+ structure; it does not supply open-range chrome, nav/button copy, filter
191
+ labels, SEO sentence templates, or amount↔unit arrangement. Dropped the
192
+ private chrome-copy package dependency entirely. Closed ranges use
193
+ `Intl.NumberFormat.prototype.formatRange` (locale owns separator,
194
+ repeated currency/magnitude, and bidi marks). Open-range chrome and
195
+ amount↔timeframe order are application-owned via `bound` + `timeframe`.
196
+ `buildJobBreadcrumbs` no longer takes `language` or `labels` parameters —
197
+ it returns structural `kind`s for chrome crumbs (`home` / `jobs`) instead
198
+ of display names; applications supply the words when mapping to
199
+ `BreadcrumbList`. `seniorityLabels` / `sortLabels` / `formatSeniority`
200
+ are removed. Type exports removed with the chrome surface:
201
+ `AlertsCopy`, `ApplyCopy`, `BlogCopy`, `BoardLabelOverrides`,
202
+ `BreadcrumbsCopy`, `CopyLinkCopy`, `EntityCopy`, `FooterCopy`,
203
+ `JobCardCopy`, `JobDetailCopy`, `JobSearchCopy`, `NavCopy`,
204
+ `PaginationCopy`, `SalaryCopy`, `SalaryFrames`, `SalaryLexicon`,
205
+ `SeniorityKey`, `UiCopy`.
206
+
207
+ - **Breaking:** `listingHead` requires a caller-supplied `description` (and
208
+ now `title` — see top of this heading). It no longer composes
209
+ `Browse … on …` (English on every locale) or case-folds the heading with
210
+ locale-blind `.toLowerCase()`.
211
+
212
+ - **Breaking:** `buildSalaryFaq` returns structured
213
+ `SalaryFaqEntry` values (`kind: 'average' | 'methodology'` plus label,
214
+ `formatRange` range, and `jobCount`) instead of English FAQ prose.
215
+ Map entries to `{ q, a }` in the application (with proper plural rules),
216
+ then pass them to `faqJsonLd`.
217
+
218
+ - **Breaking:** Salary JSON-LD builders
219
+ (`titleSalaryJsonLd`, `skillSalaryJsonLd`, `locationSalaryJsonLd`,
220
+ `crossAxisSalaryJsonLd`, `companySalaryJsonLd`,
221
+ `companyCategorySalaryJsonLd`) use data-only `name` fields (entity
222
+ names). Removed English framing
223
+ (`… salary (all levels)`, `Average Salary in …`, `Jobs at …`,
224
+ `… salary in …`, trailing ` salary`).
225
+
226
+ - **Breaking:** Dropped the dead `locale` parameter from
227
+ `titleSalaryJsonLd`, `crossAxisSalaryJsonLd`, `companySalaryJsonLd`,
228
+ and `companyCategorySalaryJsonLd` — it was discarded (`void locale`)
229
+ and did not change output. Per-seniority distribution names are no
230
+ longer title-cased wire enums (`entry_level` → `Entry level`) and are
231
+ no longer joined as `` `${seniority} ${entity}` `` (English word order).
232
+ Pass optional `seniorityName({ seniority, entity })` that returns the
233
+ finished string in board-language order, or omit it so those
234
+ distributions ship without a `name` (valid structured data;
235
+ wrong-language / wrong-order names are not).
236
+
237
+ - **Breaking:** `formatSalaryRange` never invents USD when
238
+ `salaryCurrency` is null/empty — returns `null`, matching
239
+ `createJobPostingJsonLd` which omits `baseSalary` without a currency.
240
+ Invalid locales and rejected currency formats also return `null` rather
241
+ than English `M`/`k`/`$` or a bare number.
242
+
243
+ - **Breaking:** `formatMonthYear` returns `string | null` — `null` on
244
+ missing/unparseable input (never the English literal `"Invalid Date"`),
245
+ and accepts full ISO timestamps in addition to date-only forms.
246
+
247
+ - **Breaking:** `locationSalaryJsonLd` and `companySalaryJsonLd` no
248
+ longer emit an `Occupation` / `OccupationAggregationByEmployer`
249
+ envelope with the place or employer as `name` (neither is an
250
+ occupation). They emit an `ItemList` of the occupations present on
251
+ that page (`topCategories` / `byCategory`), each linking to its own
252
+ occupation page via required `occupationUrl`. Return `null` when
253
+ there are no per-occupation rows (or when the location is not
254
+ city/locality). The other four builders keep `Occupation`.
255
+
256
+ - **Breaking:** `board.taxonomy.suggestions.list` and
257
+ `GET /v1/boards/{identifier}/suggestions` are removed. Use
258
+ `board.search.suggest({ types: ['category', 'skill'], … })` instead. The
259
+ `taxonomy` namespace itself stays (`categories`, `skills`, `places`,
260
+ `remotePermits`, resolve methods).
261
+
262
+ - `board.search.suggest` accepts
263
+ `types?: ('company' | 'category' | 'skill' | 'market' | 'post' | 'tag')[]`.
264
+ Omit for every kind. `limit` applies **after** type filtering, so
265
+ `{ types: ['skill'], limit: 10 }` returns up to ten skills.
266
+
267
+ - `SuggestionItem` gains a `market` variant (`type: 'market'`, `slug`, `name`,
268
+ `companyCount`) for company-scope typeahead alongside companies.
269
+
270
+ - `SuggestionItem` gains `post` (`type: 'post'`, `id`, `slug`, `title`) and
271
+ `tag` (`type: 'tag'`, `id`, `slug`, `name`) for blog typeahead. Blog posts
272
+ and tags are public, so no extra access control.
273
+
274
+ - `@cavuno/board/paths` adds `suggestionPath(suggestion, { scope, location? })`
275
+ plus `jobsLocationCategoryPath` / `jobsLocationSkillPath`. A jobs-scope
276
+ company suggestion returns `null` — apply a `companySlug` filter; do not
277
+ invent `/jobs/companies/<slug>`. Blog scope routes posts via
278
+ `blogPostPath` and tags via `blogTagPath`.
279
+
280
+ - Added `sdk/reference/search.mdoc` and a docs-contract gate that every
281
+ `BoardSdk` namespace has a matching reference page.
282
+
283
+ - The sitemap no longer advertises `/talent` on boards whose talent directory
284
+ is `employers_only`. That page is gated behind an approved employer session,
285
+ so listing it pointed crawlers at the gate rather than the directory. Only a
286
+ `public` directory is listed.
287
+
288
+ - **Breaking:** `board.context().customFields` is now keyed by model instead of
289
+ a flat job-only array. Today only `job` is present; `company` / `talent` keys
290
+ land when those models ship (absent keys are not empty arrays).
291
+
292
+ Before:
293
+
294
+ ```json
295
+ "customFields": [
296
+ { "key": "security_clearance", "label": "Security clearance", "type": "single_select", "required": false }
297
+ ]
298
+ ```
299
+
300
+ After:
301
+
302
+ ```json
303
+ "customFields": {
304
+ "job": [
305
+ { "key": "security_clearance", "label": "Security clearance", "type": "single_select", "required": false }
306
+ ]
307
+ }
308
+ ```
309
+
310
+ Resolve job values with `resolveCustomFieldDisplay(context.customFields.job, job.customFieldValues)`
311
+ — the helper still takes a definitions array and did not change.
312
+
313
+ - **Breaking:** `features.talentDirectory` is now the tri-state access mode
314
+ (`off` | `public` | `employers_only`). The top-level
315
+ `talentDirectoryVisibility` field is removed — it duplicated the same gate.
316
+
317
+ Before:
318
+
319
+ ```json
320
+ "features": { "talentDirectory": true },
321
+ "talentDirectoryVisibility": "employers_only"
322
+ ```
323
+
324
+ After:
325
+
326
+ ```json
327
+ "features": { "talentDirectory": "employers_only" }
328
+ ```
329
+
330
+ Link `/talent` whenever `features.talentDirectory !== 'off'`. Do not treat
331
+ the field as a boolean — the string `'off'` is truthy in JavaScript.
332
+
333
+ - **Breaking:** `board.seo()` no longer returns presentation. Removed
334
+ `icons` (ico/svg/appleTouch/icon192/icon512/iconMaskable512) and
335
+ `manifest.themeColor`. Kept `canonicalBase`, `adsTxt`, `indexNowKey`,
336
+ `googleSiteVerification`, and `manifest.name`. Applications own brand
337
+ assets and theme tokens.
338
+
339
+ Before:
340
+
341
+ ```json
342
+ {
343
+ "canonicalBase": "https://jobs.example.com",
344
+ "adsTxt": null,
345
+ "indexNowKey": null,
346
+ "googleSiteVerification": null,
347
+ "icons": { "ico": "…", "svg": null, "appleTouch": null, "icon192": null, "icon512": null, "iconMaskable512": null },
348
+ "manifest": { "name": "Acme Jobs", "themeColor": "#1d4ed8" }
349
+ }
350
+ ```
351
+
352
+ After:
353
+
354
+ ```json
355
+ {
356
+ "canonicalBase": "https://jobs.example.com",
357
+ "adsTxt": null,
358
+ "indexNowKey": null,
359
+ "googleSiteVerification": null,
360
+ "manifest": { "name": "Acme Jobs" }
361
+ }
362
+ ```
363
+
364
+ - **Breaking:** `board.context().footer` is gone. Operator-authored contact
365
+ and social identity move to a top-level `contact` group; editor-authored
366
+ presentation (`description`, `navigationOrder`, `customLinks`) is removed
367
+ — applications own footer layout.
368
+
369
+ Before:
370
+
371
+ ```json
372
+ "footer": {
373
+ "description": "Roles from {{board_name}}.",
374
+ "contactEmail": "hello@acme.com",
375
+ "websiteUrl": "https://acme.com/",
376
+ "xUrl": "https://x.com/acmejobs",
377
+ "facebookUrl": null,
378
+ "linkedinUrl": "https://linkedin.com/company/acme",
379
+ "navigationOrder": ["home", "blog"],
380
+ "customLinks": [{ "id": "abc", "label": "Hub", "url": "/hub" }]
381
+ }
382
+ ```
383
+
384
+ After:
385
+
386
+ ```json
387
+ "contact": {
388
+ "email": "hello@acme.com",
389
+ "websiteUrl": "https://acme.com/",
390
+ "xUrl": "https://x.com/acmejobs",
391
+ "facebookUrl": null,
392
+ "linkedinUrl": "https://linkedin.com/company/acme"
393
+ }
394
+ ```
395
+
396
+ Brand/social URL sanitization is unchanged (absolute http(s) only).
397
+ `companyLegalName` / `companyAddress` were never on this public context
398
+ payload (they ride the legal/impressum surface); no `company` group was
399
+ invented here.
400
+
401
+ - **Breaking:** Chrome copy leaves the public `@cavuno/board` surface. Removed from `@cavuno/board/format`: `uiCopy`,
402
+ `PUBLIC_LABEL_GROUPS`, `fieldLabel`, `getSalaryLexicon`, `locationLabel`,
403
+ `cardLocationLabel`, `companyIntro`, and the `UiCopy` type re-export.
404
+ Removed from `@cavuno/board/seo`: `formatSeniority`. Removed from
405
+ `@cavuno/board/filters`: `seniorityLabels`, `sortLabels`.
406
+
407
+ Hosted boards import chrome words from a private package. Third-party applications own their copy in code (or their
408
+ message catalogs). The published SDK no longer imports or bundles that
409
+ package.
410
+
411
+ What stays on `@cavuno/board/format`: `formatSalaryRange` (`Intl`-backed
412
+ amounts, wire `timeframe` enum, and a `bound` discriminant for open ranges),
413
+ date helpers, `resolveCustomFieldDisplay`, `normalizeWebsiteUrl`,
414
+ `buildJobBreadcrumbs` (kinds + data names; no `language`/`labels` args),
415
+ `COUNTRY_CODES` / `countryOptions`, and the salary-stat formatters. Filter
416
+ vocabularies (wire enums + parsers only) stay on `@cavuno/board/filters`.
417
+
418
+ - **Breaking:** `board.legal.retrieve`, the legal types (`PublicLegalPage`,
419
+ `LegalPageType`, `LegalEntity`), and `GET /v1/boards/{identifier}/legal/{type}`
420
+ (`getBoardLegal`) are removed. Legal prose is editor-authored
421
+ application content, not a Board API concern.
422
+
423
+ - **Breaking:** `board.context().labels` is removed. Operator label overrides
424
+ no longer ride the public Board API — applications own chrome words.
425
+ Hosted storage for operator overrides is unchanged (hosted chrome still
426
+ reads the bag).
427
+
428
+ Before:
429
+
430
+ ```json
431
+ "labels": {
432
+ "jobCardLabels": { "featuredLabel": "Top Job" },
433
+ "navLabels": { "home": "Jobs" }
434
+ }
435
+ ```
436
+
437
+ After: field absent.
438
+
6
439
  ## 3.2.0 — 2026-08-03
7
440
 
8
441
  - Added `formatSalaryStatUsd`, `formatSalaryStatRange`, `normalizeWebsiteUrl`,
package/README.md CHANGED
@@ -53,8 +53,12 @@ const { name, language, features } = await board.context();
53
53
  const page = await board.jobs.list({ limit: 20 });
54
54
  const job = await board.jobs.retrieve('senior-chef');
55
55
 
56
- // Federated search-dropdown suggestions (companies + taxonomy terms).
57
- const { items } = await board.search.suggest({ q: 'acme', limit: 10 });
56
+ // Federated search-dropdown suggestions (companies, markets, taxonomy terms).
57
+ const { items } = await board.search.suggest({
58
+ q: 'acme',
59
+ limit: 10,
60
+ types: ['company', 'skill'],
61
+ });
58
62
  // Or the headless controller (debounce / abort / stale-drop built in):
59
63
  // import { createSuggestController } from '@cavuno/board/suggest';
60
64
  // const suggest = createSuggestController(board);