Skip to content

v0.79.0: the pre-beta window — the surface-pruning contract freeze and the code-polish pass

Choose a tag to compare

@glw907 glw907 released this 02 Jul 22:30

The pre-beta window: the surface-pruning contract freeze and the code-polish pass, batched. The last 0.x release; 1.0.0-beta.1 follows at beta.

The pre-beta surface-pruning pass shrinks and firms the public contract to exactly what the beta
freeze will promise. An adversarial audit (19 agents: a prosecutor per export subpath, an Opus
defender, three shape auditors) convicted every demoted name for having no real consumer import
across the showcase, ecxc-ski, and 907-life; docs/internal/api-surface.md's diff for this pass is
the exhaustive, generated list, since the counts below summarize rather than enumerate.

The root @glw907/cairn-cms barrel sheds 72 content-graph, manifest, component-grammar, and
field-arm internals plus the duplicate ResolvedReference re-export (its home is /delivery);
every demoted symbol keeps living in its module for the engine's own relative imports, nothing is
deleted. /sveltekit loses isPublicAdminPath, parseAdminPath, AdminView, and NavRoutesDeps,
and ContentRoutesDeps loses its backend test-injection member from the published type (the
factories now resolve their backend from event.locals.backend, the same seam the dev double
already rides). /components loses ComponentInsertDialog, ComponentForm, IconPicker, and
LinkPicker, plus the three spellcheck-worker/spellcheck-assets export-map keys;
MarkdownEditor already resolves the spellcheck worker and its two assets itself through a
module-relative new URL(..., import.meta.url), so no consumer needs a frozen public subpath for
them, and the showcase's spike route that imported them is deleted. /delivery and
/delivery/data each lose createSiteResolver, ConceptIndex, createContentIndex, RawFile,
fromGlob, wordCount, and permalink, all internals of the createSiteIndexes hand-assembly
escape hatch. /media loses 14 names: the manifest CRUD, the content-hash naming helpers, the
Cloudflare Images transform-URL builders, and manifestMediaResolver. /vite loses
writeManifest, readAdapterFacts, AdapterFacts, verifyManifestFromVite,
buildManifestFromVite, and stripCairnManifest, keeping only cairnManifest and
CairnManifestOptions.

Five shape reshapes ride the same window. ConceptConfig.routing closes to the
'feed' | 'page' | 'embedded' shorthand union; the RoutingRule object form leaves the barrel, and
resolveRouting now throws on a defined-but-unrecognized routing value instead of silently
resolving to undefined, closing the runtime edge a cast can still reach around the closed
compile-time union. parseSiteConfig becomes loud about the config boundary: an unrecognized
top-level site.config.yaml key throws, and a closed set of adapter-owned keys
(content, backend, email, rendering, media, editor) names cairn.config.ts as their
correct home. createMediaRoute now takes the composed runtime and reads resolvedAssets
itself, matching every other route factory's convention, so ResolvedAssetConfig stops being
load-bearing /sveltekit surface (it stays exported from /media). CairnAdminDeps and
ContentRoutesDeps regroup their flat branding/send/anthropic/tidyTimeoutMs members into
two cohesive bags, auth and tidy. A new CairnPlatformBindings interface names every binding
the engine reads off platform.env at runtime: AUTH_DB, EMAIL, PUBLIC_ORIGIN, and
GITHUB_APP_PRIVATE_KEY_B64 are required, and the opt-in tidy action's ANTHROPIC_API_KEY is
optional, so an app.d.ts intersection missing a required binding now fails at compile time
instead of surfacing as a runtime config.bindings-missing error. The GitHub App's id and
installation id are not runtime bindings; they are compile-time adapter config passed to
githubApp({ appId, installationId }). A separate CairnMediaBindings carries MEDIA_BUCKET,
present only on a media-enabled site. Finally, src/lib leaves the npm tarball's files array; the package ships only dist and
CHANGELOG.md, and a packaging boundary test locks a deep import of shipped source or an
unexported dist path to fail closed with ERR_PACKAGE_PATH_NOT_EXPORTED.

The pass also lands the gate-enforced three-tier stability vocabulary: Extension API and
Scaffold API (both existing) are the frozen contract, and a new Unstable API tier marks an
export that stays importable with no stability promise across minors. check:reference now fails
on any enumerated export missing a tier and on any backticked name in a page's export table or
heading that no longer exists in that subpath's exports (the reverse stale-prose lock). The sweep
assigns Unstable API to the eleven page-level components, the four piecewise route factories and
their config/deps/result satellite types, and feedView; MarkdownEditor's stable contract
narrows to eleven props (value, name, registerInsert, registerFormat,
completionSources, focusMode, typewriter, surface, spellcheck, spellcheckDictionary,
siteDictionary), with every other prop documented as Unstable API EditPage wiring.

This is breaking. Consumers must:

  • Import ResolvedReference from @glw907/cairn-cms/delivery, not the root barrel (no known
    consumer imported it from root).
  • Pass the runtime to createMediaRoute(runtime) instead of runtime.resolvedAssets.
  • Regroup createCairnAdmin deps: anthropic becomes tidy.client, tidyTimeoutMs becomes
    tidy.timeoutMs, and branding/send become auth.branding/auth.send.
  • Declare a concept's routing with only the string shorthand; the RoutingRule object form no
    longer type-checks or resolves at runtime (no known consumer used the object form).
  • Fix a site.config.yaml that carries an adapter-owned or otherwise unrecognized top-level key;
    parseSiteConfig now throws with the key's correct home.
  • Declare Platform.env via CairnPlatformBindings & CairnMediaBindings & { /* the site's own bindings */ } (recommended, not strictly required) instead of hand-listing each binding.
  • Stop importing any name demoted above; docs/internal/api-surface.md's diff for this pass is
    the authoritative list, and grepping the showcase, ecxc-ski, and 907-life found no consumer
    import touching any of them, so no site is known to need this action.

The code-polish pass converges the whole engine, the gate scripts, the tests, the Wayfinder
showcase, and the dev package on one agent-facing idiom charter (docs/internal/code-idioms.md),
behavior-preserving throughout: a survey of ~10 subsystems catalogued every divergent pattern
family (error and result shapes, validation, factory anatomy, module layout, naming, async
patterns, logging, test structure, Svelte component anatomy), and a module-by-module sweep applied
the picked convention with check:surface and the signatures gate as a machine-checked invariant
at every cluster. content-routes.ts, the codebase's largest file at 3,435 lines and its densest
duplication cluster, decomposes into per-domain internal modules (media, tidy, settings and
vocabulary, dictionary, core content actions) behind an unchanged createContentRoutes composer.
The sweep also dedupes real duplication across clusters: the sveltekit-routes cluster's dead
exports and entry-action preamble, the tests cluster's shared harnesses, the admin cluster's
dialog and segmented-control and typed-confirm and fetch idioms (dropping redundant ARIA roles the
convergence made unnecessary), the editor cluster's async-race guards and fetch round trips, the
content and nav-config clusters' shared helpers, the media and delivery clusters' error-message
idiom, the auth-github and tooling and scripts clusters' module layout and indentation, and the
showcase's structural idioms. A new root gate, check:consumers, typechecks the showcase and the
dev package against the published surface so a public reshape can no longer silently break either,
closing the incident class the pruning pass's Task 6 surfaced. The admin CSS build's Tailwind
content detection now scopes to the components glob instead of scanning the whole repo, shrinking
the shipped dist/components/cairn-admin.css by 31% (415,976 to 286,719 bytes) with the
admin-visual baseline proving no real utility dropped. A guarded rider wrote a component-test
suite pinning the leaf-field-rendering family's two deliberately different conventions
(FieldInput's native uncontrolled form participation, ComponentForm's controlled
touched-tracking validation, and the phase-3a multi-instance focus model) before evaluating whether
to merge them; the merge proved architecturally wrong on all four walls the guard suite now pins
permanently, so it did not land (see ROADMAP.md, "Later," for the narrower prop-context dedup
that remains).

This is breaking. Consumers must: rename any direct call onto the createContentRoutes return that
dropped the Action suffix (mediaLibraryUpload to mediaLibraryUploadAction, mediaBulkDelete
to mediaBulkDeleteAction, mediaOrphanScan to mediaOrphanScanAction, mediaPurgeOrphans to
mediaPurgeOrphansAction, mediaReplacePreview to mediaReplacePreviewAction,
mediaReplaceApply to mediaReplaceApplyAction, mediaAltPreview to mediaAltPreviewAction,
mediaAltApply to mediaAltApplyAction, addDictionaryWord to addDictionaryWordAction); the
SvelteKit named-action wire names (?/mediaBulkDelete and kin) are unchanged. Unstable API tier
makes the rename legal across minors, and this pre-beta window makes it cheap.

The owner-gated editor-management actions (add, remove, role change) now log editor.added,
editor.removed, and editor.role_changed, each carrying the acting owner's and the target
editor's email and, where relevant, the role, closing the one route-layer path with no audit
trail (docs/reference/log-events.md). No consumer action.