Bosia Bosia v0.9.5

Roadmap

What's done, what's next, and where Bosia is headed.

Track what's done, what's next, and where we're headed. Current version: 0.9.5


bosia 0.9.5 (2026-09-05) — a guard that never ran on a click

Reported from pay as a fake "500 Internal Server Error" when a signed-out visitor clicked into /admin. The server never errored. Underneath the 500 sat a leak: the guard the docs teach did not run on client navigations at all, so the route returned its loader data to a caller with no session.

  • 🔴 event.url is the page URL before the hooks run, not after. The /__bosia/data/<route>.json/admin rewrite sat inside resolve() (server.ts:253), so a guard — which runs before await resolve(event) — saw the transport path and its startsWith("/admin") was false. curl /__bosia/data/admin.json was the whole exploit.
  • 🔴 throw redirect() / throw error() from a hook could never work: dist/hooks.server.js keeps bosia external (build.ts:24), so the hook's Redirect came from the app's node_modules while the server bundle carried its own class. instanceof compared two different objects and was always false → real 500. Branded both classes with Symbol.for and swapped all 23 instanceof sites for isRedirect()/isHttpError().
  • 🟠 A hook's raw 3xx on a data request becomes {redirect, status} JSON. fetch follows a 303 silently, so the router used to receive the login page's HTML at 200. Location ships verbatim — redirect() already rebased it through withBase(), so rebasing again would double a BASE_PATH prefix.
  • 🟠 The router stopped reading "not JSON" as "the loader crashed". readDataResponse() reads redirected, the status and the content-type that were sitting on the response all along and were thrown away by .catch(() => null). A hook's text/plain 404 now renders a 404, not a 500.
  • 🟡 event.isDataRequest (SvelteKit parity) tells a hook which kind of request it has, for the cases that genuinely differ. Authorization is not one of them — url is the same either way on purpose.
  • 🟡 The parse is keyed on the incoming Request in a WeakMap, since event.url no longer carries the marker and hooks call resolve(event) themselves. Handing resolve() a fabricated Request detaches it — documented on Handle, pinned by a test rather than papered over with a path sniff that could not fire anyway.
  • 🟠 test/hooks-redirect.test.ts boots a built server whose fixture guard is verbatim the one the docs teach — a normalising guard would have dodged the leak and passed against the broken build. 11 of its 13 assertions verified red beforehand, the security one included.
  • 🟡 apps/demo gained /guard-test. Verified in Chrome against both builds: unfixed, the click renders the panel and its rahasia sentinel to a signed-out visitor; fixed, both the click and the hard load land on login with a clean console.

bosia 0.9.4 (2026-09-05) — `page.url` was invented during SSR

Every server-rendered page reported pathname /. bosia-page-shell and bosia-sidebar both teach active-nav state from page.url.pathname, so each page shipped with the homepage highlighted until hydration corrected it.

  • 🟠 Two hardcoded fallbacks stood in for data the renderer already had: page.svelte.ts returned http://localhost/ with no window, and router.currentRoute initialised to "/". renderer.ts has the real url and already passes it to every load() and metadata().
  • 🟠 One renderWithPageContext() wrapper seeds router.currentRoute/.origin and appState.routeParams, then renders — at all four render() sites, the two error paths included. Partial seeding would leave an error render holding the previous request's URL: real cross-request leakage, worse than a wrong-but-fixed default.
  • 🔴 The seed is browser-space, not app-space. server.ts strips BASE_PATH once at the top of handleRequest, but hydrate.ts assigns raw window.location.pathname and clientRoutes carry the prefix. Seeding the stripped path would render /profil and let hydration flip it to /sso/profil — the same jump, inverted. withBase(currentBase(), …) puts it back.
  • 🟡 A deliberate exception to the rule in App.svelte and appState.svelte.ts (server singletons hold no per-request state): page is imported directly by user components, so props cannot reach it. Safe only because bosia destructures render() synchronously — svelte's RenderOutput is PromiseLike, so this is bosia's commitment, not svelte's. The wrapper makes it structural.
  • 🟡 page.params was {} on the server for the same reason — appState.routeParams is never written during SSR. Seeded from pageDataFull.params, the source App.svelte already reads. Dropped the write-only router.params cell (router is not public API).
  • 🟠 test/ssr-page-url.test.ts boots a built server: pathname, full href, active-nav state, params, sequential and concurrent requests, and both error paths. Verified 8 of 9 failing before the fix with Received: "/". A client-side test would agree with both the broken and fixed code — the fallback never fires in a browser.
  • 🟠 test/basePath-server.test.ts gained the mounted case, asserting the prefixed path. Verified failing with an app-space seed (data-path="/profil"). BASE_PATH must be set at build time too, so it extends the existing fixture rather than standing up a second build.
  • 🟡 apps/demo nav now sources active state from page.url.pathname per bosia-page-shell R6, plus a /page-url-test route printing the server-rendered values. Verified in Chrome against the built server: SSR HTML and post-hydration DOM agree, no console warnings, no flash of "Home".

bosia 0.9.3 (2026-09-02) — client nav only synced half the head

