@cavuno/board 2.0.3 → 3.0.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 (74) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/CONTRIBUTING.md +0 -11
  3. package/README.md +8 -7
  4. package/dist/{_spec-BKtVFcV9.d.mts → _spec-nkWAraS0.d.mts} +4 -18
  5. package/dist/{_spec-BKtVFcV9.d.ts → _spec-nkWAraS0.d.ts} +4 -18
  6. package/dist/bin.mjs +40 -57
  7. package/dist/{board-3Xj_Y-Tx.d.ts → board-NE98PlwN.d.ts} +2 -3
  8. package/dist/{board-pNN710Zc.d.mts → board-muu8BI8P.d.mts} +2 -3
  9. package/dist/doctor.js +21 -47
  10. package/dist/doctor.mjs +24 -50
  11. package/dist/filters.d.mts +2 -2
  12. package/dist/filters.d.ts +2 -2
  13. package/dist/format.d.mts +3 -3
  14. package/dist/format.d.ts +3 -3
  15. package/dist/index.d.mts +11 -27
  16. package/dist/index.d.ts +11 -27
  17. package/dist/index.js +3 -3
  18. package/dist/index.mjs +3 -3
  19. package/dist/{jobs-C4qRfCdP.d.ts → jobs-BiEvL9d7.d.ts} +1 -1
  20. package/dist/{jobs-k84C0v_8.d.mts → jobs-lVvKCBCU.d.mts} +1 -1
  21. package/dist/{salaries--J5mTylN.d.ts → salaries-BahBpxWT.d.ts} +2 -2
  22. package/dist/{salaries-C9YLO5NG.d.mts → salaries-ChSqDBAM.d.mts} +2 -2
  23. package/dist/{search-CE_qQiM7.d.mts → search-CCyFWPi3.d.mts} +1 -1
  24. package/dist/{search-CLRQ_El9.d.ts → search-CyRvVMUp.d.ts} +1 -1
  25. package/dist/seo.d.mts +4 -4
  26. package/dist/seo.d.ts +4 -4
  27. package/dist/server.d.mts +5 -5
  28. package/dist/server.d.ts +5 -5
  29. package/dist/sitemap.d.mts +5 -5
  30. package/dist/sitemap.d.ts +5 -5
  31. package/dist/skills.d.mts +15 -4
  32. package/dist/skills.d.ts +15 -4
  33. package/dist/skills.js +30 -7
  34. package/dist/skills.mjs +30 -8
  35. package/dist/suggest.d.mts +2 -2
  36. package/dist/suggest.d.ts +2 -2
  37. package/package.json +3 -13
  38. package/skills/cavuno-board-account/SKILL.md +54 -66
  39. package/skills/cavuno-board-api-client/SKILL.md +74 -0
  40. package/skills/cavuno-board-applications/SKILL.md +38 -52
  41. package/skills/cavuno-board-auth/SKILL.md +36 -61
  42. package/skills/cavuno-board-blog/SKILL.md +60 -49
  43. package/skills/cavuno-board-companies/SKILL.md +71 -53
  44. package/skills/cavuno-board-errors/SKILL.md +29 -46
  45. package/skills/cavuno-board-filters/SKILL.md +64 -75
  46. package/skills/cavuno-board-format/SKILL.md +63 -68
  47. package/skills/cavuno-board-i18n/SKILL.md +33 -62
  48. package/skills/cavuno-board-job-alerts/SKILL.md +68 -57
  49. package/skills/cavuno-board-jobs/SKILL.md +63 -55
  50. package/skills/cavuno-board-messaging/SKILL.md +53 -59
  51. package/skills/cavuno-board-paywall/SKILL.md +48 -54
  52. package/skills/cavuno-board-post-a-job/SKILL.md +118 -0
  53. package/skills/cavuno-board-salaries/SKILL.md +73 -50
  54. package/skills/cavuno-board-search-suggestions/SKILL.md +82 -0
  55. package/skills/cavuno-board-seo/BLOG.md +40 -0
  56. package/skills/cavuno-board-seo/JOB_AND_BREADCRUMBS.md +49 -0
  57. package/skills/cavuno-board-seo/LISTING.md +36 -0
  58. package/skills/cavuno-board-seo/SALARY.md +61 -0
  59. package/skills/cavuno-board-seo/SKILL.md +32 -163
  60. package/skills/cavuno-board-server-sessions/SKILL.md +143 -0
  61. package/skills/cavuno-board-setup/SKILL.md +43 -80
  62. package/skills/cavuno-board-sitemap/SKILL.md +76 -93
  63. package/skills/cavuno-board-smoke-test/SKILL.md +34 -69
  64. package/skills/manifest.json +43 -57
  65. package/dist/theme.d.mts +0 -115
  66. package/dist/theme.d.ts +0 -115
  67. package/dist/theme.js +0 -195
  68. package/dist/theme.mjs +0 -174
  69. package/skills/cavuno-board-client/SKILL.md +0 -89
  70. package/skills/cavuno-board-job-posting/SKILL.md +0 -116
  71. package/skills/cavuno-board-server/SKILL.md +0 -229
  72. package/skills/cavuno-board-suggest/SKILL.md +0 -119
  73. package/skills/cavuno-board-theme/SKILL.md +0 -57
  74. package/skills/flavors/tanstack-start/SKILL.md +0 -100
@@ -1,42 +1,30 @@
1
1
  ---
2
2
  name: cavuno-board-sitemap
3
- description: Build the board's sitemap with @cavuno/board/sitemap — the hosted 8-bucket model (marketing, jobs taxonomies, job details, companies, salaries, blog), 45k chunking, XML rendering, and the buildBucketUrls walker that enumerates a board's content through the BoardSdk with the hosted SEO rules built in (feature gating, ≥5-job thin-content floor, pagination backstops). Use when adding /sitemap.xml and /sitemap/:file routes to a custom board frontend.
3
+ description: Board sitemap generation with @cavuno/board/sitemap. Use for sitemap indexes, bucket routes, XML rendering, catalog walking, chunking, or robots.txt discovery.
4
4
  ---
5
5
 
6
- # Sitemap: the 8-bucket model
6
+ # Sitemap generation
7
7
 
8
- `@cavuno/board/sitemap` ships the hosted board's sitemap architecture: a
9
- sitemap INDEX at `/sitemap.xml` pointing at one file per content bucket,
10
- each an ordinary `<urlset>`. The XML byte layout and the bucket rules are
11
- tested against the hosted implementation, so a custom frontend's
12
- sitemap corpus lines up bucket-for-bucket with what cavuno.com would emit.
8
+ `@cavuno/board/sitemap` implements the hosted eight-bucket sitemap: an index at
9
+ `/sitemap.xml` points to ordinary `<urlset>` files for `marketing`,
10
+ `jobs-categories`, `jobs-skills`, `jobs-locations`, `jobs-details`,
11
+ `companies`, `salaries`, and optionally `blog`.
13
12
 
14
- Two tiers, use both:
13
+ Structured data and head tags use `cavuno-board-seo`. The host app owns feeds.
15
14
 
16
- - **XML primitives** (pure, no I/O): `SITEMAP_BUCKETS`,
17
- `SITEMAP_CHUNK_SIZE` (45,000), `chunk`, `bucketFilename` /
18
- `parseBucketFilename`, `renderUrlset`, `renderSitemapIndex`.
19
- - **The walker** (opinionated, injected I/O): `listedBuckets(board)` and
20
- `buildBucketUrls(board, origin, bucket)` take your `BoardSdk` instance
21
- and return plain URL strings with the hosted rules applied — you never
22
- re-derive the SEO policy.
15
+ ## Use both tiers
23
16
 
24
- ## When to use
17
+ The pure XML tier exports `SITEMAP_BUCKETS`, `SITEMAP_CHUNK_SIZE` (45,000),
18
+ `chunk`, `bucketFilename`, `parseBucketFilename`, `renderUrlset`, and
19
+ `renderSitemapIndex`. The catalog tier exports `listedBuckets(board)` and
20
+ `buildBucketUrls(board, origin, bucket)`; it walks the SDK and applies the
21
+ hosted indexing policy.
25
22
 
26
- - Adding `/sitemap.xml` + `/sitemap/:file` routes to a board frontend.
27
- - Deciding which listing pages deserve indexing (the walker already does).
23
+ ## Serve the index
28
24
 
29
- ## When not to use
30
-
31
- - Structured data / meta tags — `cavuno-board-seo`.
32
- - Feeds (RSS/Atom) — app-owned.
33
-
34
- ## The two routes
35
-
36
- The index lists one entry per bucket; each bucket route parses its filename
37
- back to a bucket + chunk and renders a urlset. Empty buckets render a valid
38
- empty urlset — only `blog` is dropped from the index when the feature is
39
- off (`listedBuckets` handles that).
25
+ List every non-empty chunk filename for every listed bucket. Keep one filename
26
+ for an empty bucket so its route returns a valid empty urlset. `listedBuckets`
27
+ removes blog when the feature is off.
40
28
 
41
29
  ```ts snippet
42
30
  import {
@@ -45,15 +33,13 @@ import {
45
33
  bucketFilename,
46
34
  chunk,
47
35
  listedBuckets,
48
- parseBucketFilename,
49
36
  renderSitemapIndex,
50
- renderUrlset,
51
37
  } from '@cavuno/board/sitemap';
52
38
 
53
- // GET /sitemap.xml — one <sitemap> per listed bucket (plus -2, -3… chunks).
54
39
  const origin = 'https://jobs.example.com';
55
40
  const buckets = await listedBuckets(board);
56
41
  const locs: string[] = [];
42
+
57
43
  for (const bucket of buckets) {
58
44
  const urls = await buildBucketUrls(board, origin, bucket);
59
45
  const chunks = chunk(urls, SITEMAP_CHUNK_SIZE);
@@ -61,87 +47,84 @@ for (const bucket of buckets) {
61
47
  locs.push(`${origin}/sitemap/${bucketFilename(bucket, i)}`);
62
48
  }
63
49
  }
50
+
64
51
  return xmlResponse(renderSitemapIndex(locs));
65
52
  ```
66
53
 
54
+ ## Serve a bucket file
55
+
56
+ Parse the filename, rebuild that bucket, select its zero-based chunk, and
57
+ render it. A filename outside the bucket grammar resolves to 404.
58
+
67
59
  ```ts snippet
68
- // GET /sitemap/:file — parse, enumerate, slice, render. Unknown file → 404.
60
+ import {
61
+ SITEMAP_CHUNK_SIZE,
62
+ buildBucketUrls,
63
+ chunk,
64
+ parseBucketFilename,
65
+ renderUrlset,
66
+ } from '@cavuno/board/sitemap';
67
+
69
68
  const parsed = parseBucketFilename(params.file);
70
69
  if (!parsed) return notFound();
70
+
71
71
  const urls = await buildBucketUrls(board, origin, parsed.bucket);
72
72
  const page = chunk(urls, SITEMAP_CHUNK_SIZE)[parsed.chunkIndex] ?? [];
73
73
  return xmlResponse(renderUrlset(page));
74
74
  ```
