@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
@@ -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, job and company custom-field filters, 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,129 @@ 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
+ ## Filter jobs by a job custom field
154
+
155
+ The operator's job custom fields arrive as definitions on
156
+ `board.context().customFields.job`. Expose a field as a filter only when the
157
+ operator intends it as one: keep an explicit list of filter keys in your app
158
+ config. Do not render a dropdown for every select field automatically; most
159
+ custom fields are details for the job page, not listing facets.
160
+
161
+ ```ts snippet
162
+ const context = await board.context();
163
+
164
+ // The operator picked these; everything else stays a job-page detail.
165
+ const FILTER_FIELD_KEYS = ['work_type'];
166
+ const filterFields = context.customFields.job.filter(
167
+ (field) =>
168
+ FILTER_FIELD_KEYS.includes(field.key) &&
169
+ (field.type === 'single_select' || field.type === 'multi_select'));
170
+
171
+ // Controls: option KEYS are the values, option labels are the text.
172
+ const controls = filterFields.map((field) => ({
173
+ key: field.key,
174
+ label: field.label,
175
+ options: (field.options ?? []).map((option) => ({
176
+ value: option.key,
177
+ label: option.label,
178
+ })),
179
+ }));
180
+
181
+ // URL state holds option keys (`/jobs?work_type=contract`), never labels.
182
+ // Drop anything that is not a current option before it reaches the API.
183
+ const customFields = filterFields.flatMap((field) => {
184
+ const values = searchParams
185
+ .getAll(field.key)
186
+ .filter((value) => field.options?.some((option) => option.key === value));
187
+ return values.length > 0 ? [{ key: field.key, values }] : [];
188
+ });
189
+
190
+ const results = await board.jobs.search({
191
+ query: filters.q,
192
+ filters: {
193
+ seniority: filters.seniority,
194
+ customFields: customFields.length > 0 ? customFields : undefined,
195
+ },
196
+ limit: 20,
197
+ });
198
+ ```
199
+
200
+ Clauses are AND-matched; values inside one clause are alternatives. Each
201
+ request takes at most 10 clauses of 10 values. An unknown key or an option key
202
+ the field no longer offers returns `invalid_filter`, so validate URL input
203
+ against the definitions as above. Boolean and number fields filter the same way
204
+ with `values: [true]` or `values: [3]`. Labels change when the operator renames
205
+ an option; keys do not, so bookmarked URLs keep working.
206
+
207
+ ## Build filters from public profile fields
208
+
209
+ `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.
210
+
211
+ ```ts snippet
212
+ const companyFields = await board.profileFields.retrieve('company');
213
+ const technologyChoices = await board.profileFields.choices(
214
+ 'company',
215
+ 'technologies',
216
+ { search: 'typescript', limit: 20 });
217
+
218
+ const companies = await board.companies.search({
219
+ query: 'engineering',
220
+ customFields: [{ key: 'member_tier', values: ['gold'] }],
221
+ objectReferences: [
222
+ { key: 'technologies', recordIds: [technologyChoices.data[0]!.id] },
223
+ ],
224
+ });
225
+
226
+ const candidateFields = await board.profileFields.retrieve('candidate');
227
+ const certifications = await board.profileFields.choices(
228
+ 'candidate',
229
+ 'certifications',
230
+ { limit: 20 });
231
+ const talent = await board.talent.list({
232
+ customFields: [{ key: 'available_for_mentoring', values: [true] }],
233
+ objectReferences: [
234
+ { key: 'certifications', recordIds: [certifications.data[0]!.id] },
235
+ ],
236
+ });
237
+
238
+ void companyFields.definitions;
239
+ void candidateFields.referenceDefinitions;
240
+ void companies.data;
241
+ void talent.data;
242
+ ```
243
+
244
+ 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.
245
+
246
+ ## Filter jobs by job and company custom fields
247
+
248
+ `jobs.search` takes two custom-field filters. `filters.customFields` matches the job's own fields, defined in `board.context().customFields.job`. `filters.companyCustomFields` matches the public profile fields of the job's company, defined in `board.profileFields.retrieve('company')`. Both use the clause shape above; the two lists never share keys, so a job field and a company field may both be called `type`.
249
+
250
+ ```ts snippet
251
+ const [context, companyFields] = await Promise.all([
252
+ board.context(),
253
+ board.profileFields.retrieve('company'),
254
+ ]);
255
+ const opportunityType = context.customFields.job.find(
256
+ (field) => field.key === 'opportunity_type');
257
+ const employerType = companyFields.definitions.find(
258
+ (field) => field.key === 'employer_type');
259
+
260
+ const results = await board.jobs.search({
261
+ query: filters.q,
262
+ filters: {
263
+ customFields: [{ key: 'opportunity_type', values: ['identified'] }],
264
+ companyCustomFields: [{ key: 'employer_type', values: ['acco'] }],
265
+ },
266
+ limit: 20,
267
+ });
268
+
269
+ void opportunityType;
270
+ void employerType;
271
+ void results.data;
272
+ ```
273
+
274
+ Render each dropdown from the definition's `options` in order: show `label`, submit `key`, and keep the key in the URL so shared links survive a label change. Accept only keys the current definition offers; drop anything else from a hand-typed URL. Only `jobs.search` takes these filters. A company's jobs are re-indexed automatically when its values change. Worked example: [Custom field filters and badges](https://cavuno.com/docs/sdk/cookbook/custom-field-filters-and-badges).
275
+
126
276
  ## Completion gate
127
277
 
128
278
  Finish only after every applicable check passes:
@@ -135,7 +285,16 @@ Finish only after every applicable check passes:
135
285
  relevance, newest, and salary high.
136
286
  - Category and skill filtering sends `sourceSlug`, while links use
137
287
  `canonicalSlug`.
288
+ - `/jobs?category=engineering,design` sends
289
+ `filters.categories: ['engineering', 'design']` to `jobs.search`.
138
290
  - Every paged taxonomy request forwards the previous opaque `nextCursor`.
291
+ - Job custom-field controls exist only for keys the operator chose as
292
+ filters, read their options from `board.context().customFields.job`, and send
293
+ option keys in `filters.customFields`; an unknown option in the URL never
294
+ reaches the API.
295
+ - Company and talent custom controls come from `board.profileFields`; private definitions and archived choices never become filter options.
296
+ - Profile-filter requests use stored scalar values and returned collection entry IDs, not display labels.
297
+ - Job-field dropdowns send `filters.customFields` and company-field dropdowns send `filters.companyCustomFields` to `jobs.search`, both built from the current definitions; a stale option key in the URL is dropped, not sent.
139
298
 
140
299
  ## Cavuno SDK reference
141
300
 
@@ -96,6 +96,55 @@ the same card view-model as listings — do not convert a full job client-side.
96
96
  const card = toJobCardVM(saved.job, { language, /* … */ });
97
97
  ```