Reported from Fisika: after an in-app link, canonical and og:title still held the homepage's values. Two defects — the data endpoint dropped the fields, and the router applied only the two that survived.

  • 🟠 /__bosia/data/*.json sends meta, link and lang alongside title/description. Explicit whitelist, not a spread: metadata.data feeds load() server-side and may hold secrets, so it stays off the wire.
  • 🟠 The router rebuilds the head from that payload instead of poking document.title and one description tag. metadataTags() stamps data-bosia-meta on what it owns, so headExtras and <svelte:head> output are never touched.
  • 🟡 A route with no metadata() clears the previous page's tags rather than inheriting them — matches what a hard load of that URL renders. A failed fetch still leaves the head alone; no title means the old title stays, not Bosia App.
  • 🟠 test/formAction-metadata.test.ts asserts the data endpoint carries every head field and omits metadata.data. Verified failing on the old server before the fix — the DOM half is straight-line createElement, the wire was the root cause.

bosia 0.9.2 (2026-08-22) — `metadata()` was skipped on form-action re-renders

Reported from Fisika: after a plain (non-enhance) form POST the tab title flipped to Bosia App on two unrelated routes. renderPageWithFormData never called loadMetadata() and rendered through a buildHtml that had no metadata parameter at all — so the title was only the visible part of the loss.

  • 🔴 Form-action re-renders resolve metadata() like GET does. metadataTags() extracted from buildMetadataChunk and shared with buildHtml, so the streaming and non-streaming renderers can't drift on this again. Metadata sits ahead of <svelte:head> on both paths — first <title> wins.
  • 🟠 metadata.data reaches load() on POST. It was hardcoded null, so a loader consuming it silently returned different content after a submit — the one part of this that was never cosmetic. metadata.lang and plugin head/bodyEnd fragments restored on the same path.
  • 🟠 redirect() / error() thrown inside metadata() work. loadMetadata caught everything, so every caller's Redirect/HttpError branch was unreachable dead code; both are now re-thrown ahead of the catch-all log.
  • 🟡 Error pages title themselves 404 — Not Found instead of Bosia App. Synthesized only when the error component set no <title>, so a custom +error.svelte still wins. The route's own metadata() is deliberately not run — the route may be what failed.
  • 🟠 test/formAction-metadata.test.ts boots a real built server and submits a real form: title, description, og tag, lang, metadata.data, GET parity, redirect() from metadata(), and the 404 title. A unit test on buildHtml would have passed the whole time the bug shipped — the missing piece was the call site.

bosia 0.9.1 (2026-08-18) — closing out `BASE_PATH`

The six items 0.9.0 left open. One was a live bug: with BASE_PATH in .env.production, build.ts calls loadEnv() after its imports, so the import-time paths.BASE_PATH was "" while the lazy currentBase() was /sso — CSS silently kept origin-root url()s.

  • 🟡 One base source. paths.BASE_PATH deleted; twHash.ts, html.ts, server.ts, prerender.ts and client/base.ts all read currentBase(). Fixes the .env.production divergence: twHash.ts now reads post-loadEnv.
  • 🟡 basePath stamped into manifest.json at build; the server warns on boot when it disagrees. !== undefined so older dist/ artifacts stay silent.
  • 🟠 navigate() warns in dev when a path matched no route but would have with the prefix — the one choke point every goto/link/form falls through. Warn only; the router still converts nothing.
  • 🟡 Dev SSE opens ${base}/__bosia/sse (hydrate.ts) — the last hardcoded root among the injected copies.
  • 🟡 srcset/imagesrcset rebased per candidate, descriptors and spacing intact. A value containing data: is skipped whole — its commas are not separators.
  • 🟠 test/basePath-server.test.ts boots a real built server under /sso: off-mount 404, __BOSIA_BASE__, trailing-slash 308, redirect() Location, prerender output. The gap both 🔴s shipped through.

bosia 0.9.0 (2026-08-17) — `BASE_PATH`: mount an app under a sub-path

Found in SSO Fisika: three apps share one origin (fi.uinsgd.ac.id, /obe, /sso) because no subdomain was available. Serving from a prefix was impossible — route paths are compiled root-absolute into the client bundle, every redirect() emits a root-absolute Location, and nothing in the framework rewrote a leading segment. An nginx-only attempt (strip + proxy_redirect + sub_filter) was verified in a real browser and fails two ways: app-authored href="/daftar" walks off to the neighbouring app, and hydrate.ts matches location.pathname against a route table that has no prefix, so the app mounts into a permanent "Loading…".

  • 🔴 BASE_PATH=/sso — normalized in core/basePath.ts (pure, env-free, shared by both bundles so the two halves of the router cannot disagree). server.ts strips the prefix once, before anything reads a pathname; off-mount requests 404.
  • 🔴 clientRoutes are generated with the prefix (routeFile.ts), so the route table, an anchor's href and the address bar are all the same strings. The client router does no conversion at all: a click pushes exactly the URL that was in the link. serverRoutes stay app-space, since the server strips at the edge and app code should never learn it is mounted.
  • 🔴 Root-absolute literals in .svelte markup are rewritten at compile time (svelteCompiler.ts), <script> left alone. This is what makes the href in the DOM correct for a crawler, a middle-click and "copy link address" — an earlier version fixed only the server-rendered HTML and let the client re-render write the origin-root path back.
  • 🟠 Redirect rebases in its constructor — one choke point, so every redirect() in every app needs no change. Validation runs first: a base must never launder a target the validator rejects.
  • 🟠 Rendered markup rebased (href/src/action/formaction + CSS url()), never the JSON data islands — those are loader output and a path rewrite would corrupt them. Covers src/app.html via interpolateSegment.
  • 🟠 Compiled Tailwind CSS rebased at build time (twHash.ts, before hashing). A @font-face src inside a .css file is out of reach of any markup rewrite, and the miss is silent — fallback font, blank icon, nothing in the console.
  • 🟠 Cookie default path is the mount, resolved per-jar rather than at import. On a shared origin this is what stops two bosia apps from overwriting each other's session.
  • 🟡 base exported from bosia, for URLs built in <script> — the compile-time rewrite only sees markup. Verified against the Fisika design system: Icon.svelte builds url("/icons/…") in a $derived and needs it.
  • 🔴 Trailing-slash 308 was emitting an app-space Location (server.ts), so /sso/about/ bounced the visitor to /about. A trailingSlash: "always" route did it on every request. Rebased — it was the one Location not coming from Redirect.
  • 🔴 Prerendering never ran under a base: prerender.ts fetched the spawned server at the origin root, which now 404s, so /_health never answered and the step bailed with "server failed to start" — without failing the build. Prefixed the fetch base; the files written stay app-space, which is what the server looks them up by.
  • svelteCompiler.ts masked <script> blocks with literal NUL sentinels, which made git treat the whole module as binary — no diffs, no blame. Swapped for an HTML comment.

The six items left open here were closed in 0.9.1 above.


bosia (open) — success-only action data is wiped by the default `enhance`

Found in Ujiku (2026-08-07), building /akun/profil: the save worked and the row was written, but the "Perubahan tersimpan." message never rendered. A successful action that returns data for the form prop silently loses it under the default use:enhance.

client/enhance.ts:53 sets appState.form = result.data, then (with invalidateAll on, the default) bumps appState.invalidationTick at :60. That tick fires the nav effect in client/App.svelte, which does appState.form = null at :144 before refetching the loader. The data is wiped between the assignment and the render — no error, no console warning, the UI just never shows it.

Why it stays hidden: the failure branch (enhance.ts:48-51) sets appState.form and returns early without invalidating, so fail() messages always render fine. Only success-with-payload is affected — and most actions return { ok: true } and redirect, so nobody looks.

  • 🟠 Don't null appState.form for an invalidation the form itself just triggered. The nav effect can't currently tell "loader refresh after my own action" from a real navigation; carrying the action's tick (or clearing on route change rather than on tick) would keep the payload alive across the refetch.
  • 🟡 Failing that, make it loud rather than silent: warn once in dev when a success result carries data and invalidateAll is about to discard it, naming invalidateAll: false as the fix.
  • 🟡 Document the pairing in the forms guide — an action whose whole point is its return value (preview, dry-run, computed quote) wants await update({ invalidateAll: false }); one that writes and needs fresh loader data wants the callback to latch what it needs before update().
  • ⚪ App-side workaround in use today (Ujiku akun/profil, akun/keamanan): latch result.type === "success" into local $state inside the enhance callback, then await update() — keeps the invalidation and the confirmation. invalidateAll: false alone is wrong here, since the page header reads the row that was just written.

bosia 0.8.16 (2026-08-07) — `$env` export list shouldn't depend on the mode

Found in Ujiku CI (2026-08-07): bun run check failed with Module '"$env"' has no exported member 'PUBLIC_GTM_ID'. Runtime values already come from process.env as designed — but envCodegen.ts derives the export names from whatever loadEnv(mode) found, so a runtime var vanishes from $env when its file isn't loaded. CI has only .env.production, and bosia sync runs dev mode unless NODE_ENV=production.

  • 🟠 loadEnv() harvests names from every root file matching /^\.env(\..+)?$/ (incl. .env.example, excl. .envrc); values still only from the mode's four files. Name-only keys stay out of process.env so ?? default fallbacks survive.
  • 🟡 Test: .env.production alone still yields the PUBLIC_* export (empty string), no .env needed. Plus mode-scoped values, .env.example names, .envrc ignored, shell env fills a name-only key, malformed off-mode file throws.
  • 🟢 guides/environment-variables (EN+ID) "Names vs values" section + bosia-env R4/R5 — .env* is the name manifest; a host-supplied var still must be declared or bun run check fails.

docs 0.8.8 (2026-07-30) — Purge stale `ui/icon` refs

The <Icon> wrapper died in 2bdf95d (2026-06-06) but its references outlived it. bosia-dashboard and bosia-mobile-screen still ran bosia add … ui/icon — a registry id that no longer exists, so the CLI fails mid-workflow.

  • 🟠 ui/icon dropped from bosia-dashboard + bosia-mobile-screen (frontmatter requires.components and the bosia add line). No replacement dep — @lucide/svelte is npm, and registry components declare it in their own npmDeps.
  • 🟡 bosia-design-review P1 item + references/checklist.md §11 rephrased around @lucide/svelte; noted it's a convention to check, not something a wrapper enforces.
  • 🟡 Sidebar docs (EN + ID) taught the dead <Icon name="home"> API for a snippet that wants a lucide component — now <House size={16} /> with a real import.
  • 🟢 "Migrating from the old <Icon> wrapper" section deleted from components/ui/icon.md (EN + ID); the npmDeps note folded into Install. Docs now have zero mentions of the old API.
  • 🟢 bosia-icon added to the skills index table — it was on disk and served by the API, but absent from the human-facing list.
  • 🟢 Drifted counts fixed against registry/index.json: 61 components, 162 blocks, 23 pages, 19 themes (both READMEs + LandingPage.svelte).

bosia 0.8.15 (2026-07-30) — One server per port

Found in Ujiku (2026-07-29): two bun run start processes sat on :9000 at once, no error. Elysia's Bun adapter defaults to reusePort: true (elysia/dist/adapter/bun/index.js:163-190), so SO_REUSEPORT let the second bind succeed and the kernel load-balanced between two different dist/ builds — a save's side effect silently didn't happen, guarded routes 500'd on the old build only. Cost hours to spot.

  • 🟠 A second server on a busy port fails loudly with the offender's pid and exits 1. Delivered as a catch on the failed bind, not a pre-probe: no TOCTOU race, smaller diff, and it covers every boot path (Docker, PM2, systemd), not just the CLI.
  • 🟡 reusePort off in server.ts's served config — the kernel is the source of truth. Applies to prod, the dev inner app server and the prerender build server. bosia start now propagates the child's exit code.
  • 🟢 BOSIA_REUSE_PORT=1 opts back in, for the N-workers-one-port case Elysia's default was meant for. Docs + skill + CLI reference updated.
  • 🟢 pidsOnPort() extracted to core/port.ts from dev.ts's inline lsof; shared by the reaper and the refusal message. One test.

bosia 0.8.14 (2026-07-25) — Static-host hash-nav fix

  • 🟢 Hash-only nav guard in App.svelte: same-page TOC anchors scroll only, no loader refetch/POST — fixes error page on static hosts (Cloudflare Assets).

bosia 0.8.14 (2026-07-25) — Perf Phase 3: prerender

Phase 3 of 4 (plan: snazzy-dream) — parallelize prerenderStaticRoutes. Items 1–3 only.

  • 🟢 detectPrerenderRoutes: read files + run dynamic entries() imports concurrently via Promise.all (was two serial loops). All warnings/skip semantics preserved.
  • 🟢 Bounded worker pool (PRERENDER_CONCURRENCY, default 6) over prerender fetches; per-target try/catch keeps one failure from aborting the batch.
  • 🟢 Health poll checks first, sleeps 50ms only on failure — drops the guaranteed 200ms floor.
  • ⚪ Item 4 skipped: mtime-skip of the cpSync static mirror — marginal gain on a once-per-build copy, real staleness risk.

bosia 0.8.13 (2026-07-23) — CSRF webhook exemption

  • 🟢 CSRF_EXEMPT_PATHS env var: exempt server-to-server webhook paths (no Origin/Referer) from the CSRF origin check. Exact or path-boundary match; exempt routes self-authenticate. Docs + tests.

bosia 0.8.12 (2026-07-19) — Perf Phase 1: runtime hot path

Phase 1 of 4 (plan: quizzical-storm) — per-request wins on the production hot path.

  • 🟢 Map-based exact-route lookup in matcher.ts — O(1) before the dynamic/catch-all linear scan.
  • 🟢 /dist/ + /__bosia/ static requests short-circuit before the API scan.
  • 🟢 Sync fast path in resolveApiMatch — non-.json paths skip the microtask hop.
  • 🟢 Lazy cookie parsing; skip page-module import on cache hits via build-time cache flag; module-level TextEncoder; alloc-free CSRF origin check.
  • 🟢 Deferred from item 6: Set-based LoaderDeps (ripples into serialization) + CORS origin Set (opt-in, tiny list) — not worth it on 1-5 element lists.

Phase 4 of 4 (plan: mellow-jingling-dragon) — the registry gap-fill is complete.

  • 🟢 registry/blocks/{newsletter/{centered,split},consent/banner} — signup forms POST to /api/newsletter; consent bar persists accept/decline in localStorage with onDecision hook.
  • 🟢 registry/features/newsletter/ — dialect-split subscribers table (email unique, lowercased), duplicate-safe subscribe, POST/GET /api/newsletter; drizzle append-line.
  • 🟢 registry/features/seo/ — no DB: /sitemap.xml, /robots.txt (staging noindex), /rss.xml fed by src/features/seo/config.ts; PUBLIC_BASE_URL envVar, request-origin fallback.
  • 🟢 Docs: blocks/{newsletter,consent}.md, guides/{newsletter,seo}.md + id/ mirrors, 2 demos, nav; bosia-sections → 27 blocks / 13 families; bosia-seo points at feat seo.

Phase 3 of 4 (plan: mellow-jingling-dragon): purely frontend — no feature. Gallery/process section blocks + standalone error pages.

  • 🟢 registry/blocks/{gallery/{grid,masonry},process/{steps,timeline}} — dialog-lightbox grid, CSS-columns masonry (no JS), numbered steps row, milestone timeline.
  • 🟢 registry/pages/errors/{not-found,server-error,maintenance} — standalone single-file pages, zero deps.
  • 🟢 Docs: blocks/{gallery,process}.md, pages/errors.md (one doc for all three) + id/ mirrors, 3 demos, nav, overviews (Sections 21→25); bosia-sections skill → 24 blocks / 11 families.

docs 0.8.6 (2026-07-17) — Registry gap-fill Phase 2: blog

Phase 2 of 4 (plan: mellow-jingling-dragon): blog blocks + pages + a drizzle-backed blog feature, so the bosia-blog skill composes real registry blocks instead of scaffolding from scratch.

  • 🟢 registry/blocks/blog/{post-list,post-header,post-body,related} — editorial post cards, header with byline + cover, 70ch prose wrapper (html / snippet / sample via ui/typography), related row.
  • 🟢 registry/pages/blog/{index,post} — navbar/footer compositions; optional posts / post / bodyHtml / related props match the feature loaders' shape.
  • 🟢 registry/features/blog/ — dialect-split posts table (slug unique, title, excerpt, body, cover, tag, published_at), repository, service, loaders for /blog + /blog/[slug] (404 unknown slugs), seed 003_blog; drizzle append-line.
  • 🟢 Docs: blocks/blog.md, pages/blog/{index,post}.md, guides/blog.md + id/ mirrors, 3 demos, nav, overviews (+Blog category, nine→ten); bosia-blog skill now requires the blocks and points at feat blog.

docs 0.8.6 (2026-07-17) — Registry gap-fill Phase 1: contact + about + team

General-purpose sites hit registry gaps at once: no contact/about/team. Phase 1 of 4 (plan: mellow-jingling-dragon); Phases 2–4 (blog, gallery/process/errors, newsletter/consent/SEO) await approval.

  • 🟢 registry/blocks/{contact/{split,simple},team/{grid,spotlight}} — form sections POST JSON to /api/contact (action prop overrides); team cards + founder spotlight.
  • 🟢 registry/pages/company/{contact,about} — composed from navbar/hero/stats/team/cta/footer blocks.
  • 🟢 registry/features/contact-form/ — dialect-split submissions table, repository, service (plain validation), POST/GET /api/contact; drizzle append-line.
  • 🟢 Docs: blocks/{contact,team}.md, pages/company/{about,contact}.md, guides/contact-form.md + id/ mirrors, 4 demos, nav; bosia-sections skill → 20 blocks / 9 families.
  • 🟢 Overviews backfilled (rule: every registry item appears in its overview): blocks overview +Footers/+Sections/+counts, pages overview +Landing/+Company; id/ mirrors (id blocks overview gained the whole catalog).
  • cards/pick-list and stats/sentiment-bar named in overview but have no section on their category doc pages (cards/utility.md, stats.md).
  • 🟢 Phase 2 — blog blocks/pages/feature (shipped, see Phase 2 section).
  • 🟢 Phase 3 — gallery/process/error pages (shipped, see Phase 3 section).
  • 🟢 Phase 4 — newsletter/consent/SEO (shipped, see Phase 4 section).

docs 0.8.6 (2026-07-17) — Docks: mobile bottom-nav blocks

Bosia had 19 top navbars but no mobile bottom navigation. Ported the Dockline design into a new docks block category, adapted to semantic tokens + @lucide/svelte.

  • 🟢 registry/blocks/docks/{edge,floating,pill,fab}/ — self-contained <nav> tab bars; active tab bg-primary/10 text-primary, FAB bg-primary, count/dot badges; registered in registry/index.json.
  • 🟢 Docs — blocks/docks.md (+ DocksDemo wired into slug route), overview section, bosia-docks skill.

0.8.10 (2026-07-16) — Dev DX trio (rebuild filter, cached route scan, health-poll reload)

The three v0.2.1 DX items: dev rebuilt on .md/test/editor-temp edits, re-walked src/routes every build, and slept a flat 200ms before broadcasting reload.

  • 🟡 core/devWatch.ts (new) — shouldIgnoreForRebuild (md/test/spec/swap files) + affectsRouteManifest (only + files & dirs under src/routes matter); test/devWatch.test.ts.
  • 🟡 core/dev.ts — watcher + mtime poll skip ignored files; routesDirty flag → BOSIA_SKIP_ROUTE_SCAN env on the build spawn (always scans in managed mode); Bun.sleep(200)waitForAppHealthy() polling /_health (50ms, 10s cap).
  • 🟡 core/build.ts — when flagged, reuses previous route-manifest.json (read before cleanup) instead of scanRoutes(); falls back to a real scan if missing.
  • ⚪ Known ceiling: apps importing .md from src/ lose live-reload for those files; drop .md from the ignore list if it bites.

0.8.9 (2026-07-14) — `create` into current/existing folder

create required a name and refused any existing dir. Users want create . / no-name to scaffold in place.

  • 🟠 packages/bosia/src/cli/create.ts./omitted → cwd; project name = cwd basename; existence guard → emptiness guard (ignores .git/.DS_Store); thread projectName (not path) through copy/prebuilt/finish; drop misleading cd when in place.
  • 🟢 Docs reference/cli.md (+id) updated to [name] optional syntax.

0.8.8 (2026-07-11) — Inspector: AI popover clamped to viewport

openForm() placed the popover at a fixed r.bottom+6 — off-screen for elements near the bottom of the page/iframe (rukoku editor embed).

  • 🟠 plugins/inspector/overlay.ts — mount at 0,0, measure offsetWidth/Height, flip above when no room below, clamp both axes to the (iframe) viewport with 8px margin.
  • ⚪ Known ceiling: position computed once at click; no scroll/resize reposition (popover is short-lived).

0.8.8 (2026-07-11) — `__BRAND__` guard reaches all templates

The 0.8.3 brand guard lives in bosia sync, but only the default template's check script ran sync — demo/shop/store (rukoku apps) passed bun run check with __BRAND__ still in the footer.

  • 🟠 templates/{demo,shop,store}/package.jsoncheck now runs bosia sync && first (also fixes tsc checking stale .bosia/ codegen).
  • ⚪ Existing scaffolded apps keep their baked check script; fix is the same one-line edit per app.

0.8.7 (2026-07-10) — `export const snapshot` (page state on back/forward)

SvelteKit-parity snapshot = { capture, restore } in +page.svelte. Mirrors scroll restoration: captured at the same save points keyed by history.state.bosiaIndex, restored post-tick() after the destination mounts; persisted to sessionStorage on unload (JSON-serializable values only).

  • 🟠 core/client/router.svelte.tspageSnapshots map + savePageSnapshot() at the four saveScroll() sites; pendingSnapshot handoff (popstate + reload-restore in init()); separate sessionStorage flush so a non-serializable snapshot can't kill scroll persistence.
  • 🟠 core/client/App.sveltebind:this={pageInstance} on both <PageComponent> spots; router.getPageSnapshot closure; settleSnapshot() in settleScroll() + first-hydration branch.
  • Snapshot<T> type in bosia/client + generated $types.d.ts (routeTypes.ts); routeTypes.test.ts assertion; docs guides/navigation (en+id) "Snapshots" section; bosia-navigation skill R2b; CHANGELOG.md appended.
  • ⚪ Deliberately skipped: layout snapshots (need per-depth instance refs in the recursive layout snippets); non-JSON values (no devalue); inherits the beforeunload mobile page-discard ceiling from scroll.

0.8.7 (2026-07-10) — `setHeaders()` in load functions

SvelteKit-parity setHeaders(headers) on the server-loader event. One accumulator shared across layout+page loaders (cross-loader dup detection); keys lowercased; set-cookie and duplicates throw → existing 500 path. Headers land on SSR HTML, the response-cache write, form-action re-renders, and /__bosia/data/*.json. Data endpoint: loader cache-control wins over the computed default unless cookies were accessed — then private, no-cache wins (privacy beats intent).

  • 🟡 hooks.tsLoadEvent.setHeaders type + makeSetHeaders() (lives here so tests avoid bosia:routes); renderer.ts wires it into both loader events, returns loaderHeaders, spreads onto response sites + cache write.
  • compress() + serveCached() base keys lowercased so extraHeaders override instead of comma-joining.
  • server.ts data endpoint destructures loaderHeaders out of the JSON body, merges with cookie-privacy override.
  • test/setHeaders.test.ts; docs guides/server-loaders (en+id) incl. missing depends row; bosia-routing skill R3b; roadmap check-off; CHANGELOG.md; version 0.8.7.
  • ⚪ Deliberately skipped: headers on error/redirect responses; prerender = silent no-op; masked data requests don't re-set headers.

0.8.6 (2026-07-05) — Scroll restoration on back/forward

Browser auto-restoration fired at popstate time, before the SPA rendered the destination — it clamped against the wrong document height (worse with +loading.svelte skeletons). Router now owns it: scrollRestoration="manual", position saved per history entry, restored after the nav settles.

  • 🟠 core/client/router.svelte.ts — manual mode; per-entry index in history.state.bosiaIndex; save on push/hash-push/popstate/unload; pendingScroll handoff; sessionStorage persistence + reload restore.
  • 🟠 core/client/App.svelte — shared settleScroll() replaces the two duplicated top-scroll blocks; popstate branch restores router.pendingScroll after tick().
  • ⚪ Docs guides/navigation (en+id) "Scroll behavior" section; bosia-navigation skill notes it's built-in (never hand-roll in afterNavigate). CHANGELOG.md appended.
  • ⚪ Split off: export const snapshot (form/UI state restore) — shipped 0.8.7.
  • ⚪ Known ceiling: scroll save relies on beforeunload, which mobile page-discard can skip (router.svelte.ts:192) — add visibilitychange/pagehide fallback if reports come in.
  • ⚪ Known ceiling: restore is a single post-tick() jump; images/async content loading later can shift the position (App.svelte:78) — re-clamp on load events if it bites.

0.8.6 (2026-07-05) — Unify dev/prod component CSS (drop the collision workaround)

Investigated the open 0.8.5 Bun-collision item. It does NOT reproduce on Bun 1.3.14: CSS-chunk hashes now include source-module identity, and shared CSS hoists to one chunk (proved via isolated Bun.build harness). Prod was never at risk — its client compiler uses css:"injected" (styles in JS, no chunking). Only the dev inspector used css:"external" + a hand-rolled runtime <style> injection to dodge the (now-fixed) collision — functionally identical to injected, just more code.

  • 🟠 plugins/inspector/bun-plugin.ts — inspector compile now css: generate==="client" ? "injected" : "external", mirroring svelteCompiler.ts. Deleted the virtualCss map, the VIRTUAL_NS virtual-module resolve/load handlers, and the runtime injection block (~30 lines); dropped unused basename import.
  • test/svelte-build.test.ts — new dev-inspector case: many +page routes sharing one styled component → build succeeds, zero CSS chunks, scoped keyframe present in JS.
  • ⚪ Version bump 0.8.6 + CHANGELOG.md.

0.8.5 (2026-07-04) — Content-hashed Tailwind CSS delivery

Prod served /bosia-tw.css with no Cache-Control (browser heuristic caching → stale styles after deploys); dev used ?v=Date.now().

  • 🟠 Tailwind output → dist/client/bosia-tw-<sha256:10>.css (temp file + rename via twHash.ts); tw field in dist/manifest.json; all 4 html link sites via twCssLink(), no cache buster; fallback /bosia-tw.css for old artifacts. Immutable caching free via HASHED_BASENAME.
  • 🟡 CSS output collision under splitting: true — RESOLVED in 0.8.6. Doesn't reproduce on Bun 1.3.14 (hash now includes module identity + shared CSS hoists); won't file upstream. Dev switched to css:"injected", workaround removed.

docs 0.8.2 (2026-07-03) — Footers + marketing sections

Registry had 19 navbars and 17 heros but zero footers and nothing for the middle of a landing page. Net-new design across four phases.

  • Phase A — footers/* (12 blocks): 6 standard + 6 themes, docs, demos, nav group.
  • Phase B — 7 section families (16 blocks): pricing, testimonials, faq, cta, features, stats, logos.
  • Phase C — pages/landing/* (2 pages): saas + simple.
  • Phase D — skills (bosia-footers, bosia-sections) + landing/saas/pricing skill repoints + indexes.

0.8.4 (2026-07-03) — Cache security fixes (framework review S-findings)

Review of the response-cache + static-serving layers found identity-hash collision + memory-DoS risks and 3 perf issues. Discovered during planning: Bun.brotliCompressSync doesn't exist — brotli variants were never built (fixed by P1).

  • 🔴 S1 — identity hash FNV-1a-32 → SHA-256 (64-bit hex); collision could serve user A's page to user B.
  • [~] S2 — anonymous-identity write gate — CANCELED during planning. Replaced by: [x] loud runtime warning (dev+prod, once per name) when a cached response's loader read a cookie not in CACHE_KEYS, + docs callout in response-cache guide.
  • 🟠 S3 — CACHE_MAX_BODY_BYTES per-entry cap (default 2MB, 0 unlimited); guard in cacheSet + early-skip before compression.
  • 🟡 P1 — brotli via node:zlib quality 5 (also un-breaks brotli entirely).
  • 🟡 P2 — prerendered pages: boot-time manifest (buildPrerenderManifest) instead of per-request exists() probes.
  • 🟡 P3 — percent-encoded static paths 404 fixed (decodeURIComponent in lookupStatic + dev fallthrough).
  • 🟠 Identity-aware dedup — data-endpoint dedup key now includes the CACHE_KEYS identity hash (computeCacheKey), so per-user routes dedup safely. BREAKING: (private) no longer skips dedup; custom session cookies must be in CACHE_KEYS.
  • 🟡 P4 — LRU eviction is O(evicted.tags), not O(all tags): LRU.set returns the evicted {key,value} so cacheDeleteIndexOnly uses entry.tags directly.
  • 🟠 Header-auth cache-isolation caveat — startup banner + response-cache docs now say the uncovered-cookie warning cannot see request headers; custom auth headers must be in CACHE_KEYS.
  • 🟡 Auth next round-trip — registry/features/auth login/register now honor ?next= via safeNext (same-site paths only, else /dashboard); was always forcing /dashboard.
  • 🟠 Route scope deleted — zero runtime consumers after identity-aware dedup; (private) is an ordinary route group (scanner/types/ambient/codegen field removed).
  • 🟠 Miss coalescing — coalesceMiss gate in cache.ts wired into SSR + API paths; N concurrent misses on one key build once, waiters re-check after the leader's microtask write.
  • 🟡 CookieJar.peek — identity hashing no longer flips accessed/readNames, so it can't force Cache-Control: private (latent bug on all cached paths).

docs 0.8.1 (2026-07-02) — Storefront e-commerce expansion (reviews, cart, wishlist, search, account)

The storefront covered browse/buy but nothing around it. Five phases add the real-world pieces: PDP reviews, cart page + wishlist (first favs-API UI), search, account area, quick-view + mega-menu.

  • 🟠 Phase A — blocks/storefront/reviews (summary bars, list, working form) wired into pages/storefront/product; docs product family + demo.
  • 🟠 Phase B — empty-state, cart-lines, wishlist-grid blocks; pages/storefront/{cart,wishlist}; order-summary gains a cta label prop; docs family + 2 page docs + 3 demos.
  • 🟠 Phase C — search-overlay block + pages/storefront/search (header already had onSearch); layout family docs + search page docs + demo wiring.
  • 🟠 Phase D — store/orders.ts samples; account-nav, order-list, order-detail, address-book, account-settings; pages/storefront/account (client-side section switching); account family docs + page docs + 2 demos.
  • 🟡 Phase E — quick-view modal (+ product-card onQuickView eye button), mega-menu standalone panel; product/layout/catalog docs + demo wiring.

docs 0.8.1 (2026-07-02) — Extract Mode Switcher component

The dark/light/system toggle lived inline in the navbar and couldn't be reused. Extract it as a standalone mode-switcher component. Also fixes a bug: a stored theme was read into state on load but never applied, so the theme visually reset on refresh.

  • registry/components/ui/mode-switcher/ — new component (cycle button), apply theme on init.
  • ⚪ Navbar consumes <ModeSwitcher />; theme logic + Button import removed; dep → ui/mode-switcher.
  • ⚪ Register in registry/index.json; docs page + demo + overview + nav entries.
  • bosia-theme-tokens skill — "Runtime light/dark switch" note so agents reach for ui/mode-switcher.

0.8.3 / docs 0.8.1 (2026-07-02) — `__BRAND__` placeholder + guard

AI agents install navbar/storefront blocks but forget to rename the hardcoded brand ("Mercato", "Brand"), shipping someone else's name in the nav/header/footer. A real-looking word doesn't trip the "replace me" reflex. Fix: rename the literal to an unmistakable __BRAND__ sentinel, fail bosia sync (→ bun run check) while any survive, and teach the block-compose skill to rename it.

  • 🟠 registry/blocks/navbars/*/block.svelte (19) + storefront/{header,footer} — brand text → __BRAND__.
  • 🟠 packages/bosia/src/core/brandGuard.ts + cli/sync.ts — scan src/, fail on leftover __BRAND__.
  • bosia-block-compose skill — R8 + checklist gate for the placeholder.
  • ⚪ Version bump bosia 0.8.3 + docs 0.8.1 + both CHANGELOG.md.
  • ⚪ Docs landing redesign — shared LandingPage.svelte (EN/ID), code-forward hero, live registry showcase.
  • ⚪ Chart tooltip — draw with native SVG rect/text, not <foreignObject>; kills Chrome's ghost-box repaint bug.

