Skip to content

Release 1.15.0 — graduate all modules to Stable - #161

Merged
JosunLP merged 18 commits into
mainfrom
dev
Jun 30, 2026
Merged

Release 1.15.0 — graduate all modules to Stable#161
JosunLP merged 18 commits into
mainfrom
dev

Conversation

@JosunLP

@JosunLP JosunLP commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

bQuery 1.15.0 graduates the final thirteen modules to Stableview, forms, i18n, a11y, dnd, media, plugin, devtools, testing, storybook, concurrency, ssr, and server. With them, every bQuery module is now Stable and bound by the no-breaking-changes-between-minor-releases contract.

  • User-facing impact: all public module APIs are now frozen surfaces under the Stable contract; the release is fully additive — no breaking changes. New opt-in tools ship for file-based routing, i18n extraction, and view compilation.

Motivation

This is the release PR (dev → main) for 1.15.0, the final step of the stability roadmap: every remaining module reaches the Stable bar — frozen public surface, met exit criteria, and the no-breaking-change guarantee. It bundles the per-module graduation work tracked in #127#150.

Changes included

Module graduations (dev → main delta)

Release-prep changes

  • Version 1.14.2 → 1.15.0
  • Stability matrix flipped to all-Stable across the canonical source (scripts/stability-matrix.mjs) + STABILITY.md + README + docs/introduction.md (single source of truth)
  • CHANGELOG.md: [1.15.0] - 2026-06-29 (+ fresh Unreleased); new docs/release-notes/1.15.md + index row + VitePress sidebar entry
  • 13 module guides reworded "targeting Stable" → "graduated to Stable" (headings, intros, exit-criteria, anchors)
  • AI guidance refreshed: AGENT.md, llms.txt, .clinerules, .cursorrules, .github/copilot-instructions.md

Security hardening (CodeQL fixes on this branch)

  • ReDoS — replaced the \/+$ slash-trimming regexes in src/router/file-routes/path.ts and src/server/file-routes.ts with linear index scans (js/polynomial-redos, high).
  • Prototype pollutionsrc/i18n/extract unflatten() now rejects __proto__ / constructor / prototype segments inline, right before each computed-property assignment (js/prototype-polluting-assignment, medium); added a regression test.
  • Scan scope — added .github/codeql/codeql-config.yml to exclude the never-shipped test tree, clearing two js/xss-through-dom false positives (test helpers feeding trusted renderToString() output into innerHTML).
  • Result: CodeQL gate is green — "No new alerts in code changed by this pull request."

Validation

  • bun run lint — ESLint clean (CI + changed files); tsc --noEmit clean
  • bun run build — lib + umd + types
  • bun test2984 pass, 0 fail
  • Additional focused checks (if applicable): bun run check:stability (21 modules in sync across all surfaces), bun run check:ai-guidance, bun run check:doc-exports, bun run check:full-bundle; CodeQL, CodeFactor, and all CI matrix jobs (node-24 / deno-2 / bun-1.3) green

Checklist

  • I followed the contributing guidelines.
  • I added or updated tests where behavior changed.
  • I updated documentation when public behavior or developer workflow changed.
  • I confirmed the change does not introduce security-sensitive regressions.

Notes for reviewers

A focused review of the new security- and parser-critical code (server crypto / csrf / session / auth, SSR hydration, view compiler, i18n ICU, file-routing) found no critical bugs and a strong security posture: prototype-pollution guards, constant-time comparison, HMAC-signed double-submit CSRF, request-body memoization, JSON XSS escaping, and session-fixation prevention. CodeRabbit's full review surfaced no inline issues.

Known low / informational follow-ups (none release-blocking):

  • createBodyReader's unknown-content-type path (arrayBuffer()) bypasses the configured limits (potential unbounded buffering).
  • The view compiler's numeric-literal scanner can mis-handle pathological inputs (e.g. 1.5.toFixed(2)).
  • [...rest] file routes don't bind a named catch-all param server-side.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Graduated View, Forms, i18n, Router, Server, SSR, DevTools, Testing, Storybook, Concurrency, Media, Plugin, A11y, and DnD to Stable.
    • Added optional file-based routing, i18n extraction + view compilation tooling, progressive-enhanced forms actions/optimistic updates, SSR hydration mismatch detection + resumable boundaries, and a stable DevTools bridge/extension (with v1 protocol).
    • Added CSP-safe module workers and client async-concurrency primitives; expanded server session/CSRF/auth/guards and value-signing helpers.
  • Bug Fixes
    • Improved hydration recovery and a11y audit rule reporting (WCAG mapping).
  • Documentation
    • Added a dedicated STABILITY maturity model; updated guides and 1.15.0 release notes.
  • Tests
    • Added stability-focused and feature coverage suites across modules.

JosunLP and others added 10 commits June 28, 2026 00:52
)

## Problem
The **Greetings** workflow fails on every first-time issue/PR:

```
Error: Input required and not supplied: issue_message
##[warning]Unexpected input(s) 'repo-token', 'issue-message', 'pr-message',
valid inputs are ['issue_message', 'pr_message', 'repo_token']
```

`actions/first-interaction@v3` renamed its inputs to snake_case, but
`.github/workflows/greetings.yml` still passes the old kebab-case names
(`repo-token`, `issue-message`, `pr-message`). The action reads them as
missing and aborts before posting the greeting.

## Fix
Rename the three inputs to the v3 names: `repo_token`, `issue_message`,
`pr_message`. Message content is unchanged.

