@cavuno/board 4.26.0 → 4.28.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 (53) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/dist/{_spec-EgN7VmAI.d.mts → _spec-DQvmsp5o.d.mts} +589 -67
  3. package/dist/{_spec-EgN7VmAI.d.ts → _spec-DQvmsp5o.d.ts} +589 -67
  4. package/dist/bin.mjs +7989 -10
  5. package/dist/{blog-BIjFcNcm.d.ts → blog-CUkvhn3c.d.ts} +1 -1
  6. package/dist/{blog-T7gQ0ciz.d.mts → blog-CvuObn9O.d.mts} +1 -1
  7. package/dist/{board-CsEAgsKh.d.ts → board-CCG1pO6r.d.ts} +25 -4
  8. package/dist/{board-q0svBKW3.d.mts → board-D6xOidUX.d.mts} +25 -4
  9. package/dist/{breadcrumbs-CFcCjuG7.d.mts → breadcrumbs-BBvJ5gq-.d.mts} +2 -2
  10. package/dist/{breadcrumbs-xuDid8BS.d.ts → breadcrumbs-D2Cwrju4.d.ts} +2 -2
  11. package/dist/browser/cavuno-board.global.min.js +1 -1
  12. package/dist/{chunk-DP2ZZZEO.mjs → chunk-EHSB2VAJ.mjs} +5 -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 +251 -14
  22. package/dist/index.d.ts +251 -14
  23. package/dist/index.js +218 -26
  24. package/dist/index.mjs +214 -27
  25. package/dist/{jobs-ub3gywNQ.d.ts → jobs-CEIuahbz.d.ts} +26 -2
  26. package/dist/{jobs-CfrxlDMU.d.mts → jobs-DjMcghv6.d.mts} +26 -2
  27. package/dist/{search-DKS7OIn2.d.mts → search-C0LiPXVb.d.mts} +1 -1
  28. package/dist/{search-Dx1TiAZk.d.ts → search-D-EjtZH3.d.ts} +1 -1
  29. package/dist/{seo-BbG2Ujyo.d.ts → seo-CjNGqZZH.d.ts} +1 -1
  30. package/dist/{seo-CI3wLKUW.d.mts → seo-DCQp8V1c.d.mts} +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 +122 -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-errors/SKILL.md +1 -1
  48. package/skills/cavuno-board-filters/SKILL.md +160 -1
  49. package/skills/cavuno-board-format/SKILL.md +52 -0
  50. package/skills/cavuno-board-paywall/SKILL.md +5 -2
  51. package/skills/cavuno-board-post-a-job/SKILL.md +88 -0
  52. package/skills/cavuno-board-salaries/SKILL.md +7 -0
  53. package/skills/manifest.json +11 -4
package/dist/server.d.mts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { BoardSdk } from './index.mjs';
2
- import './_spec-EgN7VmAI.mjs';
3
- import './search-DKS7OIn2.mjs';
4
- import './board-q0svBKW3.mjs';
5
- import './jobs-CfrxlDMU.mjs';
6
- import './seo-CI3wLKUW.mjs';
7
- import './blog-T7gQ0ciz.mjs';
2
+ import './_spec-DQvmsp5o.mjs';
3
+ import './search-C0LiPXVb.mjs';
4
+ import './board-D6xOidUX.mjs';
5
+ import './jobs-DjMcghv6.mjs';
6
+ import './seo-DCQp8V1c.mjs';
7
+ import './blog-CvuObn9O.mjs';
8
8
 