0.8.2 (2026-07-01) — Host-managed dev mode

A multi-tenant host (rukoku) edits app source then watches the dev server self-trigger a rebuild on every file event — and the watcher can compile a half-written file mid-edit, wedging the preview with a stale-manifest 500. Add an opt-in mode where the host is the single clock: it drives one build per turn via /__bosia/rebuild and reads the real { ok } result. Default unset → normal bosia dev byte-for-byte unchanged.

  • 🟠 packages/bosia/src/core/dev.tsBOSIA_DEV_MANAGED=1 gate; skip the fs.watch + mtime-poll starts.
  • 🟠 packages/bosia/src/core/dev.tsbuildAndRestart() returns Promise<boolean> (real build result).
  • 🟠 packages/bosia/src/core/dev.tsPOST /__bosia/rebuild sibling of hold/resume, returns { ok }.
  • ⚪ Version bump 0.8.2 + CHANGELOG.md.

0.8.0 (2026-06-30) — `+loading.svelte` route skeletons

Navigating between pages leaves stale content on screen until the router atomically swaps in the new page+layouts (App.svelte). Move the per-app skeleton trick into the framework as a Next.js-style convention: a +loading.svelte in a route folder renders automatically during nav, nested inside the layouts shared with the current page. No app-side resolver, no beforeNavigate wiring.

  • 🟠 packages/bosia/src/core/types.tsPageRoute.loading: string | null.
  • 🟠 packages/bosia/src/core/scanner.ts — detect sibling +loading.svelte; update conventions header.
  • 🟠 packages/bosia/src/core/routeFile.ts — emit loading import + layoutPaths identity keys in both clientRoutes generators.
  • 🟠 packages/bosia/src/core/client/App.svelte — load + render the skeleton on real path changes under the shared-layout prefix; reset on settle.
  • 🟠 packages/bosia/src/core/server.ts — fallback manifest synthesis includes loading: null.
  • test/scanner.test.ts — detect-vs-null +loading.svelte case.
  • ⚪ Docs (en+id): guides/routing (Loading Skeletons), guides/navigation (cross-ref), project-structure table, reference/sveltekit-differences row.
  • ⚪ Skills: bosia-routing (R5b + checklist + source), bosia-navigation (R9 + source).
  • ⚪ Version bump 0.8.0 (framework + docs) + both CHANGELOG.md.
  • ⚪ Follow-up: nearest-ancestor +loading.svelte inheritance (full Next.js segment semantics) — currently per-folder only.
  • ⚪ Follow-up: warm the +loading.svelte chunk on hover/viewport in prefetch.ts so the skeleton paints instantly on click.
  • ⚪ Follow-up: cold-import lag for non-hover nav (touch/keyboard/programmatic) on first visit — skeleton shows after the chunk downloads; self-heals on 2nd visit. Fix only if noticeable: touchstart/focusin warm triggers, or idle warm-all.

0.7.8 (2026-06-27) — Inspector: drop the breadcrumb header