98
98
 
99
+ ## Badges on job cards
100
+
101
+ Every job card (listings, search, company jobs, similar, recommended, and
102
+ saved jobs) carries `customFieldValues` for the job and
103
+ `company.customFieldValues` for its company. Both hold only select, boolean,
104
+ and number values keyed by field key; select values are option keys. Text
105
+ fields stay on the full job and company, and private company fields never
106
+ appear. Both are `{}` when empty, so a card needs no extra request to show a
107
+ badge.
108
+
109
+ ```ts snippet
110
+ import { resolveCustomFieldDisplay } from '@cavuno/board/format';
111
+
112
+ const context = await board.context();
113
+ const companyFields = await board.profileFields.retrieve('company');
114
+
115
+ // Badge only the fields the operator wants on cards (an app-config list).
116
+ const JOB_BADGE_KEYS = ['work_type'];
117
+ const jobBadges = resolveCustomFieldDisplay(
118
+ context.language,
119
+ context.customFields.job.filter((field) => JOB_BADGE_KEYS.includes(field.key)),
120
+ card.customFieldValues).flatMap((entry) => {
121
+ switch (entry.kind) {
122
+ case 'text': // a single_select resolves to its option label
123
+ return [entry.value];
124
+ case 'multi_select':
125
+ return entry.values;
126
+ case 'boolean':
127
+ return entry.value ? [entry.label] : [];
128
+ case 'number':
129
+ return [`${entry.label}: ${entry.value}`];
130
+ }
131
+ });
132
+
133
+ // Company badge: resolve the stored option key to its public label.
134
+ const employerField = companyFields.definitions.find(
135
+ (field) => field.key === 'employer_category');
136
+ const employerKey = card.company?.customFieldValues.employer_category;
137
+ const employerBadge =
138
+ typeof employerKey === 'string'
139
+ ? (employerField?.options?.find((option) => option.key === employerKey)
140
+ ?.label ?? null)
141
+ : null;
142
+ ```
143
+
144
+ Load the definitions once per page (both are cacheable board metadata), not
145
+ once per card. A key with no current definition or option resolves to nothing:
146
+ drop the badge rather than printing the raw key.
147
+
99
148
  ## Completion gate
100
149
 
101
150
  Finish only after every applicable check passes:
@@ -107,6 +156,9 @@ Finish only after every applicable check passes:
107
156
  - Open ranges return `bound: 'from' | 'upTo'`; the application adds open-range
108
157
  chrome and composes order itself.
109
158
  - Saved jobs arrive as cards; no `fullJobToCard` conversion.
159
+ - Card badges read `card.customFieldValues` / `card.company.customFieldValues`
160
+ and resolve option keys to labels through the definitions; no raw key is
161
+ shown and no card triggers a per-job detail fetch.
110
162
  - Salary-stat formatters receive the detail's `currency` (never invent USD).
111
163
  - Formatting changes neither salary amounts nor invents chrome words.
112
164
 
@@ -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,93 @@ 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
+
90
+ ## Pick office locations
91
+
92
+ Back the `location` field with `board.locations.search`, a worldwide search over countries, regions, cities, and localities. `session` is required: generate one per location-field interaction, reuse it across keystrokes and the pick-time `board.locations.resolve` call, then generate a new one for the next interaction. Renew it before 50 search requests or after 180 seconds. Pass `jobForm.location.allowedCountries` as `country` to narrow suggestions to the board's allowed countries.
93
+
94
+ ```ts snippet
95
+ const session = crypto.randomUUID();
96
+ const { data: locations } = await board.locations.search({
97
+ q: 'berl',
98
+ session,
99
+ country: jobForm.location.allowedCountries?.join(','),
100
+ });
101
+
102
+ const picked = locations[0];
103
+ if (picked) {
104
+ const location = await board.locations.resolve({
105
+ locationId: picked.id,
106
+ session,
107
+ });
108
+ submission.officeLocations.push({ locationId: location.id });
109
+ }
110
+ ```
111
+
112
+ Show `fullName` in the input once picked and `contextLabel` as the secondary line in the result list. Resolve the picked `id` with the same session before committing the selection, then send the resolved `id` as `locationId` in the later job submission. `board.taxonomy.places.list({ q })` ids are accepted as `locationId` too. An id the server does not recognise throws `locations_invalid_id`, and `locations_unavailable` means the search is temporarily down, so let the poster retry.
113
+
27
114
  ## Resolve a logo
28
115
 
29
116
  Both logo paths return a stored `publicUrl` for `create`. The SDK owns multipart encoding.
@@ -111,6 +198,7 @@ Bare-email billing lookup is intentionally absent because it would expose whethe
111
198
 
112
199
  ## Completion gate
113
200
 
201
+ - 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
202
  - Every `create` call handles all four statuses.
115
203
  - Paid checkout redirects through `checkoutUrl`; the job appears only after webhook completion.
116
204
  - Free unmoderated submission returns a resolving `jobSlug`.
@@ -37,6 +37,13 @@ const companies = await board.salaries.companies.list();
37
37
  region, and city nesting from each `SalaryLocation.parentSlug`; top-level rows
38
38
  have `parentSlug: null`.
39
39
 
40
+ Every place row in a salary response (location index nodes, `topLocations`,
41
+ child, sibling, and other locations) carries `placeName` and `placeLabel`.
42
+ Render `placeLabel` wherever places are listed: it tells a city from the
43
+ region it shares a name with ("New York, NY" under "New York", "Washington,
44
+ DC" beside "Washington"). Keep `placeName` for data you pass on, such as
45
+ JSON-LD.
46
+
40
47
  ## Retrieve detail and canonicalize slugs
41
48
 
42
49
  Every axis `retrieve(slug)` accepts an inbound English or board-language slug.
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "4.26.0",
2
+ "version": "4.28.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, job and company custom-field filters, 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"