9
9
  /**
10
10
  * Session cookie codec — pure (no framework imports, no node imports) so it
package/dist/server.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-DQvmsp5o.js';
3
+ import './search-D-EjtZH3.js';
4
+ import './board-CCG1pO6r.js';
5
+ import './jobs-CEIuahbz.js';
6
+ import './seo-CjNGqZZH.js';
7
+ import './blog-CUkvhn3c.js';
8
8
 
9
9
  /**
10
10
  * Session cookie codec — pure (no framework imports, no node imports) so it
package/dist/server.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-Z4ALWIGS.mjs";
5
5
  import {
6
6
  isUnauthorized
7
- } from "./chunk-DP2ZZZEO.mjs";
7
+ } from "./chunk-EHSB2VAJ.mjs";
8
8
 
9
9
  // src/server/cookie.ts
10
10
  var COOKIE_ATTRIBUTES = "Path=/; HttpOnly; Secure; SameSite=Lax";
@@ -1,10 +1,10 @@
1
1
  import { BoardSdk } from './index.mjs';
2
- import './_spec-EgN7VmAI.mjs';
3
- import './search-DKS7OIn2.mjs';
4
- import './board-q0svBKW3.mjs';
5
- import './jobs-CfrxlDMU.mjs';
6
- import './seo-CI3wLKUW.mjs';
7
- import './blog-T7gQ0ciz.mjs';
2
+ import './_spec-DQvmsp5o.mjs';
3
+ import './search-C0LiPXVb.mjs';
4
+ import './board-D6xOidUX.mjs';
5
+ import './jobs-DjMcghv6.mjs';
6
+ import './seo-DCQp8V1c.mjs';
7
+ import './blog-CvuObn9O.mjs';
8
8
 
9
9
  /**
10
10
  * Sitemap primitives — the pure XML + bucket-filename logic behind a board
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-DQvmsp5o.js';
3
+ import './search-D-EjtZH3.js';
4
+ import './board-CCG1pO6r.js';
5
+ import './jobs-CEIuahbz.js';
6
+ import './seo-CjNGqZZH.js';
7
+ import './blog-CUkvhn3c.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-EHSB2VAJ.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-C0LiPXVb.mjs';
2
+ import './_spec-DQvmsp5o.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-D-EjtZH3.js';
2
+ import './_spec-DQvmsp5o.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.28.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
@@ -11,6 +11,31 @@ Anonymous methods here are token-based email unsubscribe and email-change confir
11
11
 
12
12
  The host application owns forms, file pickers, and cookie plumbing; this SDK surface supplies data operations.
13
13
 
14
+ ## Candidate screens are candidate-only
15
+
16
+ A board user is either a candidate or an employer (`me.role`). Only candidates have a candidate profile. Render candidate screens and fields (the profile form, profile completeness, resume upload, experience, education, skills, and languages) only when `me.role === 'candidate'`. On the account page, give an employer account the employer profile view instead: avatar, `displayName` (the one profile field every account can update), and a link to the employer screens (`board.me.companies.*`). Never show them the empty candidate form.
17
+
18
+ The API is the backstop: a profile write from an account with no candidate profile returns 403 `candidate_profile_required` before anything is saved. `displayName` alone is the one profile field every account can update. Surface this error as a message; never swallow it and report "Saved".
19
+
20
+ ```ts snippet
21
+ import { isBoardApiError } from '@cavuno/board';
22
+
23
+ const me = await board.me.retrieve();
24
+ if (me.role !== 'candidate') {
25
+ return renderEmployerProfile(me); // avatar, displayName, link to employer screens
26
+ }
27
+
28
+ try {
29
+ await board.me.profile.update({ headline: 'Staff Engineer' });
30
+ showSaved();
31
+ } catch (error) {
32
+ if (isBoardApiError(error) && error.code === 'candidate_profile_required') {
33
+ return showError('Only candidate accounts have a candidate profile.');
34
+ }
35
+ throw error;
36
+ }
37
+ ```
38
+
14
39
  ## Account and profile
15
40
 
16
41
  Account deletion is a synchronous, irreversible cascade over the profile, collections, saved jobs, alerts, avatar, and resume. Obtain explicit confirmation before calling it.
@@ -41,6 +66,97 @@ const { available } = await board.me.profile.handleAvailable('jane');
41
66
 
42
67
  Profile updates are merge-patches. Handle availability is advisory—your current handle counts as available—and the write re-checks uniqueness.
43
68
 
69
+ ## Render the profile form from the operator's layout
70
+
71
+ `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.
72
+
73
+ ```ts snippet
74
+ const { forms } = await board.context();
75
+
76
+ for (const field of forms.talent) {
77
+ if (!field.visible) continue;
78
+ if (field.kind === 'builtin') {
79
+ renderProfileBuiltin(field.key, { required: field.required });
80
+ } else if (field.definition.editableByOwner) {
81
+ renderProfileField(field, { required: field.required });
82
+ }
83
+ }
84
+ ```
85
+
86
+ 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.
87
+
88
+ `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.
89
+
90
+ ## Custom fields and collection selections
91
+
92
+ 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.
93
+
94
+ 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.
95
+
96
+ ```ts snippet
97
+ const customFields = await board.me.profile.retrieveCustomFields();
98
+ await board.me.profile.updateCustomFields({
99
+ values: { membership_number: 'A-1234' },
100
+ });
101
+
102
+ const references = await board.me.profile.retrieveObjectReferences();
103
+ const choices = await board.me.profile.listObjectReferenceChoices(
104
+ 'certifications',
105
+ { search: 'professional', limit: 20 });
106
+ await board.me.profile.updateObjectReferences({
107
+ selections: [
108
+ ...references.selections
109
+ .filter((selection) => selection.fieldKey !== 'certifications')
110
+ .map(({ fieldKey, recordId, values, entries }) => ({
111
+ fieldKey,
112
+ recordId,
113
+ values,
114
+ entries,
115
+ })),
116
+ {
117
+ fieldKey: 'certifications',
118
+ recordId: choices.data[0]!.id,
119
+ },
120
+ ],
121
+ });
122
+ ```
123
+
124
+ Approved company members use the parallel company methods. The company slug comes before the body or field key.
125
+
126
+ ```ts snippet
127
+ const companyFields = await board.me.companies.retrieveCustomFields('acme');
128
+ await board.me.companies.updateCustomFields('acme', {
129
+ values: { founded_year: 2018 },
130
+ });
131
+
132
+ const companyReferences =
133
+ await board.me.companies.retrieveObjectReferences('acme');
134
+ const technologies =
135
+ await board.me.companies.listObjectReferenceChoices(
136
+ 'acme',
137
+ 'technologies',
138
+ { search: 'typescript', limit: 20 });
139
+ await board.me.companies.updateObjectReferences('acme', {
140
+ selections: [
141
+ ...companyReferences.selections
142
+ .filter((selection) => selection.fieldKey !== 'technologies')
143
+ .map(({ fieldKey, recordId, values, entries }) => ({
144
+ fieldKey,
145
+ recordId,
146
+ values,
147
+ entries,
148
+ })),
149
+ {
150
+ fieldKey: 'technologies',
151
+ recordId: technologies.data[0]!.id,
152
+ values: { years: 4 },
153
+ },
154
+ ],
155
+ });
156
+ ```
157
+
158
+ 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.
159
+
44
160
  ## Experience and education
45
161
 
46
162
  Both collections are id-keyed CRUD with merge-patch updates. Experience creation requires `title`, `companyName`, and `startDate`; education creation requires `institutionName`.
@@ -161,7 +277,12 @@ await board.me.notificationPreferences.unsubscribeWithToken({
161
277
 
162
278
  ## Completion gate
163
279
 
280
+ - Candidate screens render only for `me.role === 'candidate'`; an employer account gets the employer profile view (avatar, `displayName`, link to employer screens).
281
+ - A `candidate_profile_required` 403 shows an error and never a "Saved" state.
282
+ - The profile form follows `forms.talent`: order matches, hidden fields are absent, and required fields block save while empty.
164
283
  - Profile update is visible after retrieval.
284
+ - Scalar field updates preserve omitted values; collection selection updates preserve every selection the user meant to keep.
285
+ - Choice searches use the returned record `id`, and an optional returned `logoUrl` renders without a separate logo map.
165
286
  - Adding a skill preserves every existing skill.
166
287
  - Uploaded `avatarUrl` renders.
167
288
  - 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
@@ -44,7 +44,7 @@ try {
44
44
  }
45
45
  ```
46
46
 
47
- The remaining status guards are `isForbidden` for 403 and `isConflict` for 409. Record `code` and `requestId` in support diagnostics. Because v1 may add error codes, unmatched `BoardApiError` instances still need a safe generic state.
47
+ The remaining status guards are `isForbidden` for 403 and `isConflict` for 409. A 403 `candidate_profile_required` means an account that is not a candidate (for example an employer) tried to write candidate profile data: gate candidate screens on `me.role === 'candidate'` and show the error rather than a success state. Record `code` and `requestId` in support diagnostics. Because v1 may add error codes, unmatched `BoardApiError` instances still need a safe generic state.
48
48
 
49
49
  **Complete when:** every expected failure has one typed branch, an invalid resource renders not-found, and unmatched failures retain their original error.
50
50