The alt+click comment form showed the framework-stripped component chain (Button.svelte → +page.svelte) as a header above the textarea. Meaningless noise for non-technical end users (e.g. rukoku's). The AI still gets full file/tree context via buildContext(el) in the payload comment, so the visible header is pure clutter — remove it.

  • 🟠 packages/bosia/src/core/plugins/inspector/overlay.tsopenForm() no longer builds the chain/header; form.innerHTML starts at the <textarea>. buildContext/submit unchanged, AI payload unaffected.
  • ⚪ Version bump 0.7.8 (framework) + CHANGELOG.md.

0.7.7 (2026-06-23) — Per-response frame-guard opt-out

The framework re-applies X-Frame-Options: SAMEORIGIN to every response after the user handle runs, so a proxy hub can't serve an embeddable preview even after stripping the upstream header. Add a per-response opt-out so the guard stays on the hub's own pages.

  • 🟠 packages/bosia/src/core/hooks.ts — export NO_FRAME_GUARD_HEADER (internal marker header).
  • 🟠 packages/bosia/src/core/server.ts — when a response carries the marker, strip it and skip only X-Frame-Options (other security headers stay).
  • packages/bosia/src/lib/index.ts — re-export NO_FRAME_GUARD_HEADER from the public API.
  • ⚪ Version bump 0.7.7 (framework) + CHANGELOG.md.

0.7.5 (2026-06-21) — `store` template (Postgres + MinIO/S3)

shop bakes SQLite + empty S3 vars. Add a sibling store template that defaults to Postgres (native Bun.SQL via drizzle-orm/bun-sql, no driver dep) and MinIO/S3 uploads — the production-shaped stack — so create … --template store needs no post-scaffold rewiring.

  • 🟠 packages/bosia/templates/store/ — copy of shop; template.json all dialects postgres; .env.example Postgres URL + MinIO defaults; instructions.txt + README.md reworded; data/ dropped (S3 uploads, no SQLite file).
  • 🟠 packages/bosia/src/cli/create.ts — register store in TEMPLATE_DESCRIPTIONS.
  • ⚪ Docs — getting-started template table + reference/cli --template line & bullets (en + id).
  • bosia-shop-template skill — cover the store sibling (postgres default, no driver/@aws-sdk deps).
  • ⚪ Version bump 0.7.5 (framework) / 0.7.8 (docs).

0.7.4 (2026-06-16) — Navbar block family (18 `navbars/*` blocks + `bosia-navbars` skill)

The registry shipped hero and auth families but no navigation bars. This ports the standalone Navbar Stock design system (18 specimens across standard, themed and app/interactive layouts) into a new navbars/ block category, mirroring the heros port end-to-end. The hardcoded acid-lime accent collapses to primary; dark bars invert to bg-foreground text-background, glass uses backdrop-blur-xl. Theme-agnostic semantic tokens only, so each navbar restyles across all 19 themes — no new theme added.

  • 🟠 18 navbar blocks under registry/blocks/navbars/* — standard ×6, themes ×6, app ×6; self-contained <header> sections, inline Tailwind on semantic tokens, inlined primitives. Registered in index.json.
  • ⚪ Docs — 3 grouped family pages (blocks/navbars/{standard,themes,app}) with stacked live previews (3 demos registered); nav gains a Blocks → Navbars group.
  • ⚪ New bosia-navbars design skill (catalog of all 18, golden rule, token map, icon list, [[bosia-navigation]]/[[bosia-heros]] cross-links) + skills-index row (count 51 → 52).
  • 🟠 Stripped the embedded site navbar from all 17 heros/* blocks — page-level blocks must not carry navbar chrome (clashed with layout navbars/*). Rule documented in bosia-heros/bosia-block-compose R7/bosia-page-shell R1.
  • 🟠 Added navbars/overlay (family now 19) — transparent absolute navbar with light text + outline CTA floating over a full-bleed hero photo. Registered; added to Themes docs + NavbarsThemesDemo; exception documented.
  • CHANGELOG.md — appended under 0.7.4.

0.7.3 (2026-06-15) — Login/auth page family (new `auth/*` pages + 9 blocks + `bosia-login` skill)

The registry shipped a storefront page family but no auth/login pages — only a single cards/login block. This ports the standalone Login Design System (12 themes, centered + split layouts, the full sign-in/up/forgot/magic-link/OTP/SSO screen set) into Bosia as a visual-only page family: 9 reusable auth/* blocks (semantic tokens, brand → primary, status → emerald/amber/destructive, social brand logos keep their official colours) and 6 pages composed from them. One card, two layouts (centered ↔ split) via a one-line variant swap, mirroring the storefront purpose pattern. Backend stays in bosia-auth-flow — pages note the pairing.

  • 🟠 9 blocks under registry/blocks/auth/*auth-shell (centered ↔ split), auth-card, brand, social-row, divider, auth-field, password-strength, otp-input, form-message. Registered in index.json.
  • 🟠 6 pages under registry/pages/auth/{login,register,forgot,magic-link,otp,sso}/ — composition only; each meta.json lists its auth/* block deps so the installer recurses; default variant="centered", swappable to split.
  • ⚪ Docs — new blocks/auth page + 6 pages/auth/* pages (7 demos registered), Pages overview gains an auth section, nav gains Blocks → Auth and Pages → Auth groups.
  • ⚪ New bosia-login design skill (catalog of the 6 pages + 9 blocks, token map, voice, centered↔split swap, [[bosia-auth-flow]] pairing) + skills-index row (count 50 → 51).
  • CHANGELOG.md — appended under 0.7.3.

0.7.3 (2026-06-15) — Prebuilt template artifacts for fast `bosia create`

bosia create --template shop was slow: installFeature fetched 150+ files serially from GitHub raw. Fix: CI bakes the finished scaffold into a version-locked Release tarball; create downloads + extracts it. Registry path stays the fallback.

  • 🟠 packages/bosia/templates/shop/template.json — opt in with "prebuilt": true (only heavy templates need it; default/demo have no template.json and are already instant).
  • 🟠 cli/create.ts — when prebuilt && !--local && !BOSIA_BUILDING_PREBUILT, download v<version>/<template>.tar.gz, extract, substitute {{PROJECT_NAME}}, restore .env; falls back to registry on 404/offline.
  • 🟠 scripts/build-prebuilt-templates.ts (new) — iterate templates/*, skip unless prebuilt; scaffold with literal {{PROJECT_NAME}} + --no-install via local registry; tar -czf dist/prebuilt/<t>.tar.gz.
  • packages/bosia/package.jsonbuild:templates script; version 0.7.3.
  • .github/workflows/publish.yml — Bun setup + bun run build:templates; attach dist/prebuilt/*.tar.gz to the release.
  • .github/workflows/refresh-prebuilt.yml (new) — registry/template changes without a version bump rebuild artifacts and gh release upload --clobber onto the existing release. Keeps the create fast path in sync.
  • ⚪ Docs — reference/cli.md documents the fast prebuilt download + registry fallback + --no-install.

0.7.2 (2026-06-14) — Brief intake: defer ALL installs to after approval

Bug: during Quick-start intake the AI ran bosia_add_theme/bosia_add_block before brief_request_approval, so the Setuju button never appeared. Fix: intake is now capture-only; all installs move to a new step 9 after BRIEF.md is complete.

  • 🟠 bosia-brief-intake/SKILL.md — steps 3.3/3.4 now record theme/first-screen choices (no install); new step 9 "Install NOW" runs after status complete; step-5 approval gate hardened; R5 bans installs before complete.
  • 🟠 bosia-brief-visual/SKILL.md — reworded to "records, doesn't install"; "Workflow side effects" → "Deferred install — NOT during intake" (run by intake step 9); checklist splits recording from the add-theme checks.
  • 🟠 bosia-brief-platform/SKILL.md — same treatment: description reworded; "Workflow side effects" → "Deferred install — NOT during intake"; format-helper + block scaffolds run in intake step 9; checklist gate annotated.
  • 🟠 bosia-brief-intake/SKILL.md (+ example-brief.md) — ## Todo seed now carries TWO items: "Redesign login & register" + new "Replace mock data with real DB integration". Step 6, BRIEF.md template, and example brief updated.

0.7.1 — Inspector: structured AI payload (labeled fields)

The alt+click → "Send to AI" comment now carries labeled fields — url, pageFile (nearest +page/+layout), component leaf, capped text, framework-stripped tree — so the AI traces props back to the page, not the leaf.

  • 🟠 inspector/overlay.tsisFrameworkFrame/fileOf/userFrames/findPageFile/ownText/buildContext helpers; submit() sends buildContext(el) + "---" + comment; form header shows the framework-stripped chain.
  • ⚪ Docs — guides/inspector.md aiEndpoint section rewritten with the labeled format + why pageFile matters.
  • ⚪ Skill — bosia-inspector-edit/SKILL.md rewritten to the new [Inspector] contract (split on ---, default to pageFile over the component leaf, use url/text to disambiguate).

0.7.0 — Deprecated `page.params` fallback

Re-adds a deprecated, working params getter on the page object (bosia/client) so legacy code reaching for page.params keeps working. Returns reactive appState.routeParams, dev-only warn-once, removed in 1.0.0. $props() stays taught.

  • 🟠 page.svelte.ts — add get params() returning appState.routeParams; @deprecated JSDoc + dev-only console.warn (warn-once, guarded by NODE_ENV !== "production" so it tree-shakes out of prod).
  • ⚪ Docs — guides/routing.md deprecated callout; reference/roadmap.md line updated; reference/changelog.md entry. Skills left as the correct $props() pattern.

Shop template defaults to sqlite-file

Flips the shop template from PostgreSQL to built-in SQLite (sqlite://./data/app.db) — zero-config, no DB server. Per-feature dialects already supported sqlite; this only changes template defaults plus an ENOENT fix for data/.

  • 🟠 packages/bosia/templates/shop/template.json — all five featureOptions dialects postgressqlite.
  • 🟠 packages/bosia/templates/shop/.env.exampleDATABASE_URLsqlite://./data/app.db; instructions.txt wording → SQLite.
  • 🟠 registry/features/drizzle/drizzle-index.sqlite.tsmkdirSync(dirname(path)) before opening the file (skip :memory:) so db:migrate against ./data/app.db doesn't ENOENT.
  • _gitignore ignores data/*.db*; ship data/.gitkeep. Docs/skills (bosia-shop-template) updated postgres-default → sqlite-file.

0.6.25 — Port the Mercato storefront (new `page` tier + 24 blocks + clay theme)

Ports the Mercato React storefront. Adds a new page registry tier (page = group of blocks, no backend) via bosia add page. 24 storefront/* blocks compose 4 pages sharing one runes cart, mapped to existing themes + a new clay theme.

  • 🟠 New page registry tier + CLI — cli/page.ts (runAddPage), routed in addRouter.ts (+ pages/... alias); pages added to Manifest and RegistryIndex; bosia add list shows pages; dispatch tests.
  • 🟠 Harvested 24 sections under registry/blocks/storefront/*store (runes cart/favs/drawer + catalogue), header/footer, home sections, product card/grid/drawer, listing + PDP + checkout blocks. Registered in index.json.
  • 🟠 Added the clay theme (registry/themes/clay/) — warm paper neutrals, terracotta primary, soft warm shadows, Newsreader/Hanken Grotesk/Geist Mono fonts; registered in registry/index.json (19 themes).
  • 🟠 Four pages under registry/pages/storefront/{home,listing,product,checkout}/ — composition only, one shared createCart() wired through header/grids/drawer; each meta.json lists its block/ui deps so the installer recurses.
  • ⚪ Docs — new Pages section + 4 page pages, 6 grouped blocks/storefront/* pages, themes/clay; 10 demos; nav groups; new bosia-storefront skill; $lib/blocks/$pages aliases + registry/pages Tailwind @source.

0.6.24 — Port the Hero Stock hero system (17 blocks)

Ports the Hero Stock React design system (17 full-bleed hero sections across 6 verticals) into a new heros/ block category. Hardcoded accents collapse to primary; dark photo heroes invert. Semantic tokens only — no new theme.

  • 🟠 Ported 17 hero blocks under registry/blocks/heros/* (commerce ×7, education ×2, food ×2, fashion ×2, services ×2, saas ×2); inline Tailwind on semantic tokens, pickers as local $state, kept Unsplash images. Registered.
  • ⚪ Docs — 17 per-hero pages (each backed by its meta.json so /api/blocks lists each with its own install line); single-hero previews; new bosia-heros skill; nav grouped by vertical; skills-index row.

0.6.24 — Port the Cardstock card system (29 blocks + 6 themes)

Replaces the single cards/feature-editorial block (now removed — it painted its numeral with accent, so it never followed the brand). The new cards map the brand colour to primary and use semantic tokens only, so they restyle across themes.

  • 🟠 Extended the theme contract on all 12 existing themes — added --font-mono and theme-scoped elevation (--shadow-xs/sm/md/lg wired through --elevation-* per :root/.dark; midnight gets a faint primary glow).
  • 🟠 Added 6 Cardstock themes — paper, carbon (brutalist hard-offset shadows), bloom (diffuse tint), terminal + grape (dark glow), sage; each with light + dark, registered in registry/index.json (18 themes) + docs pages + nav.
  • 🟠 Ported 29 card blocks under registry/blocks/cards/* (data, people, commerce, media, utility, auth); inline Tailwind on semantic tokens, status colours shadcn-style, sparkline/ring/mini-bars as inline SVG. Registered.
  • 🟠 Removed registry/blocks/cards/feature-editorial/; re-pointed tests, CLI help, docs demo/page, and the bosia-landing/bosia-saas-landing/bosia-block-compose skills to cards/feature (now takes title/body/icon/cta props).
  • ⚪ Docs — 6 per-category card pages with live demo galleries + per-card install lines; new bosia-cards skill cataloguing all 29.

0.6.24 — Remove the Todo template + feature ahead of rebuilding card blocks

The todo starter template (and its todo feature/components) was the original CRUD demo. We're rebuilding the card blocks from scratch, so it's being pulled to clear the way. The editorial card block stays until replacements exist.

  • 🟠 Deleted packages/bosia/templates/todo/, registry/features/todo/, registry/components/todo/, and docs/content/docs/components/todo/.
  • 🟠 registry/index.json — dropped todo from features and the four todo* components entries.
  • 🟠 packages/bosia/src/cli/{create,index,add,feat}.ts — removed the todo template description/example and re-pointed the stale todo doc-comment examples at still-present features.
  • ⚪ Docs — removed the Todo template rows and component pages from getting-started.md, components/overview.md, reference/cli.md, guides/inspector.md, the nav.ts Todo group, and the bosia-block-compose example.

0.6.24 — Three-state theme switcher (Light / Dark / System)

Theme switching was binary (light/dark) and the logic was duplicated across four slightly-different places.

  • 🟠 core/html.ts — extracted the inline FOUC bootstrap (duplicated 4×) into one THEME_INIT_JS constant; now resolves an explicit system value live against prefers-color-scheme, compatible with stored dark/light.
  • 🟠 navbar.svelte — replaced the buggy isDark boolean (never persisted, out of sync with FOUC) with a 3-state light/dark/system cycle that persists to localStorage, syncs the DOM, follows live OS changes. Sun/Moon/Monitor.
  • 🟠 docs/src/lib/components/ThemeToggle.svelte + docs/src/routes/+layout.svelte — mirror the same 3-state logic and system-aware FOUC script (Monitor inline SVG added).
  • docs/content/docs/guides/styling.md + docs/content/docs/components/ui/navbar.md — document the three modes, storage values, and cycle pattern.
  • ⚪ Bumped svelte to ^5.56.3 in the 5 stale package.json files (docs, 4 templates); root, packages/bosia, and apps/demo were already current.

0.6.24 — `add theme` strips the template's default `:root`/`.dark` block

A theme's tokens.css defines its own :root/.dark, but the template's app.css ships a default block earlier in the file. After add theme both coexisted at the same specificity and the template's won.

  • 🟠 templates/{default,shop,demo}/src/app.css — wrapped the default :root/.dark token block in /* bosia-theme-vars */ sentinels so removal targets only the template defaults, never user :root rules. @theme {} stays above, untouched.
  • 🟠 cli/theme.tsrunAddTheme now calls stripTemplateThemeVars(appCssPath) after wiring the @import, removing the sentinel-bounded block (idempotent). The installed theme's :root becomes the only one, so its tokens win.

0.6.23 (2026-06-11) — Surface yesterday's shop-template bugs in skills + ban the `postgres` npm pkg

0.6.22 shipped the shop template and patched 5 install-blocking bugs. Three reflect cross-cutting gotchas future authors will hit again, and the shop package.json still carried a stale postgres dep.

  • 🟠 core/dev.ts — the proxy's "App server is starting…" 503 is now an HTML page carrying the /__bosia/sse reload client (was bare text/plain), so a live-reload racing into a rebuild auto-recovers instead of sticking.
  • 🟠 core/dev.ts — reload-hold control: POST /__bosia/hold suppresses the SSE reload broadcast, POST /__bosia/resume flushes one reload if any rebuild fired. hold doubles as a heartbeat (90s safety timer)
  • 🟠 packages/bosia/templates/shop/package.json — drop unused "postgres": "^3.4.0"; scaffold uses Bun-native drizzle-orm/bun-sql against Bun.SQL, no userland driver needed.
  • 🟠 bosia-bun-runtime/SKILL.md — new ## Postgres — Bun.SQL section with the object-form snippet + Bun 1.3.x FailedToOpenSocket gotcha; banned-packages table gains postgres/pg rows pointing at Bun.SQL.
  • 🟠 bosia-database-setup/SKILL.md — reversed the misleading "bun add postgres" line into a "no install" directive cross-linking [[bosia-bun-runtime]]; R5 gains a one-liner about the URL-string gotcha.
  • 🟠 bosia-drizzle-feature/SKILL.md — new R11 ("Registry files use import paths from their target location") covering meta.json#files[].target depth, with the auth feature's 3-up import as the example; P0 gate added.
  • 🟠 sidebar-menu-item.sveltehasChildren = $derived(!!children && !href) so leaf items with a {#snippet icon()} body don't render as collapsible parents (Svelte 5 fills children from whitespace around named snippets).
  • 🟠 features/shop/admin-sidebar.svelte — Orders is now a parent grouping All orders/Pending/Refunds under one SidebarMenuItem. Logo <img> tags gain h-5 w-5 so the SVG renders at 20px instead of overflowing.
  • bosia-shop-template/SKILL.md — new skill bundling eight rules for extending a shop scaffold: don't re-install features, edit sidebars in place, no postgres/pg/@aws-sdk, routes under (private)/dashboard/, services not db.select
  • 🟠 core/server.ts — API-route handlers now treat throw redirect(...)/throw error(...) like page actions (303 / typed JSON) instead of a generic 500; also supports return redirect(...). Found via shop POST /logout.
  • 🟠 templates/{default,shop,todo,demo}/src/app.css — add @custom-variant dark (&:where(.dark, .dark *));. Tailwind v4 defaults dark: to prefers-color-scheme, so the navbar's classList.toggle("dark") was a no-op.
  • 🟠 features/shop/admin-sidebar.svelte — header rebuilt per SidebarDemo: theme-aware logo button doubling as a collapse toggle, brand row, working <SidebarTrigger>
  • 🟠 registry/features/shop/meta.json — add "ui/breadcrumb" to components so bosia create … --template shop actually scaffolds the breadcrumb files.
  • 🟠 templates/shop/src/routes/(private)/+layout.svelte — derive segments from page.url.pathname, render <Breadcrumb> above {@render children()}; last segment becomes <BreadcrumbPage>, earlier ones <BreadcrumbLink>.
  • 🟠 packages/bosia/templates/shop/src/routes/(public)/+page.svelte — add a <svelte:head> with <title>Welcome to your shop</title> + <meta name="description"> so the home page has basic SEO instead of an empty <head>.
  • 🟠 features/auth/login-page.svelte + register-page.svelte — sibling <meta name="description"> inside the existing <svelte:head> (login: "Sign in to your account."; register: "Create your account. First account becomes admin.").
  • ⚪ CLI-internal bugs (block-deps 404 routing, --local flag drop, dialect default under skipPrompts:true) deliberately omitted — code-only fixes, no public API to document.
  • 🟠 cli/block.ts — session-level installed Set mirrors add.ts:40; runAddBlock early-returns when a block is already installed this session. Without it files/upload-area was re-written during shop scaffold (two pullers).
  • registry/features/shop/meta.json — drop ui/typography (unused) and ui/form / ui/input / ui/button (already declared by auth; component installer dedupes via add.ts:116 so removal is safe).

0.6.22 (2026-06-10) — `shop` template + `auth` / `rbac` / `shop` registry features

Templates bottomed out at todo (one CRUD feature), but the most common "build me an app" prompt is a storefront.

  • 🟠 registry/features/auth/ — multi-dialect user/session tables, Bun.password.hash argon2id.
  • 🟠 registry/features/rbac/permission table with composite PK, can(userId, resource, action, scope?) with * wildcards, resources.ts registry, auth-handle.ts rewritten to attach locals.can(...), seed grants * to the first user.
  • 🟠 registry/features/shop/ — multi-dialect product/order/order_item/cart_item tables, valibot validators, repositories + services, PublicNavbar + AdminSidebar, resources.append.ts adds eight shop.* permissions.
  • 🟠 templates/shop/ — thin glue: template.json declares features: ["auth","rbac","file-upload","shop"]
  • packages/bosia/src/cli/create.tsTEMPLATE_DESCRIPTIONS map gains shop: "Online store starter with auth, RBAC, S3 uploads, products/orders/cart".
  • docs/content/docs/getting-started.md — template table gains a shop row.

0.6.22 (2026-06-10) — Sidebar docs + skill (fix three AI mis-uses) + `DropdownMenu` floating mode

AI agents hit three sidebar failures: (1) wrapping leaf SidebarMenuItems in DropdownMenu swallowed the href; (2) skipping the user footer.

  • components/ui/sidebar.md — new "Choosing the right item shape" table with a "never wrap in DropdownMenu" callout; new "User Footer" section.
  • bosia-sidebar/SKILL.md — new skill. STOP block names the three failures. R1–R7 cover leaf/parent shape, no-foreign-trigger, icon snippet, bind:collapsed, user-footer, floating/side/align, trigger="hover". P0 checklist gates.
  • 🟠 dropdown-menu.svelte — context exposes triggerEl/setTriggerEl. Trigger bind:this syncs via untrack
  • SidebarDemo.svelte — leading "Dashboard" leaf with LayoutDashboard icon (R1); user footer rebuilt with DropdownMenu + Avatar, justify-start on the trigger (fixes chevron), floating side="top" align="start" on the content.

0.6.21 (2026-06-09) — Fix three AI-agent app-building failures (block install, EACCES, `page` export)

AI agents hit three failures: (1) bosia add blocks/cards/feature-editorial 404'd (CLI only routed block singular); (2) bosia add ui/typography aborted on EACCES overwriting a foreign-uid file.

  • 🟠 packages/bosia/src/cli/addRouter.ts — new dispatcher; routes blocks/<cat>/<name> tokens to runAddBlock, splits mixed batches (components + blocks) into one runAdd call plus per-block calls.
  • 🟠 packages/bosia/src/cli/index.tsadd case calls routeAdd with injected runners; help text adds the alias.
  • 🟠 packages/bosia/src/cli/registry.ts — new writeRegistryFile(dest, content) helper does unlink + retry on EACCES/EPERM, then surfaces a chown hint if still failing.
  • 🟠 packages/bosia/src/cli/add.ts and block.ts — component/block file write loops route through the new helper.
  • 🟠 core/client/page.svelte.ts (new) — reactive page object backed by $derived over router.currentRoute; exposes page.url/page.pathname. No params getter — Bosia already plumbs params into +page/+layout via $props().
  • 🟠 packages/bosia/src/lib/client.ts — re-export page.
  • 🟠 docs/content/skills/bosia-block-compose/SKILL.md — canonical bosia add block cards/feature-editorial example.
  • 🟠 docs/content/skills/bosia-saas-landing/SKILL.md and bosia-landing/SKILL.md — split single install line into per-category calls (theme / components / block).
  • 🟠 docs/content/skills/bosia-svelte-runes/SKILL.md R6.5 — illustrative "removed import" example switched from page to a deleted cn utility so the lesson no longer contradicts the now-real page export.
  • packages/bosia/test/registry.test.ts — coverage for routeAdd dispatch (block, alias, mixed batch, multi-block, plain components), readRegistryJSON blocks-category path, writeRegistryFile happy-path/overwrite.
  • packages/bosia/test/page-store.test.ts (new) — compile-and-wiring checks for the page module (compiles via svelte/compiler, output references derived, bosia/client re-exports page).

0.6.21 (2026-06-09) — `bosia-image-dialog` skill + block (multi-image picker)

AI-generated apps seed images.unsplash.com placeholders but there was no UI primitive to swap them without clobbering. New files/image-dialog block composes UploadArea + an External URL tab + an existing-images gallery.

  • 🟠 registry/blocks/files/image-dialog/block.svelte (Dialog + Tabs + selection state, embeds UploadArea, fetches /api/files once on mount) and meta.json (deps: button, dialog, input, label, sonner, tabs, upload-area).
  • 🟠 registry/index.json — add files/image-dialog to blocks.
  • docs/content/skills/bosia-image-dialog/SKILL.md — workflow steering, P0/P1 checklist, anti-patterns (single-image flows, id-vs-url storage, merge-with-stale-state).
  • docs/content/skills/bosia-file-upload/SKILL.md — cross-reference the new dialog for replace-existing / gallery flows.
  • docs/content/docs/blocks/files/image-dialog.md + docs/src/lib/components/demos/FilesImageDialogDemo.svelte registered in docs/src/routes/(docs)/[...slug]/+page.svelte.
  • docs/src/lib/docs/nav.ts — insert Image Dialog under Blocks → Files.

0.6.21 (2026-06-09) — Drawer component (mobile bottom-sheet)

Drawer was the last unbuilt Priority-2 overlay. Mobile action sheets had no first-class component.

  • 🟠 registry/components/ui/drawer/ — 8 svelte sub-components (drawer, content, trigger, close, header, title, description, footer) + index.ts + meta.json
  • 🟠 registry/index.json — add ui/drawer.
  • 🟠 docs/content/docs/components/ui/drawer.md — usage, props, sub-components, accessibility, Drawer-vs-Dialog guidance.
  • 🟠 docs/src/lib/components/demos/DrawerDemo.svelte + register in docs/src/routes/(docs)/[...slug]/+page.svelte.
  • 🟠 docs/src/lib/docs/nav.ts — insert Drawer entry under UI children.
  • docs/content/skills/bosia-drawer/SKILL.md — workflow steering for AI agents (mobile-first contract, P0/P1 checklist).
  • backup/COMPONENT_PLAN.md — flip Drawer to [x].

0.6.19 (2026-06-08) — `.webmanifest` 404 + stale-build recovery

Two unrelated prod bugs on komba (pure-SSR) after the 0.6.18 deploy. (1) /site.webmanifest 404'd.

  • 🟠 packages/bosia/src/core/server/staticServer.ts — add .webmanifest to the isStaticPath whitelist.
  • 🟠 packages/bosia/src/core/build.ts:156-170 — hash the client entry filename (naming.entry: "[name]-[hash].[ext]") so staticManifest serves it as immutable and per-build URL changes invalidate the browser cache automatically.
  • 🟠 core/client/hydrate.ts — add a production-only unhandledrejection handler that detects failed dynamic import() and triggers location.replace(?_v=…). Loop-guard via sessionStorage["bosia:reload-attempt"] (10s window).
  • ⚪ Follow-up: surface a stale-build event on the router (onStaleChunk hook?) so apps can show a soft toast instead of a hard reload — defer until the safety net proves insufficient.

0.6.18 (2026-06-07) — pure-SSR apps still lost `public/` in production containers

Bug: komba (pure SSR, zero prerendered routes) on 0.6.17 — /bosia-tw.css 404'd because generateStaticSite() early-returned when dist/prerendered/ didn't exist, so public/ never mirrored to dist/static/. Fix: always mirror public/

  • 🟠 packages/bosia/src/core/prerender.tsgenerateStaticSite() now copies public/ unconditionally; SSG-specific copies (prerendered + client mirror) gated on dist/prerendered/ existing.
  • 🟠 Test: build emits dist/static/ with public/ contents even when no prerendered routes exist.

Same-day addition (2026-06-06) — replace custom `` wrapper with `@lucide/svelte`

The hand-curated registry/components/ui/icon (95 inline SVG paths) plus 28 components with hardcoded <svg> duplicated work per glyph. Decision: drop the wrapper, import each icon from @lucide/svelte

  • 🟠 apps/demo/package.json, docs/package.json — add @lucide/svelte dep.
  • 🟠 registry/components/ui/icon/ — delete; remove ui/icon from registry/index.json.
  • 🟠 Migrate 17 registry components (accordion, select, checkbox, pagination, calendar, carousel, sidebar, breadcrumb, command, date-picker, input-otp, radio-group, resizable, nav-menu, combobox, navbar.
  • 🟠 Update 3 skill examples (bosia-dashboard, bosia-mobile-screen, bosia-empty-states) and the docs SidebarDemo.
  • 🟠 components/ui/icon.md rewritten as a @lucide/svelte guide with a deprecation callout; overview.md, navbar.md, upload-area.md, crop-image.md callouts updated; IconGrid.svelte deleted.
  • ⚪ New docs/content/skills/bosia-icon/ skill steering AI agents toward @lucide/svelte (never lucide-svelte).

Same-day addition (2026-06-06) — production runtime needed `src/app.html`

Bug from komba Dockerfile: runner copies only dist/. Container boots → renderer.ts reads src/app.html from cwd → missing → throws.

Decision: emit parsed segments to dist/app-html.json during build; renderer reads dist first, falls back to src/app.html for dev/HMR. Zero app changes — dist/ is already in every Docker COPY.

  • 🟠 packages/bosia/src/core/appHtml.ts — add writeAppHtmlSegments(segments, outDir) (serializes to ${outDir}/app-html.json); getAppHtmlSegments(cwd) now tries persisted artifact first, falls back to loadAppHtmlTemplate(cwd).
  • 🟠 packages/bosia/src/core/build.ts — after writing route-manifest, call writeAppHtmlSegments(appHtml) so production runtime has the segments inside dist/.

0.6.17 (2026-06-07) — production runtime also needed `src/hooks.server.ts`, `bosia.config.ts`, and `public/`

Same komba Dockerfile incident. Once dist had app-html.json, the app booted but every auth request 303'd to /login: server.ts read src/hooks.server.ts from cwd with no fallback → locals.user undefined. Same for bosia.config.ts

Decision: extend the dist-first / src-fallback pattern (used for app-html.json) to hooks, config, and static assets. Build emits self-contained ESM artifacts under dist/

  • 🟠 core/build.ts — new bundleRuntimeUserFiles(cwd) step after the server bundle: externalize npm + bosia/elysia/bun/svelte, Bun.build src/hooks.server.tsdist/hooks.server.js and bosia.config.*dist/bosia.config.js
  • 🟠 packages/bosia/src/core/server.ts — hook loader checks ${OUT_DIR}/hooks.server.js first, falls back to src/hooks.server.ts. Log line reports which path won.
  • 🟠 packages/bosia/src/core/config.tsloadBosiaConfig checks ${OUT_DIR}/bosia.config.js first, dynamic-imports it directly (skips Bun.build at server start); falls back to the existing compile-from-source path for dev.
  • 🟠 packages/bosia/src/core/staticManifest.ts — walk ${outDir}/static/ (mirror of public/ written by build) so prod images can ship only dist/. addOnce keeps public/ canonical when both exist (dev double-walk).

Same-day addition (2026-06-04) — `parent()` returns `{}` on client-side JSON nav

Bug from komba: (await parent()).farmId worked on SSR but returned undefined on client-side nav (14 routes)

Decision: make parent() see the real parent chain even when layer loaders are skipped. Two viable shapes — pick one:

  • A (server-side): when handling /__bosia/data/*.json, accept a parentSnapshots payload in the POST body (the client already has cached data per skipped layer); merge into parentData before the page loader. Lowest churn.
  • B (client-side): when a page loader is selected to re-run and any ancestor layout is skipped, re-run those layouts server-side too. Simpler invariant ("parent() always sees fresh data") but defeats the whole point of selective re-runs.

A is preferred. Plus a P0 doc/skill update so the workaround (locals-based farm/user scope) is the documented pattern even after the framework fix lands, since locals is also better for typed access.

  • 🟠 Client: emit parentSnapshots: Record<depth, data> for skipped layers. Done in App.svelte (nav) + prefetch.ts via a shared buildParentSnapshots helper — POST with { parentSnapshots } only when non-empty, else keep the cacheable GET.
  • 🟠 packages/bosia/src/core/server.ts/__bosia/data branch parses parentSnapshots from the POST body (guarded try/catch → undefined on GET/malformed) and forwards into loadRouteData via a new arg.
  • 🟠 core/renderer.tsloadRouteData accepts parentSnapshots
  • bosia-routing/SKILL.md — added a rule under R3: don't use parent() for scope identifiers (farmId/orgId/userId), read from event.locals; parent() is fine for view-layer data, locals is the source of truth for cross-loader scope.
  • ⚪ Regression test under apps/demo: a layout server returning { orgId }, a child +page.server.ts asserting (await parent()).orgId. Verify SSR AND a client-nav JSON fetch. (Not yet added — needs a client-nav harness.)

Same-day addition (2026-05-30) — file-upload private-by-default + skill rule

0.6.11 made /uploads/<key>.webp reach +server.ts, but the default handler had no auth/ownership check and file had no userId

  • 🟠 registry/features/file-upload/file.{sqlite,pg,mysql}.table.ts — add user_id NOT NULL (text for sqlite/pg, varchar(36) for mysql).
  • 🟠 registry/features/file-upload/file.repository.tsgetAllByUser(userId), getByKey(key), getOwned(id, userId), remove(id, userId) — ownership is part of every query.
  • 🟠 registry/features/file-upload/file.service.tsupload(file, userId) stores userId; getAll(userId) filters; getByKey(key) exposes the row for the route's ownership check; remove(id, userId) rejects on ownership mismatch.
  • 🟠 registry/features/file-upload/api-files-server.ts, api-files-id-server.ts, uploads-static-server.ts — all gated on locals.user; uploads-static responds with Content-Type: record.mime + Cache-Control: private, no-store.
  • 🟠 bosia-file-upload/SKILL.md — new R5.5 "Files are private by default", anti-patterns (remove auth check, repoint to public/uploads, drop user_id), P0 gates on 401 + cross-user 404 curl, [[bosia-auth-flow]] declared a hard prereq.

Same-day addition (2026-05-30) — API routes shadow static fallthrough

Bug from fotoku: /uploads/<uuid>.webp 404'd even with a +server.ts at /uploads/[...path]. Root cause: the static-files block (matches by extension) ran before resolveApiMatch, so any static-extension URL short-circuited into static lookup.

  • 🟠 packages/bosia/src/core/server.ts — move the API-match block (+server.ts resolution + cache + error handling) above the static-files block and prerender block. No logic change inside the moved block.
  • apps/demo/src/routes/uploads/[...path]/+server.ts, apps/demo/uploads/sample.webp, apps/demo/src/routes/(public)/uploads-test/+page.svelte — regression demo so /uploads-test renders an image served via the +server.ts handler.

Same-day addition (2026-05-30) — fix `props_id_invalid_placement` in UI components

Svelte 5 requires $props.id() to be the entire RHS of a top-level const. Nine UI components called it inside a template literal (const baseId = \tabs-${$props.id()}`;`), which throws at init.

  • 🟠 components/ui/{accordion,collapsible,hover-card,menubar-menu,nav-menu-item,popover,sidebar-menu-item,tabs,tooltip}.svelte

Same-day addition (2026-05-30) — `bosia feat drizzle` defaults to sqlite-file, not in-memory

Bug: AI ran bosia feat drizzle and got sqlite://:memory:, losing data on restart. Three drifts: meta.json DATABASE_URL was a comment string with :memory: last; the runtime fallback was :memory:

  • 🟠 registry/features/drizzle/meta.json — single concrete DATABASE_URL=sqlite://./data/app.db, no inline comment options.
  • 🟠 registry/features/drizzle/drizzle-index.ts + drizzle.config.ts — runtime fallback now sqlite://./data/app.db, not sqlite://:memory:.
  • 🟠 docs/content/skills/bosia-database-setup/SKILL.md — default scheme updated to sqlite://./data/app.db; references to src/features/drizzle/db.ts corrected to index.ts (actual file shipped by the feature).

Same-day addition (2026-05-30) — UI ids use `$props.id()` instead of `crypto.randomUUID` / `Math.random`

Generated apps crashed with crypto.randomUUID is not a function over plain http (LAN IP, preview subdomains)

  • 🟠 field, tooltip, popover, hover-card, nav-menu-item, menubar-menu — replace crypto.randomUUID().slice(0, 8) with $props.id(). menubar-menu now prefixes its id with menubar- for consistency with siblings.
  • 🟠 registry/components/ui/tabs/tabs.svelte, sidebar/sidebar-menu-item.svelte, accordion/accordion.svelte, collapsible/collapsible.svelte — replace Math.random().toString(36).slice(2, 10) base ids with $props.id().
  • 🟠 registry/components/ui/alert-dialog/alert-dialog.svelte, dialog/dialog.svelte — collapse the two separate Math.random() calls into one $props.id() shared by titleId and descriptionId.
  • ⚪ Server-side crypto.randomUUID in features/file-upload/file.service.ts and file.{mysql,sqlite}.table.ts left as-is — runs in Node where crypto is always available, and those ids persist to the DB rather than per-render.

Same-day addition (2026-05-30) — brief intake: drop DB question + approval-button tool + `bosia-database-setup` skill

Two pain points: (a) AI kept asking follow-up questions after the Quick Start batch (heading said "six", script listed 7, no rule against follow-ups)

  • 🟠 bosia-brief-intake/SKILL.md — Quick Start now five questions (palette + aesthetic merged into Q5). Step 5 is the approval gate via brief_request_approval
  • 🟠 bosia-brief-intake/references/quick-start-script.md — rewritten as a five-question script with the merged Q5 and an updated inference table. "After answers locked" now references the brief_request_approval tool call.
  • 🟠 bosia-database-setup/SKILL.md — new skill replacing bosia-brief-database. Triggers on user intent ("pakai postgres", "buat tabel"). Workflow A = engine swap; Workflow B = new table/column. R1 keeps sqlite-file the silent default.
  • 🟠 docs/content/skills/bosia-brief-database/ — deleted; superseded by bosia-database-setup.
  • docs/content/skills/SKILL.md catalog — count bumped 44 → 45; added a bosia-database-setup row to the "Conventions — framework ·" section.
  • docs/content/skills/bosia-drizzle-usage/SKILL.md + references/troubleshooting.md — swapped every bosia-brief-database reference to bosia-database-setup.

Bosia's SPA router intercepted every <a> click, re-ran the full page load, then scrollTo(0,0). Hash-only links like <a href="#features"> never scrolled because e.preventDefault() killed the browser default.

  • 🟠 core/client/router.svelte.ts — short-circuit same-page hash nav in the click handler: when anchor.pathname + anchor.search matches the current location and a hash is present, skip navigate()
  • 🟠 core/client/router.svelte.ts — export scrollToHash(hash) helper: decodes the fragment, resolves getElementById, calls scrollIntoView(), returns whether it found a target. Used by both the click handler and App.svelte.
  • 🟠 core/client/App.svelte — replace unconditional window.scrollTo(0,0) after nav settle with tick().then(() => scrollToHash(hash) || scrollTo(0,0)) in both paths.

Same-day addition (2026-05-28) — new skills `bosia-page-shell` + `bosia-query-defaults`

AI agents kept (a) re-rendering navbar/footer in every +page.svelte instead of +layout.svelte, (b) hand-rolling <table> instead of ui/data-table, (c) shipping repository list with no limit/offset/orderBy. No skill said otherwise.

  • 🟠 bosia-page-shell/SKILL.md — new skill. R1: chrome lives in +layout.svelte, never +page.svelte. R2 layout-depth table. R3 requires (private)/+layout.server.ts to produce data.user. R5 forbids hand-rolled <table>
  • 🟠 bosia-query-defaults/SKILL.md — new skill. R1 fixes the repo signature to list(db, { limit, offset, orderBy?, where? }) returning { rows, total }. R2 defaults limit/offset/orderBy. R3 clamps limit ≤100.
  • docs/content/skills/SKILL.md — catalog updated from 42 → 44 skills; new rows added to "Conventions — design ✦" (bosia-page-shell) and "Conventions — framework ·" (bosia-query-defaults).

Same-day addition (2026-05-28) — fix `feat` block install + non-interactive `add block`

AI agent ran file_upload_install and got a silent 404 (the block path lives under blocks/, not components/). The retry hung on @clack/prompts because the MCP runner never closes stdin. Framework fixes only — no per-app patching.

  • 🟠 cli/feat.tsFeatureMeta gains blocks?: string[]. After component install, iterate meta.blocks and call runAddBlock so block deps route to the block installer (was 404'ing through addComponent
  • 🟠 cli/block.tsrunAddBlock accepts InstallOptions, gates the "Replace existing block?" prompt behind !skipPrompts, threads options into recursive addComponent calls, honors -y
  • packages/bosia/src/cli/index.tsadd block dispatch now passes through -y (was filtering to -- long flags only).
  • registry/features/file-upload/meta.json — moved files/upload-area and files/crop-image from components to the new blocks field.
  • 🟡 bosia-file-upload/SKILL.md — R5 now shows the explicit import UploadArea from "$lib/blocks/files/upload-area/block.svelte" and full props; workflow step 5 gains db:generate && db:migrate

Same-day addition (2026-05-28) — fix `file-upload` end-to-end (Bun.Image, image host, route placement)

AI agent scaffolded bosia feat file-upload and hit three regressions: (1) Bun.Image.open/decode doesn't exist, uploads crash; (2) PUBLIC_BASE_URL bakes the wrong host into stored URLs.

  • 🔴 features/file-upload/file.service.ts — rewrite the Bun.Image pipeline. Drop the decodeImage shim. Use new Bun.Image(bytes), read dims from metadata(), positional resize(w, h, opts), .webp({ quality: 85 }).bytes()
  • 🟠 features/file-upload/storage-local.tssave() returns relative /uploads/${key} (no PUBLIC_BASE_URL prefix). The browser resolves against the page origin → works for lvh.me preview, prod domains, and localhost without env tuning.
  • registry/features/file-upload/meta.json — drop misleading PUBLIC_BASE_URL=http://localhost:3000 default (now empty string). (F2)
  • 🟠 bosia-routing/SKILL.md — new R6 hard rule: authenticated UI MUST live under (private). Anti-pattern block (public)/admin/... ❌ vs (private)/admin/... ✅. Decision rule + P0 checklist entry. (F3a)
  • 🟠 docs/content/skills/bosia-dashboard/SKILL.md — STOP rule at top: files under (private)/; create (private)/+layout.server.ts if absent. (F3b)
  • 🟠 docs/content/skills/bosia-crud-flow/SKILL.md — STOP rule at top: resource routes under (private)/<resource>/...; admin CRUD never (public). (F3c)
  • 🟡 bosia-bun-runtime/SKILL.md — new Bun.Image section: constructor, metadata(), positional resize, per-format encoders (.webp({ quality: 0–100 })), .bytes(). Anti-pattern callout for Bun.Image.open/decode. (F4)
  • docs/content/skills/bosia-file-upload/SKILL.md — R2 cross-references the new Bun.Image section in bosia-bun-runtime for the API surface. (F5)

Severity: 🔴 Critical · 🟠 Major · 🟡 Minor · ⚪ Trivial


Completed (v0.0.1 – v0.1.26)

Click to expand completed items

Core Framework

  • 🔴 SSR with Svelte 5 Runes ($props, $state)
  • 🔴 File-based routing (+page.svelte, +layout.svelte, +server.ts)
  • 🟠 Dynamic routes ([param]) and catch-all routes ([...rest])
  • 🟡 Route groups ((group)) for layout grouping
  • 🟠 API routes — +server.ts with HTTP verb exports
  • 🟠 Error pages — +error.svelte

Data Loading

  • 🔴 Plain export async function load() pattern (no wrapper)
  • 🟠 $types codegen — auto-generated PageData, PageProps, LayoutData, LayoutProps
  • 🟠 parent() data threading in layouts
  • 🟠 Streaming SSR for metadata (non-blocking load())
  • 🟠 Form actions (SvelteKit-style)

Server

  • 🔴 ElysiaJS HTTP server
  • 🟡 Gzip compression
  • 🟡 Static file caching (Cache-Control headers)
  • 🟡 /_health endpoint
  • 🟠 Cookie support (cookies.get, cookies.set, cookies.delete)
  • 🟡 Cookie sameSite accepts both casings (lax/Lax) — normalized to canonical header
  • 🟠 Protocol-aware Secure cookies — auto-downgrade over HTTP with warn; TRUST_PROXY=true honors x-forwarded-proto
  • 🟠 Security headers (X-Content-Type-Options, X-Frame-Options, etc.)
  • 🟡 DISABLE_X_FRAME_OPTIONS=true env var to omit X-Frame-Options for intentional cross-origin iframe embedding
  • 🟠 Graceful shutdown handler (SIGTERM/SIGINT)
  • 🟠 .env file support with $env virtual module
  • 🟡 CORS configuration (framework-level)
  • 🟠 Session-aware fetch (cookies forwarded in internal API calls)
  • 🟡 Request timeouts on load() and metadata() functions
  • 🟠 Route PUT/PATCH/DELETE through handleRequest() — consistent CSRF, CORS, security headers, and cookie handling
  • 🟠 Graceful shutdown drain — drain in-flight requests before stopping; return 503 from health check during shutdown
  • 🟡 Concurrent build guard in dev — prevent overlapping builds when rapid file changes trigger buildAndRestart() while a build is already running
  • 🟡 Clean dev server shutdown — release Bun.serve, file watchers, and timers on SIGINT so the event loop drains naturally; outer bun run reports exit 0 instead of 130
  • 🟡 Single-^C stop — CLI wrappers survive SIGINT and wait for the child (no more orphaned server); dev skips the drain for instant exit; second ^C force-quits (130)
  • 🟠 Dev watcher safety net — 5s mtime poll of src/ complements fs.watch so atomic-write edits (temp + rename) that macOS drops still trigger rebuilds
  • 🟠 Dev crash backoff — replace the "stop after 3 crashes" silent-stop with exponential backoff (500ms → 5s) that never gives up, so a transient error or fixed source change brings the app back without manual restart

Security

  • 🔴 XSS escaping in HTML templates — sanitize JSON.stringify() output in <script> tags
  • 🔴 SSRF validation on /__bosia/data/ — validate route path segment
  • 🔴 CSRF protection — Origin/Referer header validation for state-changing requests
  • 🟠 Strip stack traces from error responses in production
  • 🟠 Request body size limits
  • 🔴 Path traversal protection — validate static/prerendered file paths stay within allowed directories
  • 🟡 Cookie parsing error recovery — wrap decodeURIComponent() in try-catch
  • 🟡 Cookie option validation — whitelist/validate domain, path, sameSite values
  • 🟠 PUBLIC_ env scoping — only expose vars declared in .env files
  • 🟠 Streaming error safety — validate route match before creating stream
  • 🟡 safeJsonStringify crash guard — try-catch for circular reference protection
  • 🟠 Open redirect validation on redirect()
  • 🟡 Cookie RFC 6265 validation — validate names against HTTP token spec; use encodeURIComponent only for values

Client

  • 🔴 Client-side hydration
  • 🔴 SPA router (client-side navigation)
  • 🟡 Navigation progress bar
  • 🟠 HMR via SSE in dev mode
  • 🟡 Per-page CSR opt-out (export const csr = false)
  • 🟡 Link prefetching — data-bosia-preload attribute for hover/viewport prefetch
  • 🟠 Fix client-side navigation with query strings/hashes
  • 🟡 Use insertAdjacentHTML for head injection — prevents re-parsing <head>, avoiding duplicate stylesheets and script re-execution

Build & Tooling

  • 🔴 Bun build pipeline (client + server bundles)
  • 🟠 Manifest generation (dist/manifest.json)
  • 🟠 Static route prerendering (export const prerender = true)
  • 🟠 Tailwind CSS v4 integration
  • 🟠 $lib alias → src/lib/*
  • 🟡 bosia:routes virtual module
  • 🟡 Validate Tailwind CSS binary exists before build
  • 🟡 Prerender fetch timeout
  • 🟡 Fix withTimeout timer leak
  • ⚪ Remove duplicate static file serving
  • 🟠 Static site output — merge prerendered HTML + client assets + public into dist/static/ for static hosting
  • 🟡 Validate .env variable names — reject invalid identifiers that break codegen
  • 🟡 .env parser escape sequence support — handle \n, \", etc. in quoted values

Routing

  • 🟠 Dynamic route prerendering with entries() export — enumerate dynamic route params for static prerendering

CLI

  • 🔴 bosia dev — dev server with file watching
  • 🔴 bosia build — production build
  • 🔴 bosia start — production server
  • 🟠 bosia create — scaffold new project (with --template flag and interactive picker)
  • 🟠 bosia add — registry-based UI component installation
  • 🟠 bosia feat — registry-based feature scaffolding
  • 🟡 bosia add index-based path resolution — resolves component names from index.json instead of blindly prefixing ui/
  • 🟡 bosia feat nested feature dependencies — features field in meta.json for recursive installation
  • 🟡 bosia feat overwrite prompt — asks before replacing existing files
  • 🟡 bosia add multi-component install — bosia add button card input installs all in one call
  • 🟡 bosia add -y / --yes flag — auto-confirm overwrite prompts for CI / scripts

Templates & Features

  • 🟠 todo template (formerly drizzle) — PostgreSQL + Drizzle ORM with full CRUD todo demo
  • 🟠 drizzle feature — bosia feat drizzle scaffolds DB connection, schema aggregator, migrations dir, seed runner
  • 🟠 Multi-engine drizzle feature — adapter, drizzle.config.ts, and seed-runner branch on DATABASE_URL scheme (postgres, mysql, sqlite file, sqlite in-memory) over Bun's built-in drivers (no per-engine npm dep)
  • 🟠 Bun-native drizzle migrate runner — src/features/drizzle/migrate.ts replaces drizzle-kit migrate for sqlite/postgres/mysql apps (drizzle-kit's sqlite migrate needs better-sqlite3
  • 🟠 bosia-brief-database skill + hook into bosia-brief-intake — captures DB engine + connection during brief intake, writes ## Database block to BRIEF.md
  • 🟠 todo feature — bosia feat todo scaffolds todo schema, repository, service, routes, components, and seed data
  • 🟡 todo component — bun x bosia@latest add todo installs todo-form, todo-item, todo-list components
  • 🟡 Registry as single source of truth — bosia create --template todo installs features from registry via template.json instead of duplicating files

Hooks & Middleware

  • 🟠 hooks.server.ts with Handle interface
  • 🟡 sequence() helper for composing middleware
  • 🟠 RequestEventrequest, params, url, cookies, locals

Docs & Ecosystem

Docs site + component registry roadmap moved to docs/ROADMAP.md. Only framework-level (packages/bosia) items remain below.

  • 🟠 GitHub Actions for auto-publishing to npm and deploying docs
  • 🟡 Dev server auto-restart on crash
  • 🟠 SEO infrastructure — Metadata type supports lang and link fields; dynamic <html lang>; <link> tag rendering in streaming SSR

v0.1.0

  • 🟡 Rename framework from bosbun to bosia
  • ⚪ Dead code cleanup (renderSSR, buildHtmlShell, unexported internals)
  • 🟡 splitCsvEnv helper for CSRF/CORS origin parsing

v0.2.0 — Production Hardening

Stability, security, and performance improvements for production workloads.

Security

Findings #1–#7 below come from the v0.4.5 security audit — see backup/SECURITY_ISSUE_1.md for full context, attack scenarios, and proposed diffs.

  • Cookie secure defaults — default HttpOnly; Secure; SameSite=Lax on cookies.set() with opt-out
  • Auto-detect Cache-Control on /__bosia/data/private, no-cache when cookies accessed; public, max-age=0, must-revalidate otherwise
  • 🔴 load() fetch cookie scoping — makeFetch now forwards the Cookie header only to same-origin requests or origins in the INTERNAL_HOSTS allowlist; third-party hosts get no cookie. User-supplied init.headers.cookie is preserved
  • 🔴 Audit #1allowExternal redirect validation — still validate against javascript:, data:, vbscript: schemes even when allowExternal: true (move DANGEROUS_SCHEMES check above the early return in errors.ts:32)
  • 🟠 Audit #4 — Trusted proxy configuration — TRUST_PROXY env to control when X-Forwarded-* headers are trusted in CSRF checks (csrf.ts:37-40)
  • 🟠 Audit #6 — CSP nonce infrastructure — per-request nonce generation, inject into all framework <script> tags, expose nonce in hooks for user scripts, opt-in CSP_DIRECTIVES env emits matching Content-Security-Policy header
  • 🟠 Audit #2 — CORS preflight validation — validate Access-Control-Request-Method / Access-Control-Request-Headers against allowed config in handlePreflight (cors.ts:53-69)
  • 🟠 Audit #3 — CORS Vary: Origin on all responses when CORS is configured — prevent CDN caching bugs on non-matching origins (set at server.ts request level, not only in getCorsHeaders)
  • 🟡 Audit #5 — Validate prerender entries() return values — reject /, \, .. in dynamic segment values before URL substitution (prerender.ts:44-50)
  • 🟡 Escape lang attribute in HTML shell — <html lang="${lang}"> injects lang raw; if a metadata() derives lang from URL/user input it can break out of the attribute
  • ⚪ Validate CORS_MAX_AGE env — reject non-numeric values instead of producing NaN header

Security test coverage (from audit)

  • 🟡 Test: allowExternal: true still rejects javascript: / data: / vbscript: URLs
  • 🟡 Test: handlePreflight rejects when Access-Control-Request-Method is not in allowedMethods
  • 🟡 Test: Vary: Origin is present on CORS-configured responses even when requesting origin doesn't match
  • 🟡 Test: dedicated safePath() unit test file (currently only covered indirectly via static file serving)
  • 🟡 Test: substituteParams() rejects malicious entry values containing path-traversal characters
  • 🟡 Test: TRUST_PROXY env gates X-Forwarded-* header trust in CSRF checks

Performance

  • 🟠 Parallelize client + server builds — run both Bun.build() calls with Promise.all() instead of sequentially (~500-1000ms savings)
  • 🟠 Parallelize Tailwind CSS with builds — run Tailwind CLI concurrently with client+server builds (~500-800ms savings); ensure output exists before manifest step
  • 🟡 Convert sequence() middleware recursion to loop — apply(i+1, e) pattern risks stack overflow with many handlers; use iterative approach

Server Reliability

  • 🟠 Stream backpressure handling — check controller.desiredSize to prevent memory buildup on slow/disconnected clients
  • 🟠 Streaming SSR error recovery — render proper error page instead of bare <p>Internal Server Error</p> when render() throws mid-stream
  • 🟠 renderPageWithFormData loader error handling — catch HttpError/Redirect thrown from loadRouteData() after a successful form action; let them surface as proper redirect/error responses instead of crashing the request.
  • 🟡 Prerender process cleanup — proper signal handling, verified termination (await child.exited after kill()), use random port instead of hardcoded 13572
  • 🟡 Fix buildAndRestart recursive tail call — replace recursion with while loop to prevent stack growth under rapid file changes

Client

  • 🟡 Bound prefetch cache size — prefetchCache grows unbounded between navigations; add LRU eviction (max ~50 entries)
  • 🟡 Prefetch cache TTL — stale prefetch data served after long idle; discard entries older than 30s on consumePrefetch()
  • 🟠 Router click handler must respect modifier/middle clicks.

Build

  • 🟡 Fail build on tsconfig.json corruption — don't silently continue with degraded config
  • 🟡 compress() threshold uses character count not byte count — body.length on a UTF-8 string under-counts multi-byte content; switch to Buffer.byteLength or TextEncoder().encode(...).length before threshold check
  • 🟡 .env parser inline-comment stripping — KEY="value" # note currently keeps # note as part of the value; strip trailing comment after the closing quote
  • ⚪ Tune gzip compression threshold — raised to 2KB (GZIP_MIN_BYTES = 2048); small responses fit in single TCP packet, gzip overhead outweighs savings below this size

DX

  • 🟠 Audit #7 — Dev proxy must forward X-Forwarded-Host/X-Forwarded-Proto to the inner app — without them the CSRF check derives the wrong expectedOrigin, 403'ing same-origin POST/form actions in dev. DX-only, prod unaffected.
  • 🟡 Stale env cleanup in dev — reset removed .env vars on hot-reload

v0.2.1 — Features & DX

New capabilities and developer experience improvements.

Data Loading

  • 🟠 depends() and invalidate() — selective data reloading
  • 🟡 Prefetch sends the loader cache mask — hover/viewport data-bosia-preload was warming the data endpoint with no mask, re-running every loader server-side; now it sends the same _invalidated bits as a real nav
  • 🟡 setHeaders() in load functions — set response headers from loaders — shipped 0.8.7
  • 🟠 beforeNavigate / afterNavigate lifecycle hooks — exported from bosia/client; fired by SPA router around pushState/popstate navs and on full-page unload (willUnload=true); cancel support via cancel() on programmatic navs
  • 🟠 Scroll restoration and snapshot support (export const snapshot) — scroll restoration shipped 0.8.6; snapshot shipped 0.8.7

Routing

  • 🟠 Layout reset ([email protected] or [email protected])
  • 🟠 Route-level +error.svelte — per-layout error boundaries instead of global-only
  • 🟡 Page option: ssr toggle (export const ssr = false)
  • 🟡 Page option: trailingSlash configuration

Forms

  • 🟠 use:enhance progressive enhancement — client-side fetch submission with automatic form state management (like SvelteKit)

Types

  • 🟠 Typed route params — generate { slug: string } from [slug] instead of Record<string, string>
  • 🟡 Error page types in generated $types.d.ts

Server

  • 🟡 Structured logging with request correlation IDs

DX

  • 🟡 Cache route scanning in dev mode — skip fs.readdirSync() re-scan when changed file is not a route file (+page/+layout/+server/+error) — shipped 0.8.10
  • 🟡 Remove hardcoded 200ms SSE delay — poll /_health instead of Bun.sleep(200) before broadcasting reload — shipped 0.8.10
  • 🟡 Smarter dev rebuild triggers — filter watcher by extension; skip rebuilds for .md, test files, and non-source changes — shipped 0.8.10

v0.2.2 — Ecosystem, Observability & Scale

Nice-to-haves for a growing framework and performance at scale.

  • 🟡 Production sourcemaps — external source maps for debuggable production errors

Performance (at scale)

  • 🟠 Request deduplication — share the in-flight loader promise for concurrent identical GET requests instead of running twice. Reworked in 0.3.1 around a folder convention: dedup ON by default keyed on URL only.
  • 🔴 Dedup key cross-user data leak — replaced cookie-fingerprint identity with a folder convention. Routes under (private) skip dedup and run per-request.
  • 🟡 Trie-based route matcher — replace linear O(n) route scan with radix trie for O(k) matching (k = URL segments). Matters when route count exceeds ~100
  • 🟡 Compiled route regex — pre-compile route patterns to RegExp at startup instead of parsing on every match
  • 🟠 Concurrency / backpressure ceiling — Bun accepted unlimited connections, the likely OOM vector under slow-loris. Add an env-gated soft cap (MAX_INFLIGHT) reusing the in-flight counter, return 503 when exceeded. Shipped v0.5.13.
  • 🟡 Response cache + brotli — Bun.gzipSync() ran on every HTML >2KB with no precompressed cache; no brotli. Add an LRU cache + brotli. Shipped v0.6.0 — skip-render cache keyed on URL + identity hash, per-route opt-out, brotli+gzip per entry.
  • 🟡 Static-asset fallthrough cost — every static hit called Bun.file().exists() up to 4×. Build a boot manifest so prod lookups are a Map check. Shipped v0.6.9 — staticManifest.ts walks the dirs once at boot.
  • 🟡 Collapse SSR render() calls — root App.svelte + error pages render in separate Svelte render() invocations. Profile under representative load first.

Server Reliability

  • 🟠 Process-level error handlers in prod — install process.on("uncaughtException"/"unhandledRejection") outside the dev inspector path.
  • 🟡 Structured logging — replace emoji-prefixed console.log/error in server.ts with a level-based logger that emits JSON in prod (pretty in dev) with a request ID.
  • ⚪ Tunable shutdown timers — server.ts:906 hardcodes the 2 s force-exit window and 10 s drain. Expose via SHUTDOWN_DRAIN_MS / SHUTDOWN_FORCE_MS for deploys with long-running streaming responses. Source: 2026-05-23 pre-prod audit
  • ⚪ Startup banner shows resolved hostname — server.ts:880-882 logs http://localhost:${PORT} even though Bun binds 0.0.0.0 by default. Cosmetic only (container is reachable). Source: 2026-05-23 pre-prod audit

v0.2.3 — CLI & Feature Installer

Per-file install strategies so features can safely contribute to shared files.

CLI / Feat

  • 🟠 bosia feat per-file strategies — meta.json files: FileEntry[] with a strategy field: write (default), skip-if-exists, append-line, append-block, merge-json. Replaces the all-or-nothing replace prompt for shared files.
  • 🟡 Document meta.json schema and strategies in docs/ (CLI / bosia feat page)
  • 🟡 bosia feat <name> --dry-run — preview file actions (write/skip/append/merge) without touching disk
  • 🟡 Validation: error early when two installed features write to the same target with write strategy (force one to declare append-line/append-block)
  • 🟠 auth feature scaffold — uses append-block to register hooks in src/hooks.server.ts and routes barrel
  • 🟡 s3/storage feature → shipped as file-upload in v0.6.4: bun x bosia feat [-y] file-upload [-d sqlite|postgres|mysql] scaffolds Drizzle metadata + local/S3 adapter + /api/files POST (WebP @0.85, fit 1920×1080)
  • 🟡 Track installed features per project (bosia.json at root, committed) — enables bosia feat list/add list. Schema { version, features, components, blocks } keyed by name.

v0.3.0 — Test Integration (Phase 1 + 2)

Built-in testing powered by bun test. See TEST_PLAN.md for full details.

DX

  • 🟡 Prettier formatting — root config + scripts (format, format:check); all 3 templates ship matching .prettierrc.json so scaffolded projects format-on-create. Pre-commit hook auto-formats staged files. No lint.

CLI

  • 🟠 bosia test command — wraps bun test with framework-aware defaults
  • 🟡 Auto-load .env.test (fallback .env) before running tests
  • 🟡 Set BOSIA_ENV=test automatically
  • 🟡 Pass through flags (--watch, --coverage, --bail, --timeout, etc.)
  • 🟡 Unit tests for core pure utilities (matcher, cookies, csrf, cors, errors, html, dedup, env)
  • 🟡 Unit tests for build/codegen helpers (scanner, routeTypes, envCodegen, hooks.sequence, paths.resolveBosiaBin, lib/utils.cn, cli/registry.mergePkgJson, prerender path/URL helpers)

Test Utilities (`bosia/testing`)

  • 🟠 createRequestEvent() — mock factory for testing +server.ts handlers and hooks
  • 🟠 createLoadEvent() — mock factory for testing load() functions
  • 🟡 createMetadataEvent() — mock factory for testing metadata() functions
  • 🟠 mockCookies() — in-memory cookie jar implementing Cookies interface
  • 🟡 mockFetch() — fetch interceptor for isolating loaders
  • 🟡 createFormData() — helper for building form action payloads

v0.3.1 — Route & API Integration Testing (Phase 3)

Test routes end-to-end without starting a real server.

  • 🟠 createTestApp() — build an in-process Elysia instance from the route manifest
  • 🟠 testRequest() — send HTTP requests to the test app, get standard Response back
  • 🟠 Support API routes, page routes (SSR HTML), and form actions
  • 🟡 Response assertion helpers: expectJson(), expectRedirect(), expectHtml()

v0.3.2 — Component Testing (Phase 4)

Render and assert on Svelte 5 components in tests.

  • 🟠 renderComponent(Component, { props }) — SSR render a component, return HTML
  • 🟠 renderPage(route, options?) — full SSR pipeline (loader → layout → page)
  • 🟡 Snapshot testing support (built into bun test)
  • 🟡 Investigate @testing-library/svelte compatibility with Bun

v0.4.0 — Plugin Core

First-party plugin system. Standardize OpenAPI / OpenTelemetry / server-timing as plugins; let third parties drop in any Elysia plugin. Full design in plans/plugin-feature.md.

Config & Types

  • 🔴 bosia.config.ts loader — packages/bosia/src/core/config.ts; resolve from process.cwd(), compile via Bun.build({ target: "bun" }), cache, default to { plugins: [] }
  • 🔴 Public types in packages/bosia/src/lib/index.tsBosiaPlugin, BosiaConfig, BuildContext, DevContext, RenderContext, defineConfig helper

Elysia Hooks

  • 🔴 backend.before / backend.after mount points in server.tsbefore runs raw routes (e.g. /openapi.json) bypassing framework middleware; after receives RouteManifest for introspection

Build Hooks

  • 🟠 build.preBuild / build.postScan / build.postBuild in build.ts — call preBuild before loadEnv, postScan after scanRoutes(), postBuild after generateStaticSite()
  • 🟠 build.bunPlugins(target) merged into client + server Bun.build() plugin arrays

Render Hooks

  • 🟠 render.head fragments injected before </head> in buildMetadataChunk
  • 🟠 render.bodyEnd fragments injected before </body> in buildHtmlTail
  • 🟠 RenderContext (request, route, metadata) threaded from renderer.ts into html.ts builders

First-Party Plugin

  • 🟠 bosia/plugins/server-timing — exercises backend.before; adds Server-Timing: handler;dur=... header

Docs & Demo

  • 🟡 docs/content/docs/guides/plugins.md — usage guide
  • 🟡 apps/demo/bosia.config.ts — server-timing wired

v0.4.1 — OpenAPI Plugin

Auto-bridge file routes to OpenAPI spec.

  • 🟠 bosia/plugins/openapi first-party plugin
  • 🟠 build.postScan reads RouteManifest, emits dist/openapi.json
  • 🟠 Runtime mount via backend.beforeGET /openapi.json, GET /docs (Scalar/Swagger UI)
  • 🟡 Optional schema export on +server.ts (TypeBox or Zod, decide later)
  • 🟡 Docs: OpenAPI usage page

v0.4.2 — OpenTelemetry Plugin

Tracing + metrics for production apps.

  • 🟠 bosia/plugins/opentelemetry first-party plugin
  • 🟠 OTLP exporter config via env vars (OTEL_EXPORTER_OTLP_ENDPOINT, etc.)
  • 🟠 Trace backend.before request → response, load() calls, render time
  • 🟡 Verify dev parity — telemetry must work in bosia dev

v0.4.1 — Inspector Plugin ✅ (shipped 2026-05-06)

Click element in browser → open exact source file:line in editor / hand off to AI agent. No Vite, no React-style fiber tree — does it via compile-time attribute injection.

Compile-Time

  • 🟠 bosia/plugins/inspector first-party plugin (dev-only)
  • 🟠 Contributes Bun plugin via build.bunPlugins() — runs before SveltePlugin() and replaces its .svelte onLoad with an injecting variant
  • 🟠 Parses .svelte source with svelte/compiler parse(), walks RegularElement nodes, injects data-bosia-loc="<relpath>:<line>:<col>" via magic-string (preserves source maps)
  • 🟡 Skips <svelte:*> and component (capitalized) tags
  • 🟡 Strips attribute from production builds (no-op when not dev)

Runtime Overlay

  • 🟠 Dev-only client overlay injected via render.bodyEnd — alt+hover highlights element, alt+click captures data-bosia-loc
  • 🟠 POST /__bosia/locate endpoint (mounted via backend.before) — receives { file, line, col }, opens editor (or POSTs to aiEndpoint with comment)
  • 🟡 Editor integration — code -g file:line (configurable via inspector({ editor: "code" | "cursor" | "zed" }))
  • 🟡 Toast feedback — overlay shows "opened :" on click

Docs

  • 🟡 docs/content/docs/guides/inspector.md — usage + AI-agent workflow

v0.4.2 — Template fixes ✅ (shipped 2026-05-07)

Make a freshly scaffolded project pass bun run check out of the box.

  • 🟠 Ship .gitignore with bun x bosia create — npm pack strips .gitignore, so templates store it as _gitignore and copyDir restores the dotfile name on copy
  • 🟡 Ignore generated Tailwind output public/bosia-tw.css in template .prettierignore and .gitignore (default, demo, todo) so bun run check succeeds on a clean scaffold
  • 🟡 bun run check:templates — packs via bun pm pack, extracts the tarball, and asserts each templates/* still has the expected files (no install, no scaffold) so this class of regression fails locally before publishing

v0.5.1 — Inspector default in all templates ✅ (shipped 2026-05-15)

Ship every scaffolding template with a minimal bosia.config.ts so freshly scaffolded projects get Alt+click-to-source out of the box.

  • 🟡 Add bosia.config.ts to templates/{default,demo,todo}/ enabling inspector({ editor: "code" }). copyDir copies it as-is; no substitutions needed. Production-safe (plugin self-disables under NODE_ENV=production).
  • ⚪ Note preconfigured state in docs/content/docs/guides/inspector.md so existing-project users still find the manual setup steps

v0.5.5 — Dev/Build dist collision ✅ (shipped 2026-05-18)

Dev and build no longer share ./dist. Dev writes to .bosia/dev/; standalone bun run build keeps writing to ./dist/.

  • 🟠 Decouple URL namespace (/dist/client/...) from on-disk location via OUT_DIR in paths.ts (reads BOSIA_OUT_DIR, default ./dist)
  • 🟠 dev.ts hardcodes .bosia/dev and passes BOSIA_OUT_DIR to spawned build + app-server children; never reads the env itself
  • 🟠 build.ts, prerender.ts, html.ts, server.ts, cli/start.ts all read from OUT_DIR instead of hardcoded ./dist literals
  • 🟡 Verification path: BOSIA_OUT_DIR=.bosia/verify bun run build produces full artifacts without touching ./dist. Catches what tsc --noEmit + svelte-check miss (route scan, prerender child, server-entry compile). Verified at apps/demo.

v0.5.6 — Build/dev `.bosia/` cleanup collision ✅ (shipped 2026-05-18)

Follow-up to v0.5.5. OUT_DIR was split, but build.ts still blanket-wiped ./.bosia at startup — clobbering a concurrently-running bosia dev whose compiled server lives at .bosia/dev/. Cleanup is now scoped.

  • 🔴 build.ts cleanup is scoped to OUT_DIR plus only the codegen files this build owns (routes.ts, routes.client.ts, env.server.ts, env.client.ts, types). No more blanket .bosia/ rmSync.

v0.5.7 — `params` as a top-level page/layout prop ✅ (shipped 2026-05-19)

Match SvelteKit: +page.svelte/+layout.svelte receive params as a sibling prop of data, not nested under data.params. Network protocol (data endpoint payload, SSR injection) is unchanged — params is stripped at the component boundary.

  • 🟠 App.svelte passes params as a separate prop on pages and layouts; SSR branch strips merged params off pageData via local helper
  • 🟠 hydrate.ts seeds appState.pageData without the merged params key (still seeds appState.routeParams from same payload)
  • 🟠 routeTypes.ts codegen: PageData / LayoutData no longer intersect { params: Params }; PageProps / LayoutProps declare params: Params as a sibling of data
  • 🟡 Update demo + template blog/[slug]/+page.svelte and docs (README.md, docs/content/docs/guides/routing.md) to consume params as a top-level prop
  • 🟡 Standardize default and todo starter templates on the (public)/ route group convention used by demo, so scaffolded projects are ready to add authenticated areas (e.g. (app)/, (admin)/) without restructuring later

Same-day addition (2026-05-19) — Inspector runtime error capture

Inspector now captures live client + server runtime errors in a passive badge inside the running app. "Send to AI" per row reuses the alt-click → aiEndpoint handoff. Live-only (no buffer/replay), dev-only (prod unaffected).

  • 🟠 Server capture: Elysia .onError() + uncaughtException/unhandledRejection listeners installed lazily inside backend.before(). uncaughtException rethrows so crash-recovery still triggers. 500ms dedup prevents render-loop floods.
  • 🟠 SSE broadcaster at /__bosia/errors — module-scoped controller Set, event: bosia-error data frames, 25s :ping keepalive, abort-driven cleanup. No replay buffer (live-only contract)
  • 🟠 Reorder the Elysia onError chain in server.ts: the base 500 responder now registers AFTER the plugin.backend.before loop so plugin handlers fire first.
  • 🟠 Client capture in overlay.ts: window.error + unhandledrejection listeners + EventSource subscription to /__bosia/errors. Unified list, stable ids, UI dedup
  • 🟠 Floating badge UI bottom-right (● N errors) → click → expandable panel with per-row stack details, Dismiss, and AI-only "Send to AI" button. Badge hidden when list empty
  • 🟠 Sourcemap resolution dev-only — build.ts emits sourcemap: "linked" in dev ("none" in prod). New inspector/sourcemap.ts lazy-resolves compiled frames → source via @jridgewell/trace-mapping, only for the clicked error.
  • 🟡 Last-interaction context: track the most recent data-bosia-loc the user clicked/keyed and append Last user interaction: <file>:<line>:<col> to the payload.
  • 🟡 errorsEnabled?: boolean (default true) config flag on InspectorOptions — opt out of the whole feature without removing the plugin
  • 🟡 AI-only action button — overlay still surfaces the badge for visibility without aiEndpoint, but the "Send to AI" button only renders when configured. Standalone bosia apps in editor-mode see display-only errors

v0.5.8 — `bind:*` shadow crash fix ✅ (shipped 2026-05-19)

Dev pages using <input bind:value={state}> crashed with RangeError: Maximum call stack size exceeded on first render. Svelte's dev output wraps the binding in a named function get(); Bun rewrites $.get to a named import get

  • 🔴 Post-process Svelte compile output in inspector/bun-plugin.ts and svelteCompiler.ts to rename the inner get/set to $$g/$$s (length-preserving so source-map columns stay accurate). Dev-only — prod uses anonymous arrows.
  • 🔴 Inject Inspector-extracted component CSS via a runtime <style> element instead of a loader: "css" virtual module.

v0.5.9 — `src/app.html` template ✅ (shipped 2026-05-20)

SvelteKit-style document shell. Users create src/app.html with %bosia.head%/%bosia.body% placeholders to control HTML chrome (lang, data attributes, favicon, analytics)

  • 🟠 packages/bosia/src/core/appHtml.ts — parse, validate, cache template with invalidation for HMR
  • 🟠 Placeholders: %bosia.head%, %bosia.body% (required); %bosia.lang%, %bosia.nonce%, %bosia.assets%, %bosia.env.PUBLIC_*% (optional)
  • 🟠 Update html.ts builders (buildHtml, buildHtmlShellOpen, buildMetadataChunk, buildHtmlTail) to accept optional segments and slot user chrome
  • 🟠 Update renderer.ts to load template once per process and thread through 6 call sites
  • 🟠 Validation at build time in build.ts — fail fast if required placeholders missing
  • 🟡 Scaffold src/app.html in templates (default, todo) and demo with %bosia.lang% and data-theme attributes
  • 🟡 Favicon detection: if user's headOpen contains rel="icon", skip framework default favicon injection
  • 🟡 Unit tests: template loading, validation, parsing, caching, interpolation, segment structure
  • 🟡 New skill bosia-app-css documenting src/app.css order + the Tailwind v4 / LightningCSS rule: font @import url(...) must come before @import "tailwindcss" or it's silently dropped. Catalog 33 → 34.
  • 🟡 New CLI command bosia add font "<Family>" "<url>" (cli/font.ts, reuses mergeFontImports()). Prepends @import url(...) to src/app.css with a /* bosia-font: <Family> */ marker so it survives LightningCSS ordering. Idempotent.

v0.5.10 — SvelteKit navigation parity ✅ (shipped 2026-05-20)

Closes the gap between Bosia's client nav API and SvelteKit's $app/navigation. Apps reached for window.location.href because goto() wasn't exported (full reload, lost state)

  • 🟠 goto(url, opts?) exported from bosia/client. Returns a Promise resolving after the nav settles. Honors replaceState, invalidateAll, noScroll
  • 🟠 beforeNavigate(fn)/afterNavigate(fn) lifecycle hooks. nav.cancel() blocks SPA navigations; popstate cancellation is a no-op since history already advanced. Auto-unregister on destroy via onDestroy.
  • 🟠 Router exposes navigation type ("link"|"goto"|"popstate"|"form"|"enter") and the Navigation object threading into both lifecycle phases.
  • 🟠 router.navigate(path, { replace, source }) supports history.replaceState (used by goto({ replaceState: true })) and threads the source through to the Navigation object
  • 🟡 beforeunload fires beforeNavigate with willUnload: true so listeners can observe (cancellation requires native beforeunload event — out of scope)
  • 🟡 Hydration safety net — wrapped main() in core/client/hydrate.ts in a .catch() so any future hydrator failure logs to console instead of silently leaving "Loading…" on screen
  • 🟠 404/error pages no longer ship a stuck #__bs__ spinner blocking the "Go home" link. buildHtml() now gates spinner injection on empty body — non-streaming SSR skips it; streaming SSR and ssr=false still get it for the TTFB gap.
  • 🟡 Demo route apps/demo/src/routes/(public)/nav-test/+page.svelte exercises all four patterns plus the cancel/event-log flow
  • 🟡 New docs page docs/content/docs/guides/navigation.md covers the four patterns and the lifecycle hooks; added to the Guides sidebar in docs/src/lib/docs/nav.ts
  • 🟡 New bosia-navigation skill so AI agents pick the right navigation pattern and use the lifecycle hooks correctly. Catalog index bumped 34 → 35; cross-references added in bosia-routing and bosia-auth-flow.

Same-day addition (2026-05-20) — Surface dev-server errors to the inspector overlay

Inspector captured runtime errors only. Dev-infra errors — build failures, app crashes, .env reload failures, port conflicts.

  • 🟠 core/dev.ts captures build/app-crash/dev-uncaught errors into a bounded ring (50 entries, 30s TTL) with a 500ms dedup. Build + app-server stderr piped + tee'd so terminal output is unchanged.
  • 🟠 New event: bosia-error over /__bosia/sse (same wire shape as inspector's ServerError). The SSE handler flushes recent buffered errors to new clients so pre-connect errors stay visible.
  • 🟠 New core/dev-error-page.ts renders the fallback HTML the dev proxy returns when fetch(app) throws on an HTML nav. Embeds the overlay, pre-seeds buffered errors, subscribes to /__bosia/sse reload to swap itself out.
  • 🟡 .env reload failures inside the dev watcher no longer crash the dev parent — caught, logged, and routed through the same buffer so the user sees the validation error in the badge instead of a dead process

Deferred (logged for follow-up)

  • 🟡 pushState(url, state) / replaceState(url, state) for shallow routing
  • 🟡 onNavigate(fn) (runs between beforeNavigate and the actual nav)
  • 🟡 preloadCode(...routes) (preloads route module without data)
  • 🟡 applyAction(result) / deserialize(result) from $app/forms
  • 🟡 disableScrollHandling() for fine-grained scroll control
  • 🟠 Diagnose & fix window.location.href stall on static builds — needs a confirmed repro; safety-net try/catch is in place so the next occurrence surfaces a console error instead of staying on "Loading…"

v0.6.0 — Server response cache (skip-render) ✅ (shipped 2026-05-24)

Before v0.6, every HTML response re-ran metadata(), layout load(), page load(), render(), and Bun.gzipSync() even when byte-identical. The new in-memory cache short-circuits all of it.

  • 🟠 New core/cache.ts — tiny LRU + tagIndex + pathIndex, computeCacheKey(url, req, cookies), serveCached(entry, req) with Accept-Encoding: br|gzip|identity negotiation, buildCompressedVariants() (brotli + gzip), tag/path eviction.
  • 🟠 Renderer integration (renderer.ts) — cache read before metadata/load/render, cache write after chunks are built, streaming preserved on miss. CSP-enabled deploys skip the cache (per-request nonce is incompatible with cached bytes).
  • 🟠 API endpoint integration (server.ts) — +server.ts GET handlers cached with the same key rules. v0.6 invalidates API entries by URL/prefix only (no depends() for API yet).
  • 🟠 Public API — invalidate(key) / invalidateAll(prefix) from bosia mirror the existing browser-side invalidate() semantics. Form actions call them after a write.
  • 🟡 Per-route opt-out — export const cache = false; in +page.ts, +page.server.ts, or +server.ts. Generated $types.d.ts exports a CacheOption type alias for IDE support.
  • 🟡 Env vars — CACHE_KEYS (default session,sid,auth,token,jwt,Authorization) controls identity-hash inputs; CACHE_MAX_ENTRIES (default 500, 0 disables). Documented in guides/environment-variables and the response-cache guide (EN+ID).
  • 🟡 Author guidance — new bosia-response-cache skill walks agents through when to call invalidate() from server code, how to tag loaders with depends(), and when to opt a route out.
  • 🟠 Dev proxy now forces the inner app to Accept-Encoding: identity. Previously it forwarded gzip,br, the inner returned compressed bytes, Bun's fetch() auto-decoded but left Content-Encoding: gzip
  • 🟠 core/cache.ts guards process.env reads — re-exported through the public bosia barrel, it evaluated in the browser bundle and threw ReferenceError: process on hydration in Safari.
  • 🟠 Server-only response-cache exports moved to bosia/servercore/cache.ts still evaluated client-side via the shared barrel. Added ./server to exports, created lib/server.ts, removed them from the shared barrel.
  • 🟡 Inspector dev-error reporter type alignment — devErrorReport.ts declared source?: "server"|... but pushServerError accepted "elysia"|..., failing bun run check (TS2322)

Deferred to v0.7+

  • 🟡 Key-based invalidation for +server.ts endpoints — give API handlers a depends() argument or support export const tags = [...] so invalidate("app:user") evicts API responses too.
  • 🟡 TTL-based expiry — author wants pure-invalidate today, but TTL is useful for "refresh every N seconds" pages.
  • 🟡 Layout-level cache = false cascade — a layout opting out should make its child routes uncached too.
  • 🟡 Multi-replica cache (pub/sub invalidation) — single-replica only in v0.6.
  • 🟡 Soft-purge / stale-while-revalidate.
  • 🟡 Custom key function — export const cache = { key: (req) => string }.

v0.6.5 — Compile-time component-import audit ✅ (shipped 2026-05-27)

A scaffolded app crashed on first SSR render with undefined is not a function: import * as Card + <Card.Root>, but index.ts exports Card/CardContent, not Root. bosia build succeeded silently.

  • 🟠 core/svelteAudit.ts — walks the modern Svelte 5 AST, extracts top-level bindings from <script>, tracks shadowing from {#each}/{#snippet}/{@const}. For namespace imports, Bun.Transpiler.scan() introspects the source's exports.
  • 🟠 core/svelteCompiler.ts — switched compile() to modernAst: true, wired the audit into onLoad, added module-scoped per-file dedupe (Map<absPath, Promise>) so it runs once across the parallel browser+bun targets.
  • 🟠 Promotes select svelte/compiler warnings to errors: component_name_lowercase, bind_invalid_value, invalid_html_attribute — silently-broken cases the user almost never wants to ship.
  • 🟡 resolveImport.ts + sourceLoc.ts — extracted from plugin.ts and inspector/bun-plugin.ts so the audit and the resolver share one alias/tsconfig-paths/relative-path implementation and one lineColFromOffset helper.
  • 🟡 BosiaConfig.strictImports (boolean | { unbound, namespaceMember, warnings }) — per-component opt-out. BOSIA_STRICT_IMPORTS=0 env var downgrades to a console.warn at runtime without failing the build.
  • 🟡 test/svelte-audit.test.ts — 8 fixtures cover the repro (missing namespace export), positives (correct member, named import, each-block shadowing, bare-package skip), and edges (unbound identifier, dotted on default import, env override).
  • 🟡 ConstTag siblings — {@const Foo = ...} now scopes its binding across the whole surrounding fragment, not just its own children. Previously a sibling-bound <DemoComponent /> false-flagged.

v0.6.4 — Combined files demo, CORS-safe ✅ (shipped 2026-05-26)

The crop block's docs demo loaded a remote Unsplash URL with crossorigin="anonymous"; the browser blocked it as CORS and the cropper rendered blank. Replaced the two demos with one: pick a file via UploadArea

  • 🟡 demos/FilesUploadCropDemo.svelte — single combined demo. UploadArea (enableCrop) → on crop, captures the (file, done) pair, opens CropImage against URL.createObjectURL(file), wraps the Blob as a File, calls done(file)
  • 🟡 docs/src/routes/api/demo-upload/+server.ts — tiny POST returning { url, ok } so the demo Upload button doesn't 500.
  • ⚪ Both blocks/files/{crop-image,upload-area}.md frontmatter demo: now points at FilesUploadCropDemo. [...slug]/+page.svelte imports only the new demo; deleted FilesCropImageDemo.svelte and FilesUploadAreaDemo.svelte.
  • 🟡 blocks/files/crop-image/block.svelte — switched the 400px viewport from h-[400px] to style="height: 400px;". The class works for end-users.
  • 🟡 docs/src/app.css — added @source "../../registry/blocks/**/*.{svelte,ts,js}" so utility classes declared inside registry blocks are emitted into bosia-tw.css from the docs build alongside registry/components/ui.
  • 🟡 docs/src/lib/docs/content.tscontentDir/demoFile no longer resolve relative to import.meta.dir (dev bundle 3 levels deep, prod 2), which missed the content dir in dev → every catch-all docs page 404'd.

Same-day addition (2026-05-26) — `file-upload` feature + CLI dialect flags

The files/upload-area block shipped since v0.6.3 but bosia had no server-side counterpart.

  • 🟠 registry/features/file-upload/ — full backend. file.service.ts validates MIME, decodes via Bun.Image, fit-resizes to 1920×1080, re-encodes WebP @0.85, persists via Drizzle. Three dialect table files target one install path.
  • 🟠 cli/feat.ts — per-feature options system. Top-level handles only -y/--local; everything after the feature name flows to resolveFeatureOptions(), parsed against the feature's own meta.json options schema. Unknown flags abort.
  • cli/index.ts — feat subcommand argv handler simplified: first non-flag token is the name, everything else (including pre-name -y) flows to runFeat. Help text updated so feature-specific flags follow the feature name.
  • packages/bosia/src/cli/registry.tsInstallOptions gained featureOptions (resolved values) and featureArgs (raw tokens for the root feature). No CLI-level dialect type — dialect is now file-upload-specific.
  • registry/index.jsonfeatures array gains file-upload.
  • 🟡 docs/content/docs/guides/file-upload.md — install / env / wiring / S3 swap docs; cross-link added from blocks/files/upload-area.md. Nav entry under Guides.
  • bosia-file-upload/SKILL.md — new skill teaching when to install file-upload (avatar/profile/media-library triggers), R1–R5, workflow, anti-patterns.

v0.6.3 — Skills API exposes references ✅ (shipped 2026-05-25)

AI agents fetching /api/skills/<name>.json saw the SKILL.md body but not the companion reference files that carry the actionable detail, so they guessed paths or scraped the site.

  • 🟡 listSkillReferences(name) in docs/src/lib/skills/list.ts — reads <SKILLS_ROOT>/<name>/references/, filters to .md, validates slugs against ^[a-z0-9-]+$, returns { file, path }[] sorted by file. Silent [] on missing dir.
  • 🟡 GET /api/skills/[name] response gained references: SkillReference[] so agents discover the available reference files in one round-trip.
  • 🟡 New route api/skills/[name]/references/[file]/+server.ts — prerendered, entries() enumerates (name, file) pairs, realpath traversal guard mirrors the [name] route. Returns { name, file, path, content } with max-age=60

Same-day addition (2026-05-25) — Files blocks (crop + upload)

Registry had no file-handling blocks. Ported two from a working CMS: an image cropper and a drag-and-drop upload area. Both installable standalone.

  • 🟡 registry/blocks/files/crop-image/ — Svelte 5 cropper wrapping svelte-easy-crop
  • 🟡 blocks/files/upload-area/ — drag-drop + click-to-pick with preview, size validation, XMLHttpRequest progress, Progress bar. Props: uploadUrl (required), accept, maxSizeMB, fieldName, extraFields, headers, enableCrop
  • registry/components/ui/icon/icons.ts — added crop and zoom-in paths (lucide-static).
  • registry/index.jsonblocks array gains files/crop-image and files/upload-area.
  • ⚪ Docs pages docs/content/docs/blocks/files/crop-image.md and upload-area.md; Files group added to docs/src/lib/docs/nav.ts; FilesCropImageDemo and FilesUploadAreaDemo registered in [...slug]/+page.svelte.
  • 🟡 core/build.ts — added conditions: ["svelte"] to both Bun.build calls so Svelte libs (like svelte-easy-crop) resolve to their svelte export. An earlier generic onResolve handler broke shiki's chunked CJS interop.

Same-day addition (2026-05-25) — Clean-architecture skill for generated apps

Bosapi-generated apps put db.select(...) directly in +page.server.ts loaders and skipped a service/repository layer.

  • 🟡 New bosia-clean-architecture skill — eight rules (R1–R8): no db in routes, repository ownership, service-owned validation, derived valibot validators, one entity per feature, cross-feature via service namespace, table home.
  • 🟡 Three companion references — feature-template.md (copy-adapt for all six files + callers), refactor-recipe.md (grep → extract → swap-import using warung-nasi), shared-folder.md (what belongs in features/shared/ and what doesn't).
  • 🟡 bosia-drizzle-feature updated — folder diagram gained *.repository.ts/*.validator.ts/*.dto.ts; R2 split into repository + service with examples; workflow now 9 steps; new anti-patterns and P1 items for the split.
  • 🟡 bosia-drizzle-usage updated — Quick Start rewritten so loaders call CatalogService.summary() not db.select(...); workflow writes the repository first, then service; new red flags for db in routes.
  • 🟡 Catalog docs/content/skills/SKILL.md bumped 38 → 39 skills; bosia-clean-architecture added under framework conventions and into the discovery-order step 2.

v0.5.13 — Inspector component call-site chain ✅ (shipped 2026-05-23)

Alt-clicking a <button> rendered by a shared Button.svelte showed only Button.svelte:5:1

  • 🟠 Compile-time injection of <!--bosia:o=...--> / <!--bosia:c--> markers around Component/SvelteComponent/SvelteSelf nodes in injectLocs
  • 🟠 Runtime collectStack(el) walks DOM ancestors + previous siblings with a depth counter matching each bosia:c to its bosia:o, so siblings don't bleed. Returns outermost-first.
  • 🟡 Tooltip widened with max-width:90vw + ellipsis so long chains don't overflow the viewport.
  • docs/content/docs/guides/inspector.md updated to describe the chain feature and extend the prod-output grep to check for both markers.
  • 🟡 bosia-inspector-edit skill updated for the new payload — parses the Component tree (outer → leaf): … prefix, defaults the target to the outermost call-site, requires a one-sentence justification when the agent picks the leaf instead.

Same-day addition (2026-05-23) — Env + CORS skills for AI agents

Bosapi preview apps (a-<uuid>.lvh.me) surfaced 403 Cross-origin request blocked and the AI kept reaching for CORS env vars.

  • 🟡 New bosia-env skill — four-tier prefix (PUBLIC_STATIC_/PUBLIC_/STATIC_/none), $env virtual module for user vars, process.env for framework-reserved vars.
  • 🟡 New bosia-cors skill — CORS env recipe (CORS_ALLOWED_ORIGINS + methods/headers/credentials/max-age), the Vary: Origin invariant, and a triage table distinguishing a real CORS failure from Bosia's CSRF rejection.
  • 🟡 Catalog SKILL.md updated 35 → 37 skills; both entries added under framework conventions and into discovery-order step 2; cross-references wired both ways and to bosia-security-review/bosia-elysia-routes.

v0.5.11 — `$types` resolution inside `.svelte` files

tsc --noEmit resolves ./$types from .svelte via the rootDirs trick, so check/build type-check correctly. But svelte-language-server doesn't honor rootDirs in its virtual TS document.

Acceptance: in a freshly scaffolded app, hovering PageProps in +page.svelte shows the generated type, autocomplete on params. lists only the route's dynamic segments, and no "module not found" appears for ./$types. Same in Zed and VS Code.

  • 🟠 Investigate options: (a) a TS Language Service plugin hooking moduleResolution for $types from .svelte files; (b) fork/extend svelte-language-server config.
  • 🟠 Ship the plugin/shim from packages/bosia and wire it into the scaffolding templates' tsconfig.json (compilerOptions.plugins or svelte.config.js) so new apps work out of the box.
  • 🟡 Verify in Zed and VS Code on apps/demo/src/routes/(public)/blog/[slug]/+page.svelte: hover shows Params = { slug: string }, autocomplete on params. lists slug, typing params.foo red-squiggles.
  • 🟡 Document the editor setup step in docs/content/docs/guides/routing.md (or a new "Editor setup" guide) — what extension to install, what tsconfig.json looks like.
  • ⚪ Note the limitation + workaround in the meantime under docs/content/docs/reference/sveltekit-differences.md. (Updated 2026-05-24 to reflect shipped features: navigation API, plugin system, response caching)

v0.5.4 — Brief intake skills ✅ (shipped 2026-05-17)

Six new design-track skills that gather product brief (identity / voice / visual / platform) into BRIEF.md at app root before any UI emit. Closes the "agent invents palette + tone every turn" drift bug.

  • 🟠 bosia-brief-intake — orchestrator. Walks the four group skills in order, writes BRIEF.md, chains bosia-brief-review. Auto-trigger surface: empty BRIEF.md.
  • 🟡 bosia-brief-identity — name, tagline, audience, language, formality, self-reference. Locks sapaan + UI string language for the rest of the session.
  • 🟡 bosia-brief-voice — tone adjectives, emoji/exclamation policy, microcopy spine table (5 rows: empty / error / confirm-destructive / success / primary action), domain glossary, copy no-go.
  • 🟡 bosia-brief-visual — palette intent → theme pick decision matrix, shape, density, type, icons, custom marks. Runs bosia_add_theme + --primary/--accent override.
  • 🟡 bosia-brief-platform — form factors, primary surface, ID format regex, number/date Intl formatters, imagery aspect ratios, first-screen scaffold queue, MVP feature list (cap 7).
  • 🟡 bosia-brief-review — quality gate. P0/P1 checks: sections complete, theme installed matches brief, formatter modules scaffolded, sapaan consistent, no emoji leak in product strings, first-screen names resolve to real catalog entries.
  • 🟡 Catalog SKILL.md index updated — 25 → 31, new section "Brief intake — design ✦", discovery order gains step 0 "check BRIEF.md".

Hotfix (same-day, 2026-05-17)

  • 🔴 Fix bosia dev build crash Multiple files share the same output path on apps with multiple style-less +page.svelte routes.

Same-day addition (2026-05-17)

  • 🟡 bosia-frontend-design — new design skill forcing an aesthetic stance (direction/typography/dominant colour + sharp accent/one memorable detail) before any UI emit, avoiding the AI-default look.
  • 🟡 bosia-frontend-design wired into bosia-brief-intake as step 4, so every BRIEF.md ends with a populated ## Aesthetic section. Quick-start opener bumped 5 → 6 questions.
  • 🟡 Stance consumption wired downstream — bosia-design-review gains a P1 check that each emit honors § Aesthetic without re-picking. Six page scaffolds (landing, saas-landing, blog, pricing, mobile-screen.
  • 🟡 bosia-brief-intake ships two reference files: references/quick-start-script.md (6-question opener with palette → direction inference) and references/example-brief.md (Dombaku-style filled BRIEF.md). Frontmatter targets.files updated.

v0.5.3 — API prerender ✅ (shipped 2026-05-16)

Same prerender ergonomics for +server.ts routes as pages already had. Drop the docs-only static-API post-build pipeline.

  • 🟠 Framework: +server.ts honors export const prerender = truedetectPrerenderRoutes scans manifest.apis, dynamic routes call entries(), prerenderApiOutPath() writes one .json per route. Fetched body written verbatim.
  • 🟡 Dev runtime alias: API routes with prerender = true are also served at <path>.json, matching the URL static hosts will serve in prod. Non-prerender routes get no alias (packages/bosia/src/core/server.ts)
  • 🟡 Unit tests for prerenderApiOutPath and substituteParams rest-segment cases (packages/bosia/test/prerender-api.test.ts)
  • 🟡 Docs API routes migrated: /api/skills, /api/skills/[name], /api/components, /api/components/[...path], /api/blocks, /api/blocks/[...path] all opt into framework prerender.
  • 🟡 Removed generateSkillsApi() + generateRegistryApi() from docs/scripts/post-build.ts — post-build returns to sitemap-only

Hotfix (same-day, 2026-05-16)

  • 🔴 Fix dev .json alias resolution: catch-all sibling routes were absorbing the .json suffix into their rest-segment param, causing 4xx in dev.
  • 🔴 Fix /api/skills/<name> JSON shape: was emitting raw SKILL.md markdown into a .json file. Handler now returns Response.json({ name, content }) with frontmatter stripped via gray-matter, matching the v0.5.2 post-build shape
  • 🟡 New packages/bosia/test/apiResolver.test.ts — 10 cases covering flat-route alias, catch-all precedence, [name] precedence, non-prerender fall-through, and module() throw → fallback
  • 🟡 New docs/test/api-prerender.test.ts — post-build sanity over dist/static/api/**/*.json: every artifact parses; list endpoints expose their array.
  • 🟡 Renamed registry detail field mdFilecontent in /api/components/<path> and /api/blocks/<path> responses to match /api/skills/<name> shape (docs/src/lib/registry/list.ts)
  • 🔴 Fix production-build docs crash on every page with code blocks (createHighlighter not a function). Lazy await import("shiki") made Bun's splitter call into the parent before its exports initialized.
  • 🟡 Normalize path on /api/skills, /api/components, /api/blocks index + detail responses to the full detail URL (e.g. /api/components/ui/button.json)

v0.5.2 — CLI ergonomics & registry API ✅ (shipped 2026-05-15)

Multi-component install and AI-discovery parity with skills.

  • 🟠 bosia add accepts multiple component names in one call; new -y/--yes flag auto-confirms overwrite prompt for CI use
  • 🟡 Static /api/components.json + /api/components/{path}.json and /api/blocks.json + /api/blocks/{path}.json emitted by docs/scripts/post-build.ts (superseded in v0.5.3 by the framework prerender)

v0.4.4 — Build CSS collision hotfix ✅ (shipped 2026-05-09)

Republish of 0.4.3 with a missed regression in the Svelte build path fixed.

  • 🔴 Restore app.css → JS no-op resolve in core/plugin.ts. Without it, every dynamic-imported route chunk reaching app.css produces an identical CSS sidecar → Bun fails with "Multiple files share the same output path"
  • 🟡 Regression test packages/bosia/test/svelte-build.test.ts — 12 dummy routes + shared app.css; fails without the no-op, passes with it

v0.4.3 — Request pipeline perf ✅ (shipped 2026-05-09)

Cut redundant work from the per-request hot path.

Done

  • 🟠 Resolve page route once per request and thread through renderSSRStream / renderPageWithFormData / form-action handler
  • 🟡 Cache getPublicDynamicEnv() at module scope
  • 🟠 Linear parent() data merging in layout loaders — O(d²) → O(d) with per-layer snapshot
  • 🟡 Drop redundant onBeforeHandle apiRoutes scan; non-GET catch-alls already cover every method
  • 🟠 Inline Svelte compile, drop bun-plugin-svelte — own .svelte/.svelte.[tj]s onLoad with css: "injected" (browser) / css: "external" (server)

Open

  • 🟠 Truly progressive SSR streamingrenderSSRStream is blocking before first byte (load → render → enqueue). The real blocker is a parallel-aware loader runner that flushes chunks as each loader resolves.
  • 🟡 Reduce safeJsonStringify cost on large loader payloads — done in v0.5.0 by moving page/layout/form data to <script type="application/json"> islands read via JSON.parse(...textContent)

Reference: backup/PERFORM_ISSUES.md (full request-pipeline review, 2026-05-08).


v0.4.5 — Blocks & Themes Registry

Two new registry kinds: Blocks (composed UI sections) and Themes (token sets). Closes the design-quality gap for LLM-generated apps (Bosapi) and hand-coders alike. Primitives stay unchanged.

CLI

  • 🟠 bun x bosia@latest add block <category>/<name> — install a block to src/lib/blocks/<path>/
  • 🟠 bun x bosia@latest add theme <name> — install a theme to src/lib/themes/<name>.css, patch app.css import
  • 🟡 Extend CLI dispatcher (packages/bosia/src/cli/index.ts) for add block/add theme sub-args
  • 🟡 Refactor add.ts — parameterize destination root; RegistryIndex gains blocks: string[], themes: string[]
  • 🟡 block.ts handler — recursive primitive deps via addComponent(), optional font @import merge into app.css
  • 🟡 theme.ts handler — copy tokens.css, swap @import in app.css (one-active-theme), font @import merge

Registry content

  • 🟠 Extend registry/index.json with blocks and themes arrays
  • 🟠 registry/themes/neutral/ — extracted from current apps/demo/src/app.css @theme block
  • 🟠 registry/themes/editorial/ — warm cream palette + Instrument Serif display
  • 🟢 Six existing themes wired into sidebar nav + skill metadata (zinc, stone, claude, ocean, forest, rose) — v0.6.22 (2026-06-10)
  • 🟢 Four new themes added: sunset (warm orange), midnight (indigo dark-first), mono (brutalist), amber (cozy hospitality) — v0.6.22 (2026-06-10)
  • 🟠 registry/blocks/cards/feature-editorial/ — first block; matches Open Design reference (eyebrow numeral, serif title, tight leading, circular CTA)
  • 🟡 Refactor apps/demo/src/app.css to @import "./lib/themes/neutral.css" (visually unchanged)

Docs

  • 🟡 docs/content/docs/blocks/overview.md + per-block pages
  • 🟡 docs/content/docs/themes/overview.md + per-theme pages + creating-themes.md
  • 🟡 CardFeatureEditorialDemo.svelte registered in nav.ts and [...slug]/+page.svelte demos map

v0.5.0 — Full Plugin Lifecycle

Complete the plugin surface; uninstall + virtual modules.

  • 🟠 dev.onStart + dev.onFileChange wired in dev.ts
  • 🟠 client.onHydrate + client.onNavigate in core/client/hydrate.ts + router.svelte.ts
  • 🟠 Virtual modules from plugins — extend core/plugin.ts resolver pattern
  • 🟡 Plugin uninstall via bosia feat
  • 🟡 Docs: full plugin authoring guide

v0.6.0 — E2E Testing & Docs (Phase 5 + 6)

Full browser testing with Playwright + comprehensive test docs.

  • 🟠 startTestServer() — spin up a real Bosia server on a random port for E2E
  • 🟠 bosia test --e2e — auto-launch Playwright with the server
  • 🟡 Playwright config template in bosia create scaffolding
  • 🟡 Test file examples in project templates
  • 🟡 bosia feat test scaffolder for generating test files
  • 🟠 Docs: testing guide for end-user apps using bun test (unit-level; integration/component/E2E pending utilities)

v0.7.0 — CSS Pipeline Overhaul (deferred)

Status (2026-07-04): deferred pending Bun upstream CSS-chunk dedup under splitting: true (issue to file — see 0.8.5). Hashed delivery shipped in 0.8.5. Per-route Tailwind chunks rejected: Tailwind v4 emits one deduplicated utilities file by design; splitting would duplicate shared utilities and ship more bytes. Component styles are already scoped + code-split via css: "injected". Acceptance items below stay open under the upstream dependency.

Problem

  • Tailwind CLI runs separately from Bun build → bundler has no view of CSS module graph
  • Bun's splitting: true emits one CSS sidecar per chunk that imports a shared CSS file → collision when N routes transitively import app.css
  • Current fix (plugin.ts intercepts app.css → empty JS module) ships ALL utilities in one public/bosia-tw.css regardless of which route uses them
  • Doesn't scale: 100+ route apps load every utility on every page; can't lazy-load route-specific CSS; can't tree-shake unused per-route styles

Goals

  • 🟠 CSS module graph dedup — bundler tracks every CSS import, identical content emitted once, referenced by N entries (Vite-style)
  • 🟠 Per-route CSS chunks — each route ships only the CSS it actually uses, loaded via <link> injected at SSR
  • 🟠 Drop app.css no-op interception in core/plugin.ts once dedup lands
  • 🟡 Component <style> blocks: continue with css: "injected" (already scoped + deduped via cssHash)
  • 🟡 Tailwind into bundler hot path — port @tailwindcss/vite shape to Bun plugin API so utilities are scanned + emitted as part of the build, not a parallel CLI step

Approach Options

  1. Wait on Bun upstream — file/track issue for CSS chunk dedup under splitting: true. Lowest effort, unbounded timeline.
  2. Custom Bun plugin — own CSS pipeline in core/cssPipeline.ts: intercept all .css imports, hash contents, emit one shared chunk per unique source, track route → chunk mapping, inject <link> tags via render.head per request.
  3. Static layout import workaround — make root +layout.svelte a static import (not dynamic) in routes.client.ts. Collapses app.css into entry chunk → no per-route duplication. Cheapest fix, but loses dynamic layout chains.

Acceptance

  • Builds with 100+ routes succeed without the app.css no-op
  • Each route ships ≤ what it imports (verified by inspecting dist/client/*.css sizes)
  • Component <style> still scoped via cssHash
  • No regression in test/svelte-build.test.ts (CSS collision regression test)

Not Planned

Intentional omissions — out of scope for the framework:

  • +page.ts / +layout.ts universal load (decided against)
  • Image optimization (infrastructure concern)
  • i18n (user's responsibility)
  • Rate limiting (reverse proxy concern)
  • Adapter system (intentionally tied to Bun + Elysia)
  • Service worker tooling (out of scope)