# Upgrade cairn To upgrade cairn you bump the version range, read the `Consumers must:` steps for the versions you cross, and run your own gates. The [CHANGELOG](../../CHANGELOG.md) records those steps per version; a version with no `Consumers must:` list is a drop-in bump. ## Upgrade 1. **Bump the version range**, to the version you are actually going to, on a branch of its own. ```sh npm install @glw907/cairn-cms@^0.94.0 ``` A prerelease pins exactly, with no caret: a caret range never resolves a prerelease, so `^0.94.0-rc.1` silently leaves you where you were. 2. **Read every `Consumers must:` list between your old version and the new one** in the [CHANGELOG](../../CHANGELOG.md). Each breaking release states its own list, so a run from `0.76.0` to `0.78.2` means reading `0.78.0`'s and `0.78.2`'s lists in order. A version with no `Consumers must:` list changed nothing you need to act on. 3. **Run your typechecker first and work its output**, before reading down the lists by hand. Most of a breaking window is renames, and the compiler enumerates those faster and more completely than you can grep. Cross the lists off as its errors take you through them. How much this finds depends on how much of the admin surface your site consumes, not on how large the window is. Most `Consumers must:` entries name an admin-side type, a route-factory member, or a return type you only annotate if you mount routes per-surface. A site that mounts through the `createCairnAdmin` facade and reads its locals through `@glw907/cairn-cms/ambient` can cross the largest window in cairn's history on a handful of errors, or one. Read a green typecheck as a fact about your site's shape, not as evidence the window was small: the compiler-invisible half in step 4 is the same size either way. 4. **Then walk the lists for what the compiler can't see.** This is what the lists are really for, and it's the half that ships broken if you skip it. Three shapes recur: - **A rename the engine owns but your `app.d.ts` also declared.** A field you duplicated rather than imported keeps compiling under its old name and is simply never read again. The `locals` rename below is the worked case: a site that declared its own `App.Locals.auditSink` compiles clean and stops persisting audit rows. - **A string.** A posted `?/action` name, an access-map key, a saved log query. None of these are types, so nothing checks them. - **A hand-built event fixture** in a test or a script. When an engine event gains a required field, a fixture that omits it throws from inside the engine rather than failing closed. 5. **Run `npx cairn-doctor`** against your site. It catches a binding, a config key, or a dependency floor the new version now expects that your site hasn't caught up to yet. Read it as a health check on the whole site rather than an upgrade-compatibility check: most of what it measures is live infrastructure that has nothing to do with the version you came from, and an upgrade is often the first time anyone runs it. On the second consumer migration through this window, the doctor's most valuable finding was a GitHub App installation that had never carried the site's own repository, so every save and publish had failed since launch. See the [`cairn-doctor` reference](../reference/doctor.md) for what it checks. Its two zone checks read the Cloudflare API, and an API token without Zone Settings Read makes both report the same `FAIL` a genuinely wrong setting would. Check the token's scopes before acting on the suggested fix. 6. **Run your own site's build and test gate** before you deploy, in that order and both of them. cairn's gates only exercise the package. They can't reach your adapter, your `render`, or your routes, and a typechecker and a test suite that both pass still leave the bundler's own errors ahead of you. See [What a green typecheck misses](#what-a-green-typecheck-misses). The published docs tree ships inside the tarball, so `node_modules/@glw907/cairn-cms/docs` changes with the version like any other file. If your site renders that tree, pages arrive, pages retire, and arm indexes reorder on every bump, and a typechecker sees none of it. Run a link check of your own over the rendered corpus. 7. **Regenerate your visual baselines** if the window you crossed changed how `/admin` renders, and read the diff rather than accepting it. Windows that did: `0.91.0` (the header stack tightens), `0.92.0` (a UA reset layer, and the stacked field register becomes the default), and `0.94.0`. If you keep no baselines, the substitute is step 8's rendered audit under a session cookie, plus a before-and-after read of the live pages the window moved. Capture the "before" from your deployed site before you deploy the upgrade, since that's the only moment it exists. 8. **Re-run `npx cairn-audit --rendered` with a session cookie**, never without one. Without `CAIRN_AUDIT_COOKIES` every `/admin` page in your config redirects to the sign-in card, and the run measures that card twelve times and reports zero errors. It looks exactly like a pass. See [Auditing an authenticated admin](../reference/cairn-audit.md) for the cookie. If you audit your own admin screens too, note that `rendered.pages` **replaces** the default page list rather than adding to it, so a config naming only your screen silently drops cairn's six core routes. Restate the defaults beside your own. See [`rendered.pages`](../reference/cairn-audit.md#configuration). ## What a green typecheck misses A typechecker and a test suite can both pass on an upgrade that doesn't build, and on an upgrade that builds and then behaves differently. Three gaps, each of which has bitten a real consumer migration: **A server-only subpath fails at bundle time, not typecheck.** `@glw907/cairn-cms/auth-crypto` and `@glw907/cairn-cms/cloudflare` publish a `browser` condition that throws on import, so the primitives can never reach a client bundle. TypeScript resolves the `types` condition either way and says nothing. If any module in a client-reachable import chain imports one of these subpaths, your build fails with `MISSING_EXPORT ... is not exported by .../auth-crypto/browser.js`, which names the stub rather than the reason. That error is usually correct about your site and not about the upgrade. It means a module the browser bundles already reached into server-side code, and adopting the subpath is only the first thing to say so. The fix is to split whatever the client chain actually wanted into its own module, not to work around the condition. Through `0.94.0-rc.1`, the same `browser` condition also fired on the server side. Wrangler re-bundles the adapter's output for `workerd`, and that resolution applies `browser` too, so a deployed Worker imported the throwing stub and never started. Nothing before deploy reported it. `0.94.0-rc.2` adds a `worker` condition ahead of `browser` in both subpaths, which fixes it, and `0.94.0` carries the same fix. If you adopt either subpath on `0.93.0` or `0.94.0-rc.1`, move forward. **A rendered audit with no session measures your sign-in card.** Covered in step 8. **A refusal that changes shape changes what a form does.** `0.94.0`'s refusal-channel work moves several refusals between `fail()`, `redirect()`, and `error()`. A `fail()` keeps the submitted body on the page. The other two navigate away and discard it, and nothing in a typecheck or a unit test sees the difference. ## Adopt the admin type grammar When you cross `0.91.0`, the release that ships the admin grammar tokens, cross straight to `0.91.1`. `0.91.0` alone dropped nineteen utility classes from the shipped admin sheet, the named type steps among them, so custom admin markup riding any of them rendered unstyled; `0.91.1` restores the full set, and on it your custom admin screens render as they did on `0.90.1`. `npx cairn-audit`'s static `type-scale` rule starts reporting named Tailwind steps (`text-sm`, `text-xs`, bracketed sizes) in your admin markup, and most of those reports are a mechanical rename with zero visual change. On the first consumer admin measured, 265 of 298 findings were pure renames. 1. **Run `npx cairn-audit`** over your site. Each `type-scale` finding names the class you wrote and the size it resolves to: `class "text-sm" sets font-size to "0.875rem", which resolves to no --cairn-type-* token`. 2. **Match that size to a grammar role** in [Admin grammar tokens](../reference/admin-grammar-tokens.md) and rename the class to the role's utility. `0.875rem` is `--cairn-type-body`, so `text-sm` body copy becomes `type-body`. A role renders at the same size and leading the named step did, so the screen doesn't move. The grammar utilities are safelisted into cairn's shipped admin sheet, so the renamed class resolves with no Tailwind configuration change on your side. 3. **Move an off-scale size onto a role.** Where no role carries the size a finding reports, the text is off the scale. Decide what it is (body, meta, label) and move it onto that role, or keep it deliberately and add a [counted suppression directive](../reference/cairn-audit.md) with its reason. The skill's derivation ladder covers the case where none of the roles fits. 4. **Re-run the audit.** The static gate reports zero `type-scale` findings when the adoption is complete. ## How cairn versions cairn is `0.x`, and until it reaches `1.0`, the number tracks scale. A minor version means a new subsystem or public surface; everything else is a patch, whether or not it breaks you. Whether a version breaks your site is stated in its `Consumers must:` list, not signaled by the version number. Check the exact number that's free to publish next with `npm view @glw907/cairn-cms versions --json` rather than assuming the next one in sequence. That scheme lasts only through `0.x`. At `1.0`, cairn moves to compatibility SemVer: a major version signals a breaking change, and the number finally carries the compatibility promise the `Consumers must:` line carries now. The beta that precedes `1.0` publishes under an npm `beta` dist-tag as `1.0.0-beta.1`, iterating `-beta.N` and still carrying a `Consumers must:` line on any bump that breaks something, until the `1.0.0` cut. `npm install @glw907/cairn-cms` keeps resolving to the latest `0.x` release until then. ## When something breaks anyway Only the latest published minor gets fixes. There's no backport branch, so check `npm view @glw907/cairn-cms version` before assuming a bug is still open. If it's open, file a GitHub issue against [`glw907/cairn-cms`](https://github.com/glw907/cairn-cms/issues) with the version, what you expected, and what happened. Attach the structured log record if the failure logged one. cairn's runtime emits one for every commit, auth, and guard failure: [Log events](../reference/log-events.md) names each event and its fields, and [Read cairn's logs](./read-cairn-logs.md) covers querying them on a deployed Worker. ## Unreleased: entry history and revert (non-breaking) Every entry now carries a publish history and a revert-as-draft, reachable from the edit screen's overflow menu. Read [Publish and discard](./publish-and-discard.md#see-an-entrys-history-and-restore-an-earlier-version) for what your editors see, and [SvelteKit](../reference/sveltekit.md) for the underlying `historyLoad` and `revertAction` route members if you mount routes per-surface instead of the single-mount facade. Consumers must: nothing. If you mount the `createCairnAdmin` facade, the History link and the `history` view arrive automatically with the version bump. If you mount routes by hand through `createContentRoutes`, its returned object now carries two additional members, `historyLoad` and `revertAction`, which you wire the same way you already wire `editLoad` and `saveAction` to reach the feature; leaving them unmounted costs you nothing else. `Backend`'s existing-branch collision on `createBranch` now throws a typed `BranchExistsError`. The packaged dev backend used to silently overwrite the branch on a name collision and now throws instead. If you've implemented your own `Backend`, this window adds a required member, `listCommits(path, ref, limit)`, and changes `createBranch`'s return type from `Promise` to `Promise` (return the sha it created the branch at). Ordinary consumers who only use the packaged backends see neither change. ## Unreleased: public preview for a non-editor (non-breaking) An editor can now hand a draft to someone who isn't an editor: the edit screen's new "Share preview" group mints an opaque link, and a new `previewLoad` route serves it through your site's own rendering. Read [Share a draft preview](./share-a-draft-preview.md) for the full setup (the migration, the route mount, the credential-shaped emissions to suppress, and the lifecycle), and [SvelteKit](../reference/sveltekit.md#public-preview) for `previewLoad`, `mintPreviewToken`, and the `previewMintAction`/`previewRevokeAction` route members if you mount routes per-surface. **Consumers must: nothing**, unless you adopt the feature. Everything here is additive and unmounted by default; the edit screen's Share preview group ships automatically with the version bump but mints nothing useful until you take the two adopter-only steps below. If you adopt it: copy and apply the new bundled migration, `migrations/0003_preview.sql`, to your `AUTH_DB` (the same way you applied `0000_auth.sql`, see [Configure auth and D1](./configure-auth-and-d1.md#provision-the-d1-database)), and mount a `+page.server.ts`/`+page.svelte` pair at `/preview/[token]`, inside the same layout group as your entry pages. If you implement your own `Backend` or call `createContentRoutes`/`createCairnAdmin` directly rather than through the single-mount facade, their returned objects each gain two new members, `previewMintAction`/`previewRevokeAction` (and the facade's `actions` gains `previewMint`/`previewRevoke`); leaving them unwired costs you nothing else, the same way an unwired `historyLoad`/`revertAction` costs nothing. ## Unreleased: vertical alignment recipes for the admin toolkit (non-breaking) A measured pass over every admin screen found three composition classes reading more than 2px off the alignment they declared, all in the settings screen's tidy-conventions panel, and cairn ships the engine-owned fix rather than leaving each site to rediscover the recipe. A new [`FieldRow`](../reference/admin-toolkit.md#fieldrow) export on `/admin-toolkit` bottom-aligns a row that mixes a stacked field with a bare control (a button, a checkbox carrying no label of its own). Two new classes ship in the compiled admin sheet: `cairn-icon-label`, for a glyph-plus-word label inside a row declaring `items-baseline`, and `cairn-line-slot`, for a painted chip that needs to level on the text line beside it rather than on its block's top edge. See [The admin toolkit](../reference/admin-toolkit.md#fieldrow). Consumers must: nothing. `FieldRow` and the two classes are additive, and nothing already shipped changed shape. ## Unreleased: `@anthropic-ai/sdk` becomes an optional peer dependency (non-breaking) `@anthropic-ai/sdk` moves from a plain dependency to an optional peer, and the tidy action reaches it through a dynamic import at call time rather than a static import at module load. A site that never turns tidy on stops installing the SDK and its transitive packages, roughly 13 MB and 1,980 files off a production install. A site that does use tidy behaves exactly as before once the package is present. See [Enable tidy](./enable-tidy.md#install-the-sdk-dependency). Consumers must: a site using the tidy action adds `@anthropic-ai/sdk` to its own dependencies (`npm install @anthropic-ai/sdk`), because npm does not auto-install an optional peer; a site not using tidy does nothing, and its install gets lighter. ## 0.94.0: an auth-channel export, a cloudflare export, an AI posture, a packaged audit sink, and a breaking convergence of the event, locals, role, nav, and refusal seams A new server-only export subpath, `@glw907/cairn-cms/cloudflare`, publishes the Cloudflare-native platform primitives two sites already copy by hand: `verifyTurnstile`, the Turnstile siteverify fetch, fail-closed on every failure mode; and `checkRateLimit` and `checkRateLimitKeys`, the Workers `RateLimit` binding wrapper, degrade-to-open on an absent binding. Reach for it when you build your own Turnstile-guarded form or your own rate-limited endpoint, so you reuse the same primitives instead of copying them by hand. `RateLimitLike` moves to this subpath as its one declaration in the source tree; if you imported it from `@glw907/cairn-cms/sveltekit`, that import keeps working unchanged. See [Cloudflare](../reference/cloudflare.md). **If you are replacing a hand-rolled copy, read the argument order at every call site.** `verifyTurnstile(token, secret, opts)` takes the secret second, and the client IP moves to `opts.ip`. One of the copies this subpath replaces took `(token, ip, secret)`. All three are strings, so a swapped pair compiles, and verification then runs against a secret that is really an IP address and fails closed on every submission. No typecheck and no unit test with a mocked siteverify sees it. The first consumer migration through this window was safe only because it changed every call site in one pass. A new factory on `@glw907/cairn-cms/sveltekit`, `createD1AuditSink(db, waitUntil)`, is the first packaged implementation of the `AdminActionAuditSink` seam: apply the bundled `migrations/0002_audit.sql` and wire the factory to persist every `ctx.audit` record into one `audit_log` table, instead of hand-rolling your own sink module. It's opt-in, fail-open, and truncates every bound field to a documented maximum. See [`createD1AuditSink`](../reference/sveltekit.md#created1auditsink). **Check for an `audit_log` table before applying that migration.** The bundled schema is one consumer's table carried whole, so a site that hand-rolled a sink before this factory existed very likely already has the table, and `CREATE TABLE audit_log` then fails against it. What the sink actually needs is a table it can bind five columns into: `actor`, `action`, `entity`, `entity_id`, and `detail`. If yours already has those, wire the factory and skip the migration. The one difference worth knowing about is `created_at`: the bundled schema defaults it to `strftime`'s ISO 8601 form, unambiguous UTC at millisecond resolution, where an older `datetime('now')` default writes a space-separated, second-resolution string that `new Date(...)` reads as local time and that sorts same-second rows non-deterministically. That is a reason to migrate the column eventually, not a reason to hold up the upgrade. Consumers must: nothing. Both additions are additive, and the `RateLimitLike` re-export keeps its existing shape and location on `/sveltekit`. A review pass across the whole seam contract settled a handful of long-standing questions, none of which changed an exported type or a route contract. It found that a bare `wrangler types`-generated `Env`, with no [`CairnPlatformBindings`](../reference/sveltekit.md#cairnplatformbindings) intersection, failed to compile `export const actions = admin.actions` (and every other route factory assignment), because `@cloudflare/workers-types`' `SendEmail.send` returns `Promise` while cairn's own `EMAIL.send` declared `Promise`. **This incompatibility dissolved in the C2 breaking-window pass** (see that section below): cairn's `EmailSender.send` now returns `Promise`, which structurally accepts the wider Cloudflare return type, so `CairnPlatformBindings` stays a recommended convenience preset rather than a requirement. If your `app.d.ts` already follows [Deploy to Cloudflare](./deploy-to-cloudflare.md#wire-the-guard), nothing changes for you either way. **Consumers must:** widen any email-binding type you declared yourself. Widening cairn's own return type is a fix from cairn's side and a break from yours: a hand-written `{ EMAIL?: { send(message): Promise } }`, the structural subset a site writes when it wants to read the binding loosely rather than trust `CairnPlatformBindings`, no longer accepts what `CairnPlatformBindings['EMAIL']` now is, and every call passing one to the other stops compiling. Widen your own declaration to `Promise` to match, or import [`EmailSender`](../reference/sveltekit.md#cairnenv) rather than restating its shape. The first consumer migration through this window hit it at five call sites. `adminAction`'s audit sink now holds its advertised fail-open promise at the engine's own call site: a hand-rolled `event.locals.cairnAuditSink` that throws, or one that rejects asynchronously, no longer fails the action it audited, and the failure logs the new `admin.action.sink_threw` event instead of disappearing. The package now declares `"engines": { "node": ">=22" }`, and a new reference page, [Supported toolchain](../reference/supported-toolchain.md), states the versions cairn promises against and proves against, for `@sveltejs/kit`, `svelte`, `vite`, `typescript`, `node`, and TypeScript module resolution. `adminAction`'s two refusals, a missing signed-in editor (authentication) and a CSRF mismatch, now throw SvelteKit's own `redirect()` and `error(403, ...)` instead of the dev-only error class, the same framework-native shapes `requireOwner`, `requireEditor`, and `requireAccess` throw for their own authorization checks (`requireSession` throws the same redirect for its own authentication check). `adminAction` itself still performs no authorization: a `none`-capability editor's session passes both of its checks and reaches your handler unchanged; add [`requireAccess`](../reference/sveltekit.md#requireaccess) inside the handler, or build on [`createSectionAction`](../reference/sveltekit.md#createsectionaction), for a capability check. If your `hooks.server.ts` defines a `handleError` only to map the old `AdminActionError` into a legible response for these two refusals, remove that mapping: it does nothing useful now, and a site relying on the old `500` these refusals produced for alerting now sees a `303` (the redirect) and a `403` (the SvelteKit-native error) instead. Both now navigate away from the submitting page (the redirect to `/admin/login`, the 403 to the nearest error boundary), discarding any unsaved form input, where the old `500` left the page itself intact and recoverable with Back. The class itself renames to [`UnauditedActionError`](../reference/sveltekit.md#types): after this refusal channel convergence it means exactly one thing, the dev-only unaudited-action defect signal, a build-time check that never reaches a production response, and the new name states that plainly. See [Refusal channels](../reference/sveltekit.md#refusal-channels). Consumers must: be on Node 22 or later for your build toolchain (already the tutorial's stated requirement, now a declared one too); replace any imported `AdminActionError` with `UnauditedActionError`; and remove any `handleError` mapping of that class for `adminAction`'s two authentication refusals (they need no mapping anymore). Nothing else in this window changes an exported type, a route contract, or a behavior you'd observe without hitting one of those two: a throwing or rejecting audit sink previously failed the action it audited and now does not. `AuthEnv` and `BackendEnv` collapse into one all-optional `CairnEnv` (`AUTH_DB`, `PUBLIC_ORIGIN`, `CAIRN_DEV_BACKEND`, `EMAIL`, `GITHUB_APP_PRIVATE_KEY_B64`), exported from both the root barrel and `/sveltekit`. `EmailSender` is named once (`{ send(message): Promise }`) and referenced from both `CairnEnv` and `CairnPlatformBindings`; the widened `Promise` return dissolves the `SendEmail.send` incompatibility above, so `CairnPlatformBindings` demotes from a requirement back to a recommended convenience preset. `PlatformContext` narrows to `{ env?: Env }` (the engine never read `ctx`/`context`) and is now exported from `/sveltekit`. See [SvelteKit](../reference/sveltekit.md#cairnenv). Consumers must: replace any imported `AuthEnv`/`BackendEnv` with `CairnEnv` on the same subpath, and drop any reliance on `PlatformContext.ctx`/`.context` (the engine never read either). `EventBase`, `RequestContext`, the content routes' `ContentEvent`, the admin facade's `AdminEvent`, and `adminAction`'s own `AdminActionEvent` collapse into one `CairnEvent`. It adds `params` and `route: { id: string | null }` (ending the anti-idiom of reading route identity out of a form body, and giving `SectionActionOptions.target` an honest derivation), and makes `cookies` and `setHeaders` unconditionally required, since a real SvelteKit event always carries all four. The entry below renames `locals`'s key names. `requireSession`, `requireOwner`, `requireEditor`, and `requireAccess` now take `CairnEvent` in place of their old minimal inline shapes. See [the event shape](../reference/sveltekit.md#the-event-shape). Consumers must: replace any imported `EventBase`, `RequestContext`, `ContentEvent`, `AdminEvent`, or `AdminActionEvent` with `CairnEvent` on the same subpath; a hand-built event fixture (a test, a script) now needs `params` and `route` alongside the fields it already carried. A fixture built with an `as unknown as` cast, the usual shape, still compiles without them and then throws `Cannot read properties of undefined (reading 'id')` from inside the engine on the first call that derives an authorization target. Set `route.id` to the bracket-form route the fixture stands for, not the concrete request path, since that is what the derivation reads. `locals`'s four keys take the flat `cairn` prefix: `editor` becomes `cairnEditor`, `backend` becomes `cairnBackend`, and `auditSink` becomes `cairnAuditSink` (`cairnAccess` already carried the prefix and is unchanged). A flat key costs a site one optional hop (`event.locals.cairnEditor`) instead of a nested `locals.cairn.editor`, and a grep for `cairnEditor` now finds every engine read of the field in any repo. There is no alias and no fallback read of the old names: the rename applies everywhere at once. See [the ambient types reference](../reference/ambient.md) for the full four-key shape and [the event shape](../reference/sveltekit.md#the-event-shape) for `CairnEvent['locals']`. Consumers must: rename every `event.locals.editor`, `event.locals.backend`, and `event.locals.auditSink` read or write in your own `hooks.server.ts` and any custom admin route to `event.locals.cairnEditor`, `event.locals.cairnBackend`, and `event.locals.cairnAuditSink`; a custom `App.Locals` augmentation that duplicates the old field names (rather than importing `@glw907/cairn-cms/ambient`) needs the same rename. Every role-*name* position (`Editor.role`, `EditorRow.role`, `insertEditor`'s and `setEditorRole`'s `role` parameters, `AccessMap`'s values, and a `navLayout` entry's or section's `roles`) widens from the implicit `'owner' | 'editor'` union to a plain `string` (or `string[]`): role names are open, since you name your own vocabulary with `defineRoles`, and the old literal union stopped being true the moment you declared a role like `webmaster`. `Capability` (`'owner' | 'editor' | 'none'`) is unchanged. The `Role` type is removed from the root barrel and `/auth-store`, along with the `CairnRolesRegister` registry interface it existed solely to narrow. See [Roles](../reference/core.md#roles) and [Auth store](../reference/auth-store.md). Consumers must: replace any imported `Role` type with `string`; if your `app.d.ts` augments `interface CairnRolesRegister { roles: typeof roles }`, remove that block, since it no longer narrows anything; drop any cast you added to force a custom role name past the old `Role` union (an `AccessMap` value, a `navLayout` entry's `roles`, or an `Editor`/`EditorRow` fixture). The route-factory members and the admin facade's `actions` keys now follow one grammar: a member that is a SvelteKit `load` ends in `Load`, a member that is a form action ends in `Action`, and a facade `actions` key is its member name minus the `Action` suffix. Twelve route-factory members rename: `createContentRoutes`'s `settingsSave` to `settingsSaveAction`, `vocabularySave` to `vocabularySaveAction`, `shellPayload` to `shellLoad`, `indexRedirect` to `indexLoad`, `addDictionaryWordAction` to `dictionaryAddAction`, `mediaPurgeOrphansAction` to `mediaOrphanPurgeAction`, `mediaReplaceApplyAction` to `mediaReplaceAction`, and `mediaAltApplyAction` to `mediaAltPropagateAction`; `createNavRoutes`'s `navSave` to `navSaveAction`; and `createEditorRoutes`'s `addEditorAction`, `removeEditorAction`, and `setRoleAction` to `editorAddAction`, `editorRemoveAction`, and `editorSetRoleAction`. Seven facade `actions` keys on `createCairnAdmin` rename to match: `saveSettings` to `settingsSave`, `saveVocabulary` to `vocabularySave`, `addDictionaryWord` to `dictionaryAdd`, `addEditor` to `editorAdd`, `removeEditor` to `editorRemove`, `setRole` to `editorSetRole`, and `mediaPurge` to `mediaOrphanPurge`. See [SvelteKit](../reference/sveltekit.md#createcontentroutes) and [admin routes](../reference/admin-routes.md#the-actions-vocabulary). Consumers must: rename every renamed route-factory member in your own `+page.server.ts` files if you mount routes per-surface rather than through the single-mount `createCairnAdmin` facade (the facade itself needs no source change). If your own markup, a form's `action="?/oldName"` or a programmatic `fetch('/admin/...?/oldName')` call, posts to a renamed facade action by name, change the posted `?/` action string to the new name (for example `?/saveSettings` to `?/settingsSave`, `?/addEditor` to `?/editorAdd`, `?/mediaPurge` to `?/mediaOrphanPurge`); a mismatched name fails at runtime as a 404 on submit, not at compile time, since the action name is a string literal. Every injectable-dependency bag renames from `*Deps` to `*Config` (the factory's primary bag) or `*Options` (a secondary or per-call bag), the parameter-bag half of the same grammar: `CairnAdminDeps` to `CairnAdminOptions`, `ContentRoutesDeps` to `ContentRoutesOptions`, `AdminActionDeps` to `AdminActionOptions`, and `PublicRoutesDeps` to `PublicRoutesConfig` (`createPublicRoutes`'s only bag, so it takes the primary-bag name). `createAuthGuard`'s and `createEditorRoutes`'s previously anonymous inline option bags are now named and exported too, as `AuthGuardOptions` and `EditorRoutesOptions`; a site typing its own wrapper around either factory can now name the parameter instead of writing the shape out. `CairnAdminOptions.auth` also stops re-declaring `AuthRoutesConfig`'s shape inline and references it directly (`Partial`), so the two stay in lockstep. Every `create*` factory's return type is named and exported too: `CairnAdminRoutes`, `ContentRoutes`, `AuthRoutes`, `EditorRoutes`, `NavRoutes`, `PublicRoutes`, and `Renderer`, so a site can name a variable or a wrapper function's return type instead of writing the returned shape out by hand. `makeMediaResolver` renames to `buildMediaResolver`, matching the four-verb grammar (`build` derives pure data from an already-resolved config; `make` is retired). The media orphan-scan result type renames from `OrphanScan` to `MediaOrphanScanResult`, and the custom-nav icon-name type from `AdminNavIcon` to `NavIcon`. `NavLayoutEngineRef.hidden` widens from the literal `hidden?: true` to `hidden?: boolean`, so a computed flag (a feature switch, a role check) is as valid as the literal; the runtime already treated a falsy `hidden` as visible. The `MakeIcon` re-export is removed from `@glw907/cairn-cms/render` (the type stays internal; it named a site's icon factory signature with zero real callers). `ConceptUrlPolicy` is removed from the root barrel (it stays internal, used by `defineConcept`); a site never constructed one directly, since `defineConcept`'s own `permalink`/`datePrefix` config builds it. See [SvelteKit](../reference/sveltekit.md). Consumers must: replace any imported `CairnAdminDeps`, `ContentRoutesDeps`, `AdminActionDeps`, or `PublicRoutesDeps` with its renamed `*Options`/`*Config` counterpart; replace `makeMediaResolver` with `buildMediaResolver`; replace any imported `OrphanScan` with `MediaOrphanScanResult` and `AdminNavIcon` with `NavIcon`; and drop any imported `MakeIcon` or `ConceptUrlPolicy` (or inline their shape, since both stay usable as unexported internals only through their owning modules' public factories). `adminNav` retires entirely. `navLayout` is now the one nav seam: `CairnAdapter.editor.adminNav`, `CairnRuntime.adminNav`, `AdminNavConfig`, `AdminNavSection`, `normalizeAdminNav`, `filterNavByRole`, `ResolvedNavItem`, and `ResolvedNavSection` are removed, and `ResolveNavLayoutOptions.adminNav` and `validateNavLayout`'s `hasAdminNav` ctx member go with them. `AdminNavEntry`'s members (`label`, `icon`, `href`, `ownerOnly?`) fold into `NavLayoutEntry`, which stops extending it and stands self-contained. The behavioral objection that kept `adminNav` alive, that it was additive (declare one link) where `navLayout` replaced the whole sidebar, is answered by `navLayout`'s own fallback: an engine screen a declared layout never references still lands in the trailing fallback group, so adding one link is still a one-entry declaration. See [the navLayout seam](../reference/sveltekit.md#the-navlayout-seam) and [Organize your admin nav](./organize-your-admin-nav.md#omission-falls-back-hiding-is-explicit). Consumers must: replace `editor.adminNav` on the adapter with `editor.navLayout`; a flat `adminNav` entry becomes a top-level `NavLayoutEntry` in the `navLayout` array, unchanged in shape (`{ label, icon, href, ownerOnly? }`), and an `adminNav` section becomes a `NavLayoutSection` (`{ label, children }`) the same way. A site whose whole reason for `adminNav` was adding one extra link declares that single entry in `navLayout` and nothing else: every one of cairn's own screens the declaration omits still renders, in the same trailing fallback group the zero-config sidebar already uses for Help. Replace any imported `AdminNavEntry`, `AdminNavSection`, `AdminNavConfig`, `ResolvedNavSection`, or `ResolvedNavItem` type with `NavLayoutEntry`, `NavLayoutSection`, `NavLayout`, `ResolvedLayoutSection`, or `ResolvedLayoutNode` respectively. `@glw907/cairn-cms/admin-fields` merges into `@glw907/cairn-cms/admin-toolkit` and is removed: two subpaths stating one charter ("primitives for a site building its own admin screens") is one subpath. The merged toolkit's form components rename to resolve a name collision with the root barrel's field *descriptor* arms (elsewhere in this window, `TextField`/`SelectField` become importable at the root as content field descriptors): `TextField` becomes `TextInput`, `SelectField` becomes `SelectInput`, and `SelectFieldOption` becomes `SelectInputOption`. `FieldLabel` is unchanged. `OfficeList` also moves from `/components` to `/admin-toolkit`, beside `PageHeader`, its own later generalization; both stay, since they cover different shapes, a header primitive versus a full list-screen scaffold. `/components` also completes its per-view seam: `VocabularyAdmin` and `WelcomeView` join the barrel, so a site on the advanced per-route mounting can now mount every view `CairnAdmin` renders internally. See [The admin toolkit](../reference/admin-toolkit.md#fields) and [Components](../reference/components.md#vocabularyadmin). Consumers must: replace any `@glw907/cairn-cms/admin-fields` import with `@glw907/cairn-cms/admin-toolkit`; rename `TextField` to `TextInput`, `SelectField` to `SelectInput`, and `SelectFieldOption` to `SelectInputOption`; replace `@glw907/cairn-cms/components`'s `OfficeList` import with `@glw907/cairn-cms/admin-toolkit`. The export rule adopted as standing doctrine: every type named in a public signature is exported from a subpath you already import, so you can always name a value a route factory or the adapter contract hands you instead of writing your own structural duplicate (the trap an AI coding agent especially falls into). Roughly ninety previously unreachable types become named, documented exports this window, closed under their own structural bodies: the field-descriptor union's fifteen arms (`TextField`, `SelectField`, `ArrayField`, and the rest, mentioned above) from the root barrel; the facade and action result and plan types (`TidyResult`, `DictionaryAddResult`, `MediaBulkDeleteResult`, `MediaOrphanPurgeResult`, `MediaOrphanScanResult`, `MediaReplacePreviewPlan`, `MediaAltPreviewPlan`) and their supporting shapes (`TidyClient`, `TidyConfig`, `TidyConventions`, `TidyKeyProbeResult`, `FragmentTarget`, `LinkTarget`, `InboundLink`, `UsageEntry`, `ReferenceEdge`, `MarkdownReferenceRow`, `MediaLibraryEntry`, `ResolvedPreview`, `CookieSetOptions`, `GettingStarted`, `ResolveOptions`) from `/sveltekit` and, where a root signature names it, the root barrel too. Three recurring anonymous inline load-payload shapes are named and exported from `/sveltekit`: `LoginData`, `ConfirmData`, and `EditorsData`, replacing the anonymous object literals the admin facade's `AdminData` union used to inline for its `'login'`, `'confirm'`, and `'editors'` views. See [Core](../reference/core.md#types), [SvelteKit](../reference/sveltekit.md#types), and [Delivery data](../reference/delivery-data.md#types). Consumers must: nothing. Every addition is a new named export; nothing already imported changed shape or name. `createSectionAction`'s `SectionActionOptions.target` now defaults to `event.route.id`, never `event.url.pathname`: on a catch-all route the request path is attacker-chosen while the route id isn't. `resolveDb`'s shape stays ratified unchanged (`(env: Env | undefined) => Db | undefined`); the engine can't conjure an absent platform. A wrapped handler's `ctx.audit` call now also defaults `action`/`entity` from the call site's own `SectionActionOptions`, so the common call names only `entityId`/`detail` (`ctx.audit({ entityId })`); a handler that touches more than one entity in one call can still override either verb. See [`createSectionAction`](../reference/sveltekit.md#createsectionaction). **Consumers must:** for any `createSectionAction`-guarded parameterized or catch-all route, rekey the site's access map from the concrete request path to the bracket-form route id (`/admin/posts/[id]`, never `/admin/posts/hello-world`); a map still keyed by the concrete path stops matching, and the section fails closed, refusing every session including owner, with no thrown error to surface the mistake. A static route's id and path are the same string, so a site with no parameterized or catch-all section behind `createSectionAction` needs no change. Route groups need no change either: the derived target drops group segments, so `/admin/(app)/roster` keeps matching a map keyed `/admin/roster`. `requireAccess`'s own `target` parameter now defaults the same way: `event.route.id`, never `event.url.pathname`. This closes the asymmetry between the two halves of the same authorization story that the C2 breaking-window post-mortem flagged. `createSectionAction` already made this change in the preceding entry, and `requireAccess` had not. The internal derivation the two share moves out of `section-action.ts` into `auth/access.ts`, invisible from a site. See [`requireAccess`](../reference/sveltekit.md#requireaccess). **Consumers must:** for any `requireAccess`-guarded parameterized or catch-all route, rekey the site's access map from the concrete request path to the bracket-form route id, the same rekey step described for `createSectionAction` in the preceding entry, or the helper fails closed and refuses every session, including owner. A static route's id and path are the same string, so a site with no parameterized or catch-all `requireAccess` route needs no change. The admin refusal channel converges on `fail()`: every built-in content, media, settings, vocabulary, and nav action's own validation and commit-conflict refusal now answers `fail(...)` in place, keeping your submitted body on the page, instead of throwing a redirect that discarded it. `settingsSaveAction`, `vocabularySaveAction`, `navSaveAction`, and `createAction` each widen from `Promise` to `Promise>` (`SettingsSaveFailure`, `VocabularySaveFailure`, `NavSaveFailure`, and `CreateFailure` respectively). If you mount these route-factory members yourself, through `createContentRoutes` or `createNavRoutes` rather than the single-mount `createCairnAdmin` facade, and you hand-annotated one of their return types as `Promise`, widen it to match. If you mount through the facade, or through these factories with no such annotation, you see no change beyond the new failure shapes reaching your own `form` prop, the same way `saveAction`'s `SaveFailure` already does. See [Refusal channels](../reference/sveltekit.md#refusal-channels). **Consumers must:** update any hand-annotated `Promise` return type on `settingsSaveAction`, `vocabularySaveAction`, `navSaveAction`, or `createAction`. Nothing else here needs a code change. The facade's `viewAction` wrapper drops its `scriptPosted` branch: every action's own unexpected failure, whether the request came from a form post or a `fetch` call, now answers `fail(500, { error })` in place instead of a form-posted action redirecting away. A failed discard, sign-in confirm, logout, or publish-all now shows the same calm retry message on the page instead of bouncing you elsewhere when something unexpected goes wrong; a validated refusal or a deliberate success on any of these four is unchanged. The `scriptPosted` and `carriesNewFlag` facade options are gone, but neither was ever public. See [Refusal channels](../reference/sveltekit.md#refusal-channels). Consumers must: nothing. `viewAction` is internal to the facade, and the behavior change only touches an unexpected-failure path. Every refusal that genuinely navigates now carries a bounded internal code on `?error=` instead of a free-form string, resolved server-side against a small closed vocabulary; an unrecognized value resolves to nothing. Only three refusals still navigate at all (an expired sign-in link, publish-all's two outcomes) plus the `/admin` landing relay that forwards one of those two onward, so six other `?error=` readers and the data field each one filled are removed outright: `EditData.error`, `EditorsData.error`, `NavLoadData.error`, `SettingsData.error`, `VocabularyLoadData.error`, and `MediaLibraryData.flashError`. See [Refusal channels](../reference/sveltekit.md#refusal-channels) and [the security model](../explanation/security-model.md#who-may-edit). **Consumers must:** drop any read of `EditData.error`, `EditorsData.error`, `NavLoadData.error`, `SettingsData.error`, `VocabularyLoadData.error`, or `MediaLibraryData.flashError` in your own code; each field is gone outright. If you never read one of these fields directly, the common case since cairn's own components already handled them, you see no change. `TidyResult.usage` renames to `TidyResult.tokens`. The name collided with `MediaDeleteRefusal.usage` (the where-used rows a refused media delete carries) inside SvelteKit's generated `ActionData` union for the admin route, once every content action carried a precise `ActionFailure` rather than the old, looser `ActionFailure`; the collision made `` fail your own `svelte-check`. See [`tidyAction`](../reference/sveltekit.md#createcontentroutes). **Consumers must:** rename any read of `TidyResult.usage` to `TidyResult.tokens`. The log-event vocabulary settles on one grammar (`area[.subject].verb_phrase`, a past-tense verb phrase for an occurrence or a state adjective for a detected condition) and every `reason`/`scope` value goes snake_case. Six events rename to fit: `admin.audit.sink_failed` to `audit.sink.write_failed` (the packaged D1 sink's own persist failure, engine infrastructure, not the action layer); `admin.action.audit_sink_failed` to `admin.action.sink_threw` (a site-supplied sink throwing at the engine's call site); `tidy.done` to `tidy.succeeded` and `tidy.error` to `tidy.failed` (matching the `commit.*` pattern); `media.orphan_reconcile` to `media.orphans_reconciled` (matching `media.orphans_purged`); and `content.field_behavior_error` to `content.field_behavior_failed`. `guard.rejected`'s `reason: 'csrf'` and `admin.action.csrf_rejected` stay distinct on purpose: different layers, the pre-resolve guard versus the action wrapper's own defense in depth. The media upload `reason` family goes snake_case (`media_disabled`, `length_required`, `too_large`, `session_expired`, `access_denied`, `unsupported_type`, `binding_missing`, `hash_collision`), reaching the upload popover's own failure-card mapping in the same change. `github.unreachable`'s `scope` values become `shell`, `help`, and `publish_advisories` (the documented `layout` scope never fired). `config.invalid` gains a `scope` (`nav`, `settings`, or `vocabulary`) so its three emit sites and four call paths stop sharing one indistinguishable log line. See [Log events](../reference/log-events.md). **Consumers must:** update any Workers Logs saved query, alert, or dashboard filter that names `admin.audit.sink_failed`, `admin.action.audit_sink_failed`, `tidy.done`, `tidy.error`, `media.orphan_reconcile`, or `content.field_behavior_error` to the new name, and any filter on a kebab-case media upload `reason` (`media-disabled`, `length-required`, `too-large`, `session-expired`, `access-denied`, `unsupported-type`, `binding-missing`, `hash-collision`) or on `github.unreachable`'s `scope: 'layout'` (which never fired) to the corrected snake_case value. Nothing breaks at compile time, since these are runtime log values, not exported types. `/admin-toolkit`'s formatters settle on one nullish rule: every display formatter, `formatMoney`, `formatCivilDate`, `formatTimestamp`, and `formatPhone`, accepts a nullish input and takes a `fallback?: string` option defaulting to `''`, so you never have to remember which formatter tolerates a missing value and which throws. `formatMoney`, `formatTimestamp`, and `formatPhone` widen their first parameter to accept `null`/`undefined` and gain the `fallback` option (`formatPhone` gains its first options parameter, `FormatPhoneOptions`); `formatCivilDate` already accepted a nullish date, and keeps that shape, but its `fallback` default drops from the opinionated `'Not yet'` to `''`. `ageFromBirthdate` doesn't change: it returns `number | null`, not a display string, and stays outside this rule on its own documented grounds. See [Admin toolkit](../reference/admin-toolkit.md#formatts). **Consumers must:** if you call `formatCivilDate` and relied on its old `'Not yet'` default for a missing date, pass `{ fallback: 'Not yet' }` explicitly; otherwise that cell now renders an empty string. This is a silent visual change, not a compile error, since `formatCivilDate`'s date parameter was already nullable. Calls to `formatMoney`, `formatTimestamp`, or `formatPhone` with a value that's statically `number`/`string` (never nullish) need no change. Your adapter can now carry `aiPosture`, set to `'decline'` or `'invite'`, read by `buildRobots`/`robotsResponse` (`@glw907/cairn-cms/delivery`). Set `'decline'` and your `robots.txt` adds a `Disallow: /` group per token in a new first-party-verified training-crawler table, `AI_CRAWLERS` (and its review date, `AI_CRAWLERS_REVIEWED`), plus `Content-Signal: ai-train=no`. Set `'invite'` and it adds `Content-Signal: search=yes, ai-train=yes` and no `Disallow` lines at all, since there's no robots directive that invites a crawler. Declining is a request that named crawlers say they honor, not enforcement: robots.txt has no mechanism to block a fetch, and OpenAI's `ChatGPT-User` and Perplexity's `Perplexity-User` are exempt from robots.txt by their own operators' first-party design. See [Choose an AI posture](./choose-an-ai-posture.md) and [Delivery data](../reference/delivery-data.md#buildrobots). **Consumers must:** nothing. `aiPosture` is optional and unset on your site today, so this window's `robots.txt` output is byte-identical to before. `cairn-doctor` gains a nineteenth check, `ai.posture-effective`: a plain, credential-free `GET /robots.txt` against your deployed origin, reporting what the live file actually carries rather than what your adapter declares. It distinguishes stating no posture, stating a posture your live site contradicts, and a managed layer, Cloudflare's AI Crawl Control or its managed robots.txt, prepending directives cairn didn't write, a shape measured live on three of the four sites in cairn's own operator estate. Only the middle case fails; stating no posture passes, and so does a managed layer, since whether that's wanted is your call as the zone's owner. See [`cairn-doctor`](../reference/doctor.md#the-checks). **Consumers must:** nothing today, since the failing case needs a declared `aiPosture` and you haven't declared one. If you adopt a posture and this check goes red afterward, your deployment doesn't carry the stance you stated. Every routable, non-`noindex` entry can now serve a raw-markdown twin of its own page. `markdownResponse` (`@glw907/cairn-cms/delivery`) wraps a body in `text/markdown; charset=utf-8`, a sibling of `robotsResponse`/`sitemapResponse`, and `createPublicRoutes` gains `markdownEntries()` and `markdownLoad(event)` to enumerate and serve one `.md`-suffixed path per entry. The twin reads only through the injected site resolver, so it can serve only what that resolver carries. Wire it through a prerendered route, the same way you already wire robots.txt and the sitemap, and the build runs against committed `main` content, which is what keeps a pending `cairn/*` edit branch structurally out of reach. A runtime route reopens that question. See [Wire the delivery surface](./wire-the-delivery-surface.md#serve-a-raw-markdown-twin-of-every-entry) and [Choose an AI posture](./choose-an-ai-posture.md). **Consumers must:** nothing. No site wires this route today, so it ships no new response on your deployed site until you add it. The admin `Strict-Transport-Security` header no longer sends `includeSubDomains` by default (the AI-posture pass, the HSTS rider). Every `/admin` response previously pinned your site's apex and every sibling subdomain to https in the visiting editor's browser for two years, including on a zone whose owner had left edge HSTS off, and you had no way to un-pin an already-pinned editor except by serving a corrective header. `max-age` still sends unconditionally; the admin surface is the one place cairn has standing to insist on https for itself. Only `includeSubDomains` becomes conditional, since pinning subdomains cairn knows nothing about is your call, not the engine's. `AuthGuardOptions` gains `includeSubDomains?: boolean`, alongside `roles` and `access`. cairn's doctor also reconciles the zone-level `edge.hsts` check's wording: a failing zone setting no longer reads as though nothing is protected, since your admin responses already carry their own header regardless of the zone. See [`createAuthGuard`](../reference/sveltekit.md#createauthguard). The guard's rejection pages and its login redirect now send no `Strict-Transport-Security` at all. RFC 6797 has a browser replace its cached policy on every header it receives, so a rejection page sending `max-age` alone would have cleared the `includeSubDomains` your guarded responses asserted, and the CSRF rejection is reachable by any cross-site POST with no session at all. **Consumers must:** if you want the previous domain-wide pinning back, set `createAuthGuard({ includeSubDomains: true })` in your `hooks.server.ts`. If you take no action, your admin responses keep `max-age` but stop pinning sibling subdomains. **Check your zone first:** if edge HSTS already sends `includeSubDomains` for the admin's host, which is the case on more than one site running this engine, set the option so cairn states the same policy rather than a weaker one on the same host. Site code can now call [`createD1AuditSink`](../reference/sveltekit.md#created1auditsink) directly with its own domain events, a roster change or a season rollover, not only through `adminAction` and `createSectionAction` (the 2026-08-05 engine-harvest sitting, ruling 1). The sink was already generic; the change is sanction, not new code. Namespace your action names (`roster.add`, not a bare `add`) so a domain row stays distinguishable from an admin-action row in the shared table. Sanctioning direct use means the record's identity field can no longer be a cairn editor specifically, so `AdminActionAuditRecord`'s field renames from `editor` to `actor`, matching the column it has always landed in. `adminAction`'s own composition follows, as does the packaged sink's own read. Two log events rename their identity field to `actor` to match: `admin.action.audited` and `audit.sink.write_failed`. `admin.action.sink_threw` keeps `editor`, since it fires only from inside `adminAction`, where the actor is always a verified cairn editor. See [SvelteKit](../reference/sveltekit.md#types) and [log events](../reference/log-events.md). **Consumers must:** rename any read of `record.editor` to `record.actor` in a hand-rolled `AdminActionAuditSink` (the [custom admin screen guide](./add-a-custom-admin-screen.md#wire-the-auditsink)'s example is the shape to check against), or in a custom `App.Locals` augmentation that duplicates `AdminActionAuditRecord`'s shape rather than importing it. A site wiring no audit sink does nothing. ## 0.93.0: an auth-store export, an auth-crypto export, a section-action factory, a first-publish stamp, and a CodeMirror dependency bump (non-breaking) A new server-only export subpath, `@glw907/cairn-cms/auth-store`, re-exports the D1 editor-provisioning functions the engine's own `editors-routes` already uses: `listEditors`, `insertEditor`, `deleteEditor`, `setEditorRole`, `removeOwnerIfNotLast`, `insertOwnerIfEmpty`, and `demoteOwnerIfNotLast`, plus the `EditorRow` and `Role` types. Reach for it when you need to provision or manage editors from your own server code, a setup script, or a migration, outside the `ManageEditors` screen. See [Auth store](../reference/auth-store.md). A second new server-only export subpath, `@glw907/cairn-cms/auth-crypto`, re-exports the token and session-id generators, the token hash, the constant-time compare, and a new `__Host-` cookie-naming primitive: `generateToken`, `generateSessionId`, `generateCsrfToken`, `hashToken`, `tokensMatch`, and `cookieName`. Reach for it when you build your own login flow for a second audience, member magic-link sessions, offer tokens, an OTP flow, so you reuse the same cryptography the engine's own login proves in production instead of copying it by hand. See [Auth crypto](../reference/auth-crypto.md). The content manifest gains `ManifestEntry.publishedAt`, an ISO 8601 UTC stamp a publish action writes once, at the commit that first lands an entry non-draft, and never overwrites or clears afterward. An entry already non-draft and unstamped before this release stays unstamped forever, unless you take it back to Hidden and publish it again, which stamps it as though it were newly published; only a future transition into published stamps otherwise. A new pure helper, `newlyPublishedEntries(before, after)` on `@glw907/cairn-cms/delivery/data`, diffs two manifests down to the entries that just carried that transition and are still currently live, so you can detect a first publish and fan out your own notification with no engine networking or scheduling involved. The same subpath also re-exports `Manifest` and `parseManifest`, so you can name and validate the manifest you fetch to build the `before`/`after` pair. See [Announce on publish](./announce-on-publish.md). A third new export on `@glw907/cairn-cms/sveltekit`, `createSectionAction`, packages the form-action guard every site-built admin section otherwise hand-rolls: SvelteKit dispatches a matched action directly, with no ancestor layout `load` run first, so a section's own POST needs its own check. The factory composes `adminAction`'s editor resolution, CSRF, and audit contract with an optional rate limit (degrade-to-open) and the same access-map check `requireAccess` runs, then hands your handler its resolved database binding; authorization runs before that binding check, so a refused session never learns whether the binding is deployed. `AdminActionEvent` becomes generic over your platform env, defaulting to `CairnEnv` (named `AuthEnv` before the C2 breaking-window pass) so no existing call site changes, and `App.Locals` gains the `cairnAccess` map the guard already attaches. See [SvelteKit](../reference/sveltekit.md#createsectionaction). The `@codemirror/*` editor dependencies moved to their latest 6.x releases within cairn's existing version ranges (`@codemirror/state` 6.6.0 to 6.7.1, `@codemirror/view` 6.43.0 to 6.43.7, plus patch bumps to `autocomplete`, `commands`, `language`, and `lang-markdown`). Lockfile-only. Consumers must: nothing. All four new seams are additive, and the `publishedAt` stamp only ever appears on a publish that happens after the upgrade. ## 0.92.0: a UA reset layer, a tightened `one-filled-action`, an exported stacked field register, and a skill-exemplar compile gate The packaged admin sheet now ships a `base` cascade layer, so a bare form control, `dialog`, `fieldset`/`legend`, or daisyUI's own `.list` container renders the admin's own face instead of the browser's UA default: a bare `