Surfaced by the Greetings run on #151.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <[email protected]>
…hydration, resumable boundaries (#127#130) (#151)

Collected implementation of all four open **`ssr`** tickets, on one
branch, targeting `dev`.

Closes #128 · Closes #129 · Closes #130 · Advances #127 (tracking)

## What & why
`ssr` is the last foundational module marked **Experimental**. This PR
resolves the three substantive prerequisites for promotion and publishes
the stability contract.

### #128 — Interactive directive parity (`bq-model` / `bq-on`)
- `RenderOptions.directives: 'full' | 'static'` (default **`'static'`**
— byte-for-byte backwards compatible).
- `'full'` renders `bq-model` initial state (`value` / `checked` /
selected `<option>` / `<textarea>` body) and emits a `data-bq-on`
hydration marker for `bq-on:*`. **Handlers are never executed on the
server**; inline `on*` attrs / `javascript:` URLs are still stripped.
- `RenderOptions.onUnsupportedDirective: 'warn' | 'throw' | 'ignore'`
(default **`'ignore'`**) — formalizes the "static-render subset" into an
explicit, enforced, documented boundary (option **b** of the ticket)
while parity (option **a**) is delivered via `'full'`.
- New shared `src/ssr/directive-support.ts` keeps the **pure
(DOM-free)** and **DOM** backends in lock-step; options also flow
through `renderToStringAsync` / `renderToStream` /
`renderToStreamSuspense` / `renderToResponse`.

### #130 — Guaranteed hydration correctness
- `hydrate(selector, context, { onMismatch, onError })` —
boundary-scoped recovery: `warn` (dev default) · `repair` (rewrite the
boundary from client state) · `error` (route to `onError`, else throw
before mounting).
- `detectHydrationMismatches(root, context)` — content-level diff over
`bq-text` / `bq-show` / `bq-bind:*` / `bq-model` plus the structural
`data-bq-h` signature, using the CSP-safe evaluator. Skips expressions
whose root identifier is absent from the context, so `bq-for` loop
variables never false-positive.

### #129 — True resumability model (resume, not replay)
- `createResumableBoundary` / `createResumableGraph` (server) serialize
**signals** (values), **handlers** (ids only — no code), and **store**
slices.
- `resume()` (client) seeds existing signals, wires handlers by id from
a caller-supplied registry (**no `eval`**), and rehydrates store slices
in place — pairs with island hydration. Opt-in, tree-shakeable,
prototype-pollution-filtered, `<script>`-escaped.

### #127 — Stability (tracking)
- SSR guide gains a **Stability** section: exit-criteria checklist (3/3
prerequisites resolved here; the freeze-for-one-minor item remains open
by definition), **frozen surface** list, and a **per-runtime support
matrix**. `introduction.md` notes the **1.15.0** target. Public exports
are now frozen for the cycle.

## New public API
Runtime: `hydrate`, `detectHydrationMismatches`,
`createResumableBoundary`, `createResumableGraph`, `resume`,
`SSR_ON_MARKER_ATTR`, `RESUMABLE_BOUNDARY_ATTR`,
`RESUMABLE_HANDLER_ATTR`, `RESUMABLE_EVENT_ATTR` — all re-exported from
`src/full.ts` and documented in `docs/guide/ssr.md`.

## Verification
- `bun test` — **2743 pass / 0 fail** (incl. new
`tests/ssr-stable.test.ts`, both backends).
- `bun run test:types` + `bun run lint:types` (tsc src + tests) — clean.
- `bunx eslint .` — clean.
- `bun run build` — succeeds; `check:full-bundle` in sync; `ssr`
doc-export coverage 54/54 (100%).
- Cross-runtime smoke (`tests/cross-runtime/run.mjs`) extended for
`'full'` mode + resumable boundaries — **13/13 on Node 24 & Bun**.

## Compatibility
Defaults preserve prior output exactly (no
`value`/`checked`/`data-bq-on`, no warnings). All additions are
optional. No version bump (release handled separately).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <[email protected]>
…able (#131, #132) (#153)

Collected work for the two open `server` tickets, landed in one branch
and targeting `dev`.

Closes #131. Closes #132.

## #132 — session, auth, and middleware primitives
- **`session()` + `memoryStore()`** — HMAC-signed session-id cookie (Web
Crypto, cross-runtime), payload in a pluggable `SessionStore` (in-memory
default; bring-your-own Redis/DB **without bundling a client**).
`ctx.session` is a Proxy: payload via plain props, lifecycle via `$id` /
`$isNew` / `$data` / `$regenerate` / `$destroy` / `$clear`. Includes
secret rotation, rolling sessions, prototype-pollution filtering, and
session-fixation defense.
- **`csrf()` + `csrfToken()`** — OWASP double-submit cookie (signed when
a `secret` is supplied); token via `x-csrf-token` header or `_csrf` body
field. Composes with the `security` module (integrity vs. output).
- **`guard()`** — predicate route guard mirroring the router's guard
ergonomics.
- **`basicAuth()` / `bearerAuth()`** — `Authorization` parsing with a
`verify()` hook; resolved user on `ctx.state`.
- **Signing utilities** — `signValue` / `unsignValue` /
`timingSafeEqual` / `randomToken` / `randomId` / `base64Url*`, all on
`globalThis.crypto.subtle` (no `node:crypto`).
- Shared cookie helpers extracted to `src/server/cookies.ts` (+
`appendSetCookie`); no behavioral change to existing `ctx.setCookie`.

## #131 — promote `server` toward Stable
- Guide **Stability** section: exit-criteria checklist, frozen
`ctx`/`app` surface, per-runtime support matrix; intro + README notes;
version history.
- `app.listen()` now supports **Deno** via `Deno.serve` (Node/Bun/Deno
covered).
- `ctx.session` is **additive/optional** — no breaking changes to the
1.14 surface.
- New exports wired through `server/index.ts` and the `/full` bundle.

## Zero-dependency & secure-by-default
No runtime dependencies added; each primitive is independently
importable/tree-shakeable. Sessions default to `httpOnly` +
`SameSite=Lax`; signing is HMAC-SHA-256; comparisons are constant-time.

## Verification
- Full suite **2785 pass / 0 fail** (42 new in
`tests/server-stable.test.ts`: sessions, CSRF, guards, auth, crypto —
incl. secret rotation, rolling, `$destroy`-revive, immutable-response
cookies).
- Cross-runtime smoke extended for server (sign/verify, session, CSRF) —
passes on **Node + Bun** locally; **Deno** in CI.
- `tsc`, `eslint`, `bun run build`, `check:full-bundle` (in sync),
`check:doc-exports` (server 23/23) all green.

## Review
Ran an adversarial multi-agent review of the diff; applied the confirmed
hardening: don't cache a rejected HMAC key-import, guard
`timingSafeEqual` against empty input, revive a session written-to after
`$destroy()`, simplify the Deno `listen()` address resolution, and
strengthened tests/docs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <[email protected]>
…+ graduate toward Stable (#133, #134, #135) (#154)

Collected work for the three open `concurrency` tickets, landed in one
branch and targeting `dev`.

Closes #133. Closes #134. Closes #135.

## #134 — CSP-safe module workers (remove the mandatory `'unsafe-eval'`)
- **`defineWorker()` / `defineRpcWorker()`** describe a pre-bundled
worker module by URL on the main thread; **`exposeTask()` /
`exposeRpc()`** wire that module up to the bQuery worker protocol inside
the worker. Because the body is a real module loaded by URL — never a
function revived with `new Function(...)` — module mode runs under a
strict CSP with **no `'unsafe-eval'`** and no `blob:` worker source.
- Every factory now accepts **either** an inline handler (dynamic mode,
opt-in, needs `'unsafe-eval'`) **or** a `WorkerModule` (module mode,
default/recommended): `runTask`, `createTaskWorker`, `createTaskPool`,
`createRpcWorker`, `createRpcPool`, `callWorkerMethod`, and the reactive
wrappers — additive and non-breaking via the `WorkerTaskSource` /
`WorkerRpcSource` unions.
- `isWorkerModule()`, `isModuleWorkerSupported()`, and
`support.moduleWorker` (module mode requires only the `Worker`
constructor, not `Blob`/`URL.createObjectURL`).

## #135 — Client async-concurrency primitives (UI scheduling)
- **`suspense()`** — declarative async boundary aggregating promises and
reactive async states (`useAsyncData`/`useResource`) into reactive
`pending` / `settled` / `error` signals; `retrigger` option;
`dispose()`.
- **`startTransition()`** — `[isPending, start]`; `start(scope)` flips
pending immediately then runs the scope on a low-priority schedule
inside a `batch`, decoupling expensive updates from urgent input. A
throwing scope is contained and reported (mirrors `effect()`), never
escapes as an uncaught timer error.
- **`deferred()`** — readonly signal that lags its source and coalesces
rapid changes to throttle expensive derived UI.
- Built on signals, zero-dependency, tree-shakeable; a distinct concern
from worker concurrency. Pairs with SSR suspense streaming
(`renderToStreamSuspense`/`defer`).

## #133 — promote `concurrency` toward Stable
- Guide **Stability** section: exit-criteria checklist, frozen surface,
runtime boundary (browser-focused per non-goals), and a per-environment
support matrix; intro + README notes; version history.
- Serializable-handler constraint documented + enforced (dynamic mode)
with a clear `TaskWorkerSerializationError`; module mode is exempt by
design.

## Zero-dependency & secure-by-default
No runtime dependencies added; each primitive is independently
importable/tree-shakeable. Module mode is the documented CSP-clean
default, resolving the `'unsafe-eval'` requirement most at odds with
bQuery's security posture.

## Verification
- Full suite **2811 pass / 0 fail** (26 new in
`tests/concurrency-stable.test.ts`: module task/RPC/pool execution with
no blob/eval available, `exposeTask`/`exposeRpc` protocol, support
detection, and the client primitives incl. throwing-scope containment,
deferred coalescing, suspense promise/state aggregation).
- `tsc`, `eslint`, `bun run build`, `check:full-bundle` (in sync),
`check:doc-exports` (concurrency **37/37**), and `test:types` all green.

## Review
Ran an adversarial multi-agent review of the diff; applied the confirmed
hardening: contain a throwing `startTransition` scope and report it via
`console.error` instead of letting it escape as an uncaught timer/idle
exception (mirrors `effect()`), with a regression test.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <[email protected]>
…ptional compiler (#136, #137, #138) (#155)

Collected PR for all three open `view` tickets, implemented on one
branch.

Closes #136
Closes #137
Closes #138

## #136 — Promote `view` to Stable (freeze the directive contract)

- **`bq-for` duplicate-key edge case resolved.** Colliding `:key` values
fall back to a deterministic, referentially-stable composite key, so
duplicate rows reuse their DOM across re-renders instead of being
recreated. The duplicate-key warning is now **dev-only** and emitted
**once per offending key** instead of on every reactive update.
- **Object-expression shorthand resolved.** `bq-class="{ active }"` (and
`bq-style` / `bq-aria` object syntax) now behaves like JS object
shorthand (`{ active: active }`) instead of silently dropping the
property.
- **Directive contract frozen** and a **per-directive SSR support
matrix** published in the View guide. README, introduction, the module
header, and the directive reference are updated to "targeting Stable in
1.15.0".

## #137 — Declarative enter/leave/move transitions

A thin declarative layer over the existing `motion` engine — no second
animation engine.

| Attribute | Applies to | Purpose |
|---|---|---|
| `bq-transition` / `bq-in` / `bq-out` | `bq-if` / `bq-show` / `bq-for`
| Named-preset enter/leave |
| `bq-transition-duration` / `bq-transition-easing` | same | Timing |
| `bq-animate="flip"` | `bq-for` | FLIP move on reorder |

Presets: `fade`, `scale`, `slide`, `slide-up/-down/-left/-right`.
Behaviour: no animation on first paint; `bq-if`/`bq-for` defer removal
until the leave finishes; rapid toggles are race-safe; and
**`prefers-reduced-motion` is honoured everywhere, including the FLIP
path**.

## #138 — Optional compiled-template path

New opt-in entry **`@bquery/bquery/view/compiler`** that pre-parses
`bq-*` expressions at build time into optimized, **`with`-free** update
functions (CSP-safe, no `new Function()` on the hot path).

- `compileViews()`, `compileToModule()`, `compileExpression()`,
`emitModule()` — small transforms usable from any bundler.
- Dependency-free CLI: `bquery-view-compile [options] <file...>`
(`runCompileCli` / `compileFiles`).
- Runtime hooks `registerCompiledExpressions()` /
`clearCompiledExpressions()` (from `@bquery/bquery/view`). The **runtime
evaluator stays the default**; expressions the compiler can't statically
handle fall back transparently, so both paths are behaviourally
identical. Skipped expressions are reported in `stats.skipped` — nothing
is silently dropped.

## Quality

- **42 new tests** across `tests/view-stable.test.ts`,
`tests/view-transitions.test.ts`, `tests/view-compiler.test.ts`
(compiler tests eval the emitted source to assert semantic identity).
- Full suite **2854 pass / 0 fail**; `tsc --noEmit`, `eslint`, and
`build:lib` / `build:types` / `build:umd` all green; `check-full-bundle`
in sync.
- An adversarial multi-agent review of the diff was run; three confirmed
findings were fixed before this PR: FLIP now honours reduced motion,
`bq-if`/`bq-show` cancel an in-flight leave before restarting, and the
SSR matrix's `bq-html-safe` row was corrected (it is client-only).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <[email protected]>
…ctions + optimistic updates (#139, #140) (#156)

Collected PR for both open `forms` tickets, implemented on one branch.

Closes #139
Closes #140

## #139 — Promote `forms` to Stable (freeze the 1.13 surface)

The `forms` surface expanded materially in 1.13.0 and has documented
sharp edges. This freezes the surface for one minor cycle and settles
those edges into **guaranteed, tested contracts**:

- **`validationStrategy` default reviewed + documented.** The default
stays `'manual'` (the least-surprising choice for "validate on submit").
The contract is now explicit: **`handleSubmit()` always runs the full
validation pass**, regardless of strategy — `validationStrategy` only
gates *automatic* per-change/per-blur validation. Behaviour is
unchanged; the surprise is removed by documentation.
- **SSR serialization boundary is now guaranteed.**
`serializeFormState()` deterministically drops functions, `File` /
`Blob` / `FileList`, `bigint`, and `symbol` via an explicit replacer,
instead of relying on incidental `JSON.stringify` behaviour. Re-attach
blobs on the client after hydration.
- **`createFieldArray()` stable-key contract validated with clear
errors.** New optional `getKey` enforces present, unique keys on every
structural mutation and throws a descriptive error naming the offending
key (e.g. *"requires stable, unique item keys, but getKey returned \"a\"
for both index 0 and index 1"*). Adds `keys()` / `keyAt(index)`. Without
`getKey` the array stays positional — **no behaviour change**.
- **Surface frozen + documented.** New Stability section in the [Forms
guide](docs/guide/forms.md) with the exit-criteria checklist and
frozen-surface reference; module header and stability matrices updated
to "targeting Stable in 1.15.0".

## #140 — Progressive-enhancement form actions + optimistic updates

The headline React-19-parity feature: forms that work before/without JS
and reconcile optimistically.

```ts
import { formAction, useFormStatus, optimistic } from '@bquery/bquery/forms';

const submit = formAction('/todos', { method: 'POST', csrf: () => csrfToken });
const { pending } = useFormStatus(submit);
const list = optimistic(todos, (cur, draft) => [...cur, draft]);

submit.enhance(document.querySelector('form')!); // native POST without JS; fetch-enhanced with JS
```

- **`formAction(target, options)`** → reactive `pending` / `error` /
`result` / `submitCount` / `submittedAt`; `enhance(form)` sets the
native `action`/`method` (+ optional hidden CSRF field) for the no-JS
path, then intercepts `submit` for a fetch-based, optimistic-aware
submit; programmatic `submit(formData)`; `reset()`. `target` is an
endpoint URL **or** a function. Non-OK responses throw
**`FormActionError`** (carrying `status` / `response`). Composes with
the validation pipeline and the `server` module's `csrf()` (#132).
Native forms only support GET/POST, so PUT/PATCH/DELETE degrade to a
native POST (the enhanced fetch keeps the real verb).
- **`useFormStatus(action)`** → read-only `readonly()` views of the
action's signals, mirroring React 19's `useFormStatus`.
- **`optimistic(base, reducer)`** → an optimistic-update primitive whose
reactive `value` folds pending drafts over the base and reverts
automatically. `add(draft)` → handle with `remove()`; `run(draft, task)`
applies the overlay around an async task; `pending` / `drafts` reactive;
`clear()`.

## Quality

- **28 new tests** across `tests/forms-stable.test.ts` (field-array key
contract, SSR boundary, validationStrategy timing) and
`tests/forms-actions.test.ts` (formAction function + string targets,
CSRF, enhance/PE, optimistic composition, useFormStatus).
- Full suite **2882 pass / 0 fail**; `tsc --noEmit`, `eslint`,
`check-full-bundle` (in sync), `check-doc-exports` (forms **46/46**),
and `build:lib` / `build:types` / `build:umd` all green.
- New public exports (`formAction`, `useFormStatus`, `optimistic`,
`FormActionError`, `FieldArrayKeyFn` + action/optimistic types) are
wired into `src/full.ts` and documented in the guide.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <[email protected]>
…ork (#159)

## Problem

Vite's library build bundled `node:*` imports into its
**browser-external stub** (an empty default export). That left `await
import('node:fs/promises')` in the optional CLI entry resolving to
`undefined`, so the published **`bquery-view-compile`** CLI crashed at
runtime with `e is not a function` when run against the `dist` build.

## Fix

Mark `node:*` as external in `rollupOptions` so those imports are
emitted verbatim and resolved by Node at runtime:

```ts
rollupOptions: {
  external: (id) => id.startsWith('node:'),
  output: { ... },
}
```

Browser entries never import `node:*`, so this is a no-op for them.

## Verification

- Rebuilt `dist`; the `vite-browser-external` stub is **gone** from
`dist/view-compiler.es.mjs` (0 references, was 1).
- `bquery-view-compile --out-dir … file.html` runs end-to-end against
the built bundle (`1/1 compiled`).

## Note

This is the standalone hotfix for `dev`. The same one-line change is
also present in the stable-graduation PR #157 (which introduces the
`bquery-i18n` CLI that surfaced the bug); since the change is
byte-identical, the branches reconcile cleanly when both land — or the
line can be dropped from #157 once this merges.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <[email protected]>
…144) (#157)

Collected PR for the next four open stabilization tickets, implemented
on one branch.

Closes #141
Closes #142
Closes #143
Closes #144

## #141 — i18n toward Stable: ICU MessageFormat + message extraction

- **ICU MessageFormat** routed through a locale-aware formatter backed
by `Intl.PluralRules`: `plural`, `selectordinal`, `select`, nested
arguments, `offset:`, exact `=N` selectors, the `#` token, and
apostrophe escaping. ICU is **detected automatically**; plain `{name}`
interpolation and the legacy `singular | plural` pipe form keep the
untouched fast path.
- New authoring helpers **`defineMessages()`** (identity + extraction
anchor) and **`formatMessage()`** (standalone single-message formatter).
- Optional, dependency-free **message-extraction tooling** at the new
`@bquery/bquery/i18n/extract` entry + **`bquery-i18n` CLI**: scans
`defineMessages` catalogs and `t()`/`tc()` calls, merges into nested
JSON catalogs **without overwriting translations** (`--prune` opt-in).
Zero-build runtime path preserved.
- i18n guide gains a Stability section: frozen surface, **ICU coverage
table** (with documented non-coverage — inline `number`/`date`
skeletons, rich-text), extraction, and lazy-loading. Tests cover ICU +
extraction + CLI.

## #142 — a11y toward Stable: documented audit scope

- Every `AuditFinding` now carries a **`wcag`** criterion; the full rule
catalog is exported as **`auditRules`** (rule → WCAG → severity → *what
it cannot detect*).
- a11y guide gains the audit-scope table with **known limitations**
(contrast, focus order, reading order, motion, meaningfulness,
cross-root refs) and a frozen surface reference. Tests cover audit
scope, focus trapping, live regions, `inert`/`scrollLock`, and
preference signals.

## #143 — dnd toward Stable: keyboard accessibility statement

- Frozen surface + **accessibility statement**: the keyboard model (pick
up / move / drop / cancel, `aria-grabbed`) and confirmation that drag
announcements route through the **shared a11y live-region announcer**
(no second channel).
- Tests cover keyboard pick/move/drop/cancel, `aria-grabbed`
transitions, and the `grid`/`delay`/`viewport` option surface.

## #144 — media toward Stable: SSR-safe defaults + cleanup
(bake-and-verify)

- **Per-composable SSR fallback** + cleanup contract documented; frozen
surface. No new features.
- Tests cover SSR-safe defaults, idempotent `destroy()`, **listener
detachment (no leaks)**, and `AbortSignal` teardown.

## Shared / build

- Externalize `node:*` in the Vite lib build so the CLI entries emit
real `import('node:fs/promises')` instead of Vite's browser-external
stub — this also **fixes the previously-shipped `bquery-view-compile`
CLI** in `dist`.
- README stability matrix, `introduction.md` stability paragraphs,
CHANGELOG, `src/full.ts` exports, and `package.json` (`./i18n/extract`
export + `bquery-i18n` bin) updated.

## Verification

- `bun test` — **2929 pass / 0 fail** (47 new tests across 5 files).
- `tsc --noEmit` clean; `check:doc-exports` 100% for
i18n/a11y/dnd/media; `check:full-bundle` and `check:ai-guidance` pass.
- `bquery-i18n extract "src/**/*.ts" --out en.json` smoke-tested
end-to-end against the built `dist`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Jonas Pfalzgraf <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
…y matrix (#149, #150) (#160)

Works through all open GitHub issues (#149, #150) in one branch.

Closes #149
Closes #150

## #149 — File-based routing with typed loaders and actions

An **opt-in, bundler-agnostic** file-route convention that bridges
`router`, `ssr`, and `server`. Programmatic routing (`createRouter`)
stays fully supported and unchanged; **no bundler is shipped** — the
input is a manifest (a bundler glob such as `import.meta.glob`, or a
hand-written map for zero-build).

**`router`**
- `createFileRoutes(manifest, options?)` → `{ routes, entries }` — plain
`RouteDefinition`s `createRouter()` already consumes.
- `parseFilePath` / `filePathToRoutePattern` —
`routes/users/[id]/+page.ts` → `/users/:id`, `[...rest]` → `*`,
`(group)` dropped; flat Next/Nuxt-style files supported.
- `sortEntriesBySpecificity` — static beats dynamic, catch-all sorts
last (correct first-match resolution).
- Typed `Load` / `Action` contracts; `createRouteData(router)` /
`useRouteData()` run `load` on client navigation with stale-response
guarding.

**`ssr`** — the router bridge now recognises `meta.load` (adapting
`SSRContext`) alongside the legacy `meta.loader`, so loaders run on the
server before render via `createSSRRouterContext`.

**`server`** — `mountFileRoutes(app, entries, options?)` /
`createFileRouteServerRoutes(entries, options?)` expose each route's
`action` over HTTP (eager no-action routes are skipped; lazy ones reply
`405`), with an optional JSON loader endpoint under `dataPath`. Composes
with `csrf()`.

Docs: new [File-based Routing guide](docs/guide/file-routing.md) +
sections in `router.md` / `server.md` + sidebar entry.

## #150 — Reconcile version history + per-module stability changelog

The 1.11.0 date conflict and the version drift were already resolved in
earlier PRs; this finishes the issue:

- **Single source of truth:** `scripts/stability-matrix.mjs` (canonical
data) + `STABILITY.md` (human-facing matrix **with per-module status
history**).
- The README "Modules at a glance" table and the docs `introduction.md`
matrix now reference it and are **validated against it** by
`scripts/check-stability-matrix.mjs` (`bun run check:stability`) — so
the three surfaces can no longer silently drift.
- CHANGELOG `[Unreleased]` gains a **"Module status"** section (per the
issue's proposed shape); `release-notes/index.md` reconciled to list
**1.14.2**; release-process docs document the new gate.
- Enforced in CI via `tests/check-stability-matrix.test.ts` (runs in
`bun test`, mirroring how `check:full-bundle` is enforced).

## Validation

All green locally:

- `bun run lint` · `bun run lint:types` · `bun run test:types`
- `bun run build`
- `bun test` — **2984 pass / 0 fail**
- `bun run check:full-bundle` · `bun run check:ai-guidance` · `bun run
check:stability`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <[email protected]>
Bump 1.14.2 → 1.15.0 and graduate view, forms, i18n, a11y, dnd, media,
plugin, devtools, testing, storybook, concurrency, ssr, and server to
Stable. Every bQuery module is now Stable; all changes are additive with
no breaking changes.

- package.json: 1.14.2 → 1.15.0
- scripts/stability-matrix.mjs: 13 modules → Stable (canonical source)
- STABILITY.md / README.md / docs/introduction.md: matrix surfaces reconciled
- CHANGELOG.md: Unreleased → [1.15.0] - 2026-06-29 (+ fresh Unreleased)
- docs/release-notes/1.15.md (new) + index row + VitePress sidebar entry
- 13 module guides: "targeting Stable" → "graduated to Stable" (+ anchors)
- AI guidance (AGENT.md, llms.txt, .clinerules, .cursorrules,
  .github/copilot-instructions.md): version + 1.15.0 highlights
- tests/check-stability-matrix.test.ts: assert the graduation outcome

Verified: check:stability, check:ai-guidance, check:doc-exports,
check:full-bundle, bun test (2984 pass, 0 fail), build (lib + umd + types).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@github-actions github-actions Bot added router Changes to the router module view Changes to the view module docs Changes to the documentation tests Chenges to the tests build Changes to the build and meta files ci/cd Changes to the action workflows github Changes to the github meta data files forms Changes to the forms module i18n Changes to the i18n module a11y Changes to the a11y module dnd Changes to the dnd module media Changes to the media module plugin Changes to the plugin module devtools Changes to the devtools module testing Changes to the testing module ssr Changes to the ssr module storybook Changes to the storybook module concurrency server labels Jun 29, 2026
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 91254867-f17d-4aff-878e-223034a6c467

📥 Commits

Reviewing files that changed from the base of the PR and between 3e740f2 and 2a13f1b.

📒 Files selected for processing (22)
  • extension/panel.js
  • src/a11y/audit.ts
  • src/concurrency/scheduling.ts
  • src/devtools/bridge.ts
  • src/forms/action.ts
  • src/forms/ssr.ts
  • src/i18n/extract/cli.ts
  • src/i18n/icu.ts
  • src/i18n/translate.ts
  • src/server/cookies.ts
  • src/server/create-server.ts
  • src/server/csrf.ts
  • src/server/types.ts
  • src/ssr/html-parser.ts
  • src/ssr/renderer.ts
  • src/view/compiler/expression.ts
  • src/view/directives/for.ts
  • src/view/directives/if.ts
  • src/view/directives/show.ts
  • src/view/evaluate.ts
  • src/view/process.ts
  • tests/repro-readd.test.ts

📝 Walkthrough

Walkthrough

bQuery v1.15.0 graduates all 13 modules to Stable and adds new APIs for view transitions/compiler, forms actions and optimistic updates, i18n ICU/extraction, concurrency module workers/scheduling, server sessions/CSRF/auth/file routes, SSR hydration/resumability, and a versioned DevTools bridge. Documentation, tests, and stability checks are updated accordingly.

Changes

v1.15.0 Feature Release

Layer / File(s) Summary
Release metadata, stability matrix, and package wiring
package.json, vite.config.ts, bin/*, scripts/*, STABILITY.md, README.md, CHANGELOG.md, AGENT.md, llms.txt, docs/introduction.md, docs/release-notes/*, docs/.vitepress/config.ts, docs/contributing/release-process.md, .clinerules, .cursorrules, .github/copilot-instructions.md
Version, exports, CLI bins, build entries, stability matrix data, drift-check tooling, and release/stability docs are updated for 1.15.0.
View, forms, and i18n runtime APIs
src/view/*, src/forms/*, src/i18n/*
View transitions/compiler hooks, forms progressive-enhancement actions and stable-key arrays, and i18n ICU/extraction helpers are implemented and re-exported.
Concurrency, server, router, and SSR runtime APIs
src/concurrency/*, src/server/*, src/router/*, src/ssr/*
CSP-safe worker descriptors and scheduling primitives, server crypto/session/CSRF/auth/file-route support, file-based router helpers, and SSR directive/hydration/resumability APIs are added and threaded through their barrels and render paths.
DevTools, small stable surfaces, and full-bundle re-exports
src/devtools/*, extension/*, src/a11y/*, src/plugin/*, src/dnd/*, src/media/*, src/storybook/*, src/testing/*, src/full.ts
The versioned DevTools bridge and reference extension are added; A11y/plugin/misc stable-surface docs are updated; and the full bundle re-exports the new APIs from all affected modules.
Stable promotion tests and smoke coverage
tests/*
New tests cover the stable surfaces for each module family, the stability matrix checker, the DevTools bridge, cross-runtime SSR/server smoke paths, and the view compiler/transitions regressions.

CI workflow input rename

Layer / File(s) Summary
Greetings workflow input key rename
.github/workflows/greetings.yml
repo-token, issue-message, and pr-message are renamed to repo_token, issue_message, and pr_message in the first-interaction workflow step.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 1.15.0, a carrot crown!
Stable sprouts pop up all around.
View hops high, forms bounce bright,
i18n sings in ICU light.
No breaking twigs—just fresh new cheer,
bQuery bounces into the year.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: releasing 1.15.0 and graduating modules to Stable.
Description check ✅ Passed All required template sections are present and filled with relevant release details, validation, checklist items, and reviewer notes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/router/file-routes/path.ts Fixed
Comment thread src/server/file-routes.ts Fixed
Comment thread tests/ssr-stable.test.ts Fixed
Comment thread tests/ssr-stable.test.ts Fixed
Comment thread src/i18n/extract/extract.ts Fixed
Comment thread src/i18n/extract/extract.ts Fixed
Address every alert flagged by github-advanced-security on this PR:

- router/server file-routes: replace the `\/+$` slash-trimming regexes
  with linear index scans. The unanchored trailing-slash quantifier
  backtracks super-linearly on repeated `/` (js/polynomial-redos, high).
- i18n unflatten(): drop `__proto__` / `constructor` / `prototype` key
  segments before any nested assignment so a malicious scanned source
  file can no longer reach Object.prototype
  (js/prototype-polluting-assignment, medium).
- codeql: scope analysis to shipped source via a config-file that ignores
  the test tree. The two js/xss-through-dom hits are test helpers feeding
  trusted renderToString() output into innerHTML to build fixtures — a
  false positive for code that never sees attacker input.
- tests: add a regression test for the unflatten prototype-pollution guard.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Comment thread src/i18n/extract/extract.ts Fixed
…lears it

The previous `parts.some(...)` pre-check left js/prototype-polluting-assignment
open: CodeQL's barrier needs the `__proto__`/`constructor`/`prototype` guard to
dominate the exact computed-property assignment. Check each segment inline,
right before it is written, and bail on the key otherwise.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Comment thread src/i18n/extract/extract.ts Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/i18n/translate.ts (1)

140-152: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the fallback locale when formatting fallback ICU messages.

When template comes from fallbackMessages, formatICU() still receives the active locale. That can select plural categories for the wrong language against the fallback catalog.

Suggested fix
 export const translate = (
   messages: LocaleMessages | undefined,
   key: string,
   params: TranslateParams,
   locale: string,
-  fallbackMessages?: LocaleMessages
+  fallbackMessages?: LocaleMessages,
+  fallbackLocale = locale
 ): string => {
   let template: string | undefined;
+  let templateLocale = locale;
 
   // Try current locale
   if (messages) {
     template = resolveKey(messages, key);
   }
 
   // Fallback locale
   if (template === undefined && fallbackMessages) {
     template = resolveKey(fallbackMessages, key);
+    if (template !== undefined) {
+      templateLocale = fallbackLocale;
+    }
   }
 
   // Key not found — return key as-is
   if (template === undefined) {
     return key;
   }
 
   // ICU MessageFormat (plural / selectordinal / select) — locale-aware path.
   if (isICUMessage(template)) {
-    return formatICU(template, params, locale);
+    return formatICU(template, params, templateLocale);
   }

Also update the createI18n() call sites to pass fallbackLocale as the new final argument.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/i18n/translate.ts` around lines 140 - 152, When `translate()` resolves
`template` from `fallbackMessages`, it still passes the active `locale` into
`formatICU()`, which can apply the wrong plural rules for fallback ICU strings.
Update `translate()` to track when the template comes from the fallback catalog
and pass `fallbackLocale` to `formatICU()` in that case, while keeping the
active locale for primary messages. Also update the `createI18n()` call sites so
they provide `fallbackLocale` as the new final argument.
src/forms/field-array.ts (1)

157-162: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Build the replacement array before tearing down the current one.

buildInitial() now throws on duplicate/invalid stable keys. Because reset() destroys the current items first, that throw leaves the field array holding already-destroyed fields. Build and validate the replacement array first, then swap and dispose the previous items.

Suggested fix
   const reset = (): void => {
+    const next = buildInitial();
+    const current = items.peek();
+    items.value = next;
+    for (const item of current) {
+      destroyItem(item);
+    }
-    for (const item of items.peek()) {
-      destroyItem(item);
-    }
-    items.value = buildInitial();
     error.value = '';
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/forms/field-array.ts` around lines 157 - 162, The reset flow in
field-array.ts currently destroys existing items before calling buildInitial(),
so if buildInitial() throws on duplicate or invalid stable keys the array is
left in a partially torn-down state. Update reset() to call buildInitial() and
validate the replacement array first, then swap items.value to the new array and
only after that iterate over the previous items and destroyItem() them; keep the
error.value cleanup as part of the successful reset path.
🟠 Major comments (28)
scripts/stability-matrix.mjs-3-13 (1)

3-13: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Stop maintaining a second canonical stability source.

This file now hard-codes the matrix and calls itself canonical, but STABILITY.md and the repo rules say the markdown record is the canonical stability source. That leaves two hand-edited authorities for the same release metadata, so the advertised “update STABILITY.md and run the check” workflow is no longer true. Please derive this module from STABILITY.md, or demote it to a generated mirror instead of another source of truth.

As per coding guidelines, STABILITY.md: “Maintain the canonical module stability record so the stable/beta/experimental status stays correct.”

Also applies to: 25-48

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/stability-matrix.mjs` around lines 3 - 13, The stability matrix is
currently being maintained as a second source of truth, which conflicts with the
canonical record in STABILITY.md. Update the module data in stability-matrix.mjs
so it is derived from or generated from STABILITY.md, or clearly demote this
file to a mirror used only for validation; keep
scripts/check-stability-matrix.mjs and the module history workflow aligned so
STABILITY.md remains the only hand-edited authority.

Source: Coding guidelines

src/plugin/define.ts-39-41 (1)

39-41: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Return the concrete plugin type from definePlugin().
BQueryPlugin<TOptions> collapses plugin-specific fields and narrow literals at the call site, so this helper no longer preserves the object shape it advertises. Use a generic like TPlugin extends BQueryPlugin<unknown> and return TPlugin instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/plugin/define.ts` around lines 39 - 41, `definePlugin()` is currently
returning the widened `BQueryPlugin<TOptions>` type, which loses plugin-specific
fields and literal precision at the call site. Update the `definePlugin` generic
so it accepts a concrete plugin type like `TPlugin extends
BQueryPlugin<unknown>` and returns `TPlugin` instead, while keeping the function
body as a simple identity return.

Source: Coding guidelines

src/server/session.ts-185-260 (1)

185-260: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Track nested session mutations before persisting.

The proxy only marks the session dirty for top-level assignment/deletion. Mutations like ctx.session.cart.items.push(item) update the in-memory object but never call markDirty(), so the change is skipped at finalize() and not written to the store.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/session.ts` around lines 185 - 260, The ServerSession proxy in
session.ts only marks dirty on top-level set/delete, so nested object/array
mutations can be missed before finalize(). Update the Proxy behavior around the
session object to detect or wrap nested mutations (for example in the get path
for data-backed values) so operations like ctx.session.cart.items.push(...)
trigger markDirty() and persist correctly. Use the existing session proxy,
markDirty(), and finalize() flow as the main touchpoints.
src/router/file-routes/data.ts-121-124 (1)

121-124: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Run loaders outside the reactive tracking frame.

run(route) starts synchronously inside effect(). If a loader reads signals before its first await, those signals can become dependencies and retrigger loading without navigation.

Proposed fix
   const dispose = effect(() => {
     const route = router.currentRoute.value;
-    void run(route);
+    queueMicrotask(() => {
+      void run(route);
+    });
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/router/file-routes/data.ts` around lines 121 - 124, The loader kickoff
inside the effect in the route watcher is running within the reactive tracking
frame, so any signals touched before the first await can become unintended
dependencies. Update the effect callback that reads router.currentRoute and
calls run(route) so the loader invocation happens outside tracking (for example
by deferring it or wrapping the call in a non-tracking boundary) while still
using the same route value. Keep the fix localized to the effect/run path in
data.ts so loaders are triggered only by navigation changes, not by reactive
reads inside loaders.
src/router/file-routes/routes.ts-80-132 (1)

80-132: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject duplicate file-route patterns.

routes/index.ts and routes/+page.ts both normalize to /; pathless groups can also collide. Returning both makes route resolution depend on manifest insertion order.

Proposed fix
   const entries: FileRoute[] = [];
+  const seenPatterns = new Map<string, string>();

   // Second pass: build a FileRoute per page.
   for (const [source, entry] of Object.entries(manifest)) {
     const parsed = parseFilePath(source, options);
     if (!parsed || parsed.kind !== 'page') continue;
+    const previousSource = seenPatterns.get(parsed.pattern);
+    if (previousSource) {
+      throw new Error(
+        `bQuery router: duplicate file route pattern "${parsed.pattern}" from "${previousSource}" and "${source}".`
+      );
+    }
+    seenPatterns.set(parsed.pattern, source);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/router/file-routes/routes.ts` around lines 80 - 132, The route builder in
routes.ts currently pushes every parsed page into entries without checking for
duplicate normalized patterns, so colliding files like routes/index.ts and
routes/+page.ts can both produce "/". Add a deduplication check in the second
pass around parseFilePath and entries.push, keyed by parsed.pattern, and reject
duplicates with a clear error instead of relying on manifest order. Use the
existing symbols parseFilePath, entries, and the page-building loop to locate
the change.
src/ssr/resumable-boundary.ts-304-327 (1)

304-327: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Deduplicate resumed handler bindings across nested boundaries.

wireBoundaryHandlers() scans every descendant for each boundary. If resumable boundaries are nested, a child handler can be wired once while scanning the parent and again while scanning the child, causing duplicate user actions.

Suggested fix
 const wireBoundaryHandlers = (
   scope: Element,
-  handlers: Record<string, ResumableHandler>
+  handlers: Record<string, ResumableHandler>,
+  wiredBindings: WeakMap<Element, Set<string>>
 ): number => {
@@
     const events = (el.getAttribute(RESUMABLE_EVENT_ATTR) ?? 'click')
       .split(/\s+/)
       .filter(Boolean);
     for (const event of events) {
+      const bindingKey = `${event}\u0000${handlerId}`;
+      let elementBindings = wiredBindings.get(el);
+      if (!elementBindings) {
+        elementBindings = new Set<string>();
+        wiredBindings.set(el, elementBindings);
+      }
+      if (elementBindings.has(bindingKey)) continue;
       el.addEventListener(event, fn as EventListener);
+      elementBindings.add(bindingKey);
       wired += 1;
     }
   }
@@
   const boundaryIds: string[] = [];
   const seededSignals: string[] = [];
+  const wiredBindings = new WeakMap<Element, Set<string>>();
   let wiredHandlers = 0;
@@
     if (root) {
       const scope = findBoundaryScope(root, boundary.id);
-      if (scope) wiredHandlers += wireBoundaryHandlers(scope, handlerRegistry);
+      if (scope) wiredHandlers += wireBoundaryHandlers(scope, handlerRegistry, wiredBindings);
     }

Also applies to: 404-407

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ssr/resumable-boundary.ts` around lines 304 - 327, wireBoundaryHandlers()
is wiring the same resumable handler multiple times when nested boundaries are
scanned by both parent and child boundaries. Update the handler discovery in
wireBoundaryHandlers to skip elements that belong to a nested resumable boundary
so each handler is bound only once, and preserve the existing
RESUMABLE_HANDLER_ATTR/RESUMABLE_EVENT_ATTR behavior while preventing duplicate
addEventListener calls across nested scopes.
src/ssr/hydration.ts-133-188 (1)

133-188: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard hydration detection against evaluator failures.

hasResolvableRoot() only checks the leading identifier. Expressions like user.name when user is null can still throw and abort hydrate() before warn, repair, or error handling runs.

Suggested fix
+type EvaluationResult<T> = { ok: true; value: T } | { ok: false };
+
+const tryEvaluateExpression = <T>(
+  expression: string,
+  context: BindingContext
+): EvaluationResult<T> => {
+  try {
+    return { ok: true, value: evaluateExpression<T>(expression, context) };
+  } catch {
+    return { ok: false };
+  }
+};
+
   const textExpr = el.getAttribute(`${prefix}-text`);
   if (textExpr !== null && hasResolvableRoot(textExpr, context)) {
-    const clientValue = String(evaluateExpression<unknown>(textExpr, context) ?? '');
-    const domValue = el.textContent ?? '';
-    if (clientValue !== domValue) {
-      out.push({ element: el, kind: 'text', directive: `${prefix}-text`, domValue, clientValue });
+    const evaluated = tryEvaluateExpression<unknown>(textExpr, context);
+    if (evaluated.ok) {
+      const clientValue = String(evaluated.value ?? '');
+      const domValue = el.textContent ?? '';
+      if (clientValue !== domValue) {
+        out.push({ element: el, kind: 'text', directive: `${prefix}-text`, domValue, clientValue });
+      }
     }
   }

Apply the same wrapper to bq-show, bq-bind:*, and bq-model.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ssr/hydration.ts` around lines 133 - 188, Guard hydration detection in
hydrate() against expression evaluator failures by wrapping the
evaluateExpression() calls used for bq-text, bq-show, bq-bind:*, and bq-model so
a bad nested access like user.name does not throw and abort the scan. Keep
hasResolvableRoot() as the precheck, but catch evaluator errors per directive,
skip reporting that candidate when evaluation fails, and continue collecting
mismatches so warn/repair/error handling still runs. Use the existing hydration
flow in src/ssr/hydration.ts and the directive-specific blocks around textExpr,
showExpr, attr.value, and modelExpr as the fix points.
src/concurrency/scheduling.ts-141-151 (1)

141-151: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Align deferred() timeout with the maximum-lag contract.

The cleanup cancels and reschedules the timeout on every source change, so continuous updates faster than timeout can keep the deferred signal stale indefinitely. That makes timeout a debounce delay, not the documented maximum lag. Either implement max-wait flushing of the latest value or rename/reword the option before this API is stabilized.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/concurrency/scheduling.ts` around lines 141 - 151, The `deferred()`
scheduling in `effect` currently behaves like a debounce because
`scheduleDeferred` is canceled and rescheduled on every source change, so
`mirror` may never update under constant churn. Update the logic around
`readDeferredSource`, `scheduleDeferred`, and the `mirror.value` assignment to
enforce a true maximum-lag contract by flushing the latest value after the
timeout even during continuous updates, or otherwise rename/reword
`options.timeout` to match the actual debounce behavior before stabilization.
src/concurrency/task.ts-125-139 (1)

125-139: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Thread options.runtime through the worker support checks in src/concurrency/task.ts and src/concurrency/rpc.ts. The module branch still falls back to plain typeof Worker, and the dynamic branch ignores the override entirely, so runtime: 'bun' | 'deno' | 'browser' | 'node' | 'unknown' cannot influence the guard and support can be reported incorrectly in tests and host-compat runtimes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/concurrency/task.ts` around lines 125 - 139, The worker support checks in
task/concurrency setup ignore the runtime override, so support detection can be
wrong for bun/deno/browser/node/unknown. Update the branching around
isWorkerModuleDescriptor in task.ts and the related guard logic in rpc.ts to
pass options.runtime through the support checks instead of relying only on
typeof Worker or the default environment. Use the existing symbols like
isConcurrencySupported, TaskWorkerUnsupportedError, createModuleWorkerInstance,
and createWorkerInstance to keep the runtime-specific path selection consistent.
src/concurrency/module-worker.ts-193-206 (1)

193-206: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Catch postMessage() serialization failures.

If a handler returns a non-cloneable result, scope.postMessage() throws from the success callback and no bq:error response is sent, leaving the main-thread call pending until timeout.

Proposed fix
-    void Promise.resolve()
-      .then(() => handler(message.payload as TInput))
-      .then(
-        (result) => {
-          scope.postMessage({ id: message.id, result, type: WORKER_RESULT_MESSAGE });
-        },
-        (error: unknown) => {
-          scope.postMessage({
-            error: serializeHostError(error),
-            id: message.id,
-            type: WORKER_ERROR_MESSAGE,
-          });
-        }
-      );
+    void (async () => {
+      try {
+        const result = await handler(message.payload as TInput);
+        scope.postMessage({ id: message.id, result, type: WORKER_RESULT_MESSAGE });
+      } catch (error) {
+        scope.postMessage({
+          error: serializeHostError(error),
+          id: message.id,
+          type: WORKER_ERROR_MESSAGE,
+        });
+      }
+    })();

Apply the same try/catch shape around the exposeRpc() handler result post.

Also applies to: 275-288

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/concurrency/module-worker.ts` around lines 193 - 206, The success path in
`module-worker.ts` is missing error handling around `scope.postMessage()` in the
`Promise.resolve().then(...).then(...)` flow, so non-cloneable handler results
can throw before a `WORKER_ERROR_MESSAGE` is sent. Wrap the `scope.postMessage({
id, result, type: WORKER_RESULT_MESSAGE })` call in the same `try/catch` shape
used elsewhere and, on failure, send a serialized error response with
`serializeHostError(error)` so the main thread doesn’t hang. Apply the same fix
to the matching `exposeRpc()` result posting path referenced by the other
affected block.
src/concurrency/module-worker.ts-43-58 (1)

43-58: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Separate task and RPC module descriptors.

defineWorker() and defineRpcWorker() currently produce identical branded descriptors, while createRpcWorker() treats any branded descriptor as RPC. Add an internal kind/separate brand and validate it in task/RPC factories so wrong descriptors fail fast instead of timing out. As per coding guidelines, src/**/*.ts: Do not weaken strict TypeScript type safety.

Also applies to: 74-99

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/concurrency/module-worker.ts` around lines 43 - 58, The branded
descriptor created by createModuleDescriptor is being reused for both task and
RPC workers, so defineWorker() and defineRpcWorker() can produce
indistinguishable objects. Add an internal kind field or separate brand in
createModuleDescriptor and the RPC descriptor creator, then make
createWorker/createRpcWorker validate that the descriptor kind matches the
factory before proceeding. Keep the TypeScript types strict by narrowing the
descriptor unions rather than weakening them, so invalid cross-use fails fast
instead of reaching createRpcWorker and timing out.

Source: Coding guidelines

src/i18n/extract/extract.ts-111-158 (1)

111-158: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Harden catalog reconstruction against prototype-polluting keys.

Both parseObjectLiteral() and unflatten() write source-derived path segments into plain {} objects. Keys like __proto__, constructor, or prototype can mutate the prototype chain and corrupt later merges/output.

Suggested fix
+const createCatalog = (): ExtractedCatalog => Object.create(null) as ExtractedCatalog;
+const isUnsafeKey = (key: string): boolean =>
+  key === '__proto__' || key === 'constructor' || key === 'prototype';
+
 const parseObjectLiteral = (
   src: string,
   start: number
 ): { value: ExtractedCatalog; end: number } => {
-  const obj: ExtractedCatalog = {};
+  const obj = createCatalog();
   let i = start + 1;
@@
     if (src[i] === '{') {
       const r = parseObjectLiteral(src, i);
-      obj[key] = r.value;
+      if (!isUnsafeKey(key)) obj[key] = r.value;
       i = r.end;
     } else if (src[i] === '"' || src[i] === "'" || src[i] === '`') {
       const r = readString(src, i);
-      obj[key] = r.value;
+      if (!isUnsafeKey(key)) obj[key] = r.value;
       i = r.end;
     } else {
       i = skipValue(src, i);
     }
@@
 export const unflatten = (messages: ExtractedMessage[]): ExtractedCatalog => {
-  const root: ExtractedCatalog = {};
+  const root = createCatalog();
   for (const { key, value } of [...messages].sort((a, b) => a.key.localeCompare(b.key))) {
     const parts = key.split('.');
     let node = root;
     for (let p = 0; p < parts.length - 1; p += 1) {
       const part = parts[p];
-      if (typeof node[part] !== 'object') node[part] = {};
+      if (isUnsafeKey(part)) throw new Error(`Unsafe catalog key: ${key}`);
+      if (typeof node[part] !== 'object') node[part] = createCatalog();
       node = node[part] as ExtractedCatalog;
     }
-    node[parts[parts.length - 1]] = value;
+    const leaf = parts[parts.length - 1];
+    if (isUnsafeKey(leaf)) throw new Error(`Unsafe catalog key: ${key}`);
+    node[leaf] = value;
   }
   return root;
 };

Also applies to: 173-185

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/i18n/extract/extract.ts` around lines 111 - 158, Harden the
catalog-building paths in parseObjectLiteral and unflatten so source-derived
keys cannot pollute object prototypes. Use a null-prototype container or
explicitly reject unsafe segments like __proto__, constructor, and prototype
before assigning into the plain object. Make the fix in the reconstruction logic
where obj is populated so both nested object parsing and flattened path
expansion are protected.

Source: Linters/SAST tools

src/i18n/extract/cli.ts-111-118 (1)

111-118: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not treat every catalog read/parse failure as “missing file.”

If options.out already exists but contains malformed JSON or is unreadable, readJsonCatalog() returns {} and extractFiles() then overwrites that file with a fresh catalog. That silently destroys existing translations instead of surfacing the real error.

Suggested fix
 const readJsonCatalog = async (file: string): Promise<ExtractedCatalog> => {
   const { readFile } = await import('node:fs/promises');
   try {
     const text = await readFile(file, 'utf8');
     return JSON.parse(text) as ExtractedCatalog;
-  } catch {
-    return {};
+  } catch (error) {
+    if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT') {
+      return {};
+    }
+    throw error;
   }
 };

Also applies to: 156-159

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/i18n/extract/cli.ts` around lines 111 - 118, The readJsonCatalog helper
currently swallows every read/parse error and returns an empty catalog, which
makes extractFiles overwrite an existing out file even when it is malformed or
unreadable. Update readJsonCatalog to distinguish a missing file from other
failures: keep returning an empty object only for the “file not found” case, and
rethrow or surface JSON parse/read errors so extractFiles does not replace
existing translations with a fresh catalog. Use the readJsonCatalog and
extractFiles flow to locate the fix.
src/i18n/extract/extract.ts-173-183 (1)

173-183: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject parent/child key collisions instead of silently dropping one translation.

unflatten() overwrites data when both a and a.b are present: the later nested write coerces a from a string into an object and loses the original value. Since the extractor accepts arbitrary dot-delimited keys, this can silently discard messages during merge/write.

Suggested fix
   for (const { key, value } of [...messages].sort((a, b) => a.key.localeCompare(b.key))) {
     const parts = key.split('.');
     let node = root;
     for (let p = 0; p < parts.length - 1; p += 1) {
       const part = parts[p];
-      if (typeof node[part] !== 'object') node[part] = createCatalog();
-      node = node[part] as ExtractedCatalog;
+      const existing = node[part];
+      if (typeof existing === 'string') {
+        throw new Error(`Catalog key collision: "${parts.slice(0, p + 1).join('.')}" conflicts with "${key}"`);
+      }
+      if (existing === undefined) node[part] = createCatalog();
+      node = node[part] as ExtractedCatalog;
     }
-    node[parts[parts.length - 1]] = value;
+    const leaf = parts[parts.length - 1];
+    if (typeof node[leaf] === 'object') {
+      throw new Error(`Catalog key collision: "${key}" conflicts with nested keys`);
+    }
+    node[leaf] = value;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/i18n/extract/extract.ts` around lines 173 - 183, unflatten currently
allows parent/child key collisions, so a key like a can be overwritten when a.b
is processed and one translation is lost. Update unflatten in extract.ts to
detect conflicts while walking parts, especially in the loop that builds node
from ExtractedMessage.key, and reject any case where an intermediate segment
already holds a non-object value or the final key already exists as an
object/value conflict. Surface this as an explicit error instead of coercing
node[part] into {} so the caller can handle the invalid catalog shape.
src/forms/types.ts-303-313 (1)

303-313: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The documented getKey guarantee is broader than the current implementation.

This says keyed arrays enforce the contract on every structural mutation, but createFieldArray.move() still just reorders items without re-running assertStableKeys(). A getKey(value, index) can be valid on insert/add and become invalid after a reorder, so either validate on move as well or narrow the public contract here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/forms/types.ts` around lines 303 - 313, The documented getKey contract in
FormFieldArray is broader than what move currently enforces, since
createFieldArray.move() only reorders items and does not re-run
assertStableKeys(). Update the move path to validate keys after reordering
(using assertStableKeys and the existing getKey contract) so invalid or
duplicate keys are caught on every structural mutation, or else narrow the
getKey documentation/comments in src/forms/types.ts to match the actual
behavior.
src/forms/action.ts-201-223 (1)

201-223: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

pending flips false too early when submits overlap.

Each submit() call sets the shared boolean back to false in finally, so the first completion clears pending even if a later submit is still in flight. Track an in-flight count/token and derive pending from that, otherwise useFormStatus() misreports active work.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/forms/action.ts` around lines 201 - 223, The shared pending state in
submit() is being cleared by whichever request finishes first, so overlapping
submissions can incorrectly report no active work. Update the useFormAction flow
around submit(), pending, and submitCount to track in-flight submits with a
count or token instead of a single boolean, and only set pending false when the
last active submit completes. Keep the existing optimistic overlay cleanup and
onSuccess/onError handling inside submit() unchanged aside from tying pending to
the active submit tracking.
src/forms/action.ts-142-148 (1)

142-148: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the clicked submitter in enhanced submits.

new FormData(form) omits the clicked submit button, so enhanced submits lose button name/value data and can diverge from native behavior when a form has multiple submit actions. Capture the SubmitEvent.submitter and build the payload from that submit context instead of the bare form.

Also applies to: 253-258

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/forms/action.ts` around lines 142 - 148, The enhanced form payload
builder in buildFormData currently uses only the form element, so the clicked
submit button’s name/value is lost and behavior can diverge from native submits.
Update the submit handling path to capture SubmitEvent.submitter and pass that
submit context into buildFormData (and any other enhanced submit helpers that
construct FormData) so the final payload preserves the clicked button data.
src/forms/action.ts-175-191 (1)

175-191: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't send FormData in the enhanced GET path.

formAction({ method: 'GET' }) still calls fetch() with body: formData. That breaks the enhanced flow, because GET submissions need to be encoded into the query string and sent without a body. Right now the native fallback and JS-enhanced behavior diverge for the same API surface.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/forms/action.ts` around lines 175 - 191, The enhanced GET path in
runTarget is still sending formData as a request body, which must be removed to
match native GET behavior. Update formAction’s runTarget logic to branch on the
normalized method before calling fetchImpl: for GET, serialize the FormData into
the target URL query string and call fetch without a body; for non-GET, keep the
existing body submission. Use the existing method, target, and runTarget symbols
to locate and adjust the request construction.
src/view/directives/show.ts-54-62 (1)

54-62: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Stop async hide commits after cleanup.

Like bq-if, a pending leave transition can still commit display: none after the directive cleanup runs. Wrap the cleanup to invalidate the token and cancel active animations.

🐛 Proposed fix
-    cleanups.push(cleanup);
+    cleanups.push(() => {
+      token++;
+      cancelTransitions(el);
+      cleanup();
+    });

Also applies to: 69-69

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/view/directives/show.ts` around lines 54 - 62, The pending leave
transition in show.ts can still commit a hide after directive cleanup, so update
the cleanup path to invalidate the current transition token and cancel any
active animations. Use the existing token check around runTransition,
cancelTransitions, and the show directive’s cleanup registration so that cleanup
prevents the then() callback from setting htmlEl.style.display = 'none' once the
directive has been torn down.
src/view/process.ts-15-15 (1)

15-15: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don’t globally shadow custom directive names.

This guard runs before plugin custom-directive lookup, so a previously valid custom directive like bq-transition or bq-in is now silently ignored even when no bq-if/bq-show/bq-for owns it. Only skip companion attributes when an owning directive is present.

🐛 Proposed fix
-const PASSIVE_DIRECTIVES = new Set<string>(['key', ...TRANSITION_ATTRS]);
+const TRANSITION_DIRECTIVES = new Set<string>(TRANSITION_ATTRS);
-    // Skip companion attributes (bq-key, bq-transition, bq-in, bq-out, …) that
-    // are read by their owning directive rather than processed here.
-    if (PASSIVE_DIRECTIVES.has(directive)) continue;
+    const hasStructuralTransitionOwner =
+      TRANSITION_DIRECTIVES.has(directive) &&
+      (el.hasAttribute(`${prefix}-if`) ||
+        el.hasAttribute(`${prefix}-show`) ||
+        el.hasAttribute(`${prefix}-for`));
+    const hasForKeyOwner = directive === 'key' && el.hasAttribute(`${prefix}-for`);
+
+    // Skip companion attributes only when their owning directive will consume them.
+    if (hasStructuralTransitionOwner || hasForKeyOwner) continue;

Also applies to: 72-74

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/view/process.ts` at line 15, The PASSIVE_DIRECTIVES guard in process.ts
is too broad and can shadow valid custom directives before the custom-directive
lookup runs. Update the logic around PASSIVE_DIRECTIVES and the directive
handling in the process flow so companion attributes like bq-transition or bq-in
are skipped only when an owning directive such as bq-if, bq-show, or bq-for is
actually present. Make sure the affected directive-processing path preserves
custom directives otherwise, using the relevant checks in the process function
and any related companion-attribute filtering.
src/view/directives/if.ts-57-63 (1)

57-63: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Cancel pending transitions during directive cleanup.

A pending leave transition can still run its .then(...) after View.destroy() because cleanup only stops the effect. Invalidate the token and cancel animations in the registered cleanup.

🐛 Proposed fix
-    cleanups.push(cleanup);
+    cleanups.push(() => {
+      token++;
+      cancelTransitions(el);
+      cleanup();
+    });

Also applies to: 74-74

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/view/directives/if.ts` around lines 57 - 63, The pending leave transition
in the if directive can still complete after cleanup because the registered
effect teardown only stops reactivity. Update the cleanup path around
runTransition, myToken/token, and cancelTransitions so that directive
destruction invalidates the current token and explicitly cancels any in-flight
animations before the promise callback can call replaceWith or change
isInserted. Make sure the cleanup used by the if directive covers both the leave
transition and any related enter/leave state so View.destroy cannot leave a
stale .then(...) running.
src/view/directives/transitions.ts-197-201 (1)

197-201: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Catch animate() setup errors and fall back to the no-transition path. Element.animate() throws synchronously for invalid easing/timing values, so runTransition() can reject before any finish handler is wired. The bq-if/bq-show leave paths only use .then(...), so the element can stay mounted or visible instead of taking the synchronous commit path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/view/directives/transitions.ts` around lines 197 - 201, `runTransition()`
is calling `animatable.animate()` in a way that can throw before the transition
promise is created, which leaves the `bq-if`/`bq-show` leave flow without a
fallback. Update the `animate()` setup in `transitions.ts` to catch synchronous
setup errors around the `animate()` call and treat them as a no-transition case,
so the existing commit path is taken when timing/easing values are invalid. Use
the `runTransition` and `animatable.animate` logic as the anchor points for the
fix.
src/view/compiler/expression.ts-24-30 (1)

24-30: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve runtime global fallback for non-whitelisted identifiers.

The runtime evaluator falls through to ambient scope for any name missing from ctx, but the compiled path only preserves that behavior for names hard-coded in DEFAULT_GLOBALS. Expressions like URL.createObjectURL(file) currently compile to __bq_ctx.URL.createObjectURL(__bq_ctx.file), which changes behavior instead of matching runtime semantics. Emit a fallback lookup such as ("URL" in ctx ? ctx.URL : URL) for unknown names, or bail out unless the identifier is known to come from the binding context.

Possible fix
-      out += `${param}.${name}`;
+      out += `(${JSON.stringify(name)} in ${param} ? ${param}.${name} : ${name})`;

Also applies to: 245-275

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/view/compiler/expression.ts` around lines 24 - 30, The compiled
expression path in expression.ts is incorrectly limiting global fallback to only
DEFAULT_GLOBALS, so unknown identifiers no longer match the runtime with(ctx)
behavior. Update the identifier compilation logic used by the expression
compiler to emit a context-or-global fallback for any name not known to be a
binding-context value, or explicitly reject/bail out when the symbol cannot be
proven local. Make sure the fix is applied in the identifier handling around
DEFAULT_GLOBALS and the expression compilation flow that produces references
like __bq_ctx.<name>, so cases such as URL.createObjectURL(file) continue to
resolve against ambient globals when ctx does not provide the name.
src/view/compiler/cli.ts-61-66 (1)

61-66: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

--out-dir can overwrite outputs from different source folders.

When outDir is set, every file is written to ${outDir}/${basename}${suffix}${ext}. Compiling pages/home/index.html and emails/home/index.html will silently target the same output path and drop one result. Preserve the relative input path under outDir, or detect collisions and fail fast.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/view/compiler/cli.ts` around lines 61 - 66, The output path logic in
cli.ts currently uses only path.basename in the write step, so different source
folders can collide when --out-dir is set. Update the compile flow around the
output construction and writeFile call to preserve each input file’s relative
path under options.outDir (or, if that’s not desired, detect duplicate targets
and throw before writing). Use the existing input, base, dir, suffix, and ext
handling in the cli compiler path to locate the fix.
src/view/compiler/expression.ts-143-152 (1)

143-152: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Don't mark syntactically invalid rewrites as compiled.

rewrite() tokenizes conservatively, but it never proves that the final body is still valid JavaScript. Inputs like count + or items] can return { ok: true } and then generate an invalid module, turning one bad directive into an import-time failure instead of a normal runtime fallback. Add a final syntax/balance validation step before returning ok: true, and convert failures to ok: false.

Also applies to: 217-223, 336-338

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/view/compiler/expression.ts` around lines 143 - 152, The rewrite() flow
is currently returning ok: true even when the rewritten expression is still
syntactically invalid, which can produce an invalid module; add a final
syntax/balance validation step before the success return and downgrade any
invalid result to ok: false. Use the existing rewrite() path in expression.ts,
including the string/token handling and any final assembly logic, to verify
parentheses/brackets/quotes and overall JavaScript validity before marking the
body as compiled.
src/view/compiler/cli.ts-95-112 (1)

95-112: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't consume the next flag as an option value.

Each option blindly uses argv[++i]. If the user forgets a value, the parser will happily treat the next flag as data (--prefix --out-dir sets prefix="--out-dir"), which produces confusing behavior instead of the documented usage error. Validate that a non-flag value exists before assigning each option.

Possible fix
+  const takeValue = (flag: string): string | null => {
+    const value = argv[i + 1];
+    if (!value || value.startsWith('-')) {
+      io.error(`Missing value for ${flag}`);
+      return null;
+    }
+    i++;
+    return value;
+  };
+
   for (let i = 0; i < argv.length; i++) {
     const arg = argv[i];
     switch (arg) {
-      case '--prefix':
-        options.prefix = argv[++i];
+      case '--prefix': {
+        const value = takeValue('--prefix');
+        if (value === null) return 1;
+        options.prefix = value;
         break;
-      case '--out-dir':
-        options.outDir = argv[++i];
+      }
+      case '--out-dir': {
+        const value = takeValue('--out-dir');
+        if (value === null) return 1;
+        options.outDir = value;
         break;
+      }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/view/compiler/cli.ts` around lines 95 - 112, The CLI parser in the argv
loop is consuming the next token with argv[++i] for option values, which can
incorrectly treat another flag as data when a value is missing. Update the
parsing logic in cli.ts around the switch handling for --prefix, --out-dir,
--suffix, --ext, and --import to first verify that argv[i + 1] exists and is not
a flag before assigning it to options. If the next token is missing or starts
with --, fail with the same usage/error path used by the parser instead of
advancing i.
extension/panel.js-68-70 (1)

68-70: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not render inspected-page data through innerHTML.

fmt(item) and e.label come from the inspected page and are injected straight into the extension DOM. In the panel this crosses a privilege boundary and lets page-controlled markup rewrite or spoof the extension UI. Build these rows with textContent/DOM nodes (or sanitize first) instead of HTML strings.

Also applies to: 83-85, 92-94

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extension/panel.js` around lines 68 - 70, The tree rendering in renderTree,
along with the related row builders at the other noted locations, is injecting
inspected-page values into the extension UI via innerHTML. Replace these HTML
string assignments with DOM node creation and textContent (or sanitize before
insertion) so node.tag, fmt(item), and e.label cannot inject markup into the
panel.

Source: Linters/SAST tools

src/devtools/bridge.ts-198-239 (1)

198-239: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject mismatched bridge protocol versions before dispatch.

handleMessage() currently accepts any v, so a v0/v2 panel can still drive the v1 server. That makes BRIDGE_PROTOCOL_VERSION informational instead of contractual and will fail open once the protocol evolves. Gate hello/request on data.v === BRIDGE_PROTOCOL_VERSION and fail fast on mismatches.

Patch sketch
   const handleMessage = (data: unknown): void => {
     if (!isInbound(data)) return;
+    if (data.v !== BRIDGE_PROTOCOL_VERSION) {
+      if (data.kind === 'request') {
+        options.post({
+          source: BRIDGE_SOURCE,
+          channel: 'page',
+          v: BRIDGE_PROTOCOL_VERSION,
+          kind: 'response',
+          id: data.id,
+          error: `Protocol mismatch: expected v${BRIDGE_PROTOCOL_VERSION}, received v${data.v}`,
+        });
+      }
+      return;
+    }
 
     if (data.kind === 'hello') {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/devtools/bridge.ts` around lines 198 - 239, handleMessage currently
dispatches every inbound message without enforcing the bridge version, so add a
strict check that data.v matches BRIDGE_PROTOCOL_VERSION before processing both
hello and request messages. If the version is missing or mismatched, reject the
message early by returning a response/error instead of calling announce() or
method(data.params), and keep the existing unknown-method and exception handling
paths unchanged. Use the handleMessage function and BRIDGE_PROTOCOL_VERSION as
the key entry points for the fix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7f35bc8e-8e1b-4e4c-be6f-c50c2bbd6ccc

📥 Commits

Reviewing files that changed from the base of the PR and between d26b720 and 21502d8.

📒 Files selected for processing (143)
  • .clinerules
  • .cursorrules
  • .github/copilot-instructions.md
  • .github/workflows/greetings.yml
  • AGENT.md
  • CHANGELOG.md
  • README.md
  • STABILITY.md
  • bin/bquery-i18n.mjs
  • bin/bquery-view-compile.mjs
  • docs/.vitepress/config.ts
  • docs/contributing/release-process.md
  • docs/guide/a11y.md
  • docs/guide/concurrency.md
  • docs/guide/devtools.md
  • docs/guide/dnd.md
  • docs/guide/file-routing.md
  • docs/guide/forms.md
  • docs/guide/i18n.md
  • docs/guide/media.md
  • docs/guide/plugin.md
  • docs/guide/router.md
  • docs/guide/server.md
  • docs/guide/ssr.md
  • docs/guide/storybook.md
  • docs/guide/testing.md
  • docs/guide/view.md
  • docs/introduction.md
  • docs/release-notes/1.15.md
  • docs/release-notes/index.md
  • extension/README.md
  • extension/background.js
  • extension/content.js
  • extension/devtools.html
  • extension/devtools.js
  • extension/manifest.json
  • extension/panel.html
  • extension/panel.js
  • llms.txt
  • package.json
  • scripts/check-stability-matrix.mjs
  • scripts/stability-matrix.mjs
  • src/a11y/audit.ts
  • src/a11y/index.ts
  • src/a11y/types.ts
  • src/concurrency/index.ts
  • src/concurrency/internal.ts
  • src/concurrency/module-worker.ts
  • src/concurrency/pool.ts
  • src/concurrency/reactive.ts
  • src/concurrency/rpc.ts
  • src/concurrency/scheduling.ts
  • src/concurrency/support.ts
  • src/concurrency/task.ts
  • src/concurrency/types.ts
  • src/devtools/bridge.ts
  • src/devtools/extensions.ts
  • src/devtools/index.ts
  • src/dnd/index.ts
  • src/forms/action.ts
  • src/forms/field-array.ts
  • src/forms/index.ts
  • src/forms/optimistic.ts
  • src/forms/ssr.ts
  • src/forms/types.ts
  • src/full.ts
  • src/i18n/define.ts
  • src/i18n/extract/cli.ts
  • src/i18n/extract/extract.ts
  • src/i18n/extract/index.ts
  • src/i18n/extract/merge.ts
  • src/i18n/i18n.ts
  • src/i18n/icu.ts
  • src/i18n/index.ts
  • src/i18n/translate.ts
  • src/media/index.ts
  • src/plugin/define.ts
  • src/plugin/index.ts
  • src/router/file-routes/data.ts
  • src/router/file-routes/path.ts
  • src/router/file-routes/routes.ts
  • src/router/file-routes/types.ts
  • src/router/index.ts
  • src/server/auth.ts
  • src/server/cookies.ts
  • src/server/create-server.ts
  • src/server/crypto.ts
  • src/server/csrf.ts
  • src/server/file-routes.ts
  • src/server/guard.ts
  • src/server/index.ts
  • src/server/session.ts
  • src/server/types.ts
  • src/ssr/directive-support.ts
  • src/ssr/hydration.ts
  • src/ssr/index.ts
  • src/ssr/render-async.ts
  • src/ssr/render.ts
  • src/ssr/renderer.ts
  • src/ssr/resumable-boundary.ts
  • src/ssr/router-bridge.ts
  • src/ssr/suspense.ts
  • src/ssr/types.ts
  • src/storybook/index.ts
  • src/testing/index.ts
  • src/view/compiler/cli.ts
  • src/view/compiler/compile.ts
  • src/view/compiler/emit.ts
  • src/view/compiler/expression.ts
  • src/view/compiler/index.ts
  • src/view/compiler/types.ts
  • src/view/directives/for.ts
  • src/view/directives/if.ts
  • src/view/directives/index.ts
  • src/view/directives/show.ts
  • src/view/directives/transitions.ts
  • src/view/evaluate.ts
  • src/view/index.ts
  • src/view/mount.ts
  • src/view/process.ts
  • tests/a11y-stable.test.ts
  • tests/check-stability-matrix.test.ts
  • tests/concurrency-stable.test.ts
  • tests/cross-runtime/run.mjs
  • tests/devtools-bridge.test.ts
  • tests/dnd-stable.test.ts
  • tests/forms-actions.test.ts
  • tests/forms-stable.test.ts
  • tests/i18n-extract.test.ts
  • tests/i18n-stable.test.ts
  • tests/media-stable.test.ts
  • tests/plugin-stable.test.ts
  • tests/router-file-routes.test.ts
  • tests/server-file-routes.test.ts
  • tests/server-stable.test.ts
  • tests/ssr-stable.test.ts
  • tests/storybook-stable.test.ts
  • tests/testing-stable.test.ts
  • tests/view-compiler.test.ts
  • tests/view-stable.test.ts
  • tests/view-transitions.test.ts
  • tests/view.test.ts
  • vite.config.ts

Replaces the `node: … | null` walk sentinel with an `unsafe` flag so there is
no `node === null` comparison — clears the github-code-quality
"comparison between inconvertible types" note while keeping the inline
prototype-pollution guards CodeQL relies on. Behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@JosunLP JosunLP self-assigned this Jun 29, 2026
@JosunLP

JosunLP commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@JosunLP

JosunLP commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

JosunLP and others added 5 commits June 30, 2026 00:33
…evtools

Multi-agent review of the 1.15.0 PR surfaced these issues; each is fixed and
covered by the existing or an added test.

Security
- server: ctx.body() buffered any unrecognized content type into memory with
  no size cap. Add a `raw` ServerLimits field and route the fall-through
  through the bounded body reader (unauthenticated memory-exhaustion DoS).
- ssr: a bq-model value reflected onto <textarea> (a raw-text element) was
  emitted unescaped by the DOM-free renderer; escape it (stored XSS).
- devtools extension: the panel rendered untrusted inspected-page data via
  innerHTML; HTML-escape every interpolated value.
- server: SameSite=None cookies now force Secure, which browsers require or
  they silently drop the cookie.

Correctness
- view: bq-if and bq-show re-ran their enter/leave transitions on every
  reactive update, not only on a real flip — a falsy condition with ticking
  dependencies could keep an element visible forever. Guard on a normalized
  boolean flip (also handles a non-boolean/undefined condition).
- view: bq-for duplicated a row when a key was re-added while its leave
  animation was still in flight. Track leaving elements and drop the stale
  node on re-add (+ regression test).
- concurrency: scheduleDeferred's requestIdleCallback path had no timeout, so a
  saturated main thread could starve startTransition/deferred; bound it to 100ms.
- devtools bridge: validate the inbound request shape so a malformed message no
  longer dispatches methods[undefined] and replies with an undefined id.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…ss dot

The numeric-literal scanner copied any run of `[0-9a-fA-FxXoObBeE._]`, so it
swallowed the dot in `1.5.toFixed(2)`. That set `prev` to a number rather than
`.`, so the following property was rewritten as a free identifier
(`1.5.__bq_ctx.toFixed(...)`) and produced broken compiled code. Consume at most
one decimal point, and only when a digit follows, so member-access dots end the
literal. Valid numbers (hex, exponent, separators, leading-dot floats) are
unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…, server, a11y

Second review pass over the modules the first (rate-limited) run left
unverified, plus the two areas it never reached (a11y, build tooling).

Security / robustness
- server/csrf: throw on a provided-but-empty `secret` instead of silently
  downgrading to unsigned double-submit (e.g. an unset `CSRF_SECRET` env that
  resolves to ''). Omitting `secret` is still the supported unsigned opt-in.
- i18n/translate: a malformed ICU string in the catalog no longer throws out
  of translate()/t(); it falls back to plain `{name}` interpolation.
- i18n/extract CLI: guard the directory walk against symlink cycles
  (canonical-path visited set) so a self-referential link can't hang it.
- forms/ssr: stripNonSerializable filtered object keys but turned
  non-serializable ARRAY elements into `null`, breaking the documented
  "dropped, never null" contract — filter them out of arrays instead.
- a11y/audit: the input-label check interpolated an element id into a
  `label[for="…"]` selector, so an id with a quote/metacharacter threw a
  SyntaxError and crashed the audit. Compare `for` attributes directly.

Correctness
- view/compiler: bail on unbalanced/mismatched brackets instead of emitting
  unparsable code that breaks the whole generated module.
- view/process: a plugin directive named like a passive companion attribute
  (key/in/out/animate/transition/…) was silently shadowed; an explicit
  custom-directive registration now wins.
- i18n/icu: honour a doubled '' inside an apostrophe-quoted literal per the
  ICU spec the module documents.
- forms/action: refresh the native-fallback CSRF hidden field before each
  submit so a rotating token also reaches the form body, not just the header.

Docs
- i18n/extract: `total` documents extracted-key count (added+kept), matching
  the value and CLI output.
- view/evaluate: state that the AoT and runtime paths agree for well-formed
  expressions and how they differ for an unresolved free identifier.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…e() complexity

CodeFactor flagged rewrite() as a "Very Complex Method" after the bracket-balance
and numeric-scanner fixes added branches. Extract the self-contained string- and
numeric-literal scanners into helpers, which removes two nested loops from the
method and brings it back under the complexity threshold. Behaviour is unchanged
(verified: member-access numbers, strings containing brackets, and unbalanced
expressions all compile/bail as before).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@JosunLP
JosunLP merged commit fef7848 into main Jun 30, 2026
21 of 22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a11y Changes to the a11y module build Changes to the build and meta files ci/cd Changes to the action workflows concurrency devtools Changes to the devtools module dnd Changes to the dnd module docs Changes to the documentation forms Changes to the forms module github Changes to the github meta data files i18n Changes to the i18n module media Changes to the media module plugin Changes to the plugin module router Changes to the router module server ssr Changes to the ssr module storybook Changes to the storybook module testing Changes to the testing module tests Chenges to the tests view Changes to the view module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants