Changed
Toolchain and CI
- Node.js floor raised to 24.20.0 (current LTS) in
package.jsonengines,.nvmrc, and.node-version; CI resolves the same version throughactions/setup-node. Forks on Node 22 must upgrade before updating. Under the versioning policy above this is a MAJOR change. - Linting and formatting moved from Biome to the Oxc toolchain:
oxlintfor linting,oxfmtfor formatting. Nothing changes about how you work —bun run check,bun lint,bun lint:fixandbun run formatkeep their names and meanings, there is still one pre-commit hook and one editor extension (oxc.oxc-vscode), and house style is untouched (80 columns, 2-space indent, single quotes, semicolons only where required). Three things do change. Unsorted Tailwind classes and unsorted imports are no longer lint errors —oxfmtsorts both when you format or save, so they fix themselves. Formatting now covers Markdown, YAML and TOML too, which Biome never touched. And a newbun run lint:typesuses the TypeScript 7 install to catch un-awaited promises and async handlers passed where a void return is expected; it runs inbun run checkand CI but stays out of the pre-commit hook so commits stay fast. It found 18 floating promises on the first run, all now marked explicitly.biome.jsonand the three GritQL plugins in.biome/are gone, replaced by built-in oxlint rules. - TypeScript 7: the typecheck gate runs on the stable native (Go) compiler — a single
typescript@^7.0.2, invoked by path (bun node_modules/typescript/bin/tsc --noEmit) fromcheck,typecheck,typecheck:watch, and the lefthook pre-commit hook. The dual-package setup (classictypescript@^6for Next's JS API plus atypescript7npm alias for the actual gate) is gone: Next 16.3 defaultsuseTypeScriptClitotrue, so it shells out to the TS CLI itself instead of requiring the classic JS compiler API that TS7 native never shipped. The preview-era machinery is gone too: the@typescript/native-preview(tsgo) dependency, thetypecheck:tsc/typecheck:comparecross-check scripts, and the obsoletetypescript.experimental.useTsgoVS Code flag. Operational wins from the native compiler: the pre-commit typecheck drops--incremental false(typescript is the sole.tsbuildinfowriter, so the ~4x-faster warm cache is safe) andtypecheck:watchuses TS7's rebuilt native file watcher. - Whole-codebase maintainability pass (net −1.2k lines): removed a dead Zustand WebGL-tunnel store; deduped the Shopify integration install path through
installBundleand the fluid/flowmap pointer handling through a sharedusePointerInput; split the 1k-lineast-transforms.tsinto focusedhelpers/remove-ops/add-ops/indexmodules (behind a barrel re-export) and trimmedsetup-project.tsunder 1k; made bundle-key typing honest (BundleId+ agetBundleaccessor for the widerRemovableIdspace) and removed dishonestly-optional types (CartLineItem.id,CartMeta, a discriminatedCanvasContextValue); and routed server-side integration env reads through the typed@/lib/envaccessor. No consumer-facing API change. - Dev scripts deduped: one shared ts-morph
Projectinast-transforms, reusedtoPascalCase/cancelGuard, and renamed the two divergentupdatePackageJsonhelpers. (#198) - Dependabot PRs now auto-sync
bun.lockvia apull_request_targetworkflow, so they pass the frozen-lockfile install in CI. (#190) ast-transformsop handlers route the ts-morph
create/getFullText/removeSourceFile lifecycle through onewithSourceFile
helper with guaranteed cleanup — behavior-preserving, ~60 fewer lines.- Barrel-file (
index.tsre-export) manipulation is unified into one
lib/scripts/barrel-file.ts(findBarrelLine/removeBarrelLines/
insertBarrelLine);setup-project,bundle-installer, and
generate-componentshare it instead of three divergent implementations.
Next.js and caching
- Next.js 16.3: bumped
nextand@next/bundle-analyzerto16.3.0and made instant navigations the starter default — top-levelpartialPrefetching: true(shell-based prefetching: one reusable loading shell per route, cached client-side and shared by every link pointing at it; supersedesexperimental.prefetchInlining) plusexperimental.cachedNavigations: true, the one piece of the instant-nav cluster still opt-in in stable (varyParams,optimisticRouting, and the Turbopack build FS cache are default-on in 16.3, so no flags for those). Replaced the placeholderapp/(site)/loading.tsxwith an accessible skeleton shell (<output>status semantics,aria-busy,sr-onlylabel,animate-pulsebars) so prefetched navigations land on something meaningful, and enabledexperimental.turbopackRustReactCompiler(native React Compiler inside Turbopack, pairs withreactCompiler: true). Addedbun run bench:nav, a client-side navigation benchmark — Lighthouse only audits cold loads, which never navigate — shipping with a 404→home scenario that runs on any build; the measured trade forpartialPrefetchingis in #259: shell paint gets faster and far more predictable, while a dynamic route's streamed content starts later because shell and data serialise. (#259) - Sanity example pages wrap
sanityFetchin a'use cache'function for Cache Components (cacheComponents) compatibility, which also dedupes the page andgenerateMetadatafetches. (#205, #208)
Security
- Content-Security-Policy is now enforced instead of Report-Only, and composed from the integration registry instead of hand-maintained: each kept integration in
lib/integrations/registry.tsdeclares the origins its browser-visible code actually needs (cspSources), andlib/integrations/csp.tsunions them into the singleContent-Security-Policyheader atnext.config.tsconfig-eval time — strip an integration withsetup:projectand its origins drop out on the next build, no manual edit required. The previous Report-Only baseline allowedhttps:/wss:everywhere and blocked nothing; the new policy is scoped to real origins (Sanity, Shopify, HubSpot's form embed) and enforced.script-src/style-srcstill need'unsafe-inline'— there's no nonce pipeline yet — so an injected<script>tag isn't blocked by this policy, butconnect-src/img-src/frame-ancestorsnow have real teeth.frame-ancestorsis unchanged. APROJECT_CSP_EXTRA_SOURCESescape hatch inlib/integrations/csp.tscovers project-specific origins the registry can't know about. (#318)
WebGL and animation
- WebGL tunnels are first-party: the
tunnel-ratdependency (stale since 2023, pinned to zustand 4) is replaced bylib/webgl/utils/tunnel.ts, a typed port of its ~60-line factory on the zustand 5 already in the manifest. SameIn/Outsemantics, one less dependency, and no second zustand copy pulled in by our own code (drei still carries it transitively). The original implementation's MIT notice is preserved inTHIRD-PARTY-NOTICES.md. - WebGL: forward-ported the newer module — typed
Fluid/Flowmapsimulations, theimage,postprocessing, andflowmap-providercomponents, and acanvas/webglrenderer; tunnels use the first-partytunnel()factory inlib/webgl/utils/tunnel. The scene lives in a single root canvas (<Canvas root>) mounted via one of two mutually-exclusive strategies: shared in the layout (lib/features, persists across routes) or per page via<Wrapper webgl>. A non-root<Canvas>mounts nothing and falls back to the root canvas. Removed the deadglobal-canvas,gpu-detection,create-renderer, andcanvas-contextmodules; device-detection probes (Safari/WebGL/autoplay) are cached at module level. (#227) - WebGL: removed the unused
localcanvas mode andcanvas/webgl.tsx, dropped vestigialSceneinheritance fromProgram, and made the flowmap/fluid sims opt-in instead of booting on every WebGL page. (#199, #206) spring()(lib/utils/animation.ts) documentation now steers to CSSlinear()easing for off-thread springs. (#189)- WebGL
Programdrops vestigialSceneinheritance (reintroduced by the
#227 forward-port) for the composition shape #199 had already established.
Integrations
- API route handlers now return the team-convention
{ data, error }shape:errorisnullon success and a message string on failure,datais the payload ornull.POST /api/cart/ensureandPOST /api/revalidate(both the Sanity and Shopify branches) are affected; every status code is unchanged. Clients that only checkedresponse.okkeep working. - Integration registry is single-source:
INTEGRATION_BUNDLESkeys are typed againstRemovableIdfromlib/integrations/registry,prepare-handoffmatches integrations by id, andnext.config.tscleanup runs through typed ts-morph AST ops — the regexupdateNextConfigis deleted. - Removing the theatre integration now also strips the Theatre.js debug wiring from the webgl fluid/flowmap hooks (via a new
removeCallStatementAST op), so keeping webgl without theatre builds cleanly. - Integrations: Shopify customer actions run through
runFormAction; Turnstile validation extracted tolib/integrations/turnstile(shared across integrations); the cart reconciler uses a discriminated union and every cart action returns oneCartActionResultshape; optimistic add wraps instartTransition; the Mailchimp error path validates with Zod. - Integrations reframed as opt-in plugins isolated under
lib/integrationswith// USAGEnotes. (#210) - Shopify cart types: named the post-reshape line item (
CartLineItem), madeCart.idrequired, and removed 11ascasts;removeItem/updateItemQuantitynow take the client-heldlineId, dropping agetCartround-trip per mutation. (#198) - Mailchimp integration returns typed
MailchimpErrorCodevalues instead of sniffing error strings; tag/note writes are best-effort. (#198) - Shopify cart actions (
removeItem/addItem/updateItemQuantity) share a
runCartActionhelper for the IP + standard rate-limit prelude instead of
inlining it three times; behavior is unchanged.
Docs and structure
- Agent-readable delivery is now a starter primitive: one typed catalog feeds
/ai,/llms.txt, the sitemap, route-specific Markdown alternates, agent use guidance, and real developer-resource links. Page requests negotiatetext/htmlandtext/markdownwith q-value handling, explicit.mdaliases, 406 responses for CMS pages that do not have a truthful Markdown body, and recoverable Markdown 404s. Vercel appendsAcceptto the final HTMLVaryheader at the edge after Next writes its router fields; self-hosted deployments need the equivalent post-render reverse-proxy rule. - App:
error.tsxandglobal-error.tsxrender a sharedErrorView;APP_BASE_URLis exported once fromlib/env;fb:app_idreads from validated env;next.config.tsexports its config directly; dead nav scroll-lock and its orphaned store removed. - components/ui: form registration is name-based instead of index-based;
real-viewportslimmed to setting only--scrollbar-width; Base UI triggers use render props;fetchJSONresponses are schema-validated; conditionalclassNamespreads replaced withcn(). - Styles: easings and colors are each defined once —
css/easings.cssis a hand-authored@themepartial outside the generator — and thenesting-rulesno-op was dropped from the PostCSS chain. - Fonts now load via
next/font/google(Oswald for display, Spline Sans Mono for body) instead of self-hosted woff2 — no font binaries ship in the repo, and the brand type scale (sizes, line-heights, tracking from the Figma spec) is encoded inlib/styles/typography.ts. (#210) - Components are now catalogued in Storybook (
bun storybook) instead of an in-app/componentspage; added stories for accordion, alert-dialog, checkbox, link, marquee, menu, switch, and tabs. (#210) app/page.tsxis now a self-contained in-app manual (clone → ship); replace it with your homepage and deleteapp/page.module.csswhen you start a project. (#210)- Consolidated root docs: folded
BOUNDARIES.mdintoARCHITECTURE.mdand
refreshed the doc maps inREADME.mdandAGENTS.md(#177). - components/ui:
Form/FormProvidercollapsed into a singleForm;
CheckboxandSwitchshare an extracted control subtree across their
label / no-label branches; deadfoldRefdropped fromFold.
Dependencies
- Dependencies: bumped every package to its latest version. The only breaking change was
tempusdev.18, whoseTempusCallbackmoved from positional(time, deltaTime)arguments to a singleTempusStateobject ({ time, deltaTime, frame, budget }) and deprecated thepriorityoption in favor oforder— migrated all seven call sites (the GSAP, Lenis, marquee, and WebGL RAF runtimes, thelib/utils/rafwrite queue, and the devStatsoverlay). Also bumped@base-ui/react1.6,next-sanity13.1,@types/node26,deslop-cli0.5.8,@biomejs/biome2.5.1, Storybook 10.4.6, and Tailwind 4.3.1;actions/checkout6 → 7 in CI. Supersedes #251–254. (#255) - Dependencies: bumped the Sanity toolchain to v6 (
sanity,@sanity/vision,groq→^6.1.0) — Studio v6's breaking changes (Node 20 dropped,auth.providersreplace semantics,enableLegacySearchremoved) don't touch the integration's API surface, so forks on Node ≥ 22 update without code changes. Also bumped@biomejs/biome2.5.0 anddeslop-cli0.0.25. Biome 2.5's newly-enabled rules required a<title>ondarkroom.svg(noSvgWithoutTitle) and an import-spacing fix in generatedsanity.types.ts(organizeImports); the inline version<Script>carries a documenteduseInlineScriptIdignore (theidis present — biome mis-detects JSX-children content). Supersedes #221–226. (#228) - Dependencies and lib/utils: removed
cross-env(theanalyzescript setsANALYZE=trueinline);@theatre/coremoved todependencies(imported bylib/webgl/utils/fluidandflowmaps); bumpedgroqandnext-sanity; deleted the unusedlib/utilsmodulescontext,animation,viewport, andeasings, with docs updated to match. (postprocessingis a dependency again — see the WebGL forward-port above.)
Removed
-
The HubSpot Forms API path:
getForm(lib/integrations/hubspot/fetch-form.ts), theformsApiregistry capability,hubspotFormsApiEnvSchema, and theNEXT_HUBSPOT_FORM_IDenv var. The embed path is the supported way to render HubSpot forms; nothing in the starter called the fetch path. -
The
evilcolor theme.Wrapper'sthemeprop,themeNames, the Storybook theme toolbar, and the generated[data-theme=evil]CSS now coverlight,dark, andredonly. -
CSS linting. oxlint has no CSS parser, so the Biome CSS rules (
noUnknownFunction,noUnknownProperty,noDescendingSpecificity,noDuplicateCustomProperties,noUnknownMediaFeatureName) have no counterpart and the 15 CSSbiome-ignorecomments they needed are deleted.oxfmtstill formats every stylesheet, CSS Modules and Tailwind v4 at-rules included. -
The
satus add/satus listCLI (bun run satus) and the GitHub-tarball payload fetch behind it. It let you restore a stripped integration into a live project, but almost nobody used it, it downloaded and extracted an archive from a remote ref with no path checks, and its--forceflag silently did nothing.setup:project— the command every project andcreate-darkroomactually run — is unaffected and stays the one way to configure a fresh clone. The one thing worth keeping from the old CLI came with it:setup:projectnow expands a kept integration to include whatever it requires (keepingtheatrealso keepswebgl, since theatre's bindings depend on it), fixing a bug where--keep theatreused to strip webgl out from under it and leave a build that doesn't compile. -
The in-app
/componentsshowcase, theapp/(examples)/example routes (R3F, Sanity, Shopify, HubSpot), and theapp/studio/route — component demos moved to Storybook and integration usage distilled into// USAGEcomments inlib/integrations/*. Only the demo surface is gone; the reusable integration code stays. (#210) -
Unused
groqdependency —next-sanity'sdefineQuerycovers GROQ and
nothing imported the standalone package (it remains available transitively). -
Unused
components/ui/scroll-restorationcomponent (zero consumers). -
Dead and duplicate internal surface: unused
batch/measureexports from
lib/utils/raf.ts(onlymutateis consumed), the duplicated
ShaderMaterial<K>/DoubleRenderTargetwebgl types (hoisted once into
lib/webgl/utils), and the flatSelect*/Menu*/Tabs*part exports that
had zero importers (the compoundSelect/Menu/TabsAPIs are unchanged). -
Hand-rolled
components/ui/dropdown/— superseded by the accessible Base UISelect. (#202, #205) -
lib/utils/animation.tsre-exports ofclamp/lerp/mapRange/modulo/truncate— import these from@/utils/mathinstead. (#198) -
In-repo marketing homepage — the
app/(marketing)landing sections (hero, features, value-props, getting-started, presets) and marketing-only WebGL effects (animated-gradient,liquid-drip,split-text). Satus is a starter kit; its marketing lives at oss.darkroom.engineering/satus. (#188) -
/homerewrite + redirect and theassets.darkroom.engineeringimageremotePatternfromnext.config.ts. (#188) -
Dead code: orphaned WebGL GLSL utilities (
noise/blend/functions) and
unusedlib/utilshelpers (normalize,isEmptyObject,twoDigits,
numberWithCommas) (#172).
Added
sitemap.xmland/llms.txtnow read from Sanity when it's configured, instead of only ever listing the home page and/ai. Both enumerate the same CMS content through one shared helper (lib/seo/routes.ts), gated onisConfigured('sanity')— a fresh clone with no CMS env set gets the static routes only, and a CMS that's configured but unreachable (bad project ID, deleted dataset) degrades the same way instead of 500ing the sitemap.lib/seo/site.tsnow exports the normalizedBASE_URLthatapp/robots.ts,app/sitemap.ts, and/llms.txtall resolve absolute URLs from, so the three can't drift out of sync on the fallback chain. (#319)post-checkoutlefthook hook that clears.next/types+.next/dev/typeson branch switches (git flag1only — file checkouts untouched, rebases skipped), killing the ghosttscerrors caused by the previous branch's generated route types. Types regenerate on the next dev/build.- Minimal Playwright E2E harness (
e2e/home.e2e.ts) — smoke spec covering page render, zero console errors, and zero critical/serious a11y violations (via@axe-core/playwright). Run withbun run test:e2e. Specs use.e2e.tsextension sobun testignores them. (#e2e) - Instant-navigation regression test (
e2e/instant-navigation.e2e.ts) using@next/playwright'sinstant()helper — wraps the 404 page's "Go Home" link click and asserts the home shell paints without waiting on the network, so a refactor that de-opts instant navigation (acookies()read leaking into a shared layout, a moved Suspense boundary) fails CI instead of going unnoticed. (#339) - Storybook themed to match Satūs: stories render on the site palette through shared CSS tokens (edit the site, the catalogue follows), with a dark/light/red/evil theme toolbar and a branded manager. (#210)
bun run check:assets(wired intobun run checkand CI) fails the build on tracked videos over 2MB or raster images wider than 2400px/heavier than 1MB, reading dimensions straight from PNG/JPEG/WEBP/GIF headers with no new dependency — a format it can't parse (AVIF, or anything corrupt) fails the check rather than passing unverified.lib/styles/css/index.cssnow declares an explicit two-layer cascade (@layer base, utilities;):reset.csssits inbase, Tailwind's utilities (built-in and custom@utility) sit inutilities, so a utility reliably beats the browser reset and an unlayered CSS Module still beats both — guarded by a new regression test.Imagestopped writingobjectFitas an inline style unless a caller passes it explicitly; thecoverdefault now comes from a zero-specificity:where()rule riding the sameutilitieslayer, so a Module or Tailwindobject-*utility can override it. The Lighthouse workflow trades Vercel's Protection Bypass for Automation secret (VERCEL_AUTOMATION_BYPASS_SECRET) for Vercel's own short-lived, deployment-scoped bypass cookie in a preflight step, and only that cookie — never the standing secret — goes into Lighthouse's settings, since Lighthouse copies its resolved settings into the report this workflow publishes; it skips loudly when the secret is unset and fails outright if the bypass doesn't actually clear the SSO redirect. A tripwire test assertsproxy.tsstill imports and callsrate-limit, so deleting or gutting it failsbun testinstead of silently turning off API rate limiting. (#317)- Optional
/storybookroute that proxies to a standalone Storybook deployment, enabled per-environment viaNEXT_PUBLIC_STORYBOOK_URLand force-disabled in Production. (#210) - One-click Deploy to Vercel button (README + in-app manual) and clearer domain-setup guidance. (#210)
parseApiResponse(lib/utils/validation.ts) — validates external API responses with Zod at the integration boundary (HubSpot, Mailchimp, Shopify GraphQL envelope), so malformed responses fail clearly at the edge instead of crashing downstream. (#198)- React Scan render profiler as an opt-in Orchestra dev tool — toggle 🔬 in the ⌘O palette, off by default (no profiler overhead unless enabled). (#209)
useRevealhook (lib/hooks/use-reveal.ts) — reveal-on-scroll primitive animatingtransform/opacityon the compositor thread via a[data-reveal]/[data-reveal-item]CSS contract inglobal.css; degrades to visible without JS and honors reduced-motion. (#189)- Animation standards section in AGENTS.md — CSS/
useRevealfor reveals, GSAP for orchestration. (#189) AGENTS.mdas the single source of truth for engineering standards;CLAUDE.md
and.cursor/rulesreduced to thin pointers (#175).- Auto-generated
COMPONENTS.mdmanifest viabun run generate:manifest, with
bun run manifest:checkto catch drift in CI (#178). SECURITY.mdsecurity policy with private vulnerability reporting (#174).- This
CHANGELOG.md, andpackage.jsonversion synced to the release tag.
Fixed
- Audit remediation, 2026-08-05 sweep (report in
docs/audits/codebase-audit-2026-08-05.md, findings cited by issue; #401–#408). Content that was published but unreachable now renders: Sanitypageandarticledocuments were listed insitemap.xmland/llms.txtand previewed in Studio at URLs no route served, so every one 404'd — pages now render at their slug via a catch-all that replaces[...unmatched]and keeps its in-chrome 404 job, articles at/articles/[slug]with their JSON-LD wired in (#378). A project configured with a read token but noSANITY_PRIVATE_TOKENrendered no CMS content anywhere, becausesanityFetchfell back to a null stub that every consumer goes through, while the README called that token optional; published content now fetches through the client and tags its cache entry so the revalidation webhook still reaches it (#379). Provisioning with Sanity's own CLI convention (SANITY_STUDIO_PROJECT_ID) read as unconfigured everywhere except/studio, blanking the front end and 404ing the Studio after hydration (#380). Form abuse is now bounded: Turnstile verification ran before the rate limiter on every public action, so garbage tokens were never throttled and each one held a serverless invocation open for up to 5s waiting on Cloudflare — the limiter runs first, verification moved insiderunFormActionso there is one ordering, and Shopify customer login and register verify for the first time (#377). Mailchimp advertised GDPR double opt-in while hardcodingstatus: 'subscribed'; new addresses now default to'pending', and onlystatus_if_newis sent so a re-submission cannot knock a confirmed subscriber back to unconfirmed.setup:projectfails recoverably: a transform marked required was skipped in silence when its target file had been renamed, and self-prune deleted the script even when transforms had failed, so the documented "fix the cause and re-run" had nothing left to run (#381, #382); the preflight now also validates transform targets (#389).generateandhandoffhung then exited 0 on closed stdin — in CI or any agent harness they reported success having done nothing — and the changelog and process-audit report both credited #370 with a fix that only ever coveredsetup:project(#383). Shopify menu links resolve: a handle merely containingpageswas rewritten into a dead route, and external links were collapsed into local ones (#384); catalog products silently lostfeaturedImageto a schema that omitted it, blanking every optimistic cart thumbnail (#386); the webhook secret is compared in constant time. Theatre.js is out of production bundles — every WebGL-capable visitor was loading its runtime and fetching a config that did not match the sheet in use, behind two comments claiming none of it shipped (#385). Negative column utilities computed widths wrong by2(N-1) × gap, and ~28 sibling utilities emitted invalid CSS (#394);mobile-vh()always resolved to the address-bar-hiddenvhits own house rule exists to avoid (#395). A required form field that unmounted leftisReadyfalse forever, making the form permanently unsubmittable with nothing on screen to fix;ProgressTextdesynced once its word count shrank (#397); one malformed CMS document emptied the whole sitemap; the custom scrollbar gained keyboard operation and ARIA value semantics;Image'sblockprop, which allowed a call that typechecked and then threw, is gone (#393);handoffno longer deletes the Turnstile site key (#387); two HubSpot embeds on one page no longer collide on a shared DOM id (#391);doctor's lefthook check no longer false-fails in a linked worktree; the Block Components generator category that could never appear in COMPONENTS.md is gone (#388). - Draft-mode previews stop losing their session to a prefetch. The "Disable Draft Mode" pill was a prefetching
Link, so the router's speculative fetch of/api/draft-mode/disableran the handler and deleted the draft cookies out from under an open preview; reloads only sometimes recovered because it was a race. The route now answers prefetch/RSC fetches with 204 and disables only on a real document navigation (the cross-site 403 guard runs first, so a cross-site request still gets 403 whatever headers it carries), the pill renders only in a conclusively standalone context — the visual-editing hook falls back tostandaloneafter 1s when the Presentation handshake is slow, so iframe and popup contexts stay hidden regardless — and the pill is a plain anchor rather than the prefetching primitive. (#372)
Documentation
-
Adversarial codebase audit, 2026-08-05 (
docs/audits/codebase-audit-2026-08-05.md): 8 high, 13 medium, 11 low, one info, filed as #373–#400. Seven parallel area auditors read their surfaces in full with a bounded cross-model verify pass over the finding list; team-knowledge reconciliation ran after the findings existed so documented decisions could reclassify severity without suppressing anything. The report carries a post-publication correction: the draft-mode surface it listed as sound was not, and #372 fixed it hours later — the audit asked what each route handler does when called, never who else can call it. -
Process-audit remediation (2026-08-04 sweep; report in
docs/audits/process-audit-2026-08-04.md, findings cited by ID; #356–#370). The three scripted journeys now survive their own documented happy paths:setup:project's output builds for every preset — the cache-components opt-out also flipspartialPrefetching(P-B1), non-integration files that consume sanity/webgl are transformed on strip via new required-match AST ops that fail loudly before self-prune instead of silently no-oping on drift (P-B7), and the kept bundle's dependency pins survive to disk instead of being reverted by a stale package.json write (P-C3) — and leavescheckgreen by reformatting exactly the files it touched and regenerating COMPONENTS.md, deferring via a malformed-proof, retry-capped marker under--skip-install(P-B2).handoffships a deliverable that builds — branding removal now also strips the footer's logo import (P-C1) — with a newline-terminated inventory (P-C2), a working--help(P-C5), an outcome-honest summary (P-C6), and dated snapshot notes on its generated docs (P-C7).generatecan no longer emit syntax errors for hyphenated names (P-C8), always passes the format gate (P-C9), and refuses to overwrite existing files (P-C10). Cross-cutting:setup:project's@clack/promptscalls fail loudly (exit 1) on closed stdin instead of exiting 0 as a silent no-op (P-D2 —generate/handoffcompleted later);setup:projectgains a pid lock with signal-safe release and stale detection (P-B3), a friendly post-prune stub (P-B4), and preset-listing--help(P-B6). Onboarding:bun run checkis self-sufficient on a fresh clone via a guardednext typegenstep (P-A2); AGENTS.md's warning prose no longer contains the literal managed-block marker that made a secondbun devtruncate the file (P-A1); the revalidate webhook returns 400, not 500, for malformed JSON (P-D1). -
Audit design decisions resolved (#268; closes out #263/#267). The registry grows capability tiers:
hasCapability('hubspot', 'formsApi' | 'embed')distinguishes token-backed Forms API access from embed-only portal configs, so a portal-id-only setup no longer reads as "configured" for an API that will always throw; Shopify, Mailchimp, and HubSpot self-gate through the registry instead of re-deriving env inline (Turnstile keeps its direct secret check on purpose — its schema requires both keys, and gating on it would change production fail-closed semantics for secret-only configs).HUBSPOT_ALLOWED_FORM_IDS(opt-in, comma-separated) pins which HubSpot forms the newsletter action may submit to. Canvas ownership keeps both of #227's strategies — shared layout canvas (default) or per-page<Wrapper webgl>— with the never-both invariant enforced at runtime (first root wins, second is a no-op) rather than by prose alone; the prop-removal AST transform now also matches aliased destructures. The Storybook proxy fails closed off-Vercel (VERCEL_ENV-allowlisted instead of!== 'production', which was open when the var was unset). Recorded decisions: deployment target is Vercel-first (in-memory rate limiter stays as documented best-effort; durable-store upgrade path noted at the source), and the{ data, error }route shape vs{ status, message, fieldErrors }form-action shape split is intentional (AGENTS.md). -
Audit-issue remediation sweep (#263–#267). Integrations: Shopify menu paths are derived by URL parsing instead of a string-replace against the raw store domain — correct for scheme-less env values and custom-domain storefronts (#263 C1/M8); Mailchimp reports missing configuration as
config_errorwith the real message instead of mislabeling it a network error (#263 M6); Turnstile siteverify now sendsremoteip(#263 L6); cart quantity/remove buttons disable while their server action is in flight, so double-clicks can't drop an update (#263 M7). Scripts: a secondbun run handoffrefuses to clobberREADME.original.mdunless--forceis passed (#264 H5);--yesis honored for real —--preset/--keepat an interactive terminal now confirm before stripping, while non-TTY (scaffolder/CI) runs stay promptless (#264 H6); self-prune derives its test-file list from a glob plus a keep allowlist instead of a hardcoded array (#264 L10); CI no longer runsmanifest:checktwice (#264 M18 residual). WebGL: the render gate honorsprefers-reduced-motion(forcestill bypasses) (#265 M9); GPU sims are opt-in —simTypesdefaults to none instead of running fluid+flowmap for zero consumers (#265 M10); a second root canvas is a real no-op in every environment, not just a dev warning (#265 M8); the fluid sim clamps frame delta so a backgrounded tab can't corrupt the simulation (audit 07-09 H3); deleted the deadProgramclass (#265 L9). Security: invalid env vars fail with a readable per-variable message instead of a raw Zod error (#267 M3);fetchWithTimeouthonors already-aborted signals and detaches its abort listener (#267 L5);draft-mode/disablerejects cross-site requests viaSec-Fetch-Site(#267 L2); the deprecatedX-XSS-Protectionheader is gone and the enforcedframe-ancestorspolicy is integration-agnostic sosetup:projectstripping keeps it valid (#267 L1). Docs:bun build→bun run buildeverywhere, real import examples, deleted-component rows removed,--dry-rundocumented,fieldErrorstype corrected,engines.node >= 22enforced in package.json, TypeScript 6 in the stack table, and Turnstile listed in the integrations table (#266). -
Adversarial audit remediation (see
docs/audits/codebase-audit-2026-07-08.md; findings cited by ID). Shopify: the Storefront endpoint now normalizes the store domain tohttps://(H1 — following the documented scheme-lessSHOPIFY_STORE_DOMAINpreviously made every API call throw on URL parse), webhook revalidation is actually wired intoapp/api/revalidatewith a strippable dispatch (H2), and everyshopifyFetchcall site validates its payload with Zod schemas at the boundary (M2) — which surfaced and fixed four interface mis-models against the Storefront API (nullableImage.width/height, nullableSEOfields, nullablePage.seo, nullableCartCost.totalTaxAmount). CLI:setup:projectvalidates every bundle path upfront and self-prunes last, so a mid-run failure can't strand a fork in an unrecoverable half-stripped state (H8);bun installfailures at the end of setup are non-fatal with clear guidance (M5); AST-transform failures now fail the run loudly instead of exiting 0 (M6);setup:projectguards against running outside the project root (L3) and warns on duplicate flags (L4). Handoff describes what ships on disk, not what's configured in the current shell — installed-but-unconfigured integrations are reported as needing configuration instead of silently omitted from.env.example/INVENTORY/DEPLOYMENT-CHECKLIST (H7). Components:<Image>requires one offill/width+height/aspectRatioat the type level instead of silently rendering a broken 1×1 (H9); forms seed optional fields as valid and gate Enter-key submission on the same validity the button uses (H10 — an untouched optional field could previously block click-submit forever); the Link component is the single source of truth for external-link detection, with anewTabintent prop (L6); marquee cleans up detached nodes (L7) andusePrefetchno longer recreates its observer on inline options (L8). WebGL: theatre is live —SheetProviderself-bootstraps the project so sheets and the ⌘O Studio actually bind (H11);WebGLImageno longer decodes the document URL beforesrcresolves and clears stale texture references on src change (M16, M14);simTypesis plumbed through the root canvas so pages can skip unused GPU sims (M17); module-scope tempus/storage listeners are HMR-idempotent (M18, L12); mounting two root canvases warns in dev (M19); the fluid splat queue is capped (L11). HubSpot forms validate Turnstile like their Mailchimp siblings (M3). CI/docs: Dependabot auto-merge gates on the CI workflow that actually exists and on the PR author instead of the run actor — the lockfile-sync commit makes the CI actor a PAT identity, which the old actor gate treated as "not Dependabot", permanently skipping exactly the PRs the automation exists for (H3); Playwright e2e runs in CI (M11);bun run checkincludesmanifest:check(M8); all bun setups pin viapackageManager(L1); and the documented-but-nonexistent surfaces are gone from the docs —/studioroute and dead Sanity env vars (H5),bun build(M7), WebGPU/TSL renderer claims (M15), removedmeasure/batchraf exports (L9), wronguseDeviceDetectionkeys (L10), and the unreachableSOURCE_MAPSflag (L2). -
Correctness fixes from a whole-codebase review:
shopifyFetchcan now validate response payloads (opt-indataSchema), not just the GraphQL envelope — and the Shopify README no longer overstates that responses are validated at the boundary;AddToCartsetsdisabledwhen no variant is selected (so it can't fire a no-op server action); the HubSpot form parser keeps every field in a multi-field group instead of silently dropping all but the first; and the WebGL fluid simulation is frame-rate independent — the frame delta is threaded into the step, so it no longer runs ~2× fast on 120 Hz displays. -
cacheTag()crash on/sanityand/sanity/[slug]under Cache Components —sanityFetchcallscacheTag(), which must run inside a'use cache'function. (#208) -
use-webgl-elementattached twoIntersectionObservers on mount; now one. (#205) -
Accordion drove
Collapsible.Rootwith a duplicate controller, and the custom scrollbar overshot when dragged on long pages. (#205) -
React Scan dev panel was hijacked by Lenis smooth scroll; added it to Lenis's
preventlist. (#207) -
Turnstile dev-mode bypass collapsed to a single path. (#205)
-
Shopify cart
addItemnow validates input before creating a cart, so invalid
requests no longer leave an orphaned cart and cookie behind (#173). -
TextareaFieldrenders throughField.Control, restoring the
aria-invalid/aria-describedby/ error-id wiringInputFieldalready had. -
Shopify
removeItemvalidatesmerchandiseIdbefore the rate-limit prelude,
so empty ids no longer consume a rate-limit slot.
Security
- Hardened HubSpot form HTML stripping into a complete
stripHtmlTagsparser
(a character scan, not regex), resolving the CodeQL
js/incomplete-multi-character-sanitizationalert (#179, #180). - Turnstile siteverify responses are validated with a Zod schema and fail
closed — an unexpected response shape now returns a failed verification
instead of reading an uncheckedas-castsuccess.