75
75
 
76
- `renderUrlset` also accepts `{ url, lastModified?, images? }` entries when
77
- you have per-URL dates (a `Date` serializes to ISO 8601). `changefreq` and
78
- `priority` are deliberately unsupported — the hosted board never emits them.
79
-
80
- ## robots.txt
81
-
82
- Point crawlers at the index — one line, at the site root:
83
-
84
- ```txt
85
- Sitemap: https://jobs.example.com/sitemap.xml
86
- ```
87
-
88
- ## The rules the walker enforces (don't re-implement)
76
+ `renderUrlset` also accepts `{ url, lastModified?, images? }`; `Date` values
77
+ serialize to ISO 8601. `renderSitemapIndex` accepts strings or
78
+ `{ url, lastModified? }`. The hosted format omits `changefreq` and `priority`.
89
79
 
90
- - **Thin-content floor**: a category/skill/location listing page is emitted
91
- only with ≥5 distinct jobs (`MIN_JOBS_PER_INDEXED_PAGE`) — below that,
92
- the page is thin content that wastes crawl budget.
93
- - **Feature gating**: `/impressum`, `/talent`, `/employers` appear in the
94
- marketing bucket only when `context().features` enables them; the blog
95
- bucket exists only when `features.blog` is on.
96
- - **Board language**: salary-index reads pass `context().language`, so a
97
- non-English board emits board-language canonical salary slugs (jobs,
98
- companies, and blog slugs arrive canonical on the wire already). No
99
- locale parameter to thread.
100
- - **Pagination backstops**: offset enumeration runs in parallel when the
101
- envelope carries `count` (capped at the API's 10,000-offset window);
102
- cursor walks cap at 200 pages. Both warn on truncation instead of
103
- hanging a build.
80
+ ## Keep the walker policy intact
104
81
 
105
- ## Stable order caveat (cursor walks)
82
+ `buildBucketUrls` is the policy boundary:
106
83
 
107
- Cursor and offset enumeration order can shift between requests on a large,
108
- churning board — two chunk requests may see slightly different orderings.
109
- That is harmless for a sitemap (URLs dedupe and the ≥5 counts are a
110
- heuristic), but do NOT reuse the walker as a general "export all jobs in
111
- order" tool; for stable ordering, pass an explicit sort/query to
112
- `board.jobs.list` yourself.
84
+ - Taxonomy and location pages enter the sitemap at five distinct jobs,
85
+ `MIN_JOBS_PER_INDEXED_PAGE`.
86
+ - Marketing URLs follow context feature flags for impressum, talent directory,
87
+ and employers. Blog is listed only when `features.blog` is enabled.
88
+ - Salary index reads use `context().language`, producing board-language
89
+ canonical salary slugs. Job, company, market, and blog slugs arrive
90
+ canonical on the wire.
91
+ - Collections with `count` enumerate offsets in parallel up to the API's
92
+ 10,000-offset window. Cursor walks stop after 200 pages. Both paths warn when
93
+ their backstop truncates output.
94
+ - XML escaping and hosted byte layout come from `renderUrlset` and
95
+ `renderSitemapIndex`.
113
96
 
114
- ## Named exclusions (v1 API gap — not bugs)
97
+ Cursor and offset order may move while a large board changes. That is safe for
98
+ this deduplicated sitemap heuristic. Use an explicit sort or search with
99
+ `board.jobs.list` for a stable ordered export.
115
100
 
116
- Two URL families the HOSTED sitemap emits are deliberately absent, because
117
- v1 exposes them only per-slug and a bulk-pairs endpoint doesn't exist yet
118
- (per-slug N+1 is ~1k+ calls per build):
101
+ ## Preserve the named v1 exclusions
119
102
 
120
- - Cross-axis salary pages (title×location, skill×location,
121
- company×category, and the per-entity `/locations` · `/titles` ·
122
- `/skills` salary index pages).
123
- - Jobs place×category / place×skill combination listings.
103
+ The walker omits families that v1 can discover only through per-slug N+1
104
+ reads: cross-axis salary pages (title×location, skill×location,
105
+ company×category, and per-entity salary indexes) and job
106
+ place×category/place×skill combinations. Internal links keep them reachable
107
+ until a bulk-pairs endpoint lets the walker add them.
124
108
 
125
- Both stay reachable through internal links. When the bulk endpoint lands,
126
- the walker picks them up additively — don't hand-enumerate them.
109
+ ## Publish discovery
127
110
 
128
- ## Anti-patterns
111
+ At the site root, point robots.txt at the index:
129
112
 
130
- ```ts snippet
131
- // NEVER emit every taxonomy page unconditionally — the ≥5-job floor exists
132
- // to keep thin pages out of the index:
133
- for (const c of allCategories) urls.push(`${origin}/jobs/${c.slug}`); // wrong
134
- // NEVER hand-build the XML with a template string per route — use
135
- // renderUrlset/renderSitemapIndex (escaping + byte-parity with hosted).
136
- // NEVER fetch every salary detail page to discover cross-axis pairs (N+1).
113
+ ```txt
114
+ Sitemap: https://jobs.example.com/sitemap.xml
137
115
  ```
138
116
 
139
- ## Verify
117
+ ## Completion gate
118
+
119
+ Finish only after every applicable check passes:
140
120
 
141
- - [ ] `/sitemap.xml` lists one file per bucket (no `blog` entry when the
142
- feature is off) and every listed file returns valid XML.
143
- - [ ] A category with 4 jobs is absent from `jobs-categories.xml`; one with
144
- 5 is present.
145
- - [ ] `jobs-details-2.xml` style chunk names round-trip through
146
- `parseBucketFilename` and unknown filenames 404.
147
- - [ ] robots.txt points at `/sitemap.xml`.
121
+ - `/sitemap.xml` lists all enabled buckets, omits blog when disabled, and every
122
+ listed file returns valid XML, including empty buckets.
123
+ - A taxonomy page with four distinct jobs is absent and one with five is
124
+ present.
125
+ - `jobs-details-2.xml` round-trips through `parseBucketFilename`; an unknown
126
+ filename returns 404.
127
+ - More than 45,000 URLs produce numbered `-2`, `-3`, and later files without
128
+ dropping or duplicating a URL.
129
+ - robots.txt points to the absolute `/sitemap.xml` URL.
130
+ - No excluded N+1 URL family is enumerated outside the walker.
@@ -1,104 +1,69 @@
1
1
  ---
2
2
  name: cavuno-board-smoke-test
3
- description: Runtime verification for a wired Cavuno board frontend — literal curl probes against the Board API plus per-surface behavioral checks and a mandatory production-build pass. Run after cavuno-board-setup finishes, after upgrading @cavuno/board, or whenever board wiring is suspect. Type checks are not enough for board wiring.
3
+ description: Prove a Cavuno board integration at runtime. Use after initial setup, an @cavuno/board upgrade, a board-key rotation, or when rendered board data is unexpectedly missing.
4
4
  ---
5
5
 
6
- # Smoke-testing a Cavuno board frontend
6
+ # Smoke-test a Cavuno board
7
7
 
8
- Type checks prove the code compiles against the SDK; they do not prove the
9
- app reaches the right board with the right credentials, or that gating,
10
- auth, and SEO surfaces behave. Verify at runtime, in this order.
8
+ Follow the layers in order. Each passing layer narrows the next failure to app wiring rather than credentials or the public API.
11
9
 
12
- ## When to use
13
-
14
- - Right after `cavuno-board-setup` completes.
15
- - After upgrading `@cavuno/board` or rotating the `pk_…` key.
16
- - When any surface renders empty and you don't know which layer is wrong.
17
-
18
- ## 0 — Run `doctor` first (deterministic pass)
10
+ ## 1. Run doctor
19
11
 
20
12
  ```bash
21
13
  PUBLIC_CAVUNO_BOARD=pk_... \
22
14
  npx @cavuno/board doctor --frontend http://localhost:3000
23
15
  ```
24
16
 
25
- Doctor codifies the static checks (env shape, API reachability) and the
26
- read probes (home/jobs render, JobPosting JSON-LD, sitemap, robots) as
27
- pass/fail/skip with a non-zero exit on failure — anything it SKIPS is
28
- named in its summary and still needs the manual checks below. Use the
29
- rest of this skill for the behavioral checks doctor does not automate
30
- (auth flows, gating semantics, production build).
17
+ Doctor checks environment shape, API reachability, installed skills, and—when `--frontend` is present—rendered pages, sitemap, robots rules, and JobPosting structured data. It is read-only and reports every skipped check.
31
18
 
32
- Doctor is non-destructive. It validates configuration, the public Board API,
33
- installed skills, and—when `--frontend` is provided—rendered pages, sitemaps,
34
- robots rules, and JobPosting structured data. It never creates users,
35
- applications, or alerts and needs no Cavuno operator credentials.
19
+ **Complete when:** doctor exits zero and every skip that applies to an implemented surface is recorded for manual verification below.
36
20
 
37
- ## 1 — Probe the API directly (before blaming app code)
21
+ ## 2. Probe the public API
38
22
 
39
- Use the real `PUBLIC_CAVUNO_BOARD` value the app reads. The SDK and doctor use `https://api.cavuno.com` by default; set `PUBLIC_CAVUNO_API_URL` only for a Cavuno-supplied non-production override. Expected outputs are exact.
23
+ Use the exact board value and API origin read by the app:
40
24
 
41
25
  ```bash
42
26
  CAVUNO_API_URL="${PUBLIC_CAVUNO_API_URL:-https://api.cavuno.com}"
43
27
 
44
- # Board context: MUST return JSON with "object": "public_board" and your
45
- # board's name — not an HTML error page, not {"error":{"code":"boards_not_found"}}.
46
28
  curl -s "$CAVUNO_API_URL/v1/boards/$PUBLIC_CAVUNO_BOARD" | head -c 300
47
-
48
- # Jobs list: MUST return {"object":"list", ... "data":[...]}. An empty data
49
- # array on a board you know has jobs means the wrong board identifier.
50
29
  curl -s "$CAVUNO_API_URL/v1/boards/$PUBLIC_CAVUNO_BOARD/jobs?limit=2" | head -c 300
51
-
52
- # Error envelope: a bogus job slug MUST return the v1 error shape with
53
- # "code":"jobs_not_found" — anything else means a proxy is rewriting responses.
54
30
  curl -s "$CAVUNO_API_URL/v1/boards/$PUBLIC_CAVUNO_BOARD/jobs/definitely-not-a-job"
55
31
  ```
56
32
 
57
- On a password-protected board, every content read above returns 401 with
58
- `"code":"board_password_required"` — that is correct behavior, not a
59
- failure; verify the grant flow in step 3 instead.
33
+ The context response has `"object":"public_board"` and the expected name. The jobs response has `"object":"list"` and a `data` array. The fabricated slug has the v1 error code `jobs_not_found`. A protected board instead returns `board_password_required` for content reads; that is the expected pre-grant state.
34
+
35
+ Pause here when the API is unreachable, resolves a different board, or returns a non-v1 envelope. Report this layer as the blocker and preserve app code.
36
+
37
+ **Complete when:** identity, list shape, and error-envelope probes match the expected board, or the protected-board challenge is confirmed.
38
+
39
+ ## 3. Probe the development app
40
+
41
+ Start the app and verify:
60
42
 
61
- ## 2 — Verify the app serves board data
43
+ - The shell renders the name and logo from `board.context()`.
44
+ - A listing renders real jobs and links to `/companies/:companySlug/jobs/:jobSlug`.
45
+ - A fabricated job route renders the app's not-found state.
62
46
 
63
- Start the app (dev is fine for this step) and probe its own routes:
47
+ **Complete when:** all three behaviors are visible through the app rather than inferred from types or source code.
64
48
 
65
- - The home/shell renders the board name and logo from `board.context()`.
66
- - A listing page shows real job cards; a card links to
67
- `/companies/:companySlug/jobs/:jobSlug` (never `/jobs/:slug`).
68
- - A fabricated job URL renders the app's not-found state — a handled
69
- `isNotFound` branch, not a crash or blank page.
49
+ ## 4. Exercise each implemented stateful surface
70
50
 
71
- ## 3 — Per-surface behavioral checks (only for surfaces you built)
51
+ Run only the branches the app exposes:
72
52
 
73
- - **Auth**: register → the verification gate appears; login → an authed
74
- read (`me.retrieve`) succeeds; after `auth.logout()` the same read fails
75
- with a 401 that the app handles by signing out, not looping.
76
- - **Password gate**: with protection enabled, an anonymous content read
77
- redirects to the password page; `password.verify()` + retry renders
78
- content; a wrong password shows `board_password_invalid` messaging.
79
- - **Saved jobs / apply**: save → reload → still saved (server state, not
80
- local); a second apply to the same job returns the same application
81
- (idempotent), not a duplicate.
82
- - **Paywall** (gated boards): anonymous list shows the `gatedCount` upsell;
83
- an entitled login makes the same URL return the ungated view.
84
- - **Alerts**: subscribe → `status: "submitted"`; repeat → same uniform `"submitted"` (never reveals already-subscribed).
53
+ - **Auth:** register, observe verification state, log in, prove `me.retrieve`, log out, then prove the app handles the next authenticated read as signed out.
54
+ - **Password gate:** confirm the anonymous challenge, verify the password, retry successfully, then confirm a wrong password renders `board_password_invalid`.
55
+ - **Saved jobs and applications:** save and reload to prove server persistence; submit the same application twice and confirm one application identity.
56
+ - **Paywall:** compare the anonymous `gatedCount` state with the entitled view at the same URL.
57
+ - **Alerts:** submit twice and confirm both responses preserve the uniform `submitted` state.
85
58
 
86
- ## 4 — Production build (mandatory)
59
+ Pause a branch when its board password or test account is unavailable and report the exact credential needed.
87
60
 
88
- Dev servers mask wiring bugs (looser env loading, dev-only middleware, no
89
- tree-shake). Run the app's real build, boot the production output, and
90
- repeat step 2 against it. `import 'server-only'`-style boundaries and env
91
- prefixes (`VITE_`, `NEXT_PUBLIC_`) frequently pass dev and fail prod.
61
+ **Complete when:** every implemented branch passes or has an explicit credential blocker; logout and refresh paths terminate without loops.
92
62
 
93
- ## Out of scope — do not invent checks
63
+ ## 5. Prove the production artifact
94
64
 
95
- No load testing, no Lighthouse/SEO scoring, no visual regression — those
96
- are app-owned. This skill verifies board wiring only.
65
+ Run the app's production build, boot that output, and repeat the three app probes from step 3 against it. Use the app's real production command and environment-loading path.
97
66
 
98
- ## Stop conditions
67
+ **Complete when:** the build succeeds and the running production artifact passes the shell, listing/detail-link, and not-found probes.
99
68
 
100
- Stop and report to the human when: step 1 fails (the problem is
101
- credentials/network, not code — do not "fix" app code to compensate); the
102
- API returns codes the app has no branch for; or a check needs a credential
103
- you don't have (board password, test account). Never commit real
104
- credentials while fixing findings.
69
+ The smoke test covers Cavuno board wiring. Performance, visual regression, and broader SEO scoring belong to their dedicated project checks.
@@ -1,173 +1,159 @@
1
1
  {
2
- "version": "2.0.3",
2
+ "version": "3.0.0",
3
3
  "skills": [
4
4
  {
5
5
  "name": "cavuno-board-account",
6
- "description": "Candidate account self-service with the @cavuno/board SDK — me.retrieve/delete, the candidate profile (merge-patch update, handle availability), experience/education CRUD, skills/languages full-replace, avatar upload, resume upload with async parse polling, and notification preferences plus the anonymous token unsubscribe. Use when building account, profile-edit, onboarding, or notification-settings pages for a signed-in board user.",
6
+ "description": "Candidate self-service boundary with @cavuno/board. Use for account, profile, resume onboarding, 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"
10
10
  },
11
+ {
12
+ "name": "cavuno-board-api-client",
13
+ "description": "Configure the @cavuno/board API client. Use when creating the shared client, adding global hooks or headers, passing framework fetch options, or calling an endpoint through the typed escape hatch.",
14
+ "path": "skills/cavuno-board-api-client/SKILL.md",
15
+ "framework": null,
16
+ "category": "core"
17
+ },
11
18
  {
12
19
  "name": "cavuno-board-applications",
13
- "description": "The native apply flow with the @cavuno/board SDK — jobs.apply (signed-in or guest), attaching a resume via jobs.uploadApplicationResume, the jobs.myApplication \"have I applied?\" check, managing submitted applications (me.applications list/retrieve/updateFacts/withdraw), and saved jobs (me.savedJobs). Use when building an apply form, an application-tracker page, or a save-job button.",
20
+ "description": "Candidate application handoff with @cavuno/board. Use for native or guest apply, application tracking, resume attachment, withdrawal, and saved jobs.",
14
21
  "path": "skills/cavuno-board-applications/SKILL.md",
15
22
  "framework": null,
16
23
  "category": "core"
17
24
  },
18
25
  {
19
26
  "name": "cavuno-board-auth",
20
- "description": "Authenticate board users with the @cavuno/board SDK — register, login, refresh, logout, email verification and password reset. Covers bearer-JWT storage modes, the deliberate no-auto-refresh-on-401 rule (and single-flight handling), and the server-side httpOnly-cookie pattern that keeps tokens out of the browser.",
27
+ "description": "Authenticate Cavuno board users. Use for registration, login, logout, token refresh, email verification, password recovery, magic links, OAuth, or authenticated board-user calls.",
21
28
  "path": "skills/cavuno-board-auth/SKILL.md",
22
29
  "framework": null,
23
30
  "category": "core"
24
31
  },
25
32
  {
26
33
  "name": "cavuno-board-blog",
27
- "description": "Build the blog with the @cavuno/board SDK — post archives (blog.posts.list with tagSlug/authorSlug/featured), the post page (blog.posts.retrieve + adjacent + similar), blog.tags and blog.authors, and free-text blog.search. Covers the summary-vs-detail split (html on the single read only), cursor pagination, slug-change redirects (redirected/newSlug), and the SEO fields the API actually returns.",
34
+ "description": "Public blog reads with @cavuno/board. Use for post archives, post detail, adjacent or similar posts, tag and author pages, or blog search.",
28
35
  "path": "skills/cavuno-board-blog/SKILL.md",
29
36
  "framework": null,
30
37
  "category": "core"
31
38
  },
32
- {
33
- "name": "cavuno-board-client",
34
- "description": "Create and configure the @cavuno/board client — the pk_ board identifier, optional API-origin override, global headers, hooks, per-call FetchOptions, and the rule that keeps one shared instance safe under SSR.",
35
- "path": "skills/cavuno-board-client/SKILL.md",
36
- "framework": null,
37
- "category": "core"
38
- },
39
39
  {
40
40
  "name": "cavuno-board-companies",
41
- "description": "Build company surfaces with the @cavuno/board SDK — the companies index (companies.list / companies.search with the market filter), the company profile (companies.retrieve, listJobs, similar), the markets sub-surface (companies.markets + markets.resolve with its 308 redirectTo), and company salary pages (companies.salaries + salaries.category). Covers PublicCompany vs PublicCompanyDetail and envelope pagination.",
41
+ "description": "Company catalog reads with @cavuno/board. Use for company indexes, market archives, profiles, company job rails, similar companies, or company salary pages.",
42
42
  "path": "skills/cavuno-board-companies/SKILL.md",
43
43
  "framework": null,
44
44
  "category": "core"
45
45
  },
46
46
  {
47
47
  "name": "cavuno-board-errors",
48
- "description": "Handle errors and access gating with the @cavuno/board SDK — the BoardApiError shape, the typed guards (isNotFound, isUnauthorized, isValidationError, isRateLimited, isForbidden, isConflict), and the board-password flow (isBoardPasswordRequired → password.verify → X-Board-Access grant).",
48
+ "description": "Classify Board API failures and board-password challenges. Use when mapping SDK errors to UI states, retries, support diagnostics, or password-gated access.",
49
49
  "path": "skills/cavuno-board-errors/SKILL.md",
50
50
  "framework": null,
51
51
  "category": "core"
52
52
  },
53
53
  {
54
54
  "name": "cavuno-board-filters",
55
- "description": "The canonical listing-filter vocabulary and URL search-param parsing with @cavuno/board/filters — remote/employment/seniority/company/sort sets, localized seniority labels, and parseListingFilters for jobs-listing routes. Use when building listing pages, filter sidebars, sort dropdowns, or validating listing URL params.",
55
+ "description": "Listing-filter contracts with @cavuno/board. Use for job filter controls, sort controls, listing URL validation, or taxonomy-backed filter options.",
56
56
  "path": "skills/cavuno-board-filters/SKILL.md",
57
57
  "framework": null,
58
58
  "category": "core"
59
59
  },
60
60
  {
61
61
  "name": "cavuno-board-format",
62
- "description": "Display formatting for board frontends with @cavuno/board/format — salary ranges, seniority/employment/remote labels, location labels, dates, and the board-language salary lexicon. Use when rendering job cards, detail pages, or salary figures, and whenever a value like salaryMin/salaryTimeframe/seniority needs to become display text.",
62
+ "description": "Board-language display formatting with @cavuno/board/format. Use for salary ranges, dates, job labels, location labels, card conversion, or salary metadata copy.",
63
63
  "path": "skills/cavuno-board-format/SKILL.md",
64
64
  "framework": null,
65
65
  "category": "core"
66
66
  },
67
67
  {
68
68
  "name": "cavuno-board-i18n",
69
- "description": "Multi-language chrome for a board frontend — the single-language board model, the uiCopy catalog ⊕ operator labels ⊕ generated copy layering, the one copy seam, and how to add path-prefixed locales (/de/, /fr/) with Paraglide JS on TanStack Start. Use when localizing a starter's chrome, adding a language, or wiring locale routing. Do NOT use to translate job content — that stays the board's language.",
69
+ "description": "One-seam board localization with @cavuno/board. Use when localizing UI chrome, adding locale routing, merging operator labels, or preserving board-language content.",
70
70
  "path": "skills/cavuno-board-i18n/SKILL.md",
71
71
  "framework": null,
72
72
  "category": "core"
73
73
  },
74
74
  {
75
75
  "name": "cavuno-board-job-alerts",
76
- "description": "Build job-alert flows with the @cavuno/board SDK — the anonymous double-opt-in surface (jobAlerts.subscribe/confirm/resendConfirmation + HMAC-token manage/unsubscribe/resubscribe/updatePreference/deletePreference) and the authenticated board.me.alerts CRUD. Covers which surface to use when, how the manage token rides, which filters actually scope delivery, and the full-replace update trap.",
76
+ "description": "Job-alert two-lane model with @cavuno/board. Use for anonymous double opt-in, token-managed preferences, or signed-in alert CRUD.",
77
77
  "path": "skills/cavuno-board-job-alerts/SKILL.md",
78
78
  "framework": null,
79
79
  "category": "core"
80
80
  },
81
- {
82
- "name": "cavuno-board-job-posting",
83
- "description": "Build the anonymous \"Post a job\" funnel with the @cavuno/board SDK — jobPosting.plans, create and its status-discriminated JobPostingResult (checkout / published / pending_approval / invoice_sent), logo upload + fetchLogoByDomain, and the email-verified billing helpers (sendBillingVerification, getBillingOptions).",
84
- "path": "skills/cavuno-board-job-posting/SKILL.md",
85
- "framework": null,
86
- "category": "core"
87
- },
88
81
  {
89
82
  "name": "cavuno-board-jobs",
90
- "description": "Browse, search, and render jobs with the @cavuno/board SDK — jobs.list, jobs.search, jobs.retrieve, jobs.similar. Covers the slim card vs full job shapes, catalog pagination (count/limit/offset + opaque cursor), filters, and the candidate-paywall gatedCount.",
83
+ "description": "Job catalog reads with @cavuno/board. Use for job browse or search, job detail, similar jobs, candidate gating, or full-catalog iteration.",
91
84
  "path": "skills/cavuno-board-jobs/SKILL.md",
92
85
  "framework": null,
93
86
  "category": "core"
94
87
  },
95
88
  {
96
89
  "name": "cavuno-board-messaging",
97
- "description": "Build the board-user messaging inbox with the @cavuno/board SDK — me.conversations (list, unreadCount, retrieve, listMessages, start, reply, markRead, archive), me.messages (edit, unsend, report), me.blocks. Covers the polled-REST contract (no realtime transport in v1), visibility-aware polling, the unread badge, and read receipts.",
90
+ "description": "Polled messaging contract with @cavuno/board. Use for inboxes, unread badges, threads, read receipts, message moderation, or user blocks.",
98
91
  "path": "skills/cavuno-board-messaging/SKILL.md",
99
92
  "framework": null,
100
93
  "category": "core"
101
94
  },
102
95
  {
103
96
  "name": "cavuno-board-paywall",
104
- "description": "Build the candidate job-access paywall with the @cavuno/board SDK — anonymous offer reads (paywall.offers), the gated browse experience (gatedCount on job lists), the embedded Stripe checkout mount kit (me.access.checkout → retrieveCheckout → grant), and the billing portal (me.access.portal for recurring grants).",
97
+ "description": "Grant-is-truth candidate paywall with @cavuno/board. Use for offers, gated job counts, embedded checkout handoff, access confirmation, or subscription portal links.",
105
98
  "path": "skills/cavuno-board-paywall/SKILL.md",
106
99
  "framework": null,
107
100
  "category": "core"
108
101
  },
102
+ {
103
+ "name": "cavuno-board-post-a-job",
104
+ "description": "Post a job publicly with @cavuno/board. Use for plan selection, logos, checkout handoff, or verified existing credit.",
105
+ "path": "skills/cavuno-board-post-a-job/SKILL.md",
106
+ "framework": null,
107
+ "category": "core"
108
+ },
109
109
  {
110
110
  "name": "cavuno-board-salaries",
111
- "description": "Build salary pages with the @cavuno/board SDK — salaries.titles/skills/locations (list + retrieve + cross-axis .locations/.location/.titles/.skills), salaries.companies.list. Covers the sourceSlug/canonicalSlug 308 contract, the { locale } overlay, aggregate field shapes (avg/median/percentile bands + jobCount), and why salary math must never be recomputed client-side.",
111
+ "description": "Salary read models with @cavuno/board. Use for title, skill, or location salary indexes and detail, cross-axis pages, or the salary companies hub.",
112
112
  "path": "skills/cavuno-board-salaries/SKILL.md",
113
113
  "framework": null,
114
114
  "category": "core"
115
115
  },
116
+ {
117
+ "name": "cavuno-board-search-suggestions",
118
+ "description": "Build server-ranked search suggestions with @cavuno/board. Use for company-and-term typeahead, autocomplete dropdowns, or suggestion-controller wiring.",
119
+ "path": "skills/cavuno-board-search-suggestions/SKILL.md",
120
+ "framework": null,
121
+ "category": "core"
122
+ },
116
123
  {
117
124
  "name": "cavuno-board-seo",
118
- "description": "Structured data + head builders for board frontends with @cavuno/board/seo — Google for Jobs JobPosting JSON-LD, breadcrumbs, blog Article/ProfilePage, salary-page Occupation/FAQ structured data, and listing <head> descriptors. Use when building a job-detail, listing, blog, or salary page and it needs JSON-LD or SEO meta tags, or whenever \"structured data\", \"rich results\", or \"Google for Jobs\" comes up.",
125
+ "description": "SEO builders with @cavuno/board/seo. Use for JobPosting or breadcrumb JSON-LD, blog schema, salary rich results, or job-listing head metadata.",
119
126
  "path": "skills/cavuno-board-seo/SKILL.md",
120
127
  "framework": null,
121
128
  "category": "core"
122
129
  },
123
130
  {
124
- "name": "cavuno-board-server",
125
- "description": "SSR session plumbing with @cavuno/board/server — the __Host- httpOnly session-cookie codec (BoardSession serialize/parse/clear + the 5-minute isExpiringSoon window), the board-password grant-cookie codec with the open-redirect guards (safeRedirectPath, currentPathFromReferer), and createSessionRefresher, the single-flight rotation helper for the single-use refresh token. Use when wiring board-user auth or the board-password gate into any server-rendered frontend (TanStack Start, Next.js, Remix, Workers).",
126
- "path": "skills/cavuno-board-server/SKILL.md",
131
+ "name": "cavuno-board-server-sessions",
132
+ "description": "Wire Cavuno server sessions and board-password access. Use when a server-rendered app needs httpOnly cookies, per-request bearer headers, single-flight refresh, grant cookies, or safe redirect-back handling.",
133
+ "path": "skills/cavuno-board-server-sessions/SKILL.md",
127
134
  "framework": null,
128
135
  "category": "core"
129
136
  },
130
137
  {
131
138
  "name": "cavuno-board-setup",
132
- "description": "End-to-end orchestrator for building a headless Cavuno job board with the @cavuno/board SDK. Start here after `npx @cavuno/board setup` copies the skills — detect the framework, wire the client, render board context, jobs browsing and detail, board-user auth and saved jobs, handle errors and access gating, then verify.",
139
+ "description": "Orchestrate a headless Cavuno board integration. Use when starting a frontend with @cavuno/board or adding Cavuno board surfaces to an existing app.",
133
140
  "path": "skills/cavuno-board-setup/SKILL.md",
134
141
  "framework": null,
135
142
  "category": "core"
136
143
  },
137
144
  {
138
145
  "name": "cavuno-board-sitemap",
139
- "description": "Build the board's sitemap with @cavuno/board/sitemap — the hosted 8-bucket model (marketing, jobs taxonomies, job details, companies, salaries, blog), 45k chunking, XML rendering, and the buildBucketUrls walker that enumerates a board's content through the BoardSdk with the hosted SEO rules built in (feature gating, ≥5-job thin-content floor, pagination backstops). Use when adding /sitemap.xml and /sitemap/:file routes to a custom board frontend.",
146
+ "description": "Board sitemap generation with @cavuno/board/sitemap. Use for sitemap indexes, bucket routes, XML rendering, catalog walking, chunking, or robots.txt discovery.",
140
147
  "path": "skills/cavuno-board-sitemap/SKILL.md",
141
148
  "framework": null,
142
149
  "category": "core"
143
150
  },
144
151
  {
145
152
  "name": "cavuno-board-smoke-test",
146
- "description": "Runtime verification for a wired Cavuno board frontend — literal curl probes against the Board API plus per-surface behavioral checks and a mandatory production-build pass. Run after cavuno-board-setup finishes, after upgrading @cavuno/board, or whenever board wiring is suspect. Type checks are not enough for board wiring.",
153
+ "description": "Prove a Cavuno board integration at runtime. Use after initial setup, an @cavuno/board upgrade, a board-key rotation, or when rendered board data is unexpectedly missing.",
147
154
  "path": "skills/cavuno-board-smoke-test/SKILL.md",
148
155
  "framework": null,
149
156
  "category": "core"
150
- },
151
- {
152
- "name": "cavuno-board-suggest",
153
- "description": "Federated keyword search suggestions with the @cavuno/board SDK — board.search.suggest for companies + taxonomy terms in one server-ranked list, and the headless createSuggestController under @cavuno/board/suggest (debounce, abort, stale-drop). Use when building a search dropdown, typeahead, or autocomplete that mirrors the hosted board.",
154
- "path": "skills/cavuno-board-suggest/SKILL.md",
155
- "framework": null,
156
- "category": "core"
157
- },
158
- {
159
- "name": "cavuno-board-tanstack-start",
160
- "description": "TanStack-Start-on-Cloudflare-Workers reference wiring for a headless Cavuno board — SSR loaders calling @cavuno/board server-side, the session held in an __Host- httpOnly cookie owned by the app, a single-flight refresh helper, and FetchOptions cache passthrough on Workers.",
161
- "path": "skills/flavors/tanstack-start/SKILL.md",
162
- "framework": "tanstack-start",
163
- "category": "flavor"
164
- },
165
- {
166
- "name": "cavuno-board-theme",
167
- "description": "Optional hosted-board theme compatibility with @cavuno/board/theme. Use only when a custom frontend deliberately mirrors or migrates an existing Cavuno hosted theme; the consuming application normally owns tokens, fonts, and color mode.",
168
- "path": "skills/cavuno-board-theme/SKILL.md",
169
- "framework": null,
170
- "category": "core"
171
157
  }
172
158
  ]
173
159
  }