@cavuno/board 4.26.0 → 4.27.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 (50) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/dist/{_spec-EgN7VmAI.d.mts → _spec-B_pBNVUd.d.mts} +487 -47
  3. package/dist/{_spec-EgN7VmAI.d.ts → _spec-B_pBNVUd.d.ts} +487 -47
  4. package/dist/bin.mjs +7989 -10
  5. package/dist/{blog-BIjFcNcm.d.ts → blog-DzGRUsj5.d.ts} +1 -1
  6. package/dist/{blog-T7gQ0ciz.d.mts → blog-fhXRik6k.d.mts} +1 -1
  7. package/dist/{board-q0svBKW3.d.mts → board-DEiEHcpe.d.mts} +25 -4
  8. package/dist/{board-CsEAgsKh.d.ts → board-fVAXZ8T5.d.ts} +25 -4
  9. package/dist/{breadcrumbs-xuDid8BS.d.ts → breadcrumbs-DNaaP3xa.d.ts} +2 -2
  10. package/dist/{breadcrumbs-CFcCjuG7.d.mts → breadcrumbs-qE3CD--N.d.mts} +2 -2
  11. package/dist/browser/cavuno-board.global.min.js +1 -1
  12. package/dist/{chunk-DP2ZZZEO.mjs → chunk-DUTQOLOF.mjs} +1 -0
  13. package/dist/doctor.js +7989 -10
  14. package/dist/doctor.mjs +7989 -10
  15. package/dist/filters.d.mts +19 -3
  16. package/dist/filters.d.ts +19 -3
  17. package/dist/filters.js +10 -1
  18. package/dist/filters.mjs +10 -1
  19. package/dist/format.d.mts +4 -4
  20. package/dist/format.d.ts +4 -4
  21. package/dist/index.d.mts +173 -13
  22. package/dist/index.d.ts +173 -13
  23. package/dist/index.js +175 -23
  24. package/dist/index.mjs +175 -24
  25. package/dist/{jobs-ub3gywNQ.d.ts → jobs-BRysn2m0.d.ts} +25 -3
  26. package/dist/{jobs-CfrxlDMU.d.mts → jobs-R-h3A3kl.d.mts} +25 -3
  27. package/dist/{search-Dx1TiAZk.d.ts → search-BK_zHHNQ.d.ts} +1 -1
  28. package/dist/{search-DKS7OIn2.d.mts → search-BlB690ma.d.mts} +1 -1
  29. package/dist/{seo-CI3wLKUW.d.mts → seo-0mbZ7M_E.d.mts} +1 -1
  30. package/dist/{seo-BbG2Ujyo.d.ts → seo-C7gMl6Y7.d.ts} +1 -1
  31. package/dist/seo-files.d.mts +2 -2
  32. package/dist/seo-files.d.ts +2 -2
  33. package/dist/seo.d.mts +6 -6
  34. package/dist/seo.d.ts +6 -6
  35. package/dist/server.d.mts +6 -6
  36. package/dist/server.d.ts +6 -6
  37. package/dist/server.mjs +1 -1
  38. package/dist/sitemap.d.mts +6 -6
  39. package/dist/sitemap.d.ts +6 -6
  40. package/dist/sitemap.mjs +1 -1
  41. package/dist/suggest.d.mts +2 -2
  42. package/dist/suggest.d.ts +2 -2
  43. package/package.json +2 -1
  44. package/skills/cavuno-board-account/SKILL.md +95 -1
  45. package/skills/cavuno-board-collections/SKILL.md +148 -0
  46. package/skills/cavuno-board-companies/SKILL.md +43 -1
  47. package/skills/cavuno-board-filters/SKILL.md +71 -1
  48. package/skills/cavuno-board-paywall/SKILL.md +5 -2
  49. package/skills/cavuno-board-post-a-job/SKILL.md +64 -0
  50. package/skills/manifest.json +11 -4
package/dist/sitemap.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { BoardSdk } from './index.js';
2
- import './_spec-EgN7VmAI.js';
3
- import './search-Dx1TiAZk.js';
4
- import './board-CsEAgsKh.js';
5
- import './jobs-ub3gywNQ.js';
6
- import './seo-BbG2Ujyo.js';
7
- import './blog-BIjFcNcm.js';
2
+ import './_spec-B_pBNVUd.js';
3
+ import './search-BK_zHHNQ.js';
4
+ import './board-fVAXZ8T5.js';
5
+ import './jobs-BRysn2m0.js';
6
+ import './seo-C7gMl6Y7.js';
7
+ import './blog-DzGRUsj5.js';
8
8
 
9
9
  /**
10
10
  * Sitemap primitives — the pure XML + bucket-filename logic behind a board
package/dist/sitemap.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-DLSO6XXY.mjs";
4
4
  import {
5
5
  isNotFound
6
- } from "./chunk-DP2ZZZEO.mjs";
6
+ } from "./chunk-DUTQOLOF.mjs";
7
7
  import {
8
8
  BOARD_PATHS,
9
9
  blogAuthorPath,
@@ -1,5 +1,5 @@
1
- import { h as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, g as SuggestResult } from './search-DKS7OIn2.mjs';
2
- import './_spec-EgN7VmAI.mjs';
1
+ import { h as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, g as SuggestResult } from './search-BlB690ma.mjs';
2
+ import './_spec-B_pBNVUd.mjs';
3
3
 
4
4
  /**
5
5
  * `@cavuno/board/suggest` — framework-agnostic headless search-suggest
package/dist/suggest.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { h as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, g as SuggestResult } from './search-Dx1TiAZk.js';
2
- import './_spec-EgN7VmAI.js';
1
+ import { h as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, g as SuggestResult } from './search-BK_zHHNQ.js';
2
+ import './_spec-B_pBNVUd.js';
3
3
 
4
4
  /**
5
5
  * `@cavuno/board/suggest` — framework-agnostic headless search-suggest
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cavuno/board",
3
- "version": "4.26.0",
3
+ "version": "4.27.0",
4
4
  "description": "Build custom job boards and careers pages with Cavuno’s TypeScript SDK.",
5
5
  "keywords": [
6
6
  "api-client",
@@ -236,6 +236,7 @@
236
236
  "@playwright/test": "^1.59.1",
237
237
  "@types/node": "24.9.1",
238
238
  "esbuild": "^0.27.7",
239
+ "parse5": "7.3.0",
239
240
  "publint": "^0.3.12",
240
241
  "tsup": "^8.4.0",
241
242
  "tsx": "^4.19.2",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cavuno-board-account
3
- description: Candidate self-service boundary with @cavuno/board. Use for account, profile, resume onboarding, recommended jobs, avatar, experience, education, skills, languages, or notification preferences.
3
+ description: Candidate self-service boundary with @cavuno/board. Use for account, profile, custom profile fields, collection selections, resume onboarding, recommended jobs, avatar, experience, education, skills, languages, or notification preferences.
4
4
  ---
5
5
 
6
6
  # Candidate self-service boundary
@@ -41,6 +41,97 @@ const { available } = await board.me.profile.handleAvailable('jane');
41
41
 
42
42
  Profile updates are merge-patches. Handle availability is advisory—your current handle counts as available—and the write re-checks uniqueness.
43
43
 
44
+ ## Render the profile form from the operator's layout
45
+
46
+ `board.context().forms.talent` is the candidate profile form as one ordered list, set by the operator. Render it in order and skip entries with `visible: false`. Each entry has `required`; validate it in the form before saving.
47
+
48
+ ```ts snippet
49
+ const { forms } = await board.context();
50
+
51
+ for (const field of forms.talent) {
52
+ if (!field.visible) continue;
53
+ if (field.kind === 'builtin') {
54
+ renderProfileBuiltin(field.key, { required: field.required });
55
+ } else if (field.definition.editableByOwner) {
56
+ renderProfileField(field, { required: field.required });
57
+ }
58
+ }
59
+ ```
60
+
61
+ Built-in keys map to the methods below: `name` → `displayName` and `headline`, `location`, `jobSearchStatus`, `bio` → `profile.update`; `email` → `requestEmailChange`; `avatar` → `uploadAvatar`; `experience` and `education` → their CRUD methods; `skills` and `languages` → `updateSkills` / `updateLanguages`. `name` and `email` are locked (always shown and required). Skip a built-in key you do not recognise.
62
+
63
+ `custom` entries are scalar profile fields (`updateCustomFields`) and `collection` entries are collection fields (`updateObjectReferences`); both carry their public `definition` inline. Render only definitions with `editableByOwner: true` as inputs. The layout lists public fields only; private owner-editable fields come from `retrieveCustomFields` and `retrieveObjectReferences` below. A collection definition with `required: true` needs at least one selection while shown.
64
+
65
+ ## Custom fields and collection selections
66
+
67
+ Scalar custom-field updates are additive. Render controls from the returned definitions, preserve their value types, and send only owner-editable keys. Omitted keys remain unchanged.
68
+
69
+ Collection-selection updates replace the complete editable set. Read before writing when the user means to retain existing selections. Each resolved selection includes its display title, optional description and logo, shared attributes, and profile-specific details. Use `listObjectReferenceChoices` to search the active choices for one editable field; do not guess record IDs from names.
70
+
71
+ ```ts snippet
72
+ const customFields = await board.me.profile.retrieveCustomFields();
73
+ await board.me.profile.updateCustomFields({
74
+ values: { membership_number: 'A-1234' },
75
+ });
76
+
77
+ const references = await board.me.profile.retrieveObjectReferences();
78
+ const choices = await board.me.profile.listObjectReferenceChoices(
79
+ 'certifications',
80
+ { search: 'professional', limit: 20 });
81
+ await board.me.profile.updateObjectReferences({
82
+ selections: [
83
+ ...references.selections
84
+ .filter((selection) => selection.fieldKey !== 'certifications')
85
+ .map(({ fieldKey, recordId, values, entries }) => ({
86
+ fieldKey,
87
+ recordId,
88
+ values,
89
+ entries,
90
+ })),
91
+ {
92
+ fieldKey: 'certifications',
93
+ recordId: choices.data[0]!.id,
94
+ },
95
+ ],
96
+ });
97
+ ```
98
+
99
+ Approved company members use the parallel company methods. The company slug comes before the body or field key.
100
+
101
+ ```ts snippet
102
+ const companyFields = await board.me.companies.retrieveCustomFields('acme');
103
+ await board.me.companies.updateCustomFields('acme', {
104
+ values: { founded_year: 2018 },
105
+ });
106
+
107
+ const companyReferences =
108
+ await board.me.companies.retrieveObjectReferences('acme');
109
+ const technologies =
110
+ await board.me.companies.listObjectReferenceChoices(
111
+ 'acme',
112
+ 'technologies',
113
+ { search: 'typescript', limit: 20 });
114
+ await board.me.companies.updateObjectReferences('acme', {
115
+ selections: [
116
+ ...companyReferences.selections
117
+ .filter((selection) => selection.fieldKey !== 'technologies')
118
+ .map(({ fieldKey, recordId, values, entries }) => ({
119
+ fieldKey,
120
+ recordId,
121
+ values,
122
+ entries,
123
+ })),
124
+ {
125
+ fieldKey: 'technologies',
126
+ recordId: technologies.data[0]!.id,
127
+ values: { years: 4 },
128
+ },
129
+ ],
130
+ });
131
+ ```
132
+
133
+ Collection creation, collection entry mutation, and bulk imports are operator API operations. Keep that API key on the server; the Board SDK intentionally exposes only public discovery and signed-in owner reads and writes.
134
+
44
135
  ## Experience and education
45
136
 
46
137
  Both collections are id-keyed CRUD with merge-patch updates. Experience creation requires `title`, `companyName`, and `startDate`; education creation requires `institutionName`.
@@ -161,7 +252,10 @@ await board.me.notificationPreferences.unsubscribeWithToken({
161
252
 
162
253
  ## Completion gate
163
254
 
255
+ - The profile form follows `forms.talent`: order matches, hidden fields are absent, and required fields block save while empty.
164
256
  - Profile update is visible after retrieval.
257
+ - Scalar field updates preserve omitted values; collection selection updates preserve every selection the user meant to keep.
258
+ - Choice searches use the returned record `id`, and an optional returned `logoUrl` renders without a separate logo map.
165
259
  - Adding a skill preserves every existing skill.
166
260
  - Uploaded `avatarUrl` renders.
167
261
  - Resume parsing reaches `parsed`, reports `parseFailureReason`, or reaches the explicit delayed state without blocking editing.
@@ -0,0 +1,148 @@
1
+ ---
2
+ name: cavuno-board-collections
3
+ description: Custom profile fields and collection references with @cavuno/board. Use for rendering company, talent or job custom fields, collection entries, benefits/technologies/certification sections, or collection-backed filters.
4
+ ---
5
+
6
+ # Custom fields and collections
7
+
8
+ An operator defines two kinds of extra data. A **custom field** stores one typed value directly on a company, talent profile, or job. A **collection reference** points that record at reusable entries — benefits, technologies, certifications, learning resources — that the operator maintains once and every record shares.
9
+
10
+ Hosted Cavuno boards do not render either one. The API resolves them and your frontend decides the layout, so this surface is read-heavy: definitions tell you what to draw, resolved selections tell you what to draw it with.
11
+
12
+ ## The read model
13
+
14
+ Scalar values arrive opaque and keyed by field key; the labels, types, and select options live in a separate definition list. Never hard-code a label.
15
+
16
+ - Job definitions: `board.context()` → `customFields.job` (scalar) and `customFields.jobCollections` (collection fields).
17
+ - Company and talent definitions: `board.profileFields.retrieve('company' | 'candidate')` → `definitions` (scalar) and `referenceDefinitions` (collection fields).
18
+
19
+ To build an input form rather than a display, read `board.context().forms` (`job`, `company`, `talent`): the operator's field order with each custom and collection definition inlined, plus which fields are shown and required. `cavuno-board-post-a-job`, `cavuno-board-account`, and `cavuno-board-companies` show how to render each form.
20
+
21
+ A collection field definition carries `multiple`, and a multiple one may carry `maxSelections` — an operator-defined maximum, up to 100. Cap a picker at that number when it is present, at 100 when it is absent, and at one when `multiple` is false.
22
+
23
+ A job collection definition may also carry `allowOverrides` — each job can replace a selected entry's title and description — and `descriptionFieldKey`, the entry field whose value is the default description. Both are absent when the operator has not set them.
24
+
25
+ Resolved data rides on the detail responses:
26
+
27
+ ```ts snippet
28
+ const context = await board.context();
29
+ const definitions = context.customFields.job;
30
+
31
+ const job = await board.jobs.retrieve('senior-chef');
32
+ job.customFieldValues.clearance;
33
+ job.resolvedCollectionFields;
34
+
35
+ const company = await board.companies.retrieve('acme');
36
+ company.customFieldValues.member_tier;
37
+ company.objectReferences;
38
+
39
+ const profile = await board.talent.retrieve('jane-doe');
40
+ profile.customFieldValues.available_for_mentoring;
41
+ profile.objectReferences;
42
+ ```
43
+
44
+ A company or talent selection (`objectReferences[]`) carries `fieldKey`, `fieldLabel`, `recordId`, the entry's `slug` when it has one, a resolved `title` and optional `description`, optional `logoUrl`, the entry's own `fields` and `attributes`, and the record-specific `valueDefinitions` / `values` plus `entryDefinitions` / `entries`. `titleOverride` and `descriptionOverride` are present only when the profile replaced the shared wording — `title` and `description` are already resolved, so render those.
45
+
46
+ ```ts snippet
47
+ const company = await board.companies.retrieve('acme');
48
+
49
+ for (const selection of company.objectReferences) {
50
+ if (selection.fieldKey !== 'benefits') continue;
51
+ renderBenefit({
52
+ heading: selection.title,
53
+ body: selection.description,
54
+ logoUrl: selection.logoUrl,
55
+ daysPerYear: selection.values.days_per_year,
56
+ });
57
+ }
58
+ ```
59
+
60
+ `values` holds one set of typed details for the selection. `entries` holds repeatable rows, each `{ key, values }` — several awards of one certification, for example. Both are `{}` / `[]` when the field defines no details.
61
+
62
+ Jobs use a different shape: `resolvedCollectionFields` is an array of `{ key, label, entries }`, and each entry is `{ id, name, slug?, logoUrl?, title, description, titleOverride, descriptionOverride, fields, values, references? }`. `title` and `description` are the entry's wording on this job, already resolved: the job's own wording when it set one, otherwise the entry `name` and the value of the definition's `descriptionFieldKey` field (`description` is `null` when neither exists). `titleOverride` and `descriptionOverride` hold the job's own wording, or `null` when the job uses the defaults. A job's wording is plain text; when the default description field is rich text, `description` is sanitised HTML with that wording escaped into paragraphs, so insert `description` as HTML only in that case. Jobs have no per-selection details or repeatable rows.
63
+
64
+ ```ts snippet
65
+ const job = await board.jobs.retrieve('senior-chef');
66
+
67
+ for (const field of job.resolvedCollectionFields) {
68
+ for (const entry of field.entries) {
69
+ renderChip({
70
+ label: entry.title,
71
+ description: entry.description,
72
+ logoUrl: entry.logoUrl,
73
+ group: field.label,
74
+ });
75
+ }
76
+ }
77
+ ```
78
+
79
+ Visibility is enforced server-side. Public reads return only public scalar fields, public collection fields, and active entries; private definitions and archived entries are omitted entirely, not nulled. Do not render a placeholder for a key that is absent — the operator meant it to be invisible. An owner read returns the private owner-editable fields too, so the same component can receive either payload; branch on what is present.
80
+
81
+ ## Owner writes
82
+
83
+ Collection creation, entry mutation, and imports are operator API operations and are not on this SDK. A signed-in candidate or approved company member edits their own selections through `board.me`, where a selection update replaces the whole editable set and a scalar update is additive. Those read-modify-write rules, the choice search, and the repeatable-entry `key` contract live in `cavuno-board-account` — follow that skill for any write.
84
+
85
+ An approved company member sets a job's own wording with `collectionOverrides` on `board.me.companies.jobs.create` / `update`: one `{ fieldKey, recordId, title?, description? }` per selected entry, only on a field whose definition has `allowOverrides`. `cavuno-board-post-a-job` shows the form.
86
+
87
+ ## Rendering
88
+
89
+ - Take every label from the definition list (`referenceDefinitions[].label`, `definitions[].label`, `fieldLabel`, `field.label`), never from the key.
90
+ - Render in the order the API returns. Definitions are in operator display order and selections follow the record's order; do not re-sort.
91
+ - Resolve a select value through the definition's `options[].key` → `label`. A stored value is an option key, or an array of them for multi-select.
92
+ - Media attributes are already metadata objects: `{ id, name, contentType, sizeBytes, url }`. An `image` or `file` attribute is one object, an `image_gallery` attribute is an array. Load by the returned `url`; never build a URL yourself.
93
+ - `logoUrl` is optional on a selection, a job collection entry, and a choice row. The operator gives a collection a logo by adding an `image` field and marking it as the logo; `logoUrl` is that image's URL for the entry (older entries may instead carry a logo URL saved before logos were image fields). When present it is the entry's own logo — render it directly, loading it like any other media `url`, rather than keeping a second name-to-logo map, and fall back to initials or nothing when it is absent. The logo image may also appear among the entry's `attributes`; do not render it twice.
94
+ - A `rich_text` value is sanitised HTML from the server. Insert it as HTML; do not re-escape it into visible tags and do not run a second markdown pass over it.
95
+ - `references` on an entry is keyed by the reference field's key and always holds an array, even for a single reference.
96
+ - Build an entry URL from its `slug`, never from `name` or `id`. A slug is URL-safe and unique within one collection only, so keep the collection or field in the path (`/benefits/annual-leave`). Renaming an entry keeps its slug; when the operator changes a slug, the previous one stays reserved for that entry. `slug` is on selections, job collection entries, nested `references`, and choice rows, and can be absent on entries saved before slugs existed — fall back to a non-linked render then.
97
+ - Number, boolean, and date values keep their JSON types. `0` and `false` are real values — test for `undefined`, not falsiness.
98
+
99
+ ## Filters
100
+
101
+ Public collection entries are filter values. Company and talent search accept `objectReferences: [{ key, recordIds }]`; job, company, and talent search accept `customFields: [{ key, values }]`. Load the options first and submit the returned record `id`.
102
+
103
+ ```ts snippet
104
+ const choices = await board.profileFields.choices('company', 'technologies', {
105
+ search: 'typescript',
106
+ limit: 20,
107
+ });
108
+
109
+ const companies = await board.companies.search({
110
+ objectReferences: [
111
+ { key: 'technologies', recordIds: [choices.data[0]!.id] },
112
+ ],
113
+ });
114
+
115
+ const talent = await board.talent.list({
116
+ objectReferences: [
117
+ { key: 'certifications', recordIds: [choices.data[0]!.id] },
118
+ ],
119
+ });
120
+
121
+ const jobChoices = await board.jobs.collectionChoices('tech_stack', {
122
+ limit: 20,
123
+ });
124
+
125
+ void companies.data;
126
+ void talent.data;
127
+ void jobChoices.data;
128
+ ```
129
+
130
+ Clause and cursor rules, the `invalid_filter` cases, and the listing-URL contract belong to `cavuno-board-filters`.
131
+
132
+ ## Completion gate
133
+
134
+ Finish only after every applicable check passes:
135
+
136
+ - Every rendered label comes from a definition or a resolved `fieldLabel`, and no key is displayed raw.
137
+ - Select values render as their option label, and `0` / `false` survive to the screen.
138
+ - A selection or job collection entry renders its resolved `title` and `description`, so an overridden entry shows the record's wording and a non-overridden one shows the collection default.
139
+ - Media renders from the returned `url`, an `image_gallery` renders every image, and a missing `logoUrl` degrades without a broken image.
140
+ - Rich text renders as formatted HTML, not escaped tags.
141
+ - A private field and an archived entry are absent from the public render, with no empty placeholder left behind.
142
+ - Job pages read `resolvedCollectionFields`, and company or talent pages read `objectReferences` — neither shape is assumed for the other.
143
+ - Filter requests carry record IDs from a choices call, and the filter control's options come from the public definition list.
144
+ - A multi-select picker stops at the definition's `maxSelections`, or at 100 when the definition omits it.
145
+
146
+ ## Cavuno SDK reference
147
+
148
+ For setup and API details beyond this workflow, use the [Cavuno Board SDK documentation](https://cavuno.com/docs/sdk).
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cavuno-board-companies
3
- description: Company catalog reads with @cavuno/board. Use for company indexes, market archives, profiles, company job rails, similar companies, or company salary pages.
3
+ description: Company catalog reads with @cavuno/board. Use for company indexes, market archives, profiles, company job rails, similar companies, company salary pages, or employer job drafts.
4
4
  ---
5
5
 
6
6
  # Companies
@@ -39,6 +39,47 @@ await board.me.companies.revokeInvite('acme', invites[0].id);
39
39
  const { companySlug } = await board.me.acceptInvite({ token });
40
40
  ```
41
41
 
42
+ An approved member drafts jobs with `board.me.companies.jobs.create`. The body
43
+ must carry `remoteOption` (`on_site`, `hybrid`, or `remote`): `on_site` and
44
+ `hybrid` also need `officeLocations`, and `remote` needs `remotePermits`. A
45
+ salary is optional, but a `salaryMin` or `salaryMax` needs both
46
+ `salaryCurrency` and `salaryTimeframe`. `update` cannot clear `remoteOption`,
47
+ and an update that sends salary fields is checked against the stored salary.
48
+ Each violation is a 400 `validation_bad_request` naming the field.
49
+
50
+ ```ts snippet
51
+ const draft = await board.me.companies.jobs.create('acme', {
52
+ title: 'Staff Engineer',
53
+ description: 'Build the platform.',
54
+ remoteOption: 'remote',
55
+ remotePermits: [{ type: 'worldwide', value: 'worldwide' }],
56
+ salaryMin: 150000,
57
+ salaryMax: 190000,
58
+ salaryCurrency: 'USD',
59
+ salaryTimeframe: 'per_year',
60
+ });
61
+ await board.me.companies.jobs.update('acme', draft.id, { salaryMax: 200000 });
62
+ ```
63
+
64
+ ## Render the company profile form
65
+
66
+ `board.context().forms.company` is the company profile form as one ordered list, set by the operator. Use it for `board.me.companies.create` and `update`: render in order, skip entries with `visible: false`, and validate `required` in the form before saving.
67
+
68
+ ```ts snippet
69
+ const { forms } = await board.context();
70
+
71
+ for (const field of forms.company) {
72
+ if (!field.visible) continue;
73
+ if (field.kind === 'builtin') {
74
+ renderCompanyBuiltin(field.key, { required: field.required });
75
+ } else if (field.definition.editableByOwner) {
76
+ renderCompanyField(field, { required: field.required });
77
+ }
78
+ }
79
+ ```
80
+
81
+ Built-in keys `name`, `website`, `summary`, `xUrl`, `linkedinUrl`, `facebookUrl`, and `description` are the same-named body fields of `create` / `update`; `logo` is `board.me.companies.uploadLogo`. `name` is locked (always shown and required). Skip a built-in key you do not recognise. `custom` entries write through `updateCustomFields` and `collection` entries through `updateObjectReferences`; both carry their public `definition` inline, and only `editableByOwner` definitions are inputs. See `cavuno-board-account` for those write rules.
82
+
42
83
  ## List and search
43
84
 
44
85
  `companies.list` returns `CompanyListEnvelope`: `ListEnvelope<PublicCompany>`
@@ -146,6 +187,7 @@ one. Gate a Salaries tab with `company.salarySampleCount > 0` from
146
187
 
147
188
  Finish only after every applicable check passes:
148
189
 
190
+ - The company form follows `forms.company`: order matches, hidden fields are absent, and required fields block save while empty.
149
191
  - A known company is a `public_company`; only its retrieve response has a
150
192
  `markets` array.
151
193
  - A listed market slug works as `marketSlug`, while an invented one produces a
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cavuno-board-filters
3
- description: Listing-filter contracts with @cavuno/board. Use for job filter controls, sort controls, listing URL validation, or taxonomy-backed filter options.
3
+ description: Listing-filter contracts with @cavuno/board. Use for job filter controls, profile-field filters, collection choices, sort controls, listing URL validation, or taxonomy-backed filter options.
4
4
  ---
5
5
 
6
6
  # Listing filters
@@ -41,6 +41,33 @@ const page = await board.jobs.list({
41
41
  Company slugs are the URL identity. Map `filters.company` directly to
42
42
  `companySlug` in `jobs.list` queries or `jobs.search` filters.
43
43
 
44
+ ## Filter by category in search
45
+
46
+ An "All filters" panel can carry a category facet next to seniority, company,
47
+ workplace and employment type. `parseListingFilters` reads `category` the same
48
+ way as company (repeated or comma-separated slugs, first 10 kept). Send it as
49
+ `filters.categories` in a `jobs.search` body; a job matches when it has any of
50
+ the given categories, and the other filters still apply.
51
+
52
+ ```ts snippet
53
+ const results = await board.jobs.search({
54
+ query: filters.q,
55
+ sort: filters.sort,
56
+ filters: {
57
+ categories: filters.category,
58
+ seniority: filters.seniority,
59
+ companySlug: filters.company,
60
+ remoteOption: filters.remoteOption ? [filters.remoteOption] : undefined,
61
+ },
62
+ limit: 20,
63
+ });
64
+ ```
65
+
66
+ `categories` accepts a term's `sourceSlug` or its board-language
67
+ `canonicalSlug`; an unknown slug matches no jobs. `jobs.list` takes a single
68
+ `category` instead, which seeds a category landing page rather than filtering
69
+ a search. Use `jobs.search` whenever the category is one facet among several.
70
+
44
71
  ## Render controls from the vocabulary
45
72
 
46
73
  ```ts snippet
@@ -123,6 +150,45 @@ const { items } = await board.search.suggest({
123
150
  The host router owns URL serialization and saved-filter persistence.
124
151
  Locations come from `board.taxonomy.places` rather than a static filter export.
125
152
 
153
+ ## Build filters from public profile fields
154
+
155
+ `board.profileFields.retrieve('company')` and `retrieve('candidate')` return only public scalar and collection-reference definitions. Use each stored field key for filtering. For a collection reference, page or search its active public choices and submit the returned record `id`; the optional `logoUrl` is display metadata.
156
+
157
+ ```ts snippet
158
+ const companyFields = await board.profileFields.retrieve('company');
159
+ const technologyChoices = await board.profileFields.choices(
160
+ 'company',
161
+ 'technologies',
162
+ { search: 'typescript', limit: 20 });
163
+
164
+ const companies = await board.companies.search({
165
+ query: 'engineering',
166
+ customFields: [{ key: 'member_tier', values: ['gold'] }],
167
+ objectReferences: [
168
+ { key: 'technologies', recordIds: [technologyChoices.data[0]!.id] },
169
+ ],
170
+ });
171
+
172
+ const candidateFields = await board.profileFields.retrieve('candidate');
173
+ const certifications = await board.profileFields.choices(
174
+ 'candidate',
175
+ 'certifications',
176
+ { limit: 20 });
177
+ const talent = await board.talent.list({
178
+ customFields: [{ key: 'available_for_mentoring', values: [true] }],
179
+ objectReferences: [
180
+ { key: 'certifications', recordIds: [certifications.data[0]!.id] },
181
+ ],
182
+ });
183
+
184
+ void companyFields.definitions;
185
+ void candidateFields.referenceDefinitions;
186
+ void companies.data;
187
+ void talent.data;
188
+ ```
189
+
190
+ Clauses are AND-matched. Values or record IDs inside one clause are alternatives. Unknown, private, invalid, or archived choices return `invalid_filter`; each array accepts at most 10 clauses and each clause at most 10 choices. Job search supports scalar `customFields` under `filters`, but job collection references are outside this surface.
191
+
126
192
  ## Completion gate
127
193
 
128
194
  Finish only after every applicable check passes:
@@ -135,7 +201,11 @@ Finish only after every applicable check passes:
135
201
  relevance, newest, and salary high.
136
202
  - Category and skill filtering sends `sourceSlug`, while links use
137
203
  `canonicalSlug`.
204
+ - `/jobs?category=engineering,design` sends
205
+ `filters.categories: ['engineering', 'design']` to `jobs.search`.
138
206
  - Every paged taxonomy request forwards the previous opaque `nextCursor`.
207
+ - Company and talent custom controls come from `board.profileFields`; private definitions and archived choices never become filter options.
208
+ - Profile-filter requests use stored scalar values and returned collection entry IDs, not display labels.
139
209
 
140
210
  ## Cavuno SDK reference
141
211
 
@@ -26,7 +26,7 @@ for (const offer of offers) {
26
26
  }
27
27
  ```
28
28
 
29
- `offerType` is `'recurring' | 'lifetime'`; post the selected `offerKey` to checkout.
29
+ `offerType` is `'recurring' | 'lifetime'`; post the selected `offerKey` to checkout unchanged. Treat it as opaque: newer offers use plan IDs, while older offers may use tier keys. Never derive it from the billing interval.
30
30
 
31
31
  Gated catalog reads expose withheld inventory as `gatedCount`. The same jobs endpoint returns the entitled view when called with the candidate bearer token.
32
32
 
@@ -44,8 +44,11 @@ if ((page.gatedCount ?? 0) > 0) {
44
44
  ```ts snippet
45
45
  import { loadStripe } from '@stripe/stripe-js';
46
46
 
47
+ const { data: checkoutOffers } = await board.paywall.offers();
48
+ const selectedOffer = checkoutOffers[0]; // Or the offer selected in your UI.
49
+ if (!selectedOffer) throw new Error('No access offers available');
47
50
  const kit = await board.me.access.checkout({
48
- offerKey: 'monthly',
51
+ offerKey: selectedOffer.offerKey,
49
52
  returnPath: '/account/access',
50
53
  colorMode: 'light',
51
54
  });
@@ -24,6 +24,69 @@ for (const plan of plans) {
24
24
 
25
25
  Pass the chosen `plan.id` as `submission.selectedPlan`. Invoice-only plans collect `invoiceBilling` rather than immediate payment.
26
26
 
27
+ ## Render the form from the operator's layout
28
+
29
+ The operator orders the job form and chooses which fields show and which are required. `board.context().forms.job` is that form as one ordered list; render it top to bottom and never hard-code the order. Skip entries with `visible: false`: a hidden field is not collected and is never required.
30
+
31
+ ```ts snippet
32
+ const { forms, jobForm } = await board.context();
33
+
34
+ for (const field of forms.job) {
35
+ if (!field.visible) continue;
36
+ switch (field.kind) {
37
+ case 'builtin':
38
+ renderBuiltin(field.key, { required: field.required, locked: field.locked });
39
+ break;
40
+ case 'custom':
41
+ renderCustomField(field.definition, { required: field.required });
42
+ break;
43
+ case 'collection':
44
+ renderCollectionPicker(field.definition, { required: field.required });
45
+ break;
46
+ }
47
+ }
48
+ ```
49
+
50
+ A `builtin` entry is drawn by your own control for its `key`. A `locked` built-in is always shown and required; `lockReason` says why (for example `google_required` for the title and description Google for Jobs needs). Skip a built-in key you do not recognise. The keys map to submission fields:
51
+
52
+ | `key` | Submission fields |
53
+ | --- | --- |
54
+ | `employmentType` | `employmentType`, from `jobForm.employmentType.allowedOptions` |
55
+ | `seniority` | `seniority`, from `jobForm.seniority.allowedOptions` |
56
+ | `title` | `title` |
57
+ | `workArrangement` | `remoteOption`, from `jobForm.workArrangement.allowedOptions` |
58
+ | `location` | `officeLocations`, `inOfficePeriod`, `inOfficeFrequency` |
59
+ | `remoteEligibility` | `remoteWorkingPermits`, `remoteTimezones`, and `remoteSponsorship` when `jobForm.sponsorship.visible` |
60
+ | `description` | `description` |
61
+ | `salary` | `salaryRangeEnabled`, `salaryMin`, `salaryMax`, `salaryTimeframe`, `salaryCurrency` |
62
+ | `applyMethod` | `applicationUrl` |
63
+ | `company` | `companyName`, `companyWebsite`, and the resolved logo |
64
+
65
+ `contactName`, `contactEmail`, and the plan are not layout fields; always collect them. `jobForm` still carries each built-in's allowed options and salary bounds.
66
+
67
+ A `custom` entry renders from `definition` (`label`, `type`, `options`, `helpText`) and submits under `submission.customFieldValues[field.key]`; store option keys, never labels. A `collection` entry renders a picker capped by `definition.maxSelections` (one when `multiple` is false) with choices from `board.jobs.collectionChoices(field.key)`. The anonymous funnel takes no collection values, so collection fields belong on the signed-in employer job form (`board.me.companies.jobs.create`, `collectionValues`). Custom and collection rendering rules live in `cavuno-board-collections`.
68
+
69
+ When a collection definition has `allowOverrides`, the employer job form can give each selected entry its own wording for this job — "Unlimited PTO" in place of the shared "Annual leave", for example. Offer an edit action per selected entry with a title (up to 160 characters) and a plain-text description (up to 2,000 characters), and send them as `collectionOverrides`, one item per entry that has wording:
70
+
71
+ ```ts snippet
72
+ const created = await board.me.companies.jobs.create('acme', {
73
+ title: 'Head chef',
74
+ description: 'Run the kitchen.',
75
+ collectionValues: { benefits: [annualLeaveId] },
76
+ collectionOverrides: [
77
+ { fieldKey: 'benefits', recordId: annualLeaveId, title: 'Unlimited PTO' },
78
+ ],
79
+ });
80
+
81
+ await board.me.companies.jobs.update('acme', created.id, {
82
+ collectionOverrides: [],
83
+ });
84
+ ```
85
+
86
+ The list replaces the job's wording: omit it on `update` to keep what is stored, and send `[]` to restore every default. Wording for an entry the job does not select, or on a field without `allowOverrides`, is rejected with `jobs_constraint_violation`; `details.violations[].params.fieldKey` names the field, so show the message under it. Deselecting an entry drops its wording. The job's collection entries then return the resolved `title` and `description` to render.
87
+
88
+ Validate `required` client-side from these entries before submitting, so the poster sees a missing field before the request.
89
+
27
90
  ## Resolve a logo
28
91
 
29
92
  Both logo paths return a stored `publicUrl` for `create`. The SDK owns multipart encoding.
@@ -111,6 +174,7 @@ Bare-email billing lookup is intentionally absent because it would expose whethe
111
174
 
112
175
  ## Completion gate
113
176
 
177
+ - The form renders `forms.job` in order: hidden entries are absent, required and locked entries block submit while empty, and an unknown built-in key is skipped.
114
178
  - Every `create` call handles all four statuses.
115
179
  - Paid checkout redirects through `checkoutUrl`; the job appears only after webhook completion.
116
180
  - Free unmoderated submission returns a resolving `jobSlug`.
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "4.26.0",
2
+ "version": "4.27.0",
3
3
  "skills": [
4
4
  {
5
5
  "name": "cavuno-board-account",
6
- "description": "Candidate self-service boundary with @cavuno/board. Use for account, profile, resume onboarding, recommended jobs, avatar, experience, education, skills, languages, or notification preferences.",
6
+ "description": "Candidate self-service boundary with @cavuno/board. Use for account, profile, custom profile fields, collection selections, resume onboarding, recommended jobs, avatar, experience, education, skills, languages, or notification preferences.",
7
7
  "path": "skills/cavuno-board-account/SKILL.md",
8
8
  "framework": null,
9
9
  "category": "core"
@@ -36,9 +36,16 @@
36
36
  "framework": null,
37
37
  "category": "core"
38
38
  },
39
+ {
40
+ "name": "cavuno-board-collections",
41
+ "description": "Custom profile fields and collection references with @cavuno/board. Use for rendering company, talent or job custom fields, collection entries, benefits/technologies/certification sections, or collection-backed filters.",
42
+ "path": "skills/cavuno-board-collections/SKILL.md",
43
+ "framework": null,
44
+ "category": "core"
45
+ },
39
46
  {
40
47
  "name": "cavuno-board-companies",
41
- "description": "Company catalog reads with @cavuno/board. Use for company indexes, market archives, profiles, company job rails, similar companies, or company salary pages.",
48
+ "description": "Company catalog reads with @cavuno/board. Use for company indexes, market archives, profiles, company job rails, similar companies, company salary pages, or employer job drafts.",
42
49
  "path": "skills/cavuno-board-companies/SKILL.md",
43
50
  "framework": null,
44
51
  "category": "core"
@@ -52,7 +59,7 @@
52
59
  },
53
60
  {
54
61
  "name": "cavuno-board-filters",
55
- "description": "Listing-filter contracts with @cavuno/board. Use for job filter controls, sort controls, listing URL validation, or taxonomy-backed filter options.",
62
+ "description": "Listing-filter contracts with @cavuno/board. Use for job filter controls, profile-field filters, collection choices, sort controls, listing URL validation, or taxonomy-backed filter options.",
56
63
  "path": "skills/cavuno-board-filters/SKILL.md",
57
64
  "framework": null,
58
65
  "category": "core"