# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).


## 0.5.0 - 2026-06-11
### Added
* ESM compatibility layer (#M10): package.json now declares an "exports" map with dual CJS/ESM entry points — `import gina from 'gina'` and `import gna from 'gina/gna'` work natively in ESM projects and modern bundlers (default exports only; the gina/gna getter semantics are preserved). CJS `require()` resolution is byte-identical to before, and TypeScript declarations keep resolving via per-entry "types" conditions. The documented public surface is the bare specifier, ./gna and ./package.json; undeclared deep subpaths are no longer resolvable.
* Extension-keyed template-engine dispatch (#M11): a single bundle can now mix swig and nunjucks — a templates.json section declaring `"ext": ".njk"` (or a `self.setTemplate(file, '.njk')` call) renders through nunjucks and `".swig"` through swig, regardless of the bundle-level `render.engine` setting; `.html` and any other extension keep following `render.engine`, so existing bundles are unchanged. Bundles with `.njk` sections get the same fail-fast NUNJUCKS_NOT_INSTALLED startup check as `render.engine: "nunjucks"` bundles.
* Nunjucks Inspector parity (#M11): dev-mode nunjucks pages now render the Inspector statusbar (the statusbar template is rendered through nunjucks and spliced post-render with a dollar-safe function replacer) and expose the query log as `data.page.queries` alongside the already-piped flow timeline — the Inspector Queries tab no longer renders empty for nunjucks bundles. Closes the within-Inspector parity gaps against the swig render path.

## 0.4.7 - 2026-06-11
### Added
* Csp plugin: new opt-in reportOnlyOmit option — an array of directive names to omit from a Content-Security-Policy-Report-Only header and emit again automatically when reportOnly flips to false, so one directive set covers both modes. Built for engine-divergent directives such as frame-ancestors (reported in report-only by Chrome and Firefox, ignored with a console warning by Safari/WebKit): a bundle can trade the Gecko/Blink report signal for a clean WebKit console as an explicit, lifecycle-managed choice. Entries are validated against the CSP Level 3 whitelist, a factory-time warning names what was dropped, and the default output is unchanged.
* HTTP/2 server (Isaac engine): opt-in RFC 8441 extended CONNECT enablement via the new settings.json http2Options.enableConnectProtocol flag (strict boolean, default false). When enabled on an http/2 bundle (https or cleartext h2c), the server advertises SETTINGS_ENABLE_CONNECT_PROTOCOL and handles extended CONNECT streams — the WebSocket-over-HTTP/2 handshake transport — with clean HTTP-status refusals as groundwork for the upcoming WebSocket session bridge: plain CONNECT keeps its 405, non-websocket :protocol values and not-yet-consumed websocket streams get 501, and an HTTP/1.1 CONNECT arriving over the allowHTTP1 fallback is refused with a raw 405 instead of an unanswered connection drop. A new extendedConnect counter joins the http2 metrics block in /_gina/info. Deployments that leave the flag off are byte-identical to previous releases.
* New lib/ws-framing framework library: a dependency-free RFC 6455 WebSocket framing codec — frame encoder (minimal length encoding across the 7/16/64-bit forms, control-frame constraints, optional masking) and incremental per-connection parser (fragmentation reassembly, client-masking enforcement, UTF-8 validation of text payloads and close reasons, close-status-code validation, and DoS caps maxPayload/maxFragments enforced from declared frame lengths before any payload is buffered). Registered on the lib registry as wsFraming; transport-agnostic groundwork for WebSocket over HTTP/2 (RFC 8441 extended CONNECT).
* WebSocket over HTTP/2 (RFC 8441): bundles can now serve WebSocket endpoints over the extended-CONNECT transport. From onInitialize, app.onWebSocket(path, handler) registers a handler per exact pathname; matched streams are answered with :status 200 and handed to the handler as a lib/ws-session session — send/ping/close API with automatic pong replies, a timed close handshake, backpressure passthrough, full teardown on stream close/error, contained consumer-handler exceptions (a throwing handler closes the session with 1011 instead of crashing the bundle), and graceful SIGTERM drain (live sessions are closed with 1001 going away instead of blocking shutdown). Requires an http/2 bundle (https or cleartext h2c) with http2Options.enableConnectProtocol set to true; unknown pathnames are refused with 404, registrations on a non-HTTP/2 bundle warn instead of crashing, and no express middleware runs on the CONNECT path — authentication is the handler's responsibility from the request it receives.
### Fixed
* HTTP/2 server (Isaac engine): cleartext h2c bundles now receive the same hardening options as https bundles — the SETTINGS advert (maxConcurrentStreams, initialWindowSize, maxHeaderListSize, enablePush off) and the session flood caps (maxSessionRejectedStreams, maxSessionInvalidFrames) are passed to http2.createServer instead of a bare allowHTTP1 literal. Previously an h2c bundle (e.g. behind a TLS-terminating reverse proxy) advertised protocol defaults — effectively unlimited concurrent streams with server push enabled — and silently ignored its settings.json http2Options overrides. The RFC 8441 enableConnectProtocol opt-in (WebSocket over HTTP/2) now applies to h2c bundles as well; it remains a strict opt-in defaulting to false.
* Graceful shutdown: bundles with live WebSocket or engine.io connections no longer block SIGTERM shutdown until the hard timeout — every accepted engine.io socket and every /_gina/agent Inspector WebSocket now registers a per-socket closer in the framework shutdown drain (the same registry the Inspector SSE endpoints and WebSocket-over-HTTP/2 sessions already use), so shutdown closes them before the HTTP server close: engine.io sockets get a graceful transport close, agent WebSockets a clean 1001 going-away close handshake.

## 0.4.6 - 2026-06-10
### Added
* Added an opt-in async template-loader extension point for the swig engine (settings.template.swig.loader): render templates from a custom backend instead of the local filesystem through an isolated per-bundle engine, with a built-in in-memory loader and a CVE-2023-25345 path-traversal guard. Remote/object-storage loaders and full asset-injection parity land in follow-ups.
* The async swig template-loader path (settings.template.swig.loader) now injects the gina client runtime — stylesheets, scripts, external plugins and the client bootstrap — into rendered pages, so a full page served from a custom/off-disk backend ships the client bundle and is production-usable. Per-request CSP nonces are honoured on the injected bootstrap script.
* The async swig template loader gains an "http" built-in (settings.template.swig.loader.type: "http") that fetches templates over HTTP(S) from a configured origin + basePath — for templates served from a CDN, object storage or a CMS endpoint instead of the local filesystem. A Tier-1 source cache (keyed by resolved URL) collapses per-render fetches with a default 60-second TTL, plus opt-in ETag revalidation (revalidate: true). Every resolved URL is containment-checked to stay under the configured origin and basePath.
* The async template-loader extension point now supports the nunjucks engine. A bundle using render.engine "nunjucks" can configure settings.template.nunjucks.loader (the same "memory" and "http" built-ins available for swig) to render templates from a custom async backend — remote, CDN, object storage, or in-memory — instead of the local filesystem. The page template and its full extends/include chain are fetched through the loader, with the CVE-2023-25345 path-traversal guard applied on every hop. Bundles without a nunjucks loader render from the filesystem exactly as before.
* Popin plugin: opt-in `preOpen` option that fills the popin with a loading skeleton and opens it (a born native modal in dialog mode, an active overlay in div mode) before the XHR returns, then swaps in the real content on completion. Pass a `loadingShell` HTML string for custom skeleton markup, or omit it for the built-in gina skeleton. Off by default; lets a consuming app delete its own pre-open observer.
* Opt-in compiled-template reuse for async template loaders, via settings.template.swig.loader.cache and settings.template.nunjucks.loader.cache (boolean, default off, automatically disabled in development). When enabled, the per-bundle engine/environment and its compiled templates are shared across requests instead of being rebuilt per render. Off by default so template edits are always picked up live without a restart.
* New `data-gina-dialog` dialog API built on the native `<dialog>` element: `data-gina-dialog="ID"` opens an in-page dialog, `data-gina-dialog-src="URL"` loads content over Asynchronous JavaScript and XML (AJAX), `data-gina-dialog-target="#sel"` does a partial (slot-only) replace that preserves the dialog chrome, and `data-gina-dialog-modal` forces modal or non-modal. Adds hover/focus preloading of AJAX content, automatic Accessible Rich Internet Applications (ARIA) wiring (aria-haspopup, aria-controls, and aria-labelledby pointed at a real title element), focus return to the trigger on close, and `[autofocus]` support. The legacy `data-gina-popin-name` and `data-gina-popin-url` trigger attributes keep working unchanged; a legacy-only trigger still opens through its existing popin handler. The legacy attributes are aliased onto the new dialog path with a one-time deprecation warning only when combined with a new `data-gina-dialog` / `data-gina-dialog-src` attribute on the same trigger. The engine-managed `data-gina-popin-is-link` / `data-gina-popin-loading` attributes are unchanged.
* The `gina version` command (and its `gina framework:version` long form) now prints the bundled template engine name and version beneath the middleware line — for example `Template engine: @rhinostone/swig@2.7.2`. The line is shown only when the engine package can be resolved and is omitted from `gina version --short=true` output.
* Added request.rawBody on the isaac engine: the exact unparsed request body is now preserved before parsing, so controllers and middlewares can verify inbound webhook signatures (an HMAC computed over the raw bytes). Always-on and cheap; the multipart/form-data upload path is unaffected.
### Changed
* CSP plugin: report-only policies (`reportOnly: true`) now omit the `sandbox` directive, which browsers ignore in report-only mode and warn about in the console. Behaviour is unchanged — `sandbox` does nothing in report-only — and it is still emitted in enforcing mode from the same config, so a single directive set works across both modes. A report-only policy whose every directive is report-only-inert now throws at startup.
* Dialog popins now render as native modals in development too, matching production (dev/prod parity). The manual overlay is retained only for non-dialog popin mode.
* Dialogs opened via the new `data-gina-dialog` API default to non-modal (the native `<dialog>.show()` path). Set `data-gina-dialog-modal` on a trigger, or `gina.config.popin.modal: true` project-wide (then opt individual dialogs out with `data-gina-dialog-modal="false"`), to make them modal. Legacy `data-gina-popin-*` popins are unaffected and still open modal. Non-modal dialogs ship the accessibility affordances that native `showModal()` provides for free but `show()` does not — Escape-to-close, body scroll-lock, background inert, and initial focus — plus native open/close and backdrop transitions via `@starting-style` and `transition: ... allow-discrete` (replacing the Safari repaint hack), gated behind prefers-reduced-motion.
* Bumped the `@rhinostone/swig` dependency from `^2.6.0` to `^2.7.0` in both manifests. The template-engine runtime is unchanged from 2.6.0 — 2.7.0 only adds the separate `@rhinostone/swig-django` package (not used by gina) plus a changelog-only fix — so this is a version-currency update.
### Fixed
* Fixed the getWebroot template filter (swig and nunjucks engines): it threw a TypeError on every invocation because it resolved bundle config from an undefined object reference. Cross-bundle absolute webroot links now resolve correctly through the bundle env config.
* Offline project-asset CLI commands (bundle:list, secrets:scan, i18n:scan, and similar) no longer abort when a single registered project is stale (its directory exists but its manifest.json is gone). The stale project is now warned and skipped instead of crashing the whole command.
* Async template-loader renders (settings.template.swig.loader / settings.template.nunjucks.loader) no longer leak per-request context — web root, generated URLs and locale — between concurrent requests that share a template engine. The context-bearing template filters now resolve each request's context through a per-request AsyncLocalStorage store at call time, so interleaved async renders can no longer overwrite one another mid-render.
* `gina project:add` no longer crashes with a `TypeError` when registering a project alias while another already-registered project's bundles are present on disk (for example, the same path registered under a second alias). The bundle-asset loader's per-bundle default-environment write indexed the bundle map by the active project name instead of the loop's current project; it now uses the loop variable, matching every sibling write.
* Fixed the browser form-validator sending JSON request bodies with an application/x-www-form-urlencoded Content-Type. The server body parser url-decoded the JSON payload (replace + with space, then decodeURIComponent) before JSON.parse, corrupting + and % inside string values such as gmail-style email "+aliases" (alias+test@x became alias test@x). The non-binary submit path now sends application/json for a JSON body while still honoring an explicit form enctype, and the file-removal request defaults its JSON body to application/json. Browser bundle rebuilt.
* `application/json` request bodies are now parsed as JSON verbatim. Previously POST, PUT and PATCH JSON bodies were URL-decoded and form-coerced the same way as `application/x-www-form-urlencoded` data: a string value of "true", "false" or "null" was turned into a boolean or null, and a percent-escape such as %20 inside a string value was decoded, both of which could corrupt JSON payloads. JSON bodies now preserve the client's exact types and string contents; form-urlencoded handling is unchanged.
* A bundle whose `connectors.json` declares no actual connector — only the `$schema` annotation that `gina bundle:add` scaffolds, or an empty `{}` — failed to finish starting up: the model loader never invoked its completion callback, so the server never began listening (`gina bundle:start` reported "taking too long"). Such bundles now start correctly. Bundles that declare a real connector, or omit `connectors.json` entirely, are unaffected.
* Fixed self.query() POST, PUT and PATCH requests with a non-empty body to application/json routes, which returned HTTP 500 (POST/PATCH) or silently dropped the body (PUT). The built-in client percent-encoded the JSON body while the server parsed it verbatim; the client now sends raw JSON, and the server also tolerantly accepts a percent-encoded body as a fallback for backward compatibility.
* Fixed the development Inspector showing a stale value (such as the template-engine version badge) after a bundle restart. When the inspected page is reached through a window.opener tab that predates the restart, the Inspector now keeps the live agent (SSE/WebSocket) data instead of letting that stale-process snapshot override it, so the badge and environment reflect the running bundle.
* Fixed a crash where a single request carrying a malformed percent-escape in its URL or query string could take the whole bundle down. A bare "%", an invalid escape such as "%zz", or an "inheritedData" value containing a literal "%" (for example {"x":"50%off"}) made an unguarded decodeURIComponent/decodeURI throw URIError, which escalated to an uncaught exception and shut the bundle down — including from inside the error handler itself, so even a malformed-percent URL to a missing asset could crash the process instead of returning a normal 404. Such values are now decoded tolerantly — a malformed escape falls back to the raw value instead of throwing — and the redundant second decode of already-decoded query parameters on the GET and HEAD paths has been removed. A single malformed request no longer crashes the server, on either engine.
* project:add now links gina into the project by invoking the running install's own CLI instead of a PATH-resolved binary, and reports the failure instead of printing success when the node_modules/gina symlink could not be created — a scaffolded bundle no longer crashes at boot with MODULE_NOT_FOUND when gina is not on the PATH (repo checkouts, non-global installs).
* Fixed the dev-mode Inspector statusbar and its launch link disappearing on content-heavy pages. The framework spliced its inline dev scripts before the closing body tag with a plain-string String.replace, so special dollar patterns in the injected content (a backtick or quote in the statusbar template, or SQL dollar placeholders in the captured flow log) expanded and corrupted the injected script. The affected splices now use function replacers.
* framework:link's stale-node_modules repair path now invokes the running install's own CLI for link-node-modules instead of a PATH-resolved binary, and a failure is reported through the command's error path instead of crashing as an uncaught throw (the previous error check was dead code — execSync throws on failure rather than returning an Error).
* The remaining framework self-invocations no longer resolve the gina binary from the PATH: the CmdHelper auto-link step (link-node-modules and link on project/bundle start, stop and restart) now invokes the running install's own CLI in a try/catch (its previous error check was dead code — execSync throws rather than returning an Error), and framework start/restart/stop plus the running-bundles restart loop now invoke the install's own bin/gina wrapper resolved from the framework location, preserving the detached-daemon spawn semantics.
### Security
* Removed the dead external Cross-Origin Resource Sharing (CORS) proxy (`corsacme.herokuapp.com`) that the popin and link loaders rewrote cross-origin request URLs through. It was an unmaintained third-party host that would have routed user traffic — including any credentials — through it, and no longer resolves. Cross-origin requests now go directly to the target server and rely on its own CORS headers; credentials are dropped on cross-origin requests unless explicitly opted in.
* The default Swig render path now keeps the swig-core CVE-2023-25345 path-traversal confinement active: `{% include %}` / `{% extends %}` / `{% import %}` resolution is confined to the bundle templates root, so a traversal target such as `{% include "../../../etc/passwd" %}` is rejected by the loader. To restore that confinement without an opt-out, the processed-layout cache was relocated under the bundle templates root (a `.gina-layout-cache` directory, auto-ignored via a generated `.gitignore` so it never surfaces in a project repo) and the dev inspector statusbar is now inlined into the layout instead of being included from outside the root.
* Bumped the `@rhinostone/swig` floor from `^2.7.0` to `^2.7.2` in both manifests, so every install is guaranteed swig-core 2.7.2 — the version that carries the CVE-2023-25345 path-traversal loader confinement (added in 2.7.1) and also repairs a 2.7.1 regression in which a relative loader basepath made the confinement wrongly reject every in-root `{% include %}` / `{% extends %}` / `{% import %}`. The prior `^2.7.0` range still permitted the unpatched 2.7.0 (a clean install resolved higher, but a stale lockfile or a deduped 2.7.0 in the tree would not); gina relies on the swig-core loader confinement on its default render path, so the floor guarantees both the path-traversal fix and the relative-basepath repair.

## 0.4.5 - 2026-06-06

_Additive release — opt-in structured (JSON) logging with per-request `requestId` / `durationMs` correlation, plus public SDK-handle accessors (`getCluster()` on Couchbase, `getClient()` on MongoDB) for driver-level features such as multi-document transactions. No breaking changes, no settings reset._

### Added
* Opt-in structured (JSON) log output. Set `GINA_LOG_FORMAT=json` to emit one machine-parseable JSON object per log line (`{ts, level, bundle, message}`, with `group`/`msg` kept as back-compat aliases) for log collectors such as Loki, Datadog, Fluentd or CloudWatch. The default stays the coloured, human-readable format, so interactive `docker logs` output is unchanged. The existing `GINA_LOG_STDOUT` container flag continues to enable JSON output.
* Added a public `getCluster()` accessor on Couchbase entities that returns the underlying SDK Cluster handle, so applications can use SDK-level features the entity layer does not wrap — notably multi-document transactions via `cluster.transactions().run(...)` — without reaching into private connection internals. Transaction support depends on the Couchbase driver your project installs (SDK 3.2+ / 4.x).
* Added a public `getClient()` accessor on MongoDB entities that returns the underlying driver `MongoClient`, so applications can reach driver-level features the entity layer does not wrap — notably multi-document transactions via `client.startSession()` / `session.withTransaction(...)` — without reaching into private connection internals. Multi-document transactions additionally require a replica-set or sharded deployment and depend on the `mongodb` driver your project installs.
* Opt-in per-request `requestId` and `durationMs` fields in structured (JSON) logs. When `GINA_LOG_FORMAT=json` is active, every log line emitted during a request is tagged with a request id (an inbound `X-Request-Id` is honoured when well-formed, otherwise one is generated) and the elapsed milliseconds since the request began, so lines from a single request can be correlated in a collector such as Loki, Datadog, Fluentd or CloudWatch. Default text logging is unchanged and pays no overhead.

## 0.4.4 - 2026-06-05

_Additive release — `templates.json` gains comma-separated section keys (share one stylesheet/script block across several routes at once) and an optional `_common.config` block for page-level defaults. No breaking changes, no settings reset._

### Added
* templates.json section keys may now be comma-separated (for example "products, productDetail") to share a stylesheet or script block across several routes at once. The block is replicated under each named section, merging into any section you also declare on its own, so a section's own keys win on collision.
* templates.json now accepts an optional `_common.config` block for page-level defaults (such as routeNameAsFilenameEnabled or javascriptsDeferEnabled). It is flattened back into _common at load time, so existing bundles are unaffected and any direct _common key still overrides the config block.

## 0.4.3 - 2026-06-04

_Additive release — the standalone Inspector connects over an authenticated WebSocket (with `gina service:start` and dev auto-start), `FormValidator` reflects validity into `aria-invalid` (#A11Y1), the popin / link / validator plugins drop inline `onclick` for strict-CSP compatibility, and the `@rhinostone/swig` floor moves to `^2.6.0`. No breaking changes, no settings reset._

### Added
* Added an authenticated WebSocket transport for the Inspector agent endpoint (/_gina/agent), alongside the existing SSE channel. The standalone Inspector can stream a bundle data + log feed over a single WebSocket. Outside dev mode the upgrade requires the configured inspector.agent.key (presented via ?key= or the x-gina-inspector-key header) and honours an optional inspector.agent.allowedOrigins Origin allowlist.
* The standalone Inspector SPA can stream a bundle data + log feed over the WebSocket transport by opening with ?transport=ws (SSE stays the default and the automatic fallback if the socket cannot open). The optional ?key= is threaded on the WebSocket URL for an auth-gated endpoint.
* Added a `gina service:start <service>` command to start a framework-internal @gina service (such as the standalone Inspector) via the daemon-free gina-container launcher, with double-start protection, `--dry-run` and `--format=json`. In dev mode the Inspector now auto-starts when a bundle boots (a no-op unless the framework services project is scaffolded locally).
* FormValidator now reflects each managed field's validity into `aria-invalid` (#A11Y1): `true` on a committed error and `false` when valid — mirroring the native `ValidityState` where the field has native HTML constraints, so it agrees with the `:user-invalid` styling already shown. This exposes any `aria-errormessage` association on the field to assistive technology, which is otherwise inert without `aria-invalid`. When the field already references an error element via `aria-errormessage`, the redundant `form-item-error-message` div is suppressed; legacy forms with no association keep the injected div and gain an `aria-errormessage` wire to it. On a failed submit, focus moves to the first invalid field so assistive technology announces it; blur-time errors are announced through a per-form visually-hidden `aria-live="polite"` region. No public API change; existing error classes and attributes (`form-item-error`, `form-item-error-message`, `data-gina-form-errors`, submit-button state) are unchanged.
### Changed
* The standalone Inspector now connects to the /_gina/agent endpoint over a WebSocket by default, falling back to SSE automatically if the socket cannot open. Open the Inspector with ?transport=sse to force the SSE transport. Authentication and the production toggle are unchanged (the WebSocket reuses the same inspector.agent key).
* Bumped the @rhinostone/swig dependency floor from ^2.5.1 to ^2.5.3 in both the root package.json and framework/v*/package.json. swig 2.5.2 fixes dynamic {% extends %} resolution on the async-loader render path in the Twig and Jinja2 frontends; 2.5.3 turns a dynamic {% extends %} on the synchronous render path into a clear error pointing to the async render path instead of a generic failure. The native @rhinostone/swig package stays drop-in compatible with the API gina depends on (swig.compile, swig.setFilter, swig.setTag, swig.renderFile). swigResolver DEFAULT_MIN stays at 2.0.0 — the framework does not depend on any new 2.5.x-only API.
* Bumped the @rhinostone/swig floor to ^2.6.0 in both the root and framework manifests. 2.6.0 makes the native json / json_encode filters HTML-escape their output and marks them (and url_encode) safe, so {{ data|json }} is safe to embed directly inside a <script> block (url_decode is unchanged). swigResolver DEFAULT_MIN stays at 2.0.0 — the framework depends on no new 2.6.0-only API.
### Fixed
* Popin close elements no longer get an inline onclick="return false;" attribute injected at bind time, so popins work under a strict nonce-based Content-Security-Policy (the inline handler tripped the script-src-attr directive). Default-action suppression is unchanged — it was already handled by the registered close listener.
* The Link plugin and the form-validator anchor submit-triggers no longer inject an inline onclick="return false;" attribute at bind time, so they work under a strict nonce-based Content-Security-Policy (the inline handler tripped the script-src-attr directive). Default-action suppression is preserved via an addEventListener-based preventDefault listener, so behaviour is unchanged.

## 0.4.2 - 2026-06-02

_Additive release — opt-in HTTP/3 advertisement (`Alt-Svc`) plus two render-path fixes (`gina-container` HTML 500, layoutless `page.data` restore). No breaking changes, no settings reset._

### Added
* Added an opt-in `Alt-Svc` HTTP/3-advertisement response header. Set `server.http3Advertisement: true` to emit `Alt-Svc: h3=":443"; ma=86400` on every routed response, advertising HTTP/3 (QUIC) availability so QUIC-capable clients upgrade via a QUIC-capable edge proxy (Caddy / nginx-QUIC / Cloudflare). Gina does not implement QUIC — advertise only. Idempotent (an upstream-set Alt-Svc is never clobbered); off by default.
### Fixed
* `gina-container` (the Docker/K8s foreground launcher) no longer returns HTTP 500 on every HTML-rendering route. The controller read the `GINA_PID` and `GINA_CULTURE` globals directly, which the daemonless launcher does not inject; it now falls back to the bundle process id and the default culture.
* Layoutless renders (`self.renderWithoutLayout`) now expose controller data both at top level and under `page.data` again. The 0.4.1 top-level-variable change populated only the top level, so fragment templates reading `data.X` / `page.data.X` (via `{% set data = page.data %}`) rendered empty; this restores the `page.data` copy alongside the top-level merge, matching the nunjucks engine. The copy runs before the top-level merge to avoid a circular reference in the layoutless XHR-data serialization.

## 0.4.1 - 2026-06-01
### Added
* New `gina bundle:status` and `gina project:status` CLI commands report the running/stopped state, PID, port, and active env of bundles (human-readable or --format=json).
* gina minion:list lists the running bundle child-processes ("minions") of a project, or of every registered project, grouped by project. Supports --format=json for machine-readable output.
* gina minion:kill reaps the running bundle child-processes ("minions") of a project (both pidfile-tracked and orphaned ps-discovered ones) with a graceful SIGTERM-then-SIGKILL escalation, a --dry-run preview, and --format=json output.
* gina protocol:remove <bundle> @<project> reverts a bundle to the project default protocol and scheme by removing its protocol override from the bundle settings; supports --dry-run and --format=json, and guards against reverting to an unallocated port.
* Added self.setTemplate(file, ext) to override the rendered template from a controller action at run time (e.g. a catch-all dispatcher); honoured by the swig render path.
* Added bundle:copy (alias bundle:cp) to duplicate a bundle under a new name within the same project. It copies the source tree, rewrites the bundle-name footprint (controller class names, the gina require-var, app.json name, webroot) in the copied .js/.json files, allocates a fresh full port matrix, and registers the new bundle in manifest.json + env.json + the ports registry. Supports --dry-run (preview every rewrite site before writing), --force (overwrite an existing target), and --format=json.
* Added bundle:rename to rename a bundle in place under a new name within the same project. It moves the source tree, rewrites the bundle-name footprint (controller class names, the gina require-var, app.json name, webroot) in the moved .js/.json files, and rekeys manifest.json + env.json + the ports registry preserving the existing port numbers. The bundle must be stopped first (a running bundle is refused, with no --force bypass). Supports --dry-run (preview every rewrite site before writing), --force (overwrite an existing bundle that already holds the new name), and --format=json.
### Fixed
* Fixed `gina bundle:add` surfacing an opaque `ERR_USE_AFTER_CLOSE` ("readline was closed") rollback when the target bundle already exists and the command runs without an interactive terminal (container entrypoint, CI, or piped stdin). It now fails fast with a clear message to re-run with `--import` (register the existing source) or `--replace` (overwrite it).
* Extended the non-interactive (no-TTY) guard to the remaining interactive CLI commands — `bundle:remove`, `project:remove`, `protocol:set`, `port:set`, and `view:add`. When run without a terminal (container, CI, or piped stdin) and missing the values they would otherwise prompt for, they now fail fast with guidance (e.g. `--force`, or `--protocol`/`--scheme`/`--port`/`--env`) instead of throwing an opaque `ERR_USE_AFTER_CLOSE` ("readline was closed").
* Fixed the `gina` CLI crashing with an opaque `could not load package.json` / `MODULE_NOT_FOUND` when `GINA_VERSION` resolves to a framework version that is not installed (a stale version pin, or a bind-mounted development tree at a different version). `bin/cli` now verifies the framework directory exists before requiring from it and fails fast with a clear message pointing at `gina framework:install <version>`, instead of letting every CLI command fail opaquely.
* Fixed a crash that could kill a bundle serving static assets over HTTP/2 in dev mode under concurrent requests.
* Fixed renderWithoutLayout returning an empty body when the template referenced controller data as top-level variables.
* Brought the nunjucks render path to parity with swig: self.setTemplate() runtime overrides are now honoured, a bundle's controllers/setup.js can register filters via this.engine.addFilter(), and a controller passing a partial page object no longer drops framework-injected page data (webroot, view metadata, session).

## 0.4.0 - 2026-05-29
### Added
* Inspector View tab now shows the template engine version next to the engine name (e.g. `Swig | 2.4.0`). The version is sourced server-side from the cached swig / nunjucks resolver decisions and falls back to a name-only badge when no version is resolvable (very-early bootstrap, test stubs, legacy render paths).
* Inspector Query tab — SQL index badges now do column-level coverage matching. A new badge marks a query whose WHERE columns are not covered by any declared index (leftmost-prefix match against the bundle indexes.sql), instead of only checking whether the target table has any index.
* Added gina secrets:scan and gina secrets:check CLI commands that introspect the ${secret:KEY} placeholders your bundle configs require. secrets:scan reports each required key and the config file(s) that declare it; secrets:check cross-references the current environment, marks each key SET or UNSET, and exits non-zero when any required key is missing (for CI / pre-deploy gating). Both support --format=json.
* gina secrets:scan and secrets:check gain --scope=<scope> to report the effective required secrets for a deployment scope (read-only deep-merge of the sibling config_<scope>/ dirs over base config, scope wins), and secrets:check gains --env-file=<path> to validate against a .env-style file (e.g. a decrypted SOPS export) instead of the live process.env. The runtime config loader is unchanged — per-scope config selection stays a deploy-time concern.
* Added opt-in HTTP/2 response trailer support via self.sendTrailers(fields). A controller registers trailing headers before rendering, and the render pipeline sets waitForTrailers on the HTTP/2 stream and emits them after the response body (in the wantTrailers event) — useful for gRPC-style streaming (a final grpc-status / grpc-message) or a content-integrity Digest after a chunked body. Pseudo-header keys (those beginning with a colon) are stripped. The feature is fully opt-in: a no-op on HTTP/1.1 and when no trailers are registered, so existing responses are byte-for-byte unchanged.
* Added lib.job, an async-job primitive for running slow work out-of-band. lib.job.create(fn) enqueues a deferred async function and returns a job id immediately; the function runs on a concurrency-limited worker and its outcome moves through pending, running, then completed or failed, and is queryable via lib.job.get(id, cb). Stale terminal jobs are purged on a configurable TTL by a self-contained sweep. Tune via the app.json jobs block (maxConcurrency, ttl, sweepInterval, idSize); sane defaults apply when the block is omitted.
* Added the self.startJob(fn) and self.jobStatus(id, cb) controller helpers for async jobs. self.startJob runs a deferred async function out-of-band and returns a job id immediately (a thin wrapper over lib.job.create); self.jobStatus reads a job record from within your own authenticated route. The deferred function runs after the request has completed, so capture plain values rather than the request or response objects.
* Added a built-in /_gina/jobs/:id status endpoint that reports an async job state (pending, running, completed, or failed). It is always-on and state-only (it never returns the result or error payload) and works under both the Isaac and Express engines. Retrieve a completed job result from your own authenticated route via self.jobStatus.
* Added the self.inferAsync(messages, options) controller helper, which runs a model inference as an async job in one call and returns a job id immediately. It wraps the AI connector through self.startJob; the stored job result is the trimmed inference (content, model, usage). Select the connector with options.connector, poll /_gina/jobs/:id for state, and read the result from your own authenticated route via self.jobStatus.
* Added opt-in completion webhooks for async jobs. Pass a callbackUrl when creating a job (lib.job.create(fn, { callbackUrl }) or self.startJob(fn, { callbackUrl })) and the framework POSTs { id, state, result, error } to it when the job finishes. Delivery is best-effort with retry and exponential backoff; persistent failures are recorded on the job as webhookFailed and never affect the job outcome. Configure a per-bundle webhookSecret (app.json jobs.webhookSecret, ideally a secret placeholder) to sign each payload with an X-Gina-Signature HMAC-SHA256 header.
* Added an opt-in API-key gate for the dev-mode /_gina/agent Inspector SSE stream. Set inspector.agent.enabled and inspector.agent.key in settings.json to reach the endpoint outside dev mode (e.g. staging) for authenticated remote server-log streaming; the key is presented via the x-gina-inspector-key header or a ?key= query param and compared in constant time. In dev the endpoint stays open with no key (unchanged).
* Added a toggleable Inspector instrumentation window (#INS10): POST /_gina/instrument opens a time-boxed, key-authenticated window that runs query + flow capture outside dev mode and streams it over the authenticated /_gina/agent SSE (JSON/XHR responses + cross-bundle queries). Opt-in via settings.json inspector.instrumentation; 300s default window, 3600s hard cap, auto-revert.
* Inspector instrumentation windows now stream the controller queries and flow timeline of a server-rendered HTML page over the authenticated agent SSE, not just JSON and XHR responses. The page HTML is never modified; the capture leaves only over the key-authenticated channel while a window is open.
* gina.plugins.Csp({ useNonce: true }) generates a fresh per-response CSP nonce, stamps it on req._ginaCspNonce, and appends a nonce source-expression to script-src (fallback default-src). The swig and nunjucks renderers set a matching nonce attribute on the framework-injected onGinaLoaded bootstrap script — letting bundles drop unsafe-inline from script-src. Opt-in (default false); existing bundles are unaffected.
* The dev-mode Inspector inline scripts (window.__ginaData snapshot + console-capture) and the client-side metrics-patch scripts now carry the gina.plugins.Csp({ useNonce: true }) per-response nonce in both the swig and nunjucks renderers, so a bundle running strict CSP in dev no longer reports violations for those framework-injected inline scripts. Extends the #HDR16 onGinaLoaded bootstrap nonce.
* gina.plugins.Csp({ useNonce: true }) now exposes the per-response nonce to your templates as {{ page.cspNonce }} (swig) / {{ cspNonce }} (nunjucks), so you can add a matching nonce attribute to your own inline <script> tags and drop 'unsafe-inline' from script-src. The dev-mode Inspector status bar inline script carries it automatically. Opt-in (default false); bundles that don't set useNonce are unaffected.
### Changed
* Inspector View tab — the server-emitted Weight and Load fallback values now render in italic with a slightly deeper dim (new `bm-vbadge-svr` style), so a value sourced from the server reads distinctly from the client-measured primary and the engine-version dim.
* gina.plugins.Coop() — documented that the default `same-origin` value severs the `window.opener` link the dev Inspector relies on to read client-side timing, so the View tab Weight and Load badges fall back to server-rendered metrics (FCP stays browser-only). Dev-only — the Inspector is gated on NODE_ENV_IS_DEV.
* Bumped @rhinostone/swig dependency floor from ^2.4.1 to ^2.5.1 in both root package.json and framework/v*/package.json. Cumulative content: 2.5.0 introduced the Jinja2 frontend as a published sibling package (@rhinostone/swig-jinja2 — block, extends, include, import, from, macro, for, set, with, filter, raw, autoescape tags + full Jinja2 filter set + async loader via renderFileAsync/compileFileAsync); 2.5.1 added the new sibling to the workspace README family list, restored JSDoc on the top-level public API (regression from engine.install carve), and is dev-only otherwise. swigResolver DEFAULT_MIN stays at 2.0.0 since the framework does not depend on any new 2.5.x-only API. The lib/swig-resolver/src/main.js docstring is extended in the same commit to mention @rhinostone/swig-jinja2 alongside @rhinostone/swig-twig as a known sibling alternate; bundles can opt into either via swig.package + swig.useProject (same shape as the existing Twig opt-in).
* Upgrading to 0.4.0 is a shortVersion bump (0.3 to 0.4): the framework creates a fresh ~/.gina/0.4/settings.json from defaults on install, so any customizations made under 0.3 (log level, port, culture, timezone, etc.) are not carried forward. Re-apply them after upgrading with gina framework:set, or copy the values across from ~/.gina/0.3/settings.json.
### Removed
* Removed the Couchbase SDK v2 ORM connector and session store (connector.v2.js and session-store.v2.js). Couchbase Server SDK v2 reached end-of-life in 2021, so only SDK v3 and v4 are now supported, and the connector defaults to v3 when no couchbase version is pinned. Migrating is a driver bump rather than a config change — upgrade the couchbase dependency in your project to ^3 or ^4 (for example, npm install couchbase@^4). Bundles still resolving to SDK v2 now fail fast at connector load with an explicit upgrade message instead of loading the removed code path.
### Fixed
* Hardening `npm install -g gina` against npm setups that don't propagate `npm_config_global` to the post-install subprocess: `isGlobalInstall` now also detects from `__dirname`, and the local-install symlink step no-ops when its source path is missing instead of crashing the install.
* throwError(statusCode, Error|string) 2-arg form now honors the explicit status code. Previously the inner status-coercion read .status from the wrapped error rather than from the number itself, so any explicit code other than 500 was silently lost. The 2-arg-with-errorObj shape was unaffected and stays unchanged. Documented in types/index.d.ts as a new overload.
* Inspector View tab — restored the Weight and Load badges when `window.opener.performance` is unreachable (e.g. host page sets `Cross-Origin-Opener-Policy`, which nulls the popup opener reference). Server-side metrics (`weightBytes` from the response body byte length, `serverMs` from the request timeline) are now emitted on `__ginaData.user.environment.metrics` and used as fallback by the client. When both client + server values are available, the badge renders as a dual `<server-dim> | <client-primary>` pair — surfacing the compression ratio for Weight and the network/parse/paint cost for Load.
* Inspector View tab — restored the Weight and Load badges for bundles using the nunjucks template engine. Closes the deferred follow-up from the earlier swig-side restoration, so both engines now surface the metrics consistently under `Cross-Origin-Opener-Policy` (no `window.opener`).
* Inspector Flow tab — restored the request-lifecycle timeline for bundles using the nunjucks template engine. The nunjucks render delegate now builds `data.page.flow` (with N1QL queries converted into timeline entries) and late-binds the response-write/total entries via the same `_flowPatch` mechanism as the swig delegate, so the Flow waterfall is populated for nunjucks pages (previously entirely empty). Closes the deferred Flow-tab follow-up from the earlier swig-side restoration.
* SQL Inspector index-coverage badges now stay accurate after a live /_gina/indexes refresh — the MySQL, PostgreSQL and SQLite introspection paths capture each index column, so column-level coverage no longer degrades to a false no-index-for-filter badge once live introspection has run.
* The Inspector Query tab now flags SQL queries whose WHERE filter no existing index can serve, even for bundles without an indexes.sql file (resolved via live database introspection); previously these showed an optimistic index badge instead of the no-index-for-filter warning.
* Swig template engine updated to 2.4.1. Macros that call other macros imported at the top of their own defining file now resolve correctly (previously they rendered empty), recursively across import depth.
* Refreshed stale external references shipped with the framework. The man-page COLOPHON sections and the CLI framework-not-found messages now point to https://gina.io (previously http://www.gina.io); GitHub references in the man pages, the bundled lib READMEs, and the generated-project package.json template now use the gina-io organisation (previously the former rhinostone org); the vendored lib/inherits package metadata now references the canonical repository; and the framework project template no longer ships unreachable internal git-server URLs. Documentation and metadata only - no runtime or API change.
* Refreshed more stale external references flagged by the link-health sweep. The popin plugin CSS doc now links the live normalize.css domain (necolas.github.io) and drops a dead community-wiki citation (prose retained); the popin extend doc now points to current Apple developer documentation for Smart App Banners and Safari meta tags; and gina bundle:mcp emits a resolvable manifest $schema host (modelcontextprotocol.io; the retired spec. subdomain no longer resolved). Documentation and generated-metadata only - no runtime or API change.
* Fixed an HTTP 500 when rendering a request whose session uses a browser-session cookie (a session cookie with no expiry). The session-expiry display block now runs only for a real expiry date, so expiry-less sessions render normally instead of crashing.

## 0.3.15 - 2026-05-19
### Added
* New `isInList` form-validator rule for enum / membership validation. Accepts a JSON array of allowed primitive values (`"isInList": ["draft", "pending", "sent", "paid"]`) and rejects field values that are not strict-equal to any list member. Lives in the shared `form-validator.js`, so the rule fires on both server-side routing validation and client-side browser enforcement. Plugs into the existing `_case_<field>` conditional resolver without special-case handling.
### Changed
* Dropped dead `X-Powered-By: "Gina I/O - v${version}"` default from `core/template/conf/env.json`. The entry was structurally inert today — overwritten on Express by `server.js:2425`'s `setHeader` before any middleware ran, and never read by Isaac at all (`server.isaac.js` does not read `server.response.header`). Removing it has zero observable wire effect AND resolves a format inconsistency (only the canonical `Gina/<version>` shape remains on the wire). The `gina.plugins.HidePoweredBy()` middleware (Express) and `settings.json > server.hidePoweredBy: true` gate (Isaac, via the `_setPoweredByHeader()` helper at `server.isaac.js:572-577` covering all writeHead + setHeader emit sites) cleanly suppress the header. Plugin docstring, README, settings.json comments, and bundle boilerplate updated to describe the actual Phase 2 architecture.
### Security
* Added `gina.plugins.XContentTypeOptions()` — opt-in middleware that emits the `X-Content-Type-Options: nosniff` response header on every response (#HDR1). The header instructs browsers to honour the declared `Content-Type` strictly, blocking MIME-sniffing attacks where a `text/plain` response whose body starts with `<script>` could be upgraded to HTML and the script executed in the page's origin. Per RFC 7034 / WHATWG Fetch Standard, `nosniff` is the only valid value. Adoption is two lines in the bundle bootstrap: `var xContentTypeOptions = require('gina').plugins.XContentTypeOptions(); app.use(xContentTypeOptions);`. Idempotent — if an earlier middleware already set the header, the existing value is preserved and `next()` is called immediately (first- writer-wins, so the plugin is safe to stack with helmet-style upstream gates). No `enabled` flag — register to opt in, don't register to opt out. Settings template seeds `xContentTypeOptions: {}` (block reserved for future fields like per-route opt-out; future additions don't need an API break). Files: `core/plugins/lib/x-content-type-options/{src/main.js, package.json, README.md}`, registered in `core/plugins/index.js`. Boilerplate `core/template/boilerplate/bundle/index.js` advertises the adoption example as a commented-out block alongside the #CSRF1/#CSRF2 examples. Establishes the per-header response-middleware shape for the rest of Phase 1 (#HDR2 X-Frame-Options, #HDR3 Referrer-Policy, #HDR4 HSTS) and opens the new `#HDR` (Web Security Headers) roadmap track. 33 unit tests in `test/core/x-content-type-options-plugin.test.js`; full suite 5467/5467.
* Added `gina.plugins.XFrameOptions({ value })` — opt-in middleware that emits the `X-Frame-Options` response header on every response to defend against clickjacking (#HDR2). Controls whether the page may be rendered inside a `<frame>`, `<iframe>`, `<embed>` or `<object>`. Two valid values per RFC 7034: `DENY` (page may never be framed, even by same-origin pages) and `SAMEORIGIN` (default — page may be framed only by same-origin pages). Settings: `xFrameOptions.value` (defaults to `SAMEORIGIN`). Caller options always win over settings; values are normalised to uppercase (so `"deny"` is accepted and emitted as `DENY`). The legacy `"ALLOW-FROM <uri>"` value is rejected at factory call time with a dedicated error pointing at the modern `Content-Security-Policy: frame-ancestors` replacement — modern browsers ignore ALLOW-FROM (Chrome / Edge / Safari never honoured it cross-vendor, Firefox dropped it in 70). Invalid values throw at factory call time with a clear error listing the expected tokens (mirrors the #CSRF1 SameSite=None+Secure invariant and the planned #HDR4 preload-list invariant — fast-fail at bootstrap). Idempotent — if an earlier middleware already set the header, the existing value is preserved and `next()` is called immediately (first-writer-wins, safe to stack with helmet-style upstream gates). Files: `core/plugins/lib/x-frame-options/{src/main.js, package.json, README.md}`, registered in `core/plugins/index.js`. Settings template seeds `xFrameOptions: { value: "SAMEORIGIN" }` with the comment header explaining the default + the ALLOW-FROM rejection. Boilerplate `core/template/boilerplate/bundle/index.js` advertises the adoption example alongside the #HDR1 example. 51 unit tests in `test/core/x-frame-options-plugin.test.js` (full suite 5518/5518, prior 5467 + 51). Second of the four Phase 1 plugins on the `#HDR` Web Security Headers track; introduces the throw-on-invalid pattern that #HDR3 and #HDR4 will mirror.
* Added `gina.plugins.ReferrerPolicy({ value })` — opt-in middleware that emits the `Referrer-Policy` response header on every response to control referrer leakage (#HDR3). The `Referer` request header reveals the previous page URL — path and query string included — to the destination, which can leak session tokens, internal paths, account IDs, and search queries. The eight valid tokens per the W3C Referrer Policy spec: `no-referrer`, `no-referrer-when-downgrade`, `origin`, `origin-when-cross-origin`, `same-origin`, `strict-origin`, `strict-origin-when-cross-origin` (default — matches modern browsers since ~2021), `unsafe-url` (dangerous — leaks paths and queries). Settings: `referrerPolicy.value` (defaults to `strict-origin-when-cross-origin`). Caller options always win over settings; values are normalised to lowercase per the W3C spec's case-insensitive matching (so `"NO-REFERRER"` is accepted and emitted as `no-referrer`). Invalid tokens throw at factory call time with the full eight-token list + W3C spec URL in the message (mirrors the #HDR2 throw-on-invalid pattern — fast-fail at bootstrap). Idempotent — first-writer-wins. Files: `core/plugins/lib/referrer-policy/{src/main.js, package.json, README.md}`, registered in `core/plugins/index.js`. Settings template seeds `referrerPolicy: { value: "strict-origin-when-cross-origin" }` with the comment header listing all eight tokens. Boilerplate `core/template/boilerplate/bundle/index.js` advertises the adoption example alongside the #HDR1/#HDR2 examples. 56 unit tests in `test/core/referrer-policy-plugin.test.js` (full suite 5574/5574, prior 5518 + 56). Third of the four Phase 1 plugins on the `#HDR` Web Security Headers track; leaves #HDR4 HSTS as the final Phase 1 plugin (with the browser-parity invariant on `preload`).
* Added `gina.plugins.Hsts({ maxAge, includeSubDomains, preload })` — opt-in middleware that emits the `Strict-Transport-Security` response header on every response (#HDR4). Instructs browsers to access the host exclusively over HTTPS for the next maxAge seconds — defeats SSL-stripping attacks once the policy is in effect. Three configuration fields: `maxAge` (number, seconds, default 15552000 = 180 days), `includeSubDomains` (boolean, default false), `preload` (boolean, default false). Caller options always win over settings. Browser-parity invariant on `preload`: `preload:true` requires `includeSubDomains:true` AND `maxAge >= 31536000` (1 year) per the HSTS preload-list submission requirements (https://hstspreload.org/#deployment-recommendations); the factory throws at call time on invariant violations. Also throws on non-integer / negative / NaN / Infinity `maxAge`. Header value built per RFC 6797 §6.1 directive order (`max-age=<n>; includeSubDomains; preload` with max-age first and the optional directives appended in order when their fields are true). Spec deviation documented in README: emits on every response regardless of transport, matching helmet's behaviour — RFC 6797 §7.2 says senders MUST NOT include the header on HTTP, but §8.1 receiver MUST IGNORE makes the practical wire outcome identical. Design favours proxy-deployment robustness (no dependency on `x-forwarded-proto` being preserved by intermediaries) over sender-side spec purity. Idempotent — first-writer-wins. Files: `core/plugins/lib/hsts/{src/main.js, package.json, README.md}`, registered in `core/plugins/index.js`. Settings template seeds the three-field `hsts` block with `#HDR4` comment header. Boilerplate `core/template/boilerplate/bundle/index.js` advertises the adoption example alongside #HDR1/#HDR2/#HDR3 with the HTTPS-only note. 69 unit tests in `test/core/hsts-plugin.test.js` (full suite 5643/5643, prior 5574 + 69). **Fourth and final Phase 1 plugin on the `#HDR` Web Security Headers track** — closes the Phase 1 quartet of static-value response headers (X-Content-Type-Options + X-Frame-Options + Referrer-Policy + HSTS). Phase 2 (`Csp` #HDR5 + `CrossOriginPolicies` #HDR6) is deferred to `0.4.0` — dynamic / higher-break-risk headers that require template-render integration or can break legitimate cross-origin loads.
* Added `gina.plugins.OriginAgentCluster()` — opt-in middleware that emits the `Origin-Agent-Cluster: ?1` response header on every response (#HDR7). The header is a Structured Header Value (RFC 8941) requesting origin-keyed agent clustering — the page's origin gets its own agent isolated from sibling-origin pages, so same-site cross-origin pages can no longer reach in via `document.domain`. Mitigates one class of Spectre side-channel attack (the browser may place origin-keyed agents in their own OS process where possible). Per the HTML spec (https://html.spec.whatwg.org/multipage/document-sequences.html#origin-keyed-agent-clusters), `?1` is the only useful value (`?0` is the browser default and emitting it is a no-op); no tunable options today. Browser support: Chrome 88+, Edge 88+, Firefox 109+, Safari 15+ — older browsers ignore silently, safe to register unconditionally. **When NOT to register**: bundles that rely on `document.domain` to bridge same-site origins will break under origin-keyed clustering; rare in modern apps but worth checking. Mirrors the #HDR1 plugin shape EXACTLY (no validation, single static value, same `_resolveSettingsDefaults` + `_mergeOptions` helpers reserved for future-proofing). Idempotent — first-writer-wins. Files: `core/plugins/lib/origin-agent-cluster/{src/main.js, package.json, README.md}`, registered in `core/plugins/index.js`. Settings template seeds `originAgentCluster: {}` with `#HDR7` comment header. Boilerplate `core/template/boilerplate/bundle/index.js` advertises the adoption example alongside #HDR1-4. 33 unit tests in `test/core/origin-agent-cluster-plugin.test.js` (full suite 5676/5676, prior 5643 + 33). **Closes Phase 1 modern critical coverage** of the `#HDR` Web Security Headers track (5 of helmet's 9 modern headers covered; the remaining 4 are deferred to Phase 1.5 #HDR8-12 helmet-parity gap-fill and Phase 2 #HDR5 CSP + #HDR6 COEP/COOP/CORP for 0.4.0). Added 2026-05-17 after a helmet-parity audit caught the original Phase 1 framing missing Origin-Agent-Cluster.
* Added gina.plugins.Csp({ directives, reportOnly }) — opt-in middleware that emits the Content-Security-Policy response header on every response (#HDR5). Opens Phase 2 of the Web Security Headers track. v0 ships STATIC DIRECTIVES ONLY — per-response nonce wiring requires template-render integration and defers to a future CSP-aware view-layer plugin that can co-operate with swig / nunjucks template rendering. Strict whitelist of 27 CSP Level 3 standard directives (17 fetch + 2 document + 2 navigation + 2 reporting + 2 document-policies + 2 trusted-types); unknown directive names throw at factory call time because CSP typos are silent (browsers ignore unknown directives with no error, no console warning). Value parsing: array of source-list tokens (joined with space), pre-formatted string (emitted as-is), boolean true (boolean-only directives upgrade-insecure-requests / block-all-mixed-content + hybrid sandbox), boolean false (omit). reportOnly:true at top level switches the header name to Content-Security-Policy-Report-Only for non-enforcing migration testing. directives is required (factory throws if missing/empty — no sensible cross-bundle default since every bundle has its own resource graph). Mirrors the HDR1-7 plugin shape (idempotent first-writer-wins via res.getHeader) but introduces a new validation dimension: directive-name (object KEY) validation. 92 unit tests; full suite 5768/5768. Opens Phase 2 — HDR6 splits into Coep / Coop / Corp three plugins per the wrapper-consistency decision; HDR15 closes Phase 2 with gina.plugins.SecurityHeaders({...}) combined wrapper composing HDR1-7 + HDR5 + HDR6/13/14.
* Added gina.plugins.Coep({ value }) — opt-in middleware that emits the Cross-Origin-Embedder-Policy (COEP) response header on every response (#HDR6). Settings: coep.value is one of three W3C HTML spec tokens (require-corp, credentialless, unsafe-none); default require-corp matches helmet per-middleware default. Required (paired with Coop: same-origin / #HDR13) to enable SharedArrayBuffer and high-resolution performance.now() in the page. Caller options always win over settings; values are normalised to lowercase per the spec lowercase-enumeration convention. Mirrors the #HDR3 ReferrerPolicy single-enum plugin shape exactly (resolveSettingsDefaults + mergeOptions + resolveValue throw-on-invalid + idempotent first-writer-wins middleware via res.getHeader). The require-corp default BREAKS pages that load cross-origin resources (CDN fonts, images, scripts) without matching CORP/CORS headers — README walks users through the three escape hatches (set CORP via #HDR14, downgrade to credentialless, downgrade to unsafe-none). 56 unit tests; full suite 5824/5824. First of the three Phase 2 cross-origin policies — HDR13 Coop + HDR14 Corp + HDR15 SecurityHeaders combined wrapper to follow.
* Added gina.plugins.Coop({ value }) — opt-in middleware that emits the Cross-Origin-Opener-Policy (COOP) response header on every response (#HDR13). Settings: coop.value is one of four W3C HTML spec tokens (same-origin, same-origin-allow-popups, noopener-allow-popups, unsafe-none); default same-origin matches helmet per-middleware default. Required (paired with Coep: require-corp / #HDR6) to enable SharedArrayBuffer and high-resolution performance.now() in the page. Caller options always win over settings; values are normalised to lowercase. Mirrors the #HDR6 Coep single-enum plugin shape exactly (resolveSettingsDefaults + mergeOptions + resolveValue throw-on-invalid + idempotent first-writer-wins middleware via res.getHeader). The fourth token noopener-allow-popups is a W3C HTML spec addition (Chrome 119+, Firefox 131+) that severs window.opener for popups even at same-origin while keeping the popup window functional. The same-origin default BREAKS legitimate OAuth/SSO popup flows where the popup needs to call back into the opener via window.opener.postMessage(...); README walks users through the three escape hatches (pick same-origin-allow-popups for same-origin OAuth, pick noopener-allow-popups for one-way notifications back via BroadcastChannel/postMessage, downgrade to unsafe-none last resort). 61 unit tests; full suite 5885/5885. Second of the three Phase 2 cross-origin policies — HDR14 Corp + HDR15 SecurityHeaders combined wrapper to follow.
* Added gina.plugins.Corp({ value }) — opt-in middleware that emits the Cross-Origin-Resource-Policy (CORP) response header on every response (#HDR14). Settings: corp.value is one of three W3C HTML spec tokens (same-origin, same-site, cross-origin); default same-origin matches helmet per-middleware default. Resource-side complement to #HDR6 Coep require-corp enforcement — a page that sets Coep: require-corp requires its cross-origin embeds (CDN fonts, etc.) to carry a matching Corp: cross-origin (or wider) header on the embed-target bundle, otherwise the embed is blocked. Caller options always win over settings; values are normalised to lowercase. Mirrors the #HDR6 Coep + #HDR13 Coop single-enum plugin shape exactly (resolveSettingsDefaults + mergeOptions + resolveValue throw-on-invalid + idempotent first-writer-wins middleware via res.getHeader). The same-origin default defends against side-channel attacks that load a resource cross-origin to measure size/dimensions/timing for fingerprinting (and XSSI shapes are blocked) but BREAKS legitimate cross-origin embeds when the resource serves at a separate origin from the embedding page. README walks users through the three escape hatches (pick same-site for first-party multi-subdomain setups, pick cross-origin for publicly-embeddable CDN assets, per-bundle scoping where the page bundle keeps strict + the CDN bundle adopts cross-origin). 60 unit tests; full suite 5945/5945. Third and final of the three Phase 2 cross-origin policies — HDR15 SecurityHeaders combined wrapper follows as the closing slice of Phase 2.
* Added gina.plugins.SecurityHeaders({...}) — opt-in combined wrapper composing HDR1-7 + HDR5 + HDR6/13/14 in a single mount with one settings.json block (#HDR15). Closes Phase 2 of the Web Security Headers track. Batteries-included safe set: calling SecurityHeaders() with no opts mounts the 7 non-footgun plugins (xContentTypeOptions / xFrameOptions / referrerPolicy / hsts / originAgentCluster / coop / corp) with per-plugin defaults. CSP (#HDR5) and COEP (#HDR6) are opt-in-only because of known footguns (CSP throws on missing directives — no sensible cross-bundle default; COEP require-corp BREAKS embeds without CORP); bundles wanting them pass csp: { directives: {...} } or coep: true. Per-sub-config explicit opt-out via <key>: false or null (e.g. SecurityHeaders({ hsts: false }) for HTTP-only bundles). csp: {} (no directives) throws at factory call time matching standalone Csp behavior. Internal composition: SUB_PLUGINS registry array drives an Express-style sequential middleware runner; each sub-plugin factory is called at construction time (throws propagate to wrapper caller, fail-fast at bundle start); each sub-middleware emits its own header at runtime via the per-plugin idempotent first-writer-wins pattern. Individual plugins remain mountable independently as power-user escape hatches — stacking SecurityHeaders() with an upstream gina.plugins.Csp({...}) produces no double-emit. Settings precedence: per-plugin defaults < settings.json > <key>.* < settings.json > securityHeaders.<key>.* < wrapper opts. Mirrors helmet helmet() orchestrator shape per the helmet-parity narrative. 83 unit tests; full suite 6028/6028. Phase 2 closed — total 10 #HDR plugins shipped (HDR1-7 + HDR5 + HDR6/13/14 + HDR15 wrapper).
* Added gina.plugins.HidePoweredBy() — opt-in middleware that removes the X-Powered-By response header that gina emits by default at core/server.js:2425 (plus core/template/conf/env.json > response.header). Opens Phase 1.5 of the Web Security Headers track (helmet-parity gap-fill, #HDR8). Different SHAPE from #HDR1-#HDR7 — REMOVE pattern (res.removeHeader) not SET. Reduces the attacker reconnaissance surface — the server stack identity no longer leaks via the response header. No tunable options (mirrors helmet hidePoweredBy no-opts shape). Engine-asymmetric effectiveness: works as expected on the Express engine (server.js:2425 setHeader fires in early framework middleware before user app.use() mounts, so user-mounted removeHeader succeeds); on the Isaac engine, the 15+ direct response.writeHead({ X-Powered-By: ... }) call sites bypass the setHeader/removeHeader interface — middleware cannot intercept. README documents the gap; framework-level settings-flag for Isaac is a separate slice. Idempotent — removeHeader is a no-op when the header is absent. 37 unit tests in test/core/hide-powered-by-plugin.test.js.
* Added gina.plugins.XDnsPrefetchControl({ value }) — opt-in middleware that sets the X-DNS-Prefetch-Control response header on every response (#HDR9). Phase 1.5 (helmet-parity gap-fill) continues. Two valid tokens: "on" enables DNS prefetching for perceived-performance; "off" (default, matches helmet) is the privacy-respecting choice — the browser does not pre-resolve DNS for unclicked links, so the resolver only sees hostnames the user actually navigates to. API-shape divergence from helmet (which uses { allow: boolean }): gina uses { value: 'on'|'off' } matching the existing single-token-enum convention of #HDR2 / #HDR3 / #HDR6 / #HDR13 / #HDR14. README documents the helmet-to-gina mapping for migrators. Marginal practical value in 2026 — modern Chrome / Firefox have their own DNS-prefetch heuristics that mostly ignore the header; defense-in-depth + helmet-parity narrative. Caller options always win over settings; values are normalised to lowercase. Idempotent — first-writer-wins via res.getHeader. Mirrors the #HDR14 Corp single-enum plugin shape exactly. 57 unit tests in test/core/x-dns-prefetch-control-plugin.test.js including a negative-invariant lock on the helmet-shape { allow: true } silent-fallback (it does NOT enable DNS prefetching in gina — emits default "off"); user error surfaces at runtime via the gina-default rather than helmet-semantic.
* Added gina.plugins.XXssProtection() — opt-in middleware that emits the literal header X-XSS-Protection: 0 on every response, DISABLING Chrome legacy XSS auditor (#HDR10). Phase 1.5 (helmet-parity gap-fill) continues. The value "0" is deliberate — not a typo. Chrome XSS auditor itself had its own vulnerabilities (cross-site information disclosure); MDN recommendation is to DISABLE rather than enable. Use Content-Security-Policy (#HDR5) with a strong policy for the actual XSS defense; this header is defense-in-depth + helmet-parity narrative only. Browser status in 2026: Chrome dropped the auditor in v78 (2019); Edge follows Chrome; Firefox / Safari never implemented; IE11 honoured but is EOL — effectively a no-op header in modern browsers. No tunable options (mirrors helmet no-opts shape + the #HDR1 XContentTypeOptions plugin shape). Idempotent — first-writer-wins via res.getHeader; if an upstream middleware accidentally emits the unsafe "1; mode=block", this plugin does NOT override it (mount BEFORE the upstream to win). 37 unit tests in test/core/x-xss-protection-plugin.test.js including a negative-invariant lock pinning HEADER_VALUE is NOT any "1" variant + a string-not-number "0" emission lock + an idempotency assertion against upstream "1; mode=block" preservation.
* Added gina.plugins.XDownloadOptions() — opt-in middleware that emits the literal header X-Download-Options: noopen on every response (#HDR11). Phase 1.5 (helmet-parity gap-fill) continues. IE-legacy: prevents Internet Explorer 8+ from opening downloads in the site security context — an old IE vulnerability shape where the "Open" button on a download dialog opened the file in the SITE origin, allowing XSS-equivalent via downloaded HTML from a trusted site. The "noopen" value tells IE to remove the "Open" button entirely, forcing the user to "Save" the download before viewing it. Modern browsers (Chrome / Firefox / Safari / Edge) ignore the header silently; only IE10 / IE11 honour it (both EOL since June 2022) — effectively no-op in modern browsers. Defense-in-depth + helmet-parity narrative; ships for the vanishingly-rare IE11 holdout (enterprise legacy intranet, etc.). "noopen" is the only valid value per MSDN — there is no "open" alternative. No tunable options (mirrors helmet no-opts shape + the #HDR1 XContentTypeOptions / #HDR10 XXssProtection plugin shape). Idempotent — first-writer-wins via res.getHeader. 35 unit tests in test/core/x-download-options-plugin.test.js.
* Added gina.plugins.XPermittedCrossDomainPolicies({ value }) — opt-in middleware that sets the X-Permitted-Cross-Domain-Policies response header on every response (#HDR12). CLOSES Phase 1.5 (helmet-parity gap-fill). Restricts Adobe Flash and PDF readers from honouring cross-domain policy files served from this origin — defends against the cross-domain-data-read shape where a misconfigured crossdomain.xml could allow malicious Flash/PDF content on another site to read data from this one, bypassing the same-origin policy. Four valid Adobe spec tokens: "none" (default, most restrictive — no policy files honoured), "master-only" (only /crossdomain.xml is honoured), "by-content-type" (only files served with Content-Type: text/x-cross-domain-policy are treated as policy files), "all" (any policy file honoured — NOT recommended; defeats the header purpose). API-shape divergence from helmet (which uses { permittedPolicies: <enum> }): gina uses { value: <enum> } matching the existing single-token-enum convention of #HDR2 / #HDR3 / #HDR6 / #HDR9 / #HDR13 / #HDR14. README documents the helmet-to-gina mapping for migrators. Flash EOL since December 2020; Adobe Reader historically honoured the header but most modern PDF readers ignore it; defense-in-depth + helmet-parity narrative. Caller options always win over settings; values are normalised to lowercase. Idempotent — first-writer-wins via res.getHeader. Mirrors the #HDR14 Corp single-enum plugin shape exactly. 60 unit tests in test/core/x-permitted-cross-domain-policies-plugin.test.js including a negative-invariant lock on the helmet-shape { permittedPolicies: 'master-only' } silent-fallback (it does NOT switch the gina default — emits default "none"). CLOSES Phase 1.5 — all 5 helmet-parity plugins now shipped (#HDR8 + #HDR9 + #HDR10 + #HDR11 + #HDR12). Next slice: SecurityHeaders (#HDR15) wrapper extension to include HDR8-12 in the safe-set defaults.
* Extended gina.plugins.SecurityHeaders() (#HDR15) to include the Phase 1.5 helmet-parity plugins (HDR8-12) in its sub-plugin registry — wrapper safe-set grew from 7 to 12 plugins. Calling SecurityHeaders() with no opts now mounts the full 12-plugin non-footgun safe set (xContentTypeOptions, xFrameOptions, referrerPolicy, hsts, originAgentCluster, hidePoweredBy, xDnsPrefetchControl, xXssProtection, xDownloadOptions, xPermittedCrossDomainPolicies, coop, corp) with their per-plugin defaults. CSP (#HDR5) and COEP (#HDR6) remain opt-in only (unchanged — known footguns). Per-sub-config explicit opt-out via <key>: false or null continues to work for all 14 sub-plugins. Caller-supplied options always win over settings; the idempotent first-writer-wins pattern means no double-emit when stacking the wrapper with an upstream individual mount. SUB_PLUGINS registry order: HDR1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 (Phase 1.5 block inserted between HDR7 and HDR13 — strict numeric order now that the gap is filled). Wrapper JSDoc + README updated: safe-set table grew from 7 to 12 rows; per-sub-config shapes table grew from 9 to 14 rows; reference links extended from 9 to 14. Wrapper test extended from 83 to 92 unit tests: structural test pins updated (9 to 14 sub-plugin factories + SUB_PLUGINS.length 9 to 14 + emission order array + sub-config keys array + safe-set 7 to 12 plugins); 9 new wrapper-integration tests added (safe-set HDR8 actually removes upstream X-Powered-By via the inline removeHeader-capable stub; per-sub-config opt-out tests for HDR8/9/10/11/12; per-sub-config option override for HDR9 and HDR12; sub-plugin invariant throw-through for HDR12). User-visible behavior change: bundles already using gina.plugins.SecurityHeaders() WITHOUT explicit sub-config opt-outs will now emit 5 additional response headers by default — defense-in-depth shape (helmet-parity narrative). Bundles wanting the old 7-plugin behavior can pass { hidePoweredBy: false, xDnsPrefetchControl: false, xXssProtection: false, xDownloadOptions: false, xPermittedCrossDomainPolicies: false } to skip the Phase 1.5 block. Settings template + boilerplate adoption example updated to reflect the new safe-set composition. CLOSES Phase 1.5 follow-on wrapper extension.
* Adding `server.hidePoweredBy` settings flag (#HDR8 Phase 2) that suppresses the 15 Isaac-engine `X-Powered-By` writeHead emissions the Phase 1 `gina.plugins.HidePoweredBy()` middleware cannot reach (writeHead bypasses setHeader/removeHeader). Default `false` preserves shipped behaviour; Isaac-engine bundles opt in by setting `server.hidePoweredBy: true` in settings.json. No-op on the Express engine — Express bundles use the Phase 1 middleware.
* Bumped `engine.io` to ^6.6.7 and `engine.io-client` to ^6.6.4, and forced the transitive `ws` dep to ^8.20.1 via npm `overrides` — closes CVE-2026-45736 (CWE-908 Use of Uninitialized Resource, CVSS 6.9 medium). The engine.io 6.x line still transitively pins the vulnerable `ws ~8.18.3`, so the override is the only available remediation path.

## 0.3.14 - 2026-05-16
### Changed
* Bumped the @rhinostone/swig dependency floor from ^2.3.0 to ^2.4.0 in framework/v*/package.json and root package.json. Version 2.4.0 adds ternary (a ? b : c) and Elvis (a ?: b) operator support in template expressions — usable in {{ }} output and in tag arguments such as {% if %}, {% set %}, {% for %} — plus two CLI/build fixes (mocha invocation in make coverage; EEXIST guard in swig compile -o). No template-engine API change at the surface gina calls; swigResolver DEFAULT_MIN stays at 2.0.0.
### Fixed
* FormValidator::validateFormById now fails loud with an actionable error when the resolved id is not an HTMLFormElement — for example when a sibling <p id="X"> or <div id="X"> shares the id with a later-loaded <form id="X"> from a popin or AJAX fragment — instead of crashing later inside bindForm with a cryptic TypeError on the undefined .elements.length access. The error names the offending tag (e.g. `parent` resolves to <P>, not a FORM) and suggests renaming so the underlying id collision is visible from the console message. The guard mirrors the existing instanceof HTMLFormElement discriminator already used in resetErrorsDisplay elsewhere in the same file.
* FormValidator::getFormById now fails loud with the same actionable error as validateFormById when the resolved id is not an HTMLFormElement — for example when a sibling <p id="X"> or <div id="X"> shares the id with a later-loaded <form id="X"> from a popin or AJAX fragment — instead of letting initForm register the non-FORM element in instance.$forms (polluting subsequent lookups) and crashing later inside bindForm with a cryptic TypeError on the undefined .elements.length access. The error names the offending tag (e.g. `parent` resolves to <P>, not a FORM) and suggests renaming so the underlying id collision is visible from the console message. Sister guard to the validateFormById fix in the previous commit; same shape and same root cause, different code path.
* gina bundle:list — two argv parsing bugs in lib/cmd/bundle/list.js. (Bug A) Bare `gina bundle:list` no longer emits the spurious red `[error][gina] [ null ] is not a valid project name` stderr line before falling through to listAll(); CmdHelper initialises projectName to null (per lib/cmd/helper.js:77) but the previous post-loop dispatch only handled the undefined case. (Bug B) `gina bundle:list --all --format=json` now correctly prints JSON instead of text — the previous in-loop short-circuit on `!self.projectName` returned to listAll() before the `--format=json` token at a later argv position could set self.format. Restructure separates parsing from dispatch: the argv walk now only sets self.format + an allFlag, and dispatch runs once after the full scan completes (`if (allFlag || self.projectName == null) listAll() else if (isDefined(...)) listProjectOnly()`). Loose `== null` matches both null and undefined cleanly. The `--all` regex was extended from `/^--all=/` to `/^--all(=|$)/` so bare `--all` is matched explicitly instead of falling through to the projectName short-circuit. No functional change for valid argv shapes (`gina bundle:list @<project>` and `gina bundle:list --format=json --all` both work identically before and after).
* Framework VERSION file no longer drifts by 1 version after each post-publish bumpVersion cycle — post_publish.js now writes framework/v{new}/VERSION alongside the renamed framework directory's package.json, sibling to the existing gitignored-file rewrite. Without this, VERSION moved with the renameSync but kept the prior publish's content, so framework/v{new}/VERSION reported the previous version until the next npm publish fired prepare_version.js. No runtime impact today (no readers); fixes the visible drift for anyone inspecting the file directly.
* #B18 — `core/router.js` no longer poisons `require.cache[controller/index.js]` after eviction. The two sites — `refreshCoreDependencies()` (~L116-127) and the controller-require path (~L617-627) — previously did `delete require.cache[path]` then `require.cache[path] = require(path)`, which assigns the exports OBJECT (not a `Module` instance) into the slot. Latent bug because router.js was the slot's only consumer and read it back self-consistently, but the antipattern would have surfaced as `undefined`-exports crashes the moment any other module did a plain `require(controller/index.js)`. Mirror of the `refreshCore()` fix already shipped in commit `add6655e` for `core/server.isaac.js`. Both sites now use the return value of `require(...)` directly; the preceding `delete require.cache[...]` already evicts the slot so the next `require()` builds a fresh `Module` correctly. No functional change observable today — closes a documented latent bug from llms.txt #104.
* #B16 — `gina project:rm @<project> --force` no longer ENOENTs when the project state is partially broken (missing `manifest.json` and/or missing project directory). The whole point of `--force` on a rm command is to tolerate partial breakage, but the framework's shared `CmdHelper.loadAssets()` was reading `manifest.json` unconditionally before the handler even started, and the handler itself was erroring out when the project directory was gone. Two coordinated fixes: (a) `lib/cmd/helper.js` `loadAssets()` now stubs `cmd.projectData` with a minimal `{project, version, bundles}` shape when the task is `:remove` and `--force` is set, skipping both the recreate-from-template attempt (which itself fails when the dir is gone) and the downstream `requireJSON` read; (b) `lib/cmd/project/remove.js` `init()` now warns and dispatches directly to `end(true)` when the project folder is missing AND `--force` is set, bypassing the prompt and folder rmSync to land registry-only removal (ports cleanup + `~/.gina/projects.json` delete). Without `--force`, both sites preserve the original error + exit behaviour so typos and wrong-machine invocations still surface immediately. The registry-missing check (`projects[projectName] not found in ~/.gina/projects.json`) is unchanged — `--force` can't help when there's no record to remove.
### Security
* Controller::throwError now strips the `stack` field from JSON error responses outside the local scope (NODE_SCOPE_IS_LOCAL=true). Local scope keeps the stack on the wire so the dev toolbar's data-xhr panel can render server-side stack frames; beta, testing, production, and any unset scope always strip — preventing server-side internals (file paths, library versions, internal stack frames) from leaking to API clients. Fail-closed: when the scope flag is missing or false for any reason, the wire stays clean. The toolbar is the only verified consumer (events.js:394 `JSON.parse(xhr.responseText)` → `ginaToolbar.update("data-xhr", XHRData)`); it is already gated on its own load path and does not load outside local scope. Out of scope and tracked separately: the HTML error-page branch (generic `<pre class="stack">` element at controller.js:5292-5333, served only when no custom error template is configured) still leaks in non-local scopes; and consumer-side leaks where `err.stack` is passed directly as the msg argument and surfaces in the `error` field instead of the gated `stack` field — those require per-call-site sanitization on the consumer side.
* Controller::throwError now also gates the fallback HTML error page on local scope — the `<pre class="stack">` block under the L5294+ msgString construction is rendered only when NODE_SCOPE_IS_LOCAL=true, so server-side stack frames (file paths, library versions, internal frames) don't leak via the generic HTML error page in non-local scopes. Same fail-closed shape as the JSON wire gate landed in the previous Security entry; covers both render sites in the fallback path (msg-shape and generic-shape branches). Title / error / message <pre> blocks continue to render in every scope. Custom error templates dispatched via renderCustomError stay consumer-owned — what the template chooses to render from req.params.errorObject (e.g. {% if data.stack %}{{ data.stack }}{% endif %} in a consumer view) is the consumer's call, not the framework's. Closes the cross-shape part of the throwError stack-leak story; consumer-side leaks where `err.stack` is passed as the msg argument and surfaces in the `error` string still need per-call-site sanitization on the consumer side.
* Built-in /_gina/info and /_gina/cache/stats endpoints are now IP-allowlisted per bundle (#S7). They expose process state (memory, uptime, version, HTTP/2 session counters, cache contents) — admin-grade information that should not reach public callers. The new `admin.allowFrom` block in `app.json` defines the allowlist; defaults to loopback only (`["127.0.0.1", "::1"]`) when omitted. Empty array `[]` is explicit deny-everyone. The IP is read from `req.socket.remoteAddress` only — `X-Forwarded-For` is NOT trusted because reverse proxies could spoof it. `::ffff:IPv4` (IPv6-mapped IPv4) is normalised to `IPv4` so listing `127.0.0.1` matches both forms. Denied requests receive 403 with `{ error: "forbidden", message: "/_gina/<path>: client IP not in app.json admin.allowFrom" }`. Mirrors the /_gina/metrics gate convention (#OBS1) but is configured on a separate axis — bundles that disable metrics still get the admin lockdown by default. /_gina/health/check is unaffected (k8s liveness probes need it unrestricted). To allow scraping from an internal monitor on a sister host, add the monitor IP to `app.json admin.allowFrom`.

## 0.3.13 - 2026-05-14
### Added
* Released `0.3.13` — cumulative stable of the `0.3.13-alpha` cycle. Alpha-cycle headline tracks (detailed under `0.3.13-alpha.3` in the changelog): **`${secret:KEY}` config-placeholder resolver** (#SECRETS1) — `lib/secrets` substitutes `${secret:KEY}` placeholders embedded in bundle JSON configs (`settings.json`, `app.json`, `connectors.json`, `mcp.json`) from `process.env[KEY]` at config-load time, fail-closed on an unset/empty var; ships with a `settings.csrf.secret` slot for `gina.plugins.Csrf()` and `mcp.json` placeholder support for `gina bundle:mcp-start`. **Eval-safety campaign completed** (#M20–#M22) — validator nested-key rule construction refactored off `eval` (#M20); the four HTML event-callback `eval` sites and the conditional-binding evaluator fallback dropped (#M21a, #M21b); the Pattern B user-defined-validator `eval` documented as load-bearing public API with an invariant test (#M21c); the logger ↔ merge circular-require restructured so the three `eval(fs.readFileSync(...))` fallbacks are removed (#M22) — Phase 3c complete, one load-bearing framework `eval` remains by design. Plus the #M1 async-race retrofit follow-up (render-nunjucks terminal-exit closure nulling) and a `requireJSON` line-comment / URL-collision fix. The entries below are the post-alpha-cycle changes that complete the release.
* Bundle scaffolding now includes a starter Progressive Web App setup. Running view:add adds a manifest.webmanifest and a cache-first service-worker stub (sw.js) to the bundle public directory, and the default HTML layout links the manifest, declares a theme colour, and registers the service worker. Add your own icon PNGs and edit the manifest to describe the app.
* The Isaac HTTP/2 server now rate-limits rapid-reset stream floods. When a single connection opens more than maxStreamsPerSecond new streams within a rolling one-second window (default 200), the server sends a GOAWAY and closes that session — a targeted, application-level defense against CVE-2023-44487-style attacks on top of the OS-level fix in modern Node.js. Configurable via settings.json http2Options.maxStreamsPerSecond. The /_gina/info endpoint exposes a new rapidResetBlocked counter.
### Changed
* Bumped the @rhinostone/swig dependency floor from ^2.2.0 to ^2.3.0 in framework/v*/package.json. Version 2.3.0 drops yargs and terser from the published package production dependencies — CLI argument parsing is now a built-in zero-dependency parser, and terser (used only by swig compile --minify) moved to devDependencies and loads lazily. A library install of @rhinostone/swig now pulls in only @rhinostone/swig-core, so installing gina has a smaller transitive dependency tree. No template-engine API or behavior change; swigResolver DEFAULT_MIN stays at 2.0.0 since the framework does not depend on any new 2.3.0 API.
### Removed
* Removed two dormant plugin directories (`core/plugins/lib/file/` and `core/plugins/lib/intl/`) that had no consumers in any known bundle. Slightly smaller npm tarball; no functional change.
### Fixed
* Fixed a dev-mode hot-reload crash: refreshCore() overwrote the lib and plugins require.cache entries with their exports objects instead of Module instances, so the controller render delegates threw "Cannot read properties of undefined" after a hot reload.

## 0.3.13-alpha.3 - 2026-05-14 (npm only — no git tag)
### Added
* `${secret:KEY}` placeholder syntax in bundle JSON configs. Substituted at getConfig() time from process.env[KEY]. Fails closed if the env var is unset or empty. Resolution happens once per config-load cycle (cached). Pluggable-backend support is reserved for a future iteration; only the env-var backend ships in this alpha.
* `settings.json > csrf.secret` config slot in `gina.plugins.Csrf()` — flows through the `lib/secrets` resolver, so `${secret:KEY}` placeholders work for the Csrf HMAC secret. Precedence is `opts.secret` (test override) > `settings.csrf.secret` > `process.env.GINA_CSRF_SECRET` (back-compat); existing bundles that set `GINA_CSRF_SECRET` keep working unchanged.
* `${secret:KEY}` placeholder support in `mcp.json` for `gina bundle:mcp-start`. The cmd handler now calls `lib/secrets` on the parsed manifest immediately after `requireJSON()`, so `server.authToken` (and any other future placeholder field in mcp.json) gets filled from `process.env[KEY]` before downstream readers pick it up. Existing bundles that set `process.env.GINA_MCP_AUTH_TOKEN` directly keep working unchanged — that fallback is the last step of the precedence chain.
### Changed
* Added terminal-exit closure nulling to render-nunjucks for early per-request memory release, matching render-swig pattern (#M1 retrofit follow-up).
* Bundle scaffolding (project add / bundle add) now shows the `${secret:KEY}` placeholder pattern as the recommended shape for session and CSRF secrets. `core/template/conf/settings.json` documents the new `settings.csrf.secret` slot (placeholder-compatible) alongside the existing GINA_CSRF_SECRET env-var fallback; `core/template/boilerplate/bundle/index.js` shows `self.getConfig('session').secret` (with bundle/config/session.json holding a `${secret:SESSION_SECRET}` placeholder) as the recommended session-secret wiring, keeping the direct `process.env.SESSION_SECRET` shape as a back-compat note.
### Fixed
* `requireJSON` line-comment stripping no longer fails when a bundle config combines a URL string with a bare `//` line-comment separator. The previous `indexOf`-into-greedy-match pass collided on the URL `://` and silently skipped the real separator, leaving `JSON.parse` to throw `Expected double-quoted property name`. The pass is now per-line on the leftmost `//`, matching the prior heuristic without the substring collision.
### Security
* Replaced eval() in validator nested-key rule construction (Socket #SCS campaign cluster M20).
* Removing eval() from 4 HTML event-callback sites in the browser bundle (events.js + validator/main.js). The function-call shape `data-gina-...-event-on-*="fn(args)"` is no longer supported — register the handler on window and use the bare-identifier shape `="fn"`; a deprecation warning is logged at runtime if a function-call shape is encountered. Clears 4 of the 9 remaining Socket `Uses eval` flag sites (#M21a).
* Removing eval() from the conditional binding evaluator fallback in form-validator.js. Form `condition` strings that previously hit the eval escape-hatch now throw with an explicit message listing the four supported shapes (binary comparison `a OP b`, RegExp instance, boolean, /regex/flags literal). Regex-literal strings still pass through. Clears 1 of the 5 remaining Socket `Uses eval` flag sites (#M21b).
* Documenting the Pattern B eval surface at form-validator.js (user-defined validator function bodies registered on `gina.forms.validators` from disk-loaded `bundle/validators/<name>/main.js` files at framework boot). The eval is load-bearing public API and stays — no behavior change. Adds an in-source trust-model comment block and pins the invariant with `test/lib/validator-scs1i.test.js` (zero writes to `gina.forms.validators` in framework source; zero request-time identifiers around the userValidator source chain). Closes the M21 cluster (#M21c).
* Restructuring the logger ↔ merge circular-require so the eval fallbacks at lib/logger/src/main.js + the two logger containers are no longer needed and have been removed. Root cause was merge.js requiring `helpers/index.js` (the for-loop helper loader) at module-load to populate `JSON.clone`, which transitively required `lib/logger` via several helpers and produced a partial-module return under circular load. Fix swaps that for a direct require of `utils/prototypes.json_clone` (a 100-line standalone primitive with zero top-level requires) — same JSON.clone population, no circular trigger. Clears the final 3 server-side Socket `Uses eval` sites (#M22). Phase 3c complete: 1 live framework eval site remains (the load-bearing Pattern B at form-validator.js:1886 / #M21c trust model).

## 0.3.12 - 2026-05-12
### Changed
* Bumped @rhinostone/swig dependency floor from ^2.0.1 to ^2.1.0 in framework/v*/package.json. Version 2.1.0 introduces a multi-flavor architecture splitting the runtime into a shared @rhinostone/swig-core package plus per-flavor frontends (@rhinostone/swig native syntax, @rhinostone/swig-twig Twig-syntax frontend); the native @rhinostone/swig package remains drop-in compatible with the @rhinostone/swig@1.x and 2.0.x API surface gina depends on (swig.compile, swig.setFilter, swig.setTag, swig.renderFile). swigResolver DEFAULT_MIN stays at 2.0.0 since the framework does not depend on any new 2.1.0-only API. Resolves the version-currency drift surfaced by 'npm view @rhinostone/swig version' returning 2.1.0 against the current ^2.0.1 floor.
* Bumped @rhinostone/swig dependency floor from ^2.1.0 to ^2.2.0 in framework/v*/package.json. 'npm view @rhinostone/swig version' returns 2.2.0; ^2.1.0 semver-satisfies 2.2.0 but the documented floor was stale and the swig-version-sync rule mandates keeping the pin current. swigResolver DEFAULT_MIN stays at 2.0.0 since the framework does not depend on any new 2.2.0-only API.
### Fixed
* Fixed urlencoded body parser: '+' now correctly decodes to space in 'req.post', 'req.put', and 'req.patch' for 'application/x-www-form-urlencoded' content per RFC 1866 / WHATWG URL spec. The inner content-type check was inverted — '+' stayed literal in urlencoded bodies (e.g. 'name=Hello+World' surfaced as 'Hello+World' instead of 'Hello World'), while JSON bodies with a literal '+' could have it silently replaced with space.
* Fixed URL query-string parser on the Isaac engine: '+' now correctly decodes to space in 'request.query' values per RFC 1866 / WHATWG URL 'application/x-www-form-urlencoded parser' spec (e.g. 'GET /search?name=Hello+World' yields 'request.query.name === "Hello World"' instead of 'Hello+World'). Closes the parallel to commit 014ff60a's body-parser fix. Express engine is already spec-correct via qs/querystring defaults — no change there.
* Fixed FormValidator's isRequired validator for HTML5 form-reassociated radios sharing a name across sibling forms. Submitting a form whose checked radio differs from a sibling form's .checked radio no longer leaks the sibling's value at submit time. Sister fix to commit 80dd89f9 (updateRadio peer-set scoping) and 6e544411 (defaultChecked cache); same root shape — same-name radios across multiple form-owners — different code path. The unfiltered document.getElementsByName walk in the radio-group case now scopes to the validator-bound radio's form-owner per HTML5 spec, mirroring the equivalent filter applied to updateRadio's mutual-exclusion peer set.
* Fixed an ENOENT race in the per-template layout cache under concurrent renders in dev mode. Two parallel requests for the same URL whose template uses {% extends %} could see the cached layout file deleted between its priming-block write and the post-priming read, surfacing as a 500 with ENOENT: no such file or directory. The cache write now uses an atomic temp+rename pattern (writeFile to a per-process unique temp, then rename onto the target) so concurrent readers always observe either the prior or new content, never an absent file. The same atomic pattern is applied to the post-asset-injection rewrite (closing a secondary empty-content race from O_TRUNC). Production was unaffected — cached mode (_cacheIsEnabled = true) skipped the delete-then-rewrite path entirely. CVE-2023-25345 path-traversal boundary check preserved verbatim.
* Fixing async race in core/controller/controller.render-swig.js where local.req / local.res / local.next reads dereferenced null when a second self.throwError() fired during an in-flight renderCustomError, capturing them into function-scoped locals at the top of render() so post-await reads remain race-safe against controller closure null-outs
* Extending the #M1 async-race local-copy retrofit to controller.render-nunjucks.js (full call chain: renderNunjucks captures req/res/_next at top, helpers sendHtmlResponse/registerGinaFilters/writeCache take captures as parameters) and to controller.render-json.js writeCache helper (post-await throwError on invalidateOnEvents misconfiguration now goes through a captured res parameter instead of local.res)

## 0.3.11 - 2026-05-10
### Added
* Added pre-publish gate `checkDefFrameworkConsistency` (`script/check_def_framework_consistency.js` + wired into `script/prepare_version.js`). Asserts `~/.gina/main.json`'s `def_framework` field points to a framework directory that exists on disk; aborts with an actionable diagnostic listing the drifted scalar, the missing framework dir, the actual present framework dirs, and a step-by-step recovery recipe. Catches the silent state-store drift that surfaced as a deep `MODULE_NOT_FOUND` from inside `getSelectedVersion` during the v0.3.10 stable publish (2026-05-06), where main.json's def_framework had drifted to `0.3.9` while the framework dir on disk was at `v0.3.10-alpha.2`. Skips silently when main.json itself is absent (first install / fresh state). The drift root cause (a writer other than `bumpVersion` updating the scalar inconsistently — distinct from the `34115799` gina.db drift fix) is queued as a separate follow-up; this gate is the safety net so future drift surfaces with an actionable error before npm publish proceeds. 18 unit tests with an injected fs driver cover the happy path, drift detection, fresh state, malformed JSON, missing/non-string field, and the `presentDirs` diagnostic helper.
* Added `gina.t(key, params, culture, options)` global translation helper backed by per-bundle JSON catalogs at `bundle/locales/<culture>.json` (e.g. `en.json`, `en_US.json`, `fr.json`). Catalogs are eager-loaded at bundle start; lookup walks a fallback chain (specific culture → base language → `settings.region.culture` → `GINA_CULTURE` → `'en'`), applies CLDR plural rules via Node's built-in `Intl.PluralRules` when `params.count` is supplied and the resolved value is a CLDR plural-form object, then runs `{name}`-style parameter interpolation (with `{{name}}` escape for a literal). Controller actions get an auto-binding `self.t(key, params)` helper that reads the request culture from `req.culture` (formalised by slice 3) and the bundle name from `local.options.conf.bundle`. The legacy `__()` placeholder (helpers/text.js) is rewired as a one-arg alias of `t()` — existing callers keep working with no behaviour change when no catalog is loaded. Settings template gains an `i18n.*` block (`cookieName`, `devMissingKey`, `fallbackChain`, `cultures`) for runtime knobs. Bundle scaffold ships a minimal `bundle/locales/en.json` stub. New JSON Schema at `https://gina.io/schema/locales.json` (Draft-07, recursive value-or-pluralForm shape). Slice 1 of 5 from #I18N1; slices 2-5 add the swig/nunjucks `t` filter, `req.culture` formalisation with URL-prefix routing + cookie negotiation, and the `gina i18n:scan/add/export/import` CLI group.
* Added swig and nunjucks `t` template filter (#I18N1 slice 2). Translate strings directly in templates via `{{ "common.welcome" | t }}`, `{{ "common.greeting" | t({ name: user.name }) }}`, or `{{ "common.items" | t({ count: 5 }) }}` (CLDR plural). Both engines share the same surface so templates port between swig and nunjucks unchanged. The filter is auto-registered on every request (no setup.js wiring needed) and consumes the `lib/i18n` primitives — no plural / interpolation / fallback logic is duplicated locally. Auto-binds the request culture via `req.culture` (the slice 3 hook) with a fallback to `process.env.GINA_CULTURE` (the bundle's process default). Bundle name is taken from the per-request `options.conf.bundle` slot. Templates work today against the process-default culture; slice 3 upgrades to per-request URL-prefix / cookie / Accept-Language negotiation. Tests at `test/lib/i18n-filters.test.js` lock in the requires, the (key, params) signatures, the culture and bundleName resolution chains, the i18n.t pass-through shape, the singleton context access pattern, surface parity between swig and nunjucks, and negative invariants (no Intl.PluralRules / interpolation regex / _i18nCatalogs duplication inside the filter bodies). Full local suite green: 4197 tests, zero regressions.
* Added locale negotiation primitives + request-pipeline hook (#I18N1 slice 3). `req.culture` is now a first-class per-request field, set in the routing pipeline before middlewares drain. Negotiation order (highest priority first): URL prefix (`routing.json > culturePrefix: true` flag — captures `req.routing.param.culture` from a route URL like `/:culture/dashboard`) → cookie (defaults to `gina_culture`) → `Accept-Language` parsed and best-matched against the bundle's loaded catalogs with q-value ordering respected → `settings.region.culture` → `process.env.GINA_CULTURE` → `'en'`. New helpers exported on `lib.i18n`: `parseAcceptLanguage(header)`, `matchAvailable(requested, available)`, `readCookie(header, name)`, `negotiateCulture(req, opts)`. Controller `self.t()` and the swig/nunjucks `t` filter pick up `req.culture` automatically (the slice-1 hook is now wired). The controller's existing region-data lookup (`controller.js`) bridges to `req.culture` so the URL-prefix / cookie sources flow through to the legacy `page.view.lang` template scope. Per-route flag propagation mirrors the `#CSRF2 csrfExempt` pattern: `lib/routing/src/main.js` extracts `routeObject.culturePrefix` into `params.culturePrefix`; `core/server.js` hoists into the request-lifecycle params block as a sibling of `csrfExempt`. The negotiator is wrapped in try/catch so an i18n failure never blocks routing. Tests at `test/lib/i18n-negotiation.test.js` — 42 tests covering parseAcceptLanguage (q-value ordering / wildcards / whitespace / clamping), matchAvailable (exact / base-language / multi-priority), readCookie (extract / decode / fallback), negotiateCulture (full chain priority matrix), and source-inspection guards on the lib/routing + core/server wiring. Full local suite green: 4239 tests, zero regressions. Known limitation in slice 3: cookie name is hardcoded to `gina_culture` and bundle defaultCulture lookup is deferred — slice 4 or a follow-up wires bundle-settings access at the request-pipeline boundary.
* Added `gina i18n:scan` CLI command (#I18N1 slice 4 part 1). Walks every bundle in a project and reports translation coverage per culture against the keys used in source — `t("key")` / `self.t("key")` / `__("key")` / `{{ "key" | t }}` patterns are detected across `controllers/`, `middleware/`, `models/`, `lib/`, `src/`, `views/`, `public/`, plus top-level `.js` (excluding `node_modules`, `dist`, `.git`, `locales/`, and dotfiles). For each culture the command prints translated / missing-key counts and a percent score, then lists the missing keys themselves (capped at 50 in text output; no cap in JSON output). Three positional shapes: `gina i18n:scan` (every registered project), `gina i18n:scan @<project>` (every bundle in one project), `gina i18n:scan <bundle> @<project>` (one bundle). `--format=json` emits a CI-friendly machine-readable shape. Adds `'i18n:'` to `bin/cli`'s `allowedOffline` whitelist so the command runs without a framework socket (consistent with `connector:`, `bundle:`, `env:`, and the rest). Coverage walks `lib.i18n.resolveKey` so dotted-path keys (`common.welcome`) resolve through nested catalogs correctly. Tests at `test/lib/i18n-scan.test.js` — 57 source-inspection assertions covering module shape + CmdHelper wiring, argv parsing + dispatch, file walker constants, key-extraction regex behaviour (positive + negative), pure-logic coverage replica, JSON / text output structure, help.txt + arguments.json, and bin/cli wiring. The companion `gina i18n:add` command (slice 4 part 2) and `gina i18n:export` / `gina i18n:import` (slice 5) follow.
* Added `gina i18n:add <culture>` CLI command (#I18N1 slice 4 part 2). Seeds a new culture catalog at `<bundle>/locales/<culture>.json` by copying the structure of an existing source culture and prefixing every string value with `[TODO] ` so translators can grep / search-replace through the file. Three positional shapes: `gina i18n:add <culture> @<project>` (every bundle in the project), `gina i18n:add <culture> <bundle> @<project>` (single bundle), `gina i18n:add <culture> <bundle> @<project> --from=<src>` (explicit source culture). Source-culture resolution order per bundle: `--from=<culture>` → `<bundle>/config/settings.json > region.culture` → `process.env.GINA_CULTURE` → `'en'`. Refuses to overwrite an existing target catalog unless `--force` is passed; `--dry-run` prints what would be written and exits without touching the disk. Creates `<bundle>/locales/` recursively when missing. Recursion preserves nested catalogs and CLDR plural-form objects (every `zero` / `one` / `two` / `few` / `many` / `other` form gets the `[TODO] ` prefix); arrays / numbers / booleans / null pass through unchanged. Refuses when source culture equals target culture (caller would seed onto itself). Writes via `lib.generator.createFileFromDataSync` for parity with the rest of the framework's file emitters; serialises with 4-space indent + trailing newline. For project-wide invocations, iterates the `manifest.json` bundle list in sorted order and reports per-bundle ok / skipped counts at the end. Tests at `test/lib/i18n-add.test.js` — 51 source-inspection + pure-logic + sandbox-write assertions covering module shape, argv parsing, source-culture resolution priority chain, `seedCatalog` transformation (string / nested / plural / non-string leaves / array passthrough / key-order preservation), guarded write path (source-equals-target / missing source / malformed source / target exists / dry-run / mkdir), multi-bundle iteration, and a sandbox round-trip that asserts the on-disk output JSON.parses back with the `[TODO] ` prefix in place. Full local suite green: 4347 / 4347. Slice 5 (`gina i18n:export` / `gina i18n:import` for PO / CSV / JSON translator round-trip) follows.
* Added `gina i18n:export <culture>` CLI command (#I18N1 slice 5 part 1). Exports a culture's catalog from a project's bundle(s) into JSON / PO / CSV format on stdout or a file. Three positional shapes mirror `i18n:add`: `gina i18n:export <culture> @<project>` (every bundle, single combined output), `gina i18n:export <culture> <bundle> @<project>` (single bundle), with `--format=<json|po|csv>` and `--output=<path>` flags. When `--format` is omitted, the format is auto-detected from the `--output` file extension (`.po`, `.csv`, `.json`); when both are omitted, defaults to JSON on stdout. Single-bundle JSON matches the on-disk catalog shape verbatim — drop the file straight back into `<bundle>/locales/` and round-trip via `gina i18n:import`. Multi-bundle JSON nests under bundle keys at the top level. PO output is translator-friendly: standard header block (`Project-Id-Version`, `Content-Type`, `Language`, `Plural-Forms`) with a per-culture plural formula (English / Romance / Slavic / Asian / Arabic table; Romance/Germanic 2-form fallback for unknown cultures), `msgctxt` for multi-bundle disambiguation, and native `msgid_plural` + `msgstr[N]` syntax for plural forms. Each plural entry carries a `#. cldr-keys: <list>` extracted comment recording the present CLDR plural keys in canonical order (`zero` / `one` / `two` / `few` / `many` / `other`) so `i18n:import` reconstructs the catalog shape unambiguously across cultures with differing plural categories. PO escape sequences cover backslash / double-quote / newline / tab / CR. CSV output follows RFC 4180: header row `key,value` (single-bundle) or `bundle,key,value` (multi-bundle); cells containing `,` / `"` / `\n` / `\r` / leading-or-trailing whitespace are wrapped and embedded quotes are doubled; plural forms flatten to dotted-suffix rows (`items.one`, `items.other`). Bundles without a catalog for the requested culture are skipped with a warning in project-wide mode; single-bundle scope errors hard. Output to disk goes through `lib.generator.createFileFromDataSync` for parity with `i18n:add`; the parent directory must exist. Tests at `test/lib/i18n-export.test.js` — 75 source-inspection + pure-logic + sandbox-write assertions covering module shape, argv parsing, `detectFormatFromPath`, `flattenCatalog` (nested / plural-grouped / non-string-leaf-drop / bundle stamping), PO writer (escape sequences, header block, per-culture plural formula with base-language fallback, msgid_plural / msgstr[N] / cldr-keys CLDR-order round-trip), CSV writer (RFC 4180 quoting matrix, plural flattening, bundle column), JSON writer (4-space indent, identity round-trip, multi-bundle wrapper), help.txt + arguments.json + dispatcher source pins, and a sandbox export smoke that round-trips JSON / PO / CSV through minimal readers. Slice 5 part 2 (`gina i18n:import`) follows.
* Added `gina i18n:import <culture>` CLI command (#I18N1 slice 5 part 2). Imports a translator-edited PO / CSV / JSON file back into a culture's catalog at `<bundle>/locales/<culture>.json` — the complement of `gina i18n:export`. Together they close the translator round-trip and complete the #I18N1 contract (slices 1–5 all shipped on the 0.3.11-alpha track). Three positional shapes mirror `i18n:export`: `gina i18n:import <culture> @<project> --file=<path>` (project-wide; multi-bundle inputs route per bundle), `gina i18n:import <culture> <bundle> @<project> --file=<path>` (single bundle), with required `--file=<path>` flag and optional `--format=<json|po|csv>`, `--merge=<union|replace>`, `--dry-run`, `--force`. Format is auto-detected from the `--file` extension when `--format` is omitted. Two merge strategies via `--merge`: `union` (default) deep-merges — imported keys win for shared paths; existing keys absent from the import are preserved (a partial PO/CSV touches only the keys it lists, including per-CLDR-key behaviour for plural-form objects). `replace` overwrites the catalog entirely; existing keys absent from the import are dropped — surfaced via a "N key(s) dropped" log so translators see what they wiped. PO parser is a state-machine line-walker: handles header entry (empty `msgid ""` discarded), `msgctxt` for bundle disambiguation, multiline strings via PO continuation (`msgid ""` + `"line1"` + `"line2"`), backslash escapes (`\n`, `\t`, `\r`, `\"`, `\\`), and the slice 5 part 1 `#. cldr-keys: <list>` extracted comment that records which CLDR plural keys the export emitted — the importer maps `msgstr[N]` back to those exact CLDR keys, so plural-form catalogs round-trip across cultures with differing plural categories (English `{one,other}`, Russian `{one,few,many,other}`, Arabic all six). Falls back to canonical CLDR order when the marker is absent (best-effort for PO files authored outside this tooling). CSV parser is an RFC 4180 state-machine: handles quoted cells with embedded commas / doubled quotes / embedded newlines / CRLF line endings; auto-detects single-bundle (`key,value`) vs multi-bundle (`bundle,key,value`) shape from the header row. Plural-form CSV rows reach the catalog via dotted-suffix nesting (`items.one` + `items.other` → `items: { one, other }`). JSON inputs accept both shapes: a flat catalog (matches the on-disk shape verbatim) or a multi-bundle wrapper (`{ "<bundle>": { ...catalog }, ... }`); the CLI's `<bundle>` positional resolves the routing. Single-bundle scope errors hard when the target catalog does not exist (recommends running `gina i18n:add` first); pass `--force` to bypass and create. Project-wide scope warns when the input contains a bundle not registered in `manifest.json` and skips it; reports per-bundle ok/skipped counts at the end. Write-back uses the `connector:add` comment-preserving three-function pattern (`readExistingFile` / `mergeUnion` / `writeFile`) per architecture doc § 9: leading comment headers (everything before the first `{`) are preserved verbatim, the JSON body is rewritten with 4-space indent + trailing newline through `lib.generator.createFileFromDataSync`. `--dry-run` prints the would-write line per bundle and skips the disk write; the dropped-key count for `--merge=replace` shows in the dry-run preview too. Tests at `test/lib/i18n-import.test.js` — 84 source-inspection + pure-logic + sandbox-write assertions covering module shape, argv parsing + dispatch, format auto-detect, PO parser (escape sequences, msgctxt-as-bundle, plural reconstruction with `#. cldr-keys` round-trip, fallback to canonical CLDR order, 3-form Russian-style plurals, multiline continuation, header-entry skip), CSV parser (single + multi-bundle headers, embedded commas / doubled quotes / embedded newlines / CRLF, header-shape mismatch returns empty), `buildCatalogFromEntries` (flat / nested / dotted-suffix plural reassembly / atomic plural-form preservation), `looksLikeWrapper` heuristic (clear positives + clear negatives + the documented CLI-resolved deep-flat false positive), `mergeUnion` (imported wins, existing preserved, per-CLDR-key plural merging, type-divergent overrides, empty edge cases), comment-preserving writer + help.txt + arguments.json + dispatcher source pins, and a sandbox import smoke that round-trips union / replace / comment-header preservation / CSV → catalog through minimal readers. Full local suite green: 4506 / 4506 (4347 baseline + 75 export + 84 import). Closes the #I18N1 translator round-trip contract; #I18N2 (ICU MessageFormat opt-in via `t.icu(key, params, culture)` powered by `intl-messageformat`) follows.
* Added ICU MessageFormat opt-in via `t.icu()` — server-side primitive + framework wiring (#I18N2 part 1). Adds an opt-in `tIcu(key, params, culture, options)` variant powered by `intl-messageformat` to the `lib/i18n` module, exposed as `gna.t.icu(...)` (sibling of `gna.t`) and `self.t.icu(...)` (controller helper auto-binding `req.culture` from slice 3, plus `bundleName` / `devMissingKey` / `fallbackChain` from `local.options.conf.content.settings.i18n`). Existing `gna.t()` / `self.t()` shape stays unchanged — v1 catalogs (plain strings with `{name}` placeholders, plural-form objects keyed by CLDR categories, nested categories) continue to work as today. Catalog values that are NOT strings (plural-form objects, nested objects) automatically fall through to `t()` so the same bundle catalog can mix v1 and ICU shapes freely. Catalog values that ARE strings get interpreted by `intl-messageformat` — covers ICU `{count, plural, one {# item} other {# items}}`, `{gender, select, female {…} male {…} other {…}}`, nested combinators, explicit-number plural matchers (`=0`), and the cross-locale CLDR plural rules the library ships its own data for (English `one/other`, French `n in 0..1` singular, Russian `one/few/many`, Arabic `zero/one/two/few/many/other`, etc.). Adds `intl-messageformat: "^11.2.4"` to `framework/v0.3.11-alpha.2/package.json` dependencies (next to `@rhinostone/swig` and `psl`). Library is BSD-3-Clause, two transitive deps (`@formatjs/fast-memoize` + `@formatjs/icu-messageformat-parser`), zero `new Function`/`eval` calls in the published tarball — preserves the framework's post-#SCS1 eval-safety posture. After clone / merge / worktree switch, `npm install` inside `framework/v0.3.11-alpha.2/` is required to populate the dep (same convention as the existing swig / psl deps). The library is ESM-only since v10, so `lib/i18n` loads it via dynamic `import()` from a module-level loader: `ensureIcuLoaded()` returns a Promise that resolves to the `IntlMessageFormat` constructor; idempotent, cached. `loadCatalogs()` kicks off the loader fire-and-forget at bundle boot so by the time the first request is served the load has resolved and `t.icu()` is sync. Tests can `await ensureIcuLoaded()` explicitly. Failure (e.g. dep not installed) is captured on `icuLoadError`; subsequent `t.icu()` calls surface the error verbatim with an actionable "npm install intl-messageformat" hint. `IntlMessageFormat` instances are memoised per `<bundleName>::<culture>::<key>` on `process.gina._i18nIcuFormatters` so the parse cost is paid once per (bundle, culture, key) per process; `clearCatalogs(bundleName)` drops both the catalog AND the cached formatters so test isolation is clean. Tests at `test/lib/i18n-icu.test.js` — 49 source-inspection + behavioural assertions covering module exports + slice 1/3 export preservation, loader idempotence + error capture, `tIcu` basic API (null/empty key / no-culture / no-catalog / plain placeholder / missing key), plural rules (English / French `n in 0..1` / Russian `one/few/many` / `=0` explicit matchers), select / gender (female / male / other fallback / nested plural inside select), fallback chain (region → base language → bundle default → exhausted), non-string fallthrough (plural-form objects + nested categories + mixed catalog with both shapes), IcuFormatter cache (memoisation + `clearCatalogs` clearing), `gna.t.icu` source-pin (key/params/culture/options signature mirrors `gna.t`), `self.t.icu` source-pin (auto-binds `req.culture` / `local.options.conf.bundle` / `settings.i18n.devMissingKey` / `settings.i18n.fallbackChain`), framework dep declaration, and negative invariants (read-only catalogs / parse error graceful degrade / format error graceful degrade). Full local suite green: 4555 / 4555 (4506 baseline + 49 new). Slice part 2 — `tIcu` template filter for swig + nunjucks + scaffold ICU example + docs site update + roadmap status flip — follows.
* Added ICU MessageFormat opt-in via the `tIcu` template filter for swig + nunjucks (#I18N2 part 2). Closes #I18N2 — translators and template authors can now use ICU MessageFormat syntax across all three of gina's i18n surfaces (`gna.t.icu()` global, `self.t.icu()` controller helper from part 1, plus the `tIcu` template filter from this part). The filter shape mirrors the existing `t` filter exactly: `{{ "key" | tIcu({ count: 5 }) }}` for plurals, `{{ "key" | tIcu({ gender: 'female', name: user.name }) }}` for select / gender. Auto-binds the request culture from `req.culture` (slice 3) and the bundle from `options.conf.bundle` — same auto-binding pattern as the `t` filter. Identical surface across the swig and nunjucks engines so templates port between renderers unchanged. Catalog values that are NOT strings (plural-form objects, nested categories) fall through to the v1 `t()` path so a single bundle catalog can mix v1 and ICU shapes freely. Roadmap close-out lands in this commit too: #I18N1 (i18n core) and #I18N2 (ICU MessageFormat) both flip from 📋 to ✅ in `ROADMAP.md`. Tests at `test/lib/i18n-icu.test.js` extend with 11 new filter source-pin assertions covering: lib/swig-filters declares `self.tIcu`, swig tIcu reads `req.culture` + `options.conf.bundle`, swig tIcu forwards to `lib.i18n.tIcu`, swig `t` filter unchanged (no regression), the same 5 invariants for lib/nunjucks-filters, and a parity test confirming swig + nunjucks tIcu filters call `lib.i18n.tIcu` with identical arguments. Full local suite green: 4566 / 4566 (4555 from part 1 + 11 new). Closes the #I18N2 contract.
* Added built-in /_gina/metrics Prometheus exposition endpoint (#OBS1). Opt-in via app.json metrics.enabled = true; install prom-client as a peer dependency in your project (npm install prom-client). Exposes Node.js process metrics (heap, GC pause, event loop lag, active handles) via prom-client.collectDefaultMetrics(), plus a per-request HTTP counter (gina_http_requests_total{method,route,status}) and duration histogram (gina_http_request_duration_seconds{method,route,status}). Route labels come from req.routing.rule (routing.json key) — never the raw URL — to keep Prometheus cardinality bounded; status-aware fallback labels for unmatched paths (__not_found__, __method_not_allowed__, __error__, __no_route__). Endpoint is IP-restricted by default (loopback only; configurable via metrics.allowFrom in app.json); reads client IP from req.socket.remoteAddress only — does NOT trust X-Forwarded-For. Each bundle is an independent scrape target on its own port. Wired in core/server.isaac.js (source of truth) + core/server.js (engine-agnostic mirror, always-on, not gated on NODE_ENV_IS_DEV); request lifecycle hook registers response.on('finish') at request entry. New lib/metrics/src/main.js wraps prom-client (peer dep, project-resolved load via the mysql2/postgres/AI-SDK pattern). Schema/app.json extended with the metrics block. 116 new tests across test/lib/metrics.test.js (behavioural) and test/lib/metrics-wiring.test.js (source-inspection). See docs/guides/observability for adoption.
* Added ScyllaDB / Cassandra connector — ORM access via cql/<Entity>/*.sql files plus a session store using CQL USING TTL. Drop-in cassandra-driver-based connector following the same client.execute(query, args, { prepare: true }) pattern as MySQL/PostgreSQL with ? placeholders, @param type casting (text/int/bigint/uuid/timestamp/boolean/blob/list/set/map/etc.), and LWT [applied] handling. Driver is loaded from your project node_modules (npm install cassandra-driver).
* Auto-promoting `route.param.section` to `page.section` so templates that compose include paths from the section name (e.g. `{% include './includes/' + page.section + '.html' %}`) resolve without requiring the controller to set `data.page.section` itself. Mirrors the existing `param.file` → `page.view.file` auto-promotion.
* Added MongoDB connector — ORM access via JSON pipeline files at models/<db>/pipelines/<Entity>/*.json plus a session store backed by a TTL index. Drop-in mongodb-driver-based connector with {op, filter, doc, ...} JSON bodies, JSDoc-style @param/@return annotations, {$arg: N} positional placeholders for caller args, {$oid: <hex>} ObjectId literals, $scope substitution at load time, and castParam for BSON types (objectid, int/int32, long/int64, double/number, boolean, string/text, date/timestamp). Supported ops: findOne / find / aggregate / countDocuments / insertOne / insertMany / updateOne / updateMany / replaceOne / deleteOne / deleteMany. Session store auto-creates the TTL index on first set() with a one-shot guard; get/length/all filter on expiresAt to exclude documents that expired but were not yet reaped by the 60s MongoDB TTL monitor. Driver is loaded from your project node_modules (npm install mongodb).
### Changed
* Bumped `@rhinostone/swig` from 1.6.0 to 2.0.0 (`framework/v*/package.json`). The fork's 2.0.0 release carved internals into `@rhinostone/swig-core` and ships a self-contained pre-built esbuild bundle at `dist/swig.js`. Phase 7 of the asset build now copies the upstream esbuild bundle directly into `dist/swig.min.js` instead of running Closure Compiler on `bin/swig.js` — Closure does not follow npm `require()`, so post-2.0.0 the Closure path would carry unresolved `swig-core` requires. The framework's swig-resolver `DEFAULT_MIN` floor (`lib/swig-resolver/src/main.js`) shifts from `1.6.0` to `2.0.0`; bundles that opt into a project-pinned swig (`swig.useProject: true`) must now pin `>= 2.0.0` of `@rhinostone/swig` for the resolver to honour the override. Server-side API is unchanged — same entry points, same compiled output.
### Fixed
* Fixed `prepare_version.js` writing `settings.json`'s `version` field without also updating `settings.json`'s `def_framework` field. The original block at line 482 set `mainConfig.def_framework` but only `settingsConfig.version` (not `settingsConfig.def_framework`), producing a `version:<new> + def_framework:<old>` split inside settings.json (and in gina.db's settings/<short> blob via StateStore) on every npm publish. Mirrors the safe writer pattern already in `post_publish.js:413-421` (bumpVersion writes both `settings.version` AND `settings.def_framework` together). The defensive pre-publish gate added earlier in the same release window catches the symptom at publish time; this fixes the root cause for settings.json drift specifically. The `~/.gina/main.json` def_framework drift observed in the same v0.3.10 forensic snapshot (main.json showed `def_framework: "0.3.9"` while the framework dir on disk was at `v0.3.10-alpha.2`) is NOT fully explained by this fix — `main.json` gets `def_framework=targetedVersion` correctly at line 483 of the same block — so the trigger for main.json drift remains under investigation as a separate follow-up.
* Fixed `npm install -g gina@<older>` regressing `~/.gina/main.json`'s `def_framework` field to the older tarball's version. `script/post_install.js:822` previously had no semver-newer guard — any reinstall with an older version unconditionally overwrote the scalar `def_framework`. Combined with the self-perpetuating abort loop at `script/prepare_version.js:449` (once the scalar pointed to a framework dir that had been renamed away, every subsequent prepare aborted before reaching the writer that would correct it), this could leave `~/.gina/main.json` stuck pointing at a missing version. Triggered exactly this shape on the v0.3.10 stable publish (2026-05-06), where the scalar was at `0.3.9` while the framework dir on disk was at `v0.3.10-alpha.2`. Fix adds a strict-semver pre-release-aware comparator (`script/version_compare.js`, 35 unit tests + 6 source-structure pins) and gates the `def_framework` write at `post_install.js:822` so older-version reinstalls preserve the newer state and emit a `Skipping def_framework update` console.info instead. The `frameworks` LIST is still updated on regression skip (additive history). The defensive gate at `script/check_def_framework_consistency.js` (added in the same release window) continues as the publish-time safety net; this removes the trigger.
* Fixed cross-bundle leak under reverse-proxy sub-path mounts where the routing.json fetch would land on the wrong upstream. `core.js::getDependencies` (in `gina.min.js`) runs from the gina script-tag onload handler BEFORE `window.onGinaLoaded` populates `gina.config`, so `gina.config.webroot` was undefined at fetch time and the URL collapsed to `/_gina/assets/routing.json` — answered by whichever upstream served `/` rather than the bundle that rendered the page. Fix: `loader.js` now exposes `window.__ginaWebroot` synchronously at script parse time (whispered server-side from `page.environment.webroot`, which already includes any `X-Forwarded-Prefix` composition from the previous slice). `core.js` reads `window.__ginaWebroot` first, falling back to `gina.config.webroot` then `'/'` for backward compat. The synchronous global is reliably available because `gina.onload.min.js` is injected into `<head>` BEFORE `gina.min.js` by every full-page render path in `controller.render-swig.js` (`:1185-1194` dev, `:1262-1271` prod). +13 tests in `test/core/controller.test.js` § 10 (loader + core source pins, pure-logic resolution-chain replica, dist-equivalence check).
* Fixed cross-request leak of the X-Forwarded-Prefix-derived webroot prefix. `process.gina.PROXY_PREFIX` was set by `server.isaac.js` inside a `!isProxyHost && headers['x-forwarded-host']` gated block — the gate stops firing after the first proxied request flips `isProxyHost` to globally true via `setContext('isProxyHost', true)`, so subsequent requests never re-derived the prefix and the value stayed at the FIRST proxied request's value forever (until worker restart). This bit any worker handling a mix of proxied + direct calls (e.g. internal services hitting the bundle without going through the proxy — direct calls inherited the leaked prefix and rendered with the wrong webroot) and bundles behind a proxy that mounts them at multiple paths. Fix moves the prefix processing OUT of the gated block so it runs on every request, and switches the slot from process-global `process.gina.PROXY_PREFIX` to per-request `request._ginaProxyPrefix`. Reader at `controller.js` `set('page.environment.webroot')` setter site updated to read `local.req._ginaProxyPrefix` (with `local.req && typeof != 'undefined'` defensive guard). Normalisation steps (trim / strip trailing slashes / prepend leading slash / drop empty / drop '/') unchanged. +9 tests across `test/core/server.isaac.test.js` (1 new positive: writer outside gated block; 1 new negative: process.gina.PROXY_PREFIX is no longer written; 2 existing source pins re-aimed at request._ginaProxyPrefix) and `test/core/controller.test.js` § 09 (5 per-request isolation tests including the cross-request-leak repro: req A sets prefix, req B without header gets undefined and bundle-internal webroot; 1 new negative: process.gina.PROXY_PREFIX is no longer read; 2 existing source pins re-aimed at local.req._ginaProxyPrefix). Full local suite 4050/4050 green.
* Bumped @rhinostone/swig to 2.0.1. Fixes bracket-access expressions like `arr[arr.length-1]` and `arr[idx-1]` that previously failed with "Unexpected closing square bracket". Restores correct rendering of negative-literal expressions like `{{ -5 }}` whose leading sign was being eaten by the whitespace-control regex. Adds `{{- … -}}` / `{%- … -%}` whitespace-control parity to `@rhinostone/swig-twig`.

## 0.3.10 - 2026-05-06
### Added
* Reverse-proxy path-prefix awareness via the standard `X-Forwarded-Prefix` request header. When a reverse proxy (nginx, Traefik, etc.) mounts the bundle on a sub-path and forwards `proxy_set_header X-Forwarded-Prefix /sub;`, the framework composes a public webroot (proxy prefix + bundle internal `server.webroot`) and templates that into `gina.config.webroot`. Client-side URL construction (`/_gina/assets/routing.json` fetch, the `gina.min.css` link injection, etc.) now targets the correct upstream through the proxy instead of root-relative URLs that would route to whichever bundle answered `/`. Header value is normalised (leading slash, trailing slashes stripped, empty / "/" dropped); back-compat preserved when the header is absent.
### Fixed
* FormValidator binding for HTML5 form-reassociated controls (`form="X"` attribute) — `bindForm` now uses `HTMLFormControlsCollection` (`form.elements`) for owner-aware control collection, attaches per-control listeners on reassociated controls (out-of-tree controls whose events don't bubble to the form), and `unbindForm` symmetrically drains the side-table on cleanup. Parent forms no longer accidentally collect descendants reassociated to other forms.
* FormValidator radio mutual-exclusion grouping for HTML5 form-reassociated controls (`form="X"` attribute). `updateRadio` now scopes the peer set by form-owner — same-name radios in different form-owners are no longer cross-fired into each other's mutual-exclusion loop — and reconciles the IDL `.checked` property with the `checked` HTML attribute on init when they disagree, recovering author intent for radios that surface the parse-time IDL/attribute desync browsers produce in mixed DOM-tree + form-owner layouts.
* FormValidator restoring the wrong radio option on form-reset for HTML5 form-reassociated controls — sister fix to the `updateRadio` parse-time IDL/attribute reconciliation. `bindForm`'s `fieldsSet[id].defaultChecked` cache now reads the IDL `defaultChecked` property (which mirrors the HTML `checked` content attribute) instead of the live `.checked`. For form-reassociated radios hit by Chromium's parse-time IDL desync, `.checked` reads FALSE at bind time despite the attribute being present, so the cache would hold the wrong default and a `type="reset"` action on the form would clear the originally-checked option. No-op for the normal single-form-owner shape.

## 0.3.9 - 2026-05-04
### Added
* Add a `process.gina._bundleFilterWraps[bundleName]` hook so bundles can wrap nunjucks filters at render time. Bundle-level monkey-patches on `lib.nunjucksFilters` do not survive `refreshCore()` (which creates a fresh lib singleton ~per-request in dev mode); the framework now applies registered wraps inside `registerGinaFilters` after the per-request filter factory runs.
### Changed
* Drop the redundant `<namespace>-` prefix from `resolveTemplatePath()` effective name in nunjucks rendering. Routes whose name already carries the namespace (e.g. `project-get`, `client-list`) previously resolved to `project/project-get.njk` and `client/client-list.njk`; they now resolve to the cleaner `<namespace>/<action>.njk` path (e.g. `project/get.njk`, `client/list.njk`).
### Fixed
* Guard `length` filter against null/undefined input in nunjucks-filters and swig-filters. Templates piping a missing variable through `| length` (e.g. `{{ breadcrumb | length }}`) now return 0 instead of crashing with a `TypeError`, matching upstream nunjucks `runtime.length` and Jinja2.
* Handle Couchbase Node.js SDK 4.x `JsonTranscoder` pre-decoded session values in the v4 session-store. Previous code called `.toString()` on the already-parsed object (producing `"[object Object]"`) and then `JSON.parse`d it, surfacing as a 500 on every authenticated request that touched session retrieval.
* Mirror `ctxObj.envVars` into `process.env` during `gna.js` bootstrap so third-party plugins (CSRF middleware, future env-readers) see env vars that `bin/cli`'s `filterArgs()` stripped before spawn. `setEnvVar` populates Gina's own context but did not touch `process.env`, leaving plugins reading the raw `process.env` with `undefined` values.
* Preserve inner Swig string-literal quotes in mid-URL `{{ }}` expressions in `getAssets()`. The decorative-quote-strip guard was anchored (`/^\{\{/`), so an asset URL with `{{ }}` embedded mid-string had its inner Swig quotes stripped, the mangled URL landed in the cached layout `__ginaData.gina.view.assets`, and the runtime Swig pass then threw `Unexpected colon on line N` at parse time. The compile guard keeps its anchor — only fully-Swig URLs are pre-compiled at scan time; embedded `{{ }}` is left for the cached layout runtime Swig pass.
* Isolate per-request middleware dispatch state in `nextMiddleware` via a new `createNextMiddleware()` factory. The previous shape held dispatch state on the function own properties (`._index`, `._count`, `._request`, `._response`, `._next`, `._nextAction`); under concurrent requests, request B setup at the entry point overwrote request A state, so A awaited middleware callbacks resumed against B `req` object — visible as sporadic `[csrf] no req.session.id` 500s when express-session correctly populated `req.session` for A but CSRF then ran with B `req`. Each call to `createNextMiddleware()` now returns a fresh closure with isolated state.
* Add a `require.cache` fallback to the `lib` registry import in `controller.render-nunjucks.js`, mirroring `render-swig.js`. `server.isaac.js` `refreshCore()` overwrites the lib cache entry with the exports value (not a Module object); a plain `require("../../lib")` returns `undefined` after a refresh and crashes the module-level read of `Collection`, `Cache`, `nunjucksFilters`, and `nunjucksResolver`. Route every per-request lookup through a module-scope `libRef` variable that uses `|| require.cache[require.resolve("../../lib")]` as the fallback.
* Promote `userData` keys to the top-level template context in `controller.render-nunjucks.js` for ergonomic parity with swig. The previous code only wrote each key to `data.page.data[k]`, contradicting the existing comment that promised top-level promotion (`{{ foo }}`); each key is now also mirrored to `data[k]` when not already defined.
* Alias `data.data` to `data.page.data` before `env.render()` in nunjucks rendering. Page-shells under nunjucks layout inheritance (e.g. `{% set document = data.document %}`) evaluated to `undefined` because `data` was not a key in the env.render context object. Swig layouts already have this aliasing implicit via `{% set data = page.data %}` at `_layouts/layout.html:1`; nunjucks layouts now get the same lookup path framework-side.
* Substitute `{{ page.X }}`, `{{ page.environment.X }}`, and `{{ page.data.session.X }}` placeholders inside the `ginaLoader` HTML string after `injectAssets()`. `gina.onload.min.js` is inserted as a literal HTML string and so cannot rely on nunjucks to resolve its tokens; without substitution, `window.onGinaLoaded` threw a `JSON.parse` SyntaxError on every page, leaving `gina.popin`, `gina.session`, `gina.forms`, and `window.onGenericXhrResponse` undefined and breaking every legacy popin and form-submit. Mirrors `render-swig.js` flatten-then-`whisper()` approach.

## 0.3.8 - 2026-04-26
### Fixed
* Fixed `npm install -g gina` failing with `Cannot find module 'psl'` on fresh and upgrade installs (regression from 0.3.7). The pre/post-install scripts no longer load the framework `lib` registry — `psl` and `@rhinostone/swig` are now declared as top-level npm dependencies so the runtime resolves them via the standard module resolution chain, and a helpers pre-load in the install scripts ensures `lib/logger`'s circular load completes correctly so internal helpers' module-local `console` references bind to the full Logger singleton.

## 0.3.7 - 2026-04-26
### Added
* Released `0.3.7` — cumulative stable of the `0.3.7-alpha.1` through `0.3.7-alpha.10` cycle. Headline tracks: **Web Security CSRF trilogy** (`gina.plugins.Session()` cookie hardening + `gina.plugins.Csrf()` signed double-submit token middleware + Origin/Referer pre-filter; OWASP ASVS 4.0 V4.2.1 aligned; opt-in plugin shape; per-route `csrfExempt: true` for webhook receivers). **Nunjucks template engine opt-in** (`render.engine = "nunjucks"` per bundle; filter parity #NJ1, setResources/asset injection #NJ2, render cache #NJ3, Early Hints 103 auto-send #NJ4). **MCP server** (`bundle:mcp` static manifest emit + `bundle:mcp-start` stdio and HTTP transports; #AI8 Phases 1/2a/2b). **Connector CLI** (`connector:list/add/rm/migrate`; #CN10). **Eval-safety hardening** (#SCS1 family — collection safe-evaluator + validator-plugin grammar-locked walker replacements; vendored-dep CVE-visibility invariant locked in via OSV scan workflow). Supply-chain trim (`psl` swap for vendored MPL-2.0 PSL data, `optimist` removal). Release-pipeline hardening (`gna.js` stable-version sync at framework-dir rename, StateStore + gina.db sync across renames, asset-plugin build honours `--gzip-bin`/`--brotli-bin` overrides). Plus `Session.name` drop-in identity restoration in alpha.10. See per-alpha sections for per-feature detail and test counts; full suite 3822/3822 at the stable cut.

## 0.3.7-alpha.10 - 2026-04-26 (npm only — no git tag)
### Fixed
* `gina.plugins.Session()` no longer clobbers the wrapper's `Function.name` property. Bundles that introspect the wrapper (`require('gina').plugins.Session(require('express-session')).name`) now see `'session'` (matching upstream) instead of `'ginaSession'`. Achieved via a small refactor: the outer wrapper delegates to an inner `ginaSessionDispatch` named function, with `Object.defineProperty(wrapped, 'name', { value: expressSession.name, configurable: true })` overriding the outer `.name`. The inner `ginaSessionDispatch` frame stays visible in stack traces, so gina remains detectable for debugging while the public-facing identity is the upstream's. Static-surface preservation (`Store`, `MemoryStore`, `Session`, `Cookie`) and the SameSite=None invariant are unchanged. 2 new unit tests in `test/core/session-plugin.test.js` (drop-in identity assertion + stack-trace visibility lock). Full suite 3768/3768 (3766 baseline + 2 new). Surfaced from a consumer-app session: "Gina's wrapper clobbering expressSession.name — is an upstream concern".
### Security
* `gina.plugins.Csrf()` now layers an Origin/Referer pre-filter ON TOP of the signed double-submit token verify (`#CSRF3`). On every mutating request (POST/PUT/PATCH/DELETE) the middleware reads `Origin` first, falls back to parsing the host out of `Referer` when `Origin` is absent (or is the literal `"null"` sentinel browsers send for sandboxed iframes), and matches the result against `settings.json > csrf.allowedOrigins`. Both headers missing → 403 `[csrf] forbidden — missing origin/referer`. Mismatch → 403 `[csrf] forbidden — origin not allowed`. The token verify only runs after the Origin check passes, so a forged token + matching cookie still gets rejected when the request didn't come from an allowed origin (token layer ≠ Origin layer). Per-route `csrfExempt: true` bypasses BOTH layers consistently. New `settings.json` key `csrf.allowedOrigins`: empty/unset defaults to `[bundleHostname]` (auto-derived from `conf[bundle][env].hostname` or composed from `server.scheme + host + server.port`); non-empty replaces the default with an explicit allowlist for multi-domain bundles. Entries are matched literally case-insensitive after parsing down to `scheme://host[:port]` — different scheme on the same host doesn't match (`http://example.com` ≠ `https://example.com`); different port doesn't match. Factory throws at startup when `csrf.allowedOrigins` is empty AND no bundle hostname can be resolved — error message points at both fixes. 54 new unit tests (parser helpers, allowlist precedence, behavioural matrix Origin × Referer × allowlist, scheme/port discrimination, `Origin: "null"` sentinel handling, negative-invariant lock that matching token + mismatching Origin still 403s, exempt interaction, source-inspection guards pinning the pre-filter ordering); full suite 3822/3822 (prior 3768 + 54). Closes the three-phase CSRF trilogy started by `#CSRF1` (cookie hardening, alpha.8) and `#CSRF2` (signed double-submit token, alpha.9).

## 0.3.7-alpha.9 - 2026-04-26 (npm only — no git tag)
### Added
* Exposed gina.csrfToken and gina.csrfInput in swig template context (#CSRF2 follow-up). When a bundle has registered the Csrf plugin and req.csrfToken is set on the active request, controller.js > setOptions() now publishes two keys to the template data: gina.csrfToken (raw base64url string) and gina.csrfInput (pre-formatted <input type="hidden" name="<fieldName>" value="<token>"> HTML). Templates render the hidden input with either {{ gina.csrfInput | safe }} or the manual <input ... value="{{ gina.csrfToken }}"> form. The field name comes from settings.json > csrf.fieldName (default _csrf, matching the plugin) and is HTML-attribute-escaped defensively (& " < >) before interpolation. The token itself is base64url ([A-Za-z0-9_-]) by construction so it is interpolated verbatim — encoding it would break round-trip when the form is submitted. When req.csrfToken is absent (bundle has not adopted the plugin) neither key is set; templates can guard with {% if gina.csrfToken %}. 25 unit tests in test/core/controller-csrf-context.test.js cover the source-inspection guards, the missing/empty/non-string rejection, the configured-fieldName path, the four HTML-attribute escapes, and the negative invariant (no eval, no template-literal interpolation, no encoding of the token).
### Security
* Added gina.plugins.Csrf() — signed double-submit token CSRF middleware (#CSRF2). New core/plugins/lib/csrf/ middleware sits in the Express drain after the Session plugin. Token shape: HMAC-SHA256(sessionId + ':' + nonce_b64url, GINA_CSRF_SECRET); nonce is 16 bytes from crypto.getRandomValues; both halves base64url-encoded; verify uses crypto.timingSafeEqual with a length guard. Mutating methods (POST/PUT/PATCH/DELETE) require a matching value in X-Gina-CSRF-Token header or _csrf form field; safe methods (GET/HEAD/OPTIONS) issue a token cookie and pass through. Per-route opt-out via routing.json > "csrfExempt": true for webhook receivers (Stripe, GitHub, etc.) — positive-assertion key chosen so a misread breaks the webhook (obvious) rather than CSRF-vulnerable (silent). Stateless so it scales with distributed Redis/K8s sessions without server-side storage; signed so sibling subdomains cannot inject cookies. Adoption is two lines in the bundle bootstrap, AFTER the session middleware: var csrf = require('gina').plugins.Csrf(); app.use(csrf). Server secret (GINA_CSRF_SECRET, generate with openssl rand -base64 64) is required at factory-call time — there is no dev fallback. Sessionless or session-after-csrf misorder produces a clear next(err) message that points at the fix ("Csrf plugin requires the Session plugin to be registered before it"). Settings template seeds csrf.{cookieName,headerName,fieldName,rotate,safeMethods}; the secret is never stored in settings. server.js + lib/routing/src/main.js propagate routing[name].csrfExempt to req.routing.csrfExempt next to cache and queryTimeout. 69 unit tests in test/core/csrf-plugin.test.js (source-inspection guards, generateToken/verifyToken primitives, negative-invariant lock against ==/===/Buffer.compare/.includes on the token path, issue + verify middlewares end-to-end through stub req/res, per-route exempt, plugin registration, settings template integrity). Full suite 3714/3714. Followups in commit 2 (validator AJAX header injection) + commit 3 (controller template context) close the user-facing surface; this commit ships the verify gate and the seam.
* Validator AJAX requests now inject the X-Gina-CSRF-Token header on mutating methods (#CSRF2 follow-up). The browser-side validator plugin reads the gina-csrf-token cookie (set by the Csrf plugin) and adds the matching X-Gina-CSRF-Token request header on POST/PUT/PATCH/DELETE XHR calls; safe methods (GET/HEAD/OPTIONS) bypass injection. Three injection sites covered: form-submit XHR (core/plugins/lib/validator/src/main.js header loop), file-removal DELETE/POST (same file, xhrOptions block), and live-validation queries (core/plugins/lib/validator/src/form-validator.js). Adds two pure helpers — readCsrfCookie() (parses document.cookie via indexOf+slice; no regex on user-controlled segments; URL-decodes the token; returns null when absent) and isMutatingMethod() (returns false for GET/HEAD/OPTIONS, true for everything else). Cookie name (gina-csrf-token) and header name (X-Gina-CSRF-Token) hardcoded to match the plugin defaults — settings.json overrides apply server-side only; if a bundle changes the names, the validator fall-back is to ship no header (safe — server rejects, dev sees 403). Browser bundle (dist/vendor/gina/js/gina.*) rebuilt because both validator src files are aliased into the client-side AMD tree via build.json. Negative invariant locked: no eval, no Function constructor in the new helper region. 27 unit tests in test/core/validator-csrf-injection.test.js (source-inspection guards pinning the three injection sites, readCsrfCookie pure-parser behaviour, isMutatingMethod safe-method bypass, end-to-end XHR stub coverage of cookie-present/cookie-absent x mutating/safe matrix, eval / new Function rejection). Full suite 3766/3766 (3739 baseline + 27 new).

## 0.3.7-alpha.8 - 2026-04-25 (npm only — no git tag)
### Fixed
* StateStore + gina.db now stay in sync with main.json/settings.json across framework-directory renames during `bumpVersion`. Closes a silent drift that affected every alpha cut for several versions and required manual SQLite patches. Two failure modes addressed: relative `require('../state')` resolution after the rename invalidated the generator's `__dirname`, and `StateStore._homeDir` returning null in build-script context where `GINA_HOMEDIR` isn't injected. Future violators of either invariant now log a visible warning instead of silently falling through to a JSON-sidecar-only write.
* `framework/v*/core/asset/plugin/build` now honours the `--gzip-bin` and `--brotli-bin` CLI overrides. Six `-exec gzip -9 -n -k` / `-exec brotli --best` calls were hardcoded and silently defeated the flags; they now use the `${gzip_bin}` / `${brotli_bin}` variables resolved via `which` or set by the user.

## 0.3.7-alpha.7 - 2026-04-25 (npm only — no git tag)
### Fixed
* Schema `$schema` identifier updated from `http://json-schema.org/draft-07/schema#` to `https://json-schema.org/draft-07/schema#` across all seven `schema/*.json` files (app, app.crons, connectors, manifest, routing, settings, watchers). The plain-HTTP form redirects to HTTPS and is bot-blocked by json-schema.org; IDEs and JSON validators that dereference the identifier at validation time were hitting the 302/403 chain instead of fetching the canonical schema. Matches the URI shown in the JSON Schema draft-07 spec and aligns with the `$id` field, which already uses `https://`. First batch from the link-health audit (gina-io/gina#18).
* Refreshed stale URL references in code comments and man pages across the framework (link-health audit gina-io/gina#18): `core/template/boilerplate/bundle/controllers/setup.js` — dropped reference to the deleted `github.com/jmcmanus/pagedown-extra` repo (the commented-out example code uses `marked`, not pagedown-extra; the link was misleading even before the repo died). `core/connectors/couchbase/lib/session-store.v{2,3,4}.js` — updated `github.com/visionmedia/connect-redis` to `github.com/tj/connect-redis` (the maintainer renamed; old URL still 302s but relying on a stale redirect chain is brittle). `core/locales/README.md` — replaced `currency-iso.org/en/home/tables/table-a1.html` (folded into a SIX Swiss Exchange commercial page) with the canonical `iso.org/iso-4217-currency-codes.html`. `lib/cmd/gina.1.md`, `gina-dev.1.md`, `gina-framework.1.md` — modernised the `groups.google.com/forum/#!forum/ginajs` mailing-list link to the supported `groups.google.com/g/ginajs` URL format. The Google Group itself is still active; only the hash-bang URL format is obsolete. `core/server.js` — dropped dead `strongloop.com/strongblog/...` blog-post reference (domain TLS-broken, StrongLoop dissolved into IBM in 2015, the blog post is not reachable via any successor) and dead `jsperf.com/arraybuffer-string-conversion/4` test reference (410 Gone; jsperf relaunched on Cloudflare but the historical test data was not restored). Comment-only changes; no runtime behaviour modified.
* `core/asset/plugin/src/vendor/gina/utils/dom.js` — updated the `@source https://gist.github.com/1129031` comment to the current canonical form `https://gist.github.com/eligrey/1129031`. GitHub now requires the username prefix on gist URLs; the old form 302-redirects. Rebuilt dist artefacts in the same commit per the source-dist alignment rule (llms.txt §72) — `dist/vendor/gina/js/gina.js`, `gina.min.js`, and the sibling `gina.min.js.br`/`gina.min.js.gz` compressed copies regenerate identically except for the updated license header URL. No runtime behaviour change. Link-health audit (gina-io/gina#18).
### Security
* Removed the `polyfill.io` example from the popin plugin's HTML documentation (`core/asset/plugin/src/vendor/gina/popin/doc/html.md`). The domain NXDOMAINs as of April 2026 and was famously hijacked mid-2024 after its Chinese owners were reported injecting malicious code into the served polyfills. The surrounding guidance on how to include polyfills yourself (via `js/plugins.js` or `js/vendor/polyfills.js`) is preserved. Documentation-only change; no runtime code path loads the domain. Link-health audit (gina-io/gina#18).
* Added `gina.plugins.Session` — an opt-in hardened wrapper around `express-session` that injects SameSite/HttpOnly/Secure defaults from `settings.json > session.cookie.*` into the cookie options before the middleware sees them (#CSRF1). Caller-supplied `options.cookie` values always override the defaults, so bundles that rely on `httpOnly: false` (for client-side JS reading `document.cookie`) or other intentional choices are preserved. Adoption is a one-line swap in the bundle bootstrap: `var session = require('gina').plugins.Session(require('express-session'))`. The new `session.cookie.{sameSite, httpOnly, secure}` keys land in `core/template/conf/settings.json` with safe defaults ("lax" / true / "auto"). Browser-parity invariant enforced at session factory call time — `SameSite=None` without `Secure` throws. Baseline for the #CSRF2 / #CSRF3 track.

## 0.3.7-alpha.6 - 2026-04-24 (npm only — no git tag)
### Security
* Restored CVE visibility on vendored deps. `gina@0.3.7-alpha.5` stripped `dependencies`/`devDependencies`/`scripts` from `core/deps/busboy-1.6.0/package.json` and `core/deps/streamsearch-1.1.0/package.json` to reduce Socket's dep-count display — but the strip also removed the dep-graph edge that Socket, Dependabot, and `npm audit` follow to associate the vendored copies with their CVE records. Vendored copies of upstream packages are supply-chain surface exactly like npm-installed ones; stripping the metadata hid them from the tools meant to alert on them. Both files are now byte-identical to their upstream npm tarballs again. Pinning discipline going forward: the vendored `package.json` stays byte-identical to upstream until a local patch is applied; on patch, bump `version` to `<upstream>-rhinostone.N` (e.g. `1.6.0-rhinostone.1`) so CVE scanners still match the base version and readers see at a glance that the copy has diverged. Also adds an OSV-based CI scan (`.github/workflows/vendored-cve.yml`) that walks every `core/deps/*/package.json` and every `framework/v*/package.json`, queries `api.osv.dev` for each `(name, version)` pair, and fails the build on any matched vulnerability — belt-and-suspenders, independent of whichever third-party scanner happens to be in use. Runs on push to `develop`/`master`, weekly Sunday cron, and manual dispatch.
* Replaced four `eval(...)` call sites in the validator plugin with safe, grammar-locked equivalents (#SCS1e, bucket (c) partial progress). `core/plugins/lib/validator/src/main.js:2603` — `eval('gina.forms.rules.' + customRule)` replaced with a dot-path walker. `customRule` is read from the user-controlled `data-gina-form-rule` HTML attribute; the old eval executed anything that parsed as JS, so a crafted attribute like `constructor.constructor("return process.exit()")()` would fire on lookup. The walker rejects any character outside `[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*` and returns undefined on missing path (falling through to the existing "no rule found" error). `core/plugins/lib/validator/src/form-validator.js:161` — `eval('data.' + localValue)` in `compileError()` replaced with a dot+bracket path walker. `localValue` is derived from `{{...}}` placeholders in error messages; the transforms at lines 152-158 produce `ident (. ident | ["quoted"])*`, and the walker rejects anything outside that shape. `core/plugins/lib/validator/src/form-validator.js:893` — the regex-literal branch of `is()` replaced with `new RegExp(body, flags).test(value)`. Parses `/<body>/<flags>` only; `/<body>/` without flags or any non-regex input throws. `core/plugins/lib/validator/src/form-validator.js:895` — the binary-compare branch of `is()` replaced with a regex-based evaluator over the closed grammar `<operand><op><operand>` where operand is in `{number, "string", true, false, null, undefined}` and op is in `{===, !==, ==, !=, <, >, <=, >=}`. After `$var` substitution (lines 910-920) and paren/return strip (line 925), real consumer-app conditions (15 regex literals + 11 binary comparisons) all match the two grammars. Any input outside the grammars (arithmetic, function calls, semicolon injection, throw injection) is rejected. Seven validator-plugin sites remain deferred pending a separate design pass (`form-validator.js:919`, `:1722`, `main.js:2320`, `:2329`, `:2931`, `:2947`, `:2959`). Verified by +69 tests in `test/lib/validator-scs1e.test.js` (source inspection, behavioural parity against corpus, injection rejection).

## 0.3.7-alpha.5 - 2026-04-23 (npm only — no git tag)
### Changed
* Extracted `DRIVER_MAP` + `AI_DRIVER_MAP` into a new `lib/connector-registry/` module — single source of truth for the connector driver → npm package + semver range mapping. Previously duplicated inline in `lib/cmd/connector/add.js` and `lib/cmd/connector/list.js` and kept in sync by hand; the two copies had already drifted (list.js knew about `mongodb` and `scylladb`, add.js did not). Both cmd handlers now read from `lib.connectorRegistry.getDriver(type)` / `.getAIDriver(scheme)` / `.getDriverTypes()` / `.getAISchemes()`, so adding a new connector upstream means editing one table instead of three (root `package.json` `peerDependencies` was the third, removed in the previous commit). Public CLI behaviour is unchanged — the registry exposes the same tables, the install hint and `--install` resolution use the same ranges. 28 new source-inspection + behavioural tests in `test/lib/connector-registry.test.js`; the inline-map tests in `test/lib/connector-add.test.js` and `test/lib/connector-list.test.js` now assert registry consumption instead.
### Security
* Replaced the two eval-based sites in `lib/collection/src/main.js` with a safe expression parser and a dot-path walker (#SCS1d). The `tryEval` function — called from four internal sites (lines 310, 314, 406, 411) when a filter contains `<`, `>`, or `=` — used to `eval(_content + filter)`. The line-411 variant had no quote-wrapping, so a filter value like `{'rating.score': '> 0; throw new Error()'}` reached eval unsanitised and executed. The body is now a regex-based evaluator over the closed grammar `<operand><op><operand>` where operand is a number literal, a `"string literal"`, or a `new Date("...")` expression, and op is one of `=== !== == != < > <= >=`. Anything else throws `Could not evaluate condition`. The sibling `eval('_content.' + f)` inside `searchThroughProp` used to eval a user-controlled property path (the filter-object key), which accepted arbitrary JS — a key like `constructor.constructor('return ...')()` would fire on lookup. It is now a dot-path walker that rejects any non-identifier character. The `skipEval: true` bypass path is untouched (it was already eval-free). No public API change; the 15 pre-existing Collection tests pass unmodified; 7 new tests lock the guarantee (source-inspection, date-compare coverage fill, injection rejection). The browser bundle picks the fix up on next `core/asset/plugin/build` rebuild because `core/asset/plugin/src/vendor/gina/build.json:21` aliases `lib/collection` to the same server-side source.
* Removed optional connector drivers from the root `package.json` `peerDependencies` block (couchbase, mongodb, mysql2, pg, ioredis, `@scylladb/scylla-driver`, openai, `@anthropic-ai/sdk`). These were declared with `peerDependenciesMeta.optional = true` but supply-chain analyzers (Socket.dev, npm audit, Dependabot) were still aggregating them into gina's dependency graph — surfacing 7 extra packages as gina's supply-chain surface even though gina never loads any of them by default. The version ranges are still authoritative in the `DRIVER_MAP` / `AI_DRIVER_MAP` tables in `lib/cmd/connector/add.js` and `lib/cmd/connector/list.js`, which is where `connector:add --install` has been reading them all along. No runtime behaviour change — when a bundle requires an uninstalled driver, `require('<driver>')` throws the same `MODULE_NOT_FOUND` it always did. Users who want a given connector install the driver in their own project via `gina connector:add <name> @<project> --install` or by hand.
* Stripped `dependencies`, `devDependencies`, and `scripts` from the vendored `core/deps/busboy-1.6.0/package.json` (and `devDependencies` + `scripts` from `core/deps/streamsearch-1.1.0/package.json`). busboy is vendored and finds streamsearch via a relative path (`require('../../../streamsearch-1.1.0')`), so the declared `"streamsearch": "^1.1.0"` dependency was cosmetic and never consumed at runtime — but supply-chain analyzers walk sub-`package.json` files inside the published tarball and were surfacing streamsearch as a top-level gina dep. The `devDependencies` and `scripts` blocks are dead in a vendored context (no `npm install` runs against them; no `npm test` either). No runtime impact — both modules still load via the same relative paths they did before.

## 0.3.7-alpha.4 - 2026-04-23 (npm only — no git tag)
### Added
* Static HTML cache writes on the nunjucks render path (#NJ3) — bundles running `render.engine: "nunjucks"` now get the same `memory`/`fs` cache behaviour swig has shipped for years. A route declaring `"cache": { "type": "memory", "ttl": 300 }` (or the `"fs"` variant) is served from the shared server store on subsequent hits without re-entering the controller; the same `static:<bundle>:<url>` key-space + visibility/sliding-window/invalidateOnEvents controls apply. Cache writes run after asset + Inspector injection so the bytes on disk/in memory match what the client sees; the miss-path `Cache-Control: private, max-age=N` header is committed on the same response. Opt-in via your routing rule — there is no new setting to toggle.
* 103 Early Hints auto-send now works for bundles running `render.engine: "nunjucks"` (#NJ4). HTTP/2 non-dev requests to nunjucks bundles get automatic `Link: ...; rel=preload` hints for the bundle's CSS and JS assets, delivered as a 103 Early Hints informational response before the HTML body. Identical behaviour to swig — the firing point in `controller.js this.render()` is engine-agnostic and the data-feed was closed by #NJ2 (`deps.setResources` runs on both delegates, populating `h2Links` via the same `getNodeRes` path). Closes the final item in the four-session nunjucks parity track (#NJ1 filters / #NJ2 setResources & assets / #NJ3 static HTML cache / #NJ4 Early Hints) — nunjucks bundles now have full feature parity with swig.
### Security
* Removed three server-side `eval` / `new Function` call sites (#SCS1). `core/server.js` built a static-path regex via `eval('/^…/')` and now uses `new RegExp`. `lib/logger/src/main.js` generated per-level logger methods via `new Function(...)` string concatenation and now uses a closure factory. `lib/math/index.js` `operate()` wrapped `new Function('return '+ expr)()` and is now a shunting-yard evaluator supporting `+ - * / %`, parentheses, decimals, and unary sign; any non-arithmetic character throws. Public contract unchanged — the expressions `math.operate` accepts are the same as before, but it now rejects arbitrary JS beyond pure arithmetic instead of silently executing it.
* Replaced the four `eval('async function on<Name>...')` async-chain scaffolding sites (#SCS1b) with direct `promisify` + `try`/`await`/`catch` IIFEs. Affected files: `lib/cmd/framework/build.js`, `lib/cmd/framework/init.js`, `script/pre_install.js`, `script/post_install.js`. Semantics unchanged — each step is still promisified, awaited, and error-exits via `console.error` + `process.exit(1)` before recursing to the next step. Old eval lines kept as comments with the `#SCS1b` tag per the replace-don't-delete convention.

## 0.3.7-alpha.3 - 2026-04-23 (npm only — no git tag)
### Added
* The nunjucks render path now registers Gina's built-in filter registry automatically. Bundles with `render.engine: "nunjucks"` get the same 7 filters as the swig path — `getUrl`, `getWebroot`, `length`, `nl2br`, `addHours`, `addDays`, `addYears` — with no manual `env.addFilter()` calls needed. Templates can use `{{ 'home' | getUrl }}`, `{{ body | nl2br | safe }}`, or date helpers out of the box. User-registered filters on top (`env.addFilter('upper', fn)`) coexist with the built-in registry. Closes the most user-visible gap left by the 0.3.7-alpha.2 nunjucks MVP.
* Bundles with `render.engine: "nunjucks"` now get automatic asset injection — the same way swig bundles do. Stylesheets, scripts, external plugins, and the Gina loader script are placed into the rendered HTML without any manual `<link>` or `<script>` wiring in layouts. Templates can still opt in explicitly via `{{ page.view.stylesheets | safe }}` and `{{ page.view.scripts | safe }}` — when either is used, auto-injection is skipped for that slot so tags never appear twice. `javascriptsDeferEnabled` moves scripts to `<head>` instead of before `</body>`, and `javascriptsExcluded: "**"` suppresses the loader. XHR / popin responses (`isWithoutLayout`) ship only the common-gina assets, matching swig-side behaviour. Closes the biggest semantic gap left by the 0.3.7-alpha.2 nunjucks MVP.
### Changed
* Swapped lib/domain from a vendored MPL-2.0 Public Suffix List (`framework/v*/lib/domain/dist/*.dat`) to the MIT-licensed `psl@^1.15.0` npm package. The public API is preserved — `new Domain()` / `new Domain(cb)` + `getRootDomain(host).value` / `.isSLD` / `.isRegisteredTldOrSld` all behave the same on recognised domains. Under the hood, the wrapper delegates to `psl.parse()` and falls back to the hostname's last two labels for unknown suffixes (matches today's behaviour on `.local`, `.localdomain`, cluster-internal pseudo-TLDs). IPv4 literals (`192.168.1.5`), IPv6 literals (`[::1]`), and `undefined`/`null` inputs are now handled defensively rather than producing broken values or throwing. Browser-side, the psl UMD is vendored and exposed via RequireJS as `vendor/gina/psl`. Shrinks the published tarball by ~780 KB and removes the only copyleft-flagged content from Socket's scan.
### Removed
* Removed orphaned core/deps/optimist-0.6.1/ vendored dependency. optimist (X11 license, flagged by Socket) was vendored but never actually used by any framework code — bin/cli parses CLI arguments directly via process.argv. Its declared transitives (wordwrap, minimist) were never installed, so any attempt to require it would have thrown MODULE_NOT_FOUND. Removal shrinks the published tarball by ~140KB and ticks up Socket's License score.

## 0.3.7-alpha.2 - 2026-04-22 (npm only — no git tag)
### Added
* Inspector reveal toggle — dev-mode `/_gina/reveal` JSON endpoint returns the unredacted `__ginaData` snapshot when the bundle scope is `local`; bundles running in beta/production/testing return 403 and never store a raw snapshot. The Inspector Data tab has a matching red-tinted Reveal toggle that calls the endpoint and replaces the redacted payload client-side. Persisted in `localStorage.__gina_inspector_reveal`; automatically disabled and cleared when the bundle scope flips away from local.
* Explicit exports for `gna.js` globals (#M8 / #AI3) — every framework-injected helper is now also surfaced as a named property on `require('gina')` / `require('gina/gna')`. Context helpers (`setContext`, `getContext`, `joinContext`, `resetContext`, `getLib`, `whisper`, `define`, `getDefined`, `isWin32`), path helpers (`_`, `setPath`, `getPath`, `setPaths`, `getPaths`, `onCompleteCall`), model helpers (`getModel`, `getModelEntity`), JSON/Data/Text/Console/Task helpers (`requireJSON`, `encodeRFC5987ValueChars`, `formatDataFromString`, `__`, `log`, `run`) and env helpers (`getUserHome`, `getEnvVar`, `getEnvVars`, `setEnvVar`, `getProtected`, `filterArgs`, `getLogDir`, `getRunDir`, `getTmpDir`, `getBundleStartingArgv`, `getVendorsConfig`, `setVendorsConfig`, `defineDefault`, `parseTimeout`, `merge`) plus `ApiError` are now statically discoverable, with JSDoc on each entry so IDEs, static analysers and the upcoming `.d.ts` generator (#M9) can navigate to definitions. Runtime globals are unchanged — this is an additive export surface only.
* Auto-generated `types/gna.d.ts` from JSDoc (#M9) — new `script/generate_gna_types.js` reads the `GLOBAL_EXPORTS` inventory from `framework/v*/test/unit/gna-exports.test.js` (locked in by #M8) and per-export JSDoc blocks on `framework/v*/core/gna.js`, then emits `types/gna.d.ts`. For each global it prefers `typeof globalThis.<name>` when declared in `types/globals.d.ts`, otherwise synthesizes a TypeScript function signature from `@param`/`@returns`. Zero non-core dependencies. Wired into two new npm scripts — `npm run types:gen` (regenerate) and `npm run types:check` (CI-style drift detection, exit code 1 on mismatch). A new unit test `framework/v*/test/unit/gna-types-drift.test.js` re-runs the generator in-memory and asserts the output matches the checked-in file, so `.d.ts` drift is caught on every test run. Side effect — the regenerated file now covers the 12 exports that the hand-written `gna.d.ts` was missing (`joinContext`, `encodeRFC5987ValueChars`, `formatDataFromString`, `__`, `log`, `getProtected`, `filterArgs`, `getBundleStartingArgv`, `getVendorsConfig`, `setVendorsConfig`, `defineDefault`, `merge`), closing the #M8 → #M9 loop.
* `gina bundle:mcp <bundle> @<project>` — generates a Model Context Protocol (MCP) tool manifest from `routing.json` at `<bundle>/config/mcp.json` (override with `--output=<path>`). Targets MCP specification revision 2025-06-18: emits one Tool per (route × URL variant × HTTP method) combination with `name`, `title`, `description`, `inputSchema` (Draft-07, `type: "object"`, URL path params as required strings with `pattern`/`enum` from `requirements`), `annotations` (`readOnlyHint` for GET, `destructiveHint` for POST/PUT/DELETE), and `_meta` (`io.gina.*` prefix preserving `routeName`, `url`, `method`, `namespace`, `control`, `middleware`, `scopes`, `cache`, `bundle`, `hostname`, `requirements`, `_sample` for downstream dispatch). Framework-internal routes (`/_gina/*`), HEAD, and OPTIONS are skipped. Cross-bundle proxy routes outside the current emission are warned and skipped. Source-of-truth is `routing.json` directly — independent of `bundle:openapi`. No alias. No runtime MCP server is started (deferred to #AI8b). Ships alongside a new `lib/routing-introspect/` module that extracts the shared URL/method/requirement parsing used by both `bundle:openapi` and `bundle:mcp`, covered by 45 unit tests in `test/lib/routing-introspect.test.js`. #AI8
* `gina bundle:mcp-start <bundle> @<project>` — runs a live Model Context Protocol server for a single bundle over stdio (MCP specification revision 2025-06-18, JSON-RPC 2.0 framing, newline-delimited UTF-8). Reads the static manifest written by `gina bundle:mcp` from `<bundle>/config/mcp.json` and dispatches incoming `tools/call` requests as real HTTP requests against the running bundle's configured port on localhost. Implements `initialize`, `ping`, `tools/list`, `tools/call`, `notifications/initialized`, and `notifications/cancelled`; unknown methods return `-32601` (METHOD_NOT_FOUND); argument validation failures return `-32602` (INVALID_PARAMS); tool execution failures (upstream 4xx/5xx, ECONNREFUSED, timeout) are returned as `{content, isError: true}` per MCP — never as JSON-RPC errors. Tool path parameters are substituted into the URL (`/invoice/:id` → `/invoice/42`); remaining arguments become the query string for GET/DELETE/HEAD or the JSON body for POST/PUT/PATCH. `application/json` and `application/problem+json` responses become `structuredContent` + a text block; non-JSON responses fall back to a text block. Default dispatch timeout is 30 seconds. Stdio discipline is enforced by an early intercept in `bin/cli`: when `bundle:mcp-start` is detected in argv, `process.stdout.write` is redirected to stderr and the real write is stashed on `process.__ginaMcpStdout` before any framework module is loaded — the MCP wire stays clean; framework logs, bootstrap noise, and `console.log` output go to stderr where MCP clients ignore them. The server warns on stderr if `routing.json` has been modified after `mcp.json` (stale manifest) and flags tools whose middleware list looks session-scoped (`auth`/`session`/`login`). Prerequisites — the bundle MUST be started (`gina bundle:start`) and a manifest generated (`gina bundle:mcp`) first. Ships with two new framework libraries — `lib/mcp-server/` (transport-agnostic JSON-RPC + lifecycle state machine) and `lib/mcp-dispatch/` (HTTP loopback dispatcher), covered by 80 unit tests across `test/lib/mcp-server.test.js`, `test/lib/mcp-dispatch.test.js`, and `test/bin/bundle-mcp-start.test.js`. #AI8 Phase 2a — Streamable HTTP transport deferred to a later phase.
* `lib/mcp-dispatch` — concurrency cap on the HTTP loopback dispatcher. `createDispatcher({ maxInFlight })` rejects the (N+1)th concurrent `dispatch()` call with `{content, isError: true}` (never a JSON-RPC error) once the in-flight count reaches the cap; defaults to `DEFAULT_MAX_IN_FLIGHT = 16`. Early-exit paths (missing `_meta["io.gina.url"]`, unresolved path parameter, over-limit) do not consume a slot — only calls that actually issue an upstream HTTP request count against the budget. A new `getInFlightCount()` method is exposed on the dispatcher for ops instrumentation. Latent today in `bundle:mcp-start` because stdio is single-client; prevents pile-up behind a slow upstream once multiple MCP clients share one dispatcher via the Streamable HTTP transport (#AI8 Phase 2b).
* `gina bundle:mcp-start --timeout-ms=<n>` — overrides the HTTP dispatch timeout for a single MCP server process. Also reads `mcp.json > server > timeoutMs` when the CLI flag is absent. Precedence: `--timeout-ms` > manifest > 30 000 ms default. Non-numeric / non-positive values at any layer fall through to the next layer with a stderr warning, so a malformed override cannot silently disable the timeout. The resolved value is surfaced in the startup info line (`Dispatch target: <url> (timeout: <n> ms)`) for operator visibility. The previous hardcoded 30 s default still applies when neither override is set.
* Dual-mode Inspector statusbar link — the dev-mode statusbar now prefers the standalone Inspector SPA when `config/settings.json > inspector.url` is set on a bundle (e.g. `"inspector": {"url": "http://localhost:4200/inspector/"}`), opening it in a new tab with `?target=<this-origin+webroot>` pre-filled. When unset, the link falls back to the legacy embedded popup at `{webroot}/_gina/inspector/` with its persisted geometry — existing behaviour is unchanged. The resolved URL is exposed to the client shim via `window.__ginaData.gina.inspectorUrl`, injected by both `controller.render-swig.js` and `controller.render-json.js` so HTML and XHR-driven pages agree on the target. Cross-origin support for the standalone SPA is rounded out by adding `access-control-allow-origin: *` to the two remaining `/_gina/*` handlers that still lacked it — the Inspector SPA asset route (`/_gina/inspector/*`) and the log stream (`/_gina/logs`) — in both `server.isaac.js` and `server.js` so the SPA can pull CSS/JS/fonts and subscribe to the SSE logs from a different origin than the target bundle.
* Dual-mode resolution for `gina inspector:open` — the command now picks the Inspector URL the same way the dev-mode statusbar does since `0.3.7-alpha.2`. Resolution order: (1) `--url=<url>` CLI override, (2) the bundle's `config/settings.json > inspector.url` (e.g. `"inspector": {"url": "http://localhost:4200/inspector/"}`), (3) the embedded popup at `<target>/_gina/inspector/` (legacy fallback, unchanged). In every case the target bundle origin is passed as `?target=<origin>` so the standalone SPA knows which bundle to drive. The `--url` flag is the escape hatch when the Inspector is reachable at an address not declared in the bundle config. When `--port` is used alone (no project/bundle context) and `--url` is not passed, the embedded path is kept — there is no config to read.
* `gina inspector:open` now accepts a full `http(s)://` URL as the positional target — useful when bundles run on Docker or a remote environment while the Inspector SPA runs on the host (e.g. `gina inspector:open https://local.example.com/api/`). The URL is used as the target origin directly; project and bundle resolution are skipped, mirroring the `--port` short-circuit. A new per-user fallback was also added to the Inspector URL resolution chain: `~/.gina/<shortVersion>/settings.json > inspector.url`. Set it once and every invocation without `--url=` uses the same standalone SPA origin. Full resolution order: (1) `--url=<url>`, (2) bundle `config/settings.json > inspector.url`, (3) global `~/.gina/<shortVersion>/settings.json > inspector.url`, (4) embedded popup at `<target>/_gina/inspector/`.
* `gina service:list` — lists framework-internal services (bundles registered under `@gina`) with their name, preferred dev port, src-existence status, and current running state. Running state is probed from `~/.gina/run/<service>@gina.pid` combined with `process.kill(pid, 0)`; stale pidfiles report the service as stopped without being auto-deleted (clean-up stays with `bundle:stop`). Ports are read from `~/.gina/ports.reverse.json` and the preferred one picked by the `http/2.0 https` → `http/1.1 https` → `http/1.1 http` precedence (dev env first, then the first env present). Supports `--format=json` for scripting, and tolerates a missing or malformed `ports.reverse.json` without erroring. `@gina` is the only project accepted for now — `gina service:list @otherproject` is rejected with a clear message; user-defined services are not a surface yet. Ships with a new cmd group at `framework/v*/lib/cmd/service/` (`list.js`, `help.js`, `help.txt`) registered in the `bin/cli` offline allowlist, and 37 source-inspection unit tests in `test/lib/service-list.test.js`.
* `gina connector:list` — lists connectors declared across a project's `shared/config/connectors.json` and every bundle's `config/connectors.json`, with driver install status, range and version-pin info. Three invocation modes: `gina connector:list` (every registered project), `gina connector:list @<project>` (one project, shared + all bundles), `gina connector:list <bundle> @<project>` (merged shared+bundle view that the bundle sees at runtime). Each row shows a status flag (`[ ok ]` installed or Node-builtin, `[ ?! ]` driver declared but not found in `<project>/node_modules/<driver>`, `[ ?? ]` unknown connector type or unrecognised `ai` protocol), the logical name, the resolved connector type, a source label (`[shared]`, `[<bundle>]`, `[<bundle> override]`), and driver details (`<npm>@<range>` + pin + installed version, or `run npm install <npm>` when missing, or `(built-in)` for `node:sqlite`). Driver resolution follows the framework's `peerDependencies`: `couchbase` → `couchbase`, `redis` → `ioredis`, `mysql` → `mysql2`, `postgresql` → `pg`, `mongodb` → `mongodb`, `scylladb` → `@scylladb/scylla-driver`, `sqlite` → built-in `node:sqlite`, `ai` → `@anthropic-ai/sdk` (for `anthropic://`) or `openai` (for `openai://`, `deepseek://`, `qwen://`, `groq://`, `mistral://`, `together://`, `ollama://`, `gemini://`, `xai://`, `perplexity://`). Shared and bundle-level overlay is key-level with bundle winning on conflicting keys, mirroring the runtime merge in `core/config.js`. When two bundles pin the same driver at different `version` values the command emits a trailing `[ !! ] driver \`<name>\` has conflicting \`version\` pins` line — npm resolves `node_modules/<driver>/` to a single version per project, so the first install wins. Supports `--format=json` for scripting with a stable `{project, bundle?, status, connectors: [{project, bundle, name, connector, source, driver, builtin, range, version, installed, installedVersion, note, unresolved}]}` shape. `connectors.json` and `manifest.json` files are parsed with `//` and `/* */` comment tolerance via `requireJSON`. Registered as an offline command — no framework socket required. Ships with a new cmd group at `framework/v*/lib/cmd/connector/` (`list.js`, `help.js`, `help.txt`, `arguments.json`) and 89 source-inspection unit tests in `test/lib/connector-list.test.js`. First of the #CN10 multi-session plan — subsequent sessions add `connector:add`, `connector:rm`, and `connector:migrate`.
* `gina connector:add` — writes a connector entry to a project's `shared/config/connectors.json` or a bundle's `<bundle-src>/config/connectors.json`. Two invocation modes: `gina connector:add <name> @<project>` (writes to shared) and `gina connector:add <name> <bundle> @<project>` (writes to the bundle, resolved via `manifest.bundles[bundle].src`). Connector type is inferred from `<name>` when it matches one of the six allowed types (`couchbase`, `mysql`, `postgresql`, `sqlite`, `redis`, `ai`), or set explicitly via `--connector=<type>` (or the `--driver=<type>` synonym). Flags cover the common keys: `--host=`, `--connector-port=`, `--database=`, `--username=`, `--password=`, `--scope=` (one of `local`, `beta`, `production`, `testing`), and the AI-specific `--protocol=`, `--model=`, `--api-key=`, `--base-url=`. Driver version pinning via `--driver-version=<range>` writes a `version` field on the entry (new in the `schema/connectors.json`); when set, the printed install hint uses the pin (`npm install ioredis@"^5.0.0"`) instead of the framework's `peerDependencies` range. Preserves any `//` or `/* */` comment header above the first `{`; serialises the body with 4-space indentation; pins `$schema` at the top of the output with the canonical `https://gina.io/schema/connectors.json` URL; preserves existing key order and replaces an overwrite in place (not moved to the bottom). After writing, prints a one-line install hint — `Next: run `npm install <pkg>@"<range>"` inside your project root.` — using `@anthropic-ai/sdk` for `anthropic://` or `openai` for any other OpenAI-compatible AI protocol (`openai://`, `deepseek://`, `qwen://`, `groq://`, `mistral://`, `together://`, `ollama://`, `gemini://`, `xai://`, `perplexity://`). `sqlite` short-circuits to a `No install needed (Node >= 22.5.0 built-in node:sqlite)` note. Refuses to overwrite an existing entry without `--force`. Note on reserved flag names: the CLI uses `--connector-port=` (not `--port=`) because the framework reserves `--port=` for its own socket port, and `--driver-version=` (not `--version=`) because `--version=<value>` would be auto-mapped to `GINA_VERSION` and trigger a framework migration; the written JSON shape still uses `port` and `version` property names. Registered as an offline command — no framework socket required. Ships with 94 source-inspection unit tests in `test/lib/connector-add.test.js`. Second of the #CN10 multi-session plan — follow-ups are `connector:rm` and `connector:migrate` + framework-side auto-migrate hook.
* `gina connector:rm` — removes a connector entry from a project's `shared/config/connectors.json` or a bundle's `<bundle-src>/config/connectors.json`. Two invocation modes mirror `connector:add`: `gina connector:rm <name> @<project>` (removes from shared) and `gina connector:rm <name> <bundle> @<project>` (removes from the bundle, resolved via `manifest.bundles[bundle].src`). `connector:remove` is accepted as an alias. Flags: `--dry-run` prints what would be removed without touching any file (always exits `0` — includes the sibling-usage warning and driver-retention hint); `--force` skips the project-level usage guard that otherwise refuses to remove a shared connector while any bundle still references it. Bundle-level removals always proceed and leave shared untouched. After removal, prints a driver-retention hint listing any sibling bundles (or shared) that still reference the same driver and a reminder that `gina does not uninstall npm packages`; sqlite is exempt (built-in `node:sqlite`). Attempting to remove from a bundle when the entry lives only in shared prints an "inherited from shared" hint pointing at the right scope. Preserves any `//` or `/* */` comment header above the first `{`; mid-body comments are lost on rewrite (same as `connector:add`). Registered as an offline command — no framework socket required. Ships with 81 source-inspection unit tests in `test/lib/connector-rm.test.js`. Third of the #CN10 multi-session plan — follow-up is `connector:migrate` + framework-side auto-migrate hook.
* `gina connector:migrate` — lints every `connectors.json` in a project (or a single bundle's file) and, when `--fix` is passed, applies auto-fixable issues in place. Default mode is a read-only dry run — nothing is written without `--fix`. Two detection types today: `missing-schema` (top-level `$schema` key absent — auto-fixable, injects `"$schema": "https://gina.io/schema/connectors.json"` pinned at the top of the object while preserving the leading comment header and existing key order) and `bare-key-no-connector` (entry has no `connector` field and its key is not in the built-in enum `couchbase, mysql, postgresql, sqlite, redis, ai` — warn-only since the correct driver cannot be inferred; suggests the manual remediation via `gina connector:add <name> @<project> --connector=<type> --force`). Two invocation modes: `gina connector:migrate @<project>` scans shared + every bundle listed in `manifest.json`; `gina connector:migrate <bundle> @<project>` scans just the bundle's file. Flags: `--fix` applies auto-fixable issues; `--format=json` emits a machine-readable report with `{project, scope, bundle, fixApplied, files[]}` for CI / scripting. Registered as an offline command — no framework socket required. Idempotent — re-running after `--fix` is a clean no-op. Never modifies `core/config.js` and never runs at bundle boot: `connector:migrate` is explicit and opt-in, so a stale connectors.json cannot silently mutate in production. The real framework-side auto-migrate hook is deferred until `0.4.0` alongside `#CN8` (Couchbase SDK v2 removal) when a concrete old-shape → new-shape delta will justify touching the boot path. Ships with 87 source-inspection unit tests in `test/lib/connector-migrate.test.js`, a full write-path smoke test on a disposable sandbox (shared + two bundles, before/after assertions on `$schema` injection and key-order preservation), and a no-false-positive smoke on a real 7-bundle project. Fourth and final CLI session of the `#CN10` multi-session plan — `list`, `add`, `rm`, `migrate` now form a complete `connector:*` group.
* `gina connector:add --install` — opt-in flag that runs the detected package manager's install command immediately after writing the connector entry. The package manager is detected by probing the project root for lockfiles in order: `bun.lockb` → `pnpm-lock.yaml` → `yarn.lock` → `package-lock.json`. npm is the fallback when none matches. The install range is resolved in three tiers, first match wins: (1) `--driver-version=` pin set on the call (also persisted under `entry.version`); (2) the project's `package.json` (`dependencies` preferred over `devDependencies`) — reusing an already-declared range keeps the lockfile stable across repeated installs; (3) the framework's `peerDependencies` declaration. The resolved tier is labelled in the pre-install log (`(source: entry | project | framework)`) so the decision is visible in the terminal. `sqlite` short-circuits to exit 0 with a "built-in, no install needed" note. AI connectors with missing or unknown `protocol` exit 1 (the entry is still persisted to disk so the user can fix the protocol and re-run `--install` separately, or install by hand). Spawn uses `child_process.spawnSync` with `stdio: 'inherit'` so the PM's native output streams through to the user's terminal; `ENOENT` (PM binary missing from PATH) returns exit code 127. By design there is no `--no-install` / `--yes` counterpart and no configuration that silently flips the default — `--install` is opt-in and locked by a negative-invariant source-inspection test. No changes to `core/config.js` or any framework boot path. Ships with 40 new source-inspection unit tests in `test/lib/connector-add.test.js` (135/135 pass; full connector suite 392/392) and a live smoke covering sqlite short-circuit, AI unknown-protocol exit 1, real `ioredis` install via npm fallback, subsequent install via lockfile-detected npm with a project-pinned range, and `--driver-version=^5.3.0` taking priority. Fifth and final CLI session of the `#CN10` multi-session plan — `list`, `add` (with `--install`), `rm`, `migrate` now form the complete `connector:*` group.
* `lib/mcp-http` — Streamable HTTP transport primitives for the MCP server (spec revision 2025-06-18). New framework library that wraps an `mcp-server` instance with `createHttpTransport({ mcpServer, host, port, maxBodyBytes, onError })` — binds an `http.createServer` on the configured host / port (defaults to `127.0.0.1:0`, OS-assigned) and exposes `.start() / .stop() / .address()` for lifecycle control. POST accepts either a single JSON-RPC 2.0 frame or a batch (array); the `Accept` header picks the response shape — `application/json` returns a JSON frame (or JSON array for batch), `text/event-stream` returns one `event: message` per non-notification frame and closes the stream. Notifications-only POSTs return 202 Accepted with an empty body. Body size is capped at `DEFAULT_MAX_BODY_BYTES = 1 MiB` (override via `maxBodyBytes`); overflow replies 413 + JSON-RPC error frame. GET and DELETE return 405 + `Allow: POST` — v1 has no server-initiated streams (`tools/list_changed: false`, no sampling, no roots) so the spec-compliant answer is to refuse the method. OPTIONS returns a minimal preflight (full CORS Origin echo and `--auth-token` enforcement land in #AI8 Phase 2b Session 2). `Mcp-Session-Id` is generated via `crypto.randomUUID()` on the `initialize` response when the client did not provide one; client-supplied ids are echoed on every response including 202. `.stop()` sets `keepAliveTimeout = 1` and calls `closeIdleConnections()` so SIGTERM-driven shutdowns don't stall on idle keep-alive sockets. 34 unit tests in `test/lib/mcp-http.test.js`. Registered in the lib registry as `lib.mcpHttp` — not yet wired to `bundle:mcp-start`; the `--transport=http` CLI surface lands in #AI8 Phase 2b Session 3.
* `lib/mcp-http` — security layer on the MCP Streamable HTTP transport. Two new options on `createHttpTransport`: `authToken` (enables static bearer authentication — requests must carry `Authorization: Bearer <token>` on every non-OPTIONS request) and `allowedOrigins` (extends the built-in loopback `Origin` allowlist; pass `["*"]` to disable the check entirely). The built-in allowlist accepts `http(s)://localhost`, `http(s)://127.0.0.1`, and `http(s)://[::1]` on any port so MCP Inspector and browser dev tools on random dev ports work without extra configuration. Ordering is OPTIONS → Origin gate → bearer gate → method routing: preflight bypasses auth (browsers cannot carry `Authorization` on a preflight), a disallowed `Origin` returns 403 without CORS echo (self-defeating otherwise), and a missing / invalid bearer returns 401 + `WWW-Authenticate: Bearer realm="MCP"` with CORS headers preserved so browser clients can read the error body. Bearer validation uses `crypto.timingSafeEqual` with a length-mismatch short-circuit (timingSafeEqual throws on unequal lengths), and the scheme prefix is parsed case-insensitively per RFC 6750 with trailing-whitespace tolerance on the presented token. Response CORS headers (`access-control-allow-origin` + `vary: Origin`, or `*` with no `vary` under wildcard) are now echoed on every POST / 202 / 405 / 413 / 500 path when the request carried an allowed Origin. OPTIONS preflight now lists `authorization` in `access-control-allow-headers` and honours the same Origin allowlist (allowed origin → echoed; disallowed → 204 with no `access-control-allow-origin`, so the browser blocks the actual request). 36 new unit tests cover the Origin allowlist (default loopback, IPv6 `[::1]`, extra origins, `*` wildcard, non-http schemes, malformed URLs), the bearer flow (missing / wrong scheme / wrong value / length-mismatch / trailing whitespace / case-insensitive scheme), CORS echo on every response path, OPTIONS preflight behaviour under each Origin policy, and input validation (empty / non-string `authToken` → no auth; non-array `allowedOrigins` → loopback-only default). Still not wired to `bundle:mcp-start` — CLI surface lands in #AI8 Phase 2b Session 3.
* `gina bundle:mcp-start --transport=http` — Streamable HTTP transport for the live MCP server (MCP spec revision 2025-06-18). The existing stdio behaviour is preserved as the default; passing `--transport=http` wires the same dispatcher + mcp-server instance to `lib/mcp-http.createHttpTransport()` instead of `attachStdio()`. Six new CLI flags are whitelisted in `lib/cmd/bundle/arguments.json`: `--transport=stdio|http`, `--http-host=<host>`, `--http-port=<n>`, `--max-in-flight=<n>`, `--auth-token=<token>`, `--cors-origin=<comma-list>`. Every flag has a matching `mcp.json > server > <field>` manifest fallback (`transport`, `httpHost`, `httpPort`, `maxInFlight`, `authToken`, `allowedOrigins`) and, where sensible, an env-var layer (`GINA_HOST_V4` for the bind host, `GINA_MCP_AUTH_TOKEN` for the bearer token). Precedence at every resolver: CLI → manifest → env → default, with invalid values (non-numeric port, negative max-in-flight, out-of-range integer, unknown transport name) warning on stderr and falling through to the next tier. Bind host defaults to the framework-wide `host_v4` loopback convention (`127.0.0.1` by default, same mechanism as the MQ listener and CLI daemon socket) — opt into `0.0.0.0` is deliberate. HTTP port defaults to `0` (OS-assigned); the resolved port is logged to stderr on startup so wrappers can discover it. The startup info line now reads "MCP server listening on http://<host>:<port> (bearer auth: enabled)?. Dispatch target: <baseUrl> (timeout: <n> ms, maxInFlight: <n>). <N> tools exposed." On SIGTERM / SIGINT, `gracefulExit()` awaits `httpTransport.stop()` before `process.exit()` so in-flight responses drain cleanly. The `bin/cli` stdout intercept from Phase 2a is a no-op under HTTP mode (nothing writes to stdout in that path) and is therefore preserved unchanged. 27 new source-inspection tests in `test/bin/bundle-mcp-start.test.js` cover the whitelist additions, each resolver's precedence chain, the stdio|http branch in `startServer`, the HTTP transport wire-up shape, the bearer-auth log annotation, and the SIGTERM / `httpTransport.stop()` drain. `bundle:mcp-start` help.txt extended with the new flag table and default-security-posture notes. #AI8 Phase 2b Session 3.
* `swig.useProject` — opt-in `config/settings.json` key that loads a project-pinned `@rhinostone/swig` (or `@rhinostone/swig-twig`) from the project's `node_modules/` in place of the framework's bundled copy. The override is honoured only when the project pin shares the same major as the framework floor and is at or above it (currently `^1.6.0`); a rejected override falls back to the framework copy and logs a one-line `[swig-resolver]` warning at bundle startup naming the reason (`version-mismatch`, `not-installed`, `malformed-package-json`, `missing-version`). Default is `false` — existing bundles see no behaviour change. A second key `swig.package` (enum `@rhinostone/swig` | `@rhinostone/swig-twig`, default `@rhinostone/swig`) selects which frontend package to resolve; the abandoned upstream `swig` name is never accepted. The resolved module is cached on `process.gina._swig` for the life of the bundle process, so `core/server.js` and `core/controller/controller.js` both see the same instance — including after dev-mode `refreshCoreDependencies()` evictions. Ships with `lib/swig-resolver/` (`resolve`, `load`, `get`, `getDecision`, `reset`) and 51 unit tests in `test/lib/swig-resolver.test.js`. Schema updated in `schema/settings.json`. Standalone-mode note: when multiple bundles share a process, the first one to call `load()` wins; a subsequent bundle requesting a different package is warned and keeps the already-loaded copy.
* `lib/nunjucks-resolver` — primitive (N1) for project-installed nunjucks detection. Mirrors the `lib/swig-resolver` shape (resolve / load / get / getDecision / reset + dev-mode mtime hot-swap via `process.gina._nunjucks*`) with two deliberate differences: (1) no framework-bundled fallback — nunjucks is never declared in `framework/v*/package.json`, so `load()` throws an `Error` with `code: NUNJUCKS_NOT_INSTALLED` when the project has not installed the package, instead of silently returning a framework copy (bundles fail loud at startup rather than mid-render); (2) no version floor — the framework itself never calls any nunjucks API (rendering will live in `controller.render-nunjucks.js`, Gina code), so any version the user installs is accepted. Same dev-mode hot-swap mechanism: when `NODE_ENV_IS_DEV=true`, each `get()` call `fs.statSync`-probes `<projectPath>/node_modules/nunjucks/package.json` and, on mtime drift, evicts the cached module from `require.cache` and re-runs `load()` with the stashed options — `npm install nunjucks@<newer>` in a dev project takes effect on the next request without a bundle restart. Production short-circuits the probe. Ships with 38 unit tests in `test/lib/nunjucks-resolver.test.js` — resolve paths (installed / not-installed / malformed / missing-version / package-override), load success + cache + idempotency, load rejection with structured error + decision attached, get() throw-before-load contract, reset() fields, dev-mode mtime hot-swap including the uninstall-between-loads case that surfaces the error on refresh. Registered in `lib/index.js` as `lib.nunjucksResolver`. No wiring into `server.js` / `controller.js` yet — that lands in N2 alongside `controller.render-nunjucks.js` and the `render.engine` settings key.
* `settings.json > render.engine = "nunjucks"` — opt-in nunjucks rendering for a bundle (N2 MVP). New `core/controller/controller.render-nunjucks.js` delegate file, mirroring `render-swig.js`'s module signature (`async function(userData, displayInspector, errOptions, deps)`). `controller.js` `self.render()` now reads `settings.render.engine` from the bundle config and dispatches to either `controller.render-swig` (default) or `controller.render-nunjucks`. The dev-mode cacheless branch evicts the nunjucks delegate from `require.cache` alongside the existing swig / v1 entries, wrapped in try/catch so older framework dirs without the nunjucks delegate still load. `server.js` gains a new `initNunjucksEngine(conf)` helper called alongside `initSwigEngine` when `hasViews(bundle)` is true — for bundles with `render.engine === "nunjucks"`, it calls `lib.nunjucksResolver.load(self.executionPath, settings.nunjucks)`, which throws `NUNJUCKS_NOT_INSTALLED` when the project has not `npm install`ed the package (bundle startup fails loud rather than deferring the error to the first render). `render-nunjucks.js` caches a `nunjucks.Environment` per template root on `process.gina._nunjucksEnvs` (invalidated when the underlying nunjucks module itself was hot-swapped via `process.gina._nunjucksEnvsOwner`), disables nunjucks's own template cache in dev mode (`noCache: process.env.NODE_ENV_IS_DEV === "true"`) so template edits take effect without restart, and wires the namespace-aware template path resolution from `render-swig.js:271-335` (the CVE-2023-25345 path-traversal guard is swig-specific legacy and remains in `render-swig.js` only). HEAD requests send headers only; HTTP/1.1 uses `res.writeHead() + res.end()`. Schema updates in `schema/settings.json`: new top-level `render` block (`engine` enum `["swig","nunjucks"]`, default `"swig"`) and `nunjucks` block (`package`, `autoescape`). **Explicitly deferred to follow-up sessions** — documented in `render-nunjucks.js`'s header comment so nothing is hidden: (1) Inspector `__gdPayload` injection, (2) HTTP/2 `stream.respond()` direct path and Early Hints 103, (3) static HTML cache writes, (4) `setResources` asset injection and `<gina>` layout placeholder resolution, (5) Gina `SwigFilters` equivalents — users register their own filters via `nunjucks.Environment.addFilter()`, (6) error-page template routing (the framework error template is currently swig-only, so thrown errors from nunjucks pages still render via the swig path). 36 source-inspection unit tests in `test/lib/render-engine-dispatch.test.js` — schema shape, controller.js dispatch logic, cacheless eviction try/catch, render-nunjucks module shape, Environment cache invalidation, HEAD handling, deferred-features comment presence, server.js init wiring, and negative invariants (framework `package.json` must never declare nunjucks; `render-nunjucks.js` must never `require("nunjucks")` directly; dispatch default must stay `"swig"`). Full suite 3273/3273.
* `script/check_changie_entries.js` + pre-commit hook — self-enforces `llms.txt` lesson #66 (changie YAML body shapes). The script validates every staged `.changes/unreleased/*.yaml` entry: `kind:` must match the Added/Changed/Deprecated/Removed/Fixed/Security enum, `body:` must be single-quoted (`body: '...'`), double-quoted, or a block scalar (`>-` / `|-` / `>` / `|`), `time:` must be an ISO-8601 shape. Unquoted bodies are rejected unconditionally — even when today's content happens to be safe — because a later edit could introduce the ` #` silent-truncation or `:` mapping-values hazards. Single-quoted bodies are walked character-by-character to ensure every literal `'` is escaped as `''` and the scalar terminates before the `time:` key. Double-quoted bodies are checked for a closing unescaped `"`. Wired into `.githooks/pre-commit` alongside the existing local-tool-path leak guard (#S5) — runs only against staged changes files (`git diff --cached --diff-filter=AM`), so pre-existing unquoted entries on disk are grandfathered until someone edits them. The script is also callable standalone (`node script/check_changie_entries.js .changes/unreleased/*.yaml`) for CI use or on-demand audits. 25 unit tests in `test/lib/check-changie-entries.test.js` cover every safe body shape (single-quoted with escapes, double-quoted with escapes, folded `>-` and literal `|-` block scalars), all rejection patterns (unquoted with `:`, unquoted with ` #`, unquoted plain, unescaped `'` inside single-quoted, unterminated single-quoted, unterminated double-quoted), missing / empty / wrong-kind / wrong-field-order fields, and batch behaviour (every failing file reported in one pass; no-args → exit 0; missing-file-path → reported). Exits 1 with a per-file reason on any failure, exit 0 when clean. Full suite 3298/3298.
### Changed
* Inspector refresh button now performs a full soft refresh instead of only invalidating the data-diff cache. One click re-polls data, force-refetches the live index introspection (`/_gina/indexes`), refetches the unredacted `/_gina/reveal` snapshot when reveal mode is active, and reopens the passive `/_gina/agent` SSE stream when it has closed. Shift+click additionally reloads the inspected page via `window.opener.location.reload()`. The button's tooltip advertises the Shift+click modifier.
* `gina bundle:list` — each bundle line now shows a preferred-port summary and host-side running state alongside the existing `[ ok ]` / `[ ?! ]` src-existence indicator. Preferred port is read from `~/.gina/ports.reverse.json` by the `http/2.0 https` → `http/1.1 https` → `http/1.1 http` precedence (dev env first, then the first env present); bundles with no allocated port render as `(no port)`. Running state is derived from `~/.gina/run/<bundle>@<project>.pid` + `process.kill(pid, 0)`; a stale pidfile (process gone) reports the bundle as stopped without deleting the file (clean-up stays with `bundle:stop`). Text output gains a padded bundle-name column, the port summary, and a `(running, pid N)` or `(stopped)` suffix per bundle; `--format=json` output gains `ports: object|null`, `running: boolean`, and `pid: number|null` fields on each bundle object. A missing or malformed `ports.reverse.json` is tolerated — the command still renders with all bundles showing `(no port)`. Caveat: bundles running inside a Docker container do not write a pidfile to the host `~/.gina/run/` directory, so they will report as stopped from a host shell even when the container is up — use `docker ps` for the container-side view.
* `swig.useProject` — dev-mode hot-swap via mtime check. When `NODE_ENV_IS_DEV=true` and a bundle has opted in to `swig.useProject: true`, every call to `lib.swigResolver.get()` (which runs on every HTTP request via `controller.js` re-require under `refreshCoreDependencies()`) `fs.statSync`-probes the project's `<projectPath>/node_modules/<pkg>/package.json` and compares the `mtimeMs` against the value cached at the last `load()`. Identical → return the cached swig instance (near-zero cost: one `fs.stat`). Different (or the file appeared / disappeared) → evict the old project copy from `require.cache`, clear the in-process cache (`process.gina._swig` / `_swigDecision` / `_swigPackage`), and re-run `load()` with the original options so the next access picks up the new version. Use case: `npm install @rhinostone/swig@<newer>` in a project while a dev bundle is running now takes effect on the next request without a bundle restart. Production bundles (`NODE_ENV_IS_DEV` not `true`) short-circuit the mtime probe and keep the load-once semantics. Downgrades below the framework floor or wrong-major upgrades re-enter the same safety gates as the initial `load()` — the refreshed decision falls back to the framework copy with the usual `[swig-resolver]` warning. `reset()` now clears the full cache (adds `_swigOptions`, `_swigProjectPath`, `_swigMtime` to the fields wiped). 7 new unit tests in `test/lib/swig-resolver.test.js` (58 total) cover the production no-op, unchanged-mtime short-circuit, mtime-triggered reload, toggle-off and missing-projectPath short-circuits, fresh-get without prior `load()`, and version-drift-below-floor fallback. Test fixtures use a helper-local monotonic mtime counter (`Math.max(prev, Date.now(), lastMtime) + 1000`) to guarantee strict mtime progression across successive calls — `Date.now()` millisecond granularity would otherwise collide when two calls fire within the same wall-clock millisecond. `ROOT` is realpath-normalised inside `before()` because macOS `os.tmpdir()` returns `/var/folders/...` while Node's `require.cache` keys use the realpath `/private/var/folders/...`.
* `controller.render-nunjucks.js` — Inspector `__gdPayload` injection shipped (closes N2 deferred item #1). `injectInspectorScripts(html, data, self, local, displayInspector)` builds the same redacted payload shape as `render-swig.js:1041-1128` (two `JSON.parse(JSON.stringify(data.page))` clones into `__gdGina` / `__gdUser`, `view.scripts`/`view.stylesheets` scrubbed on both, `inspectorRedact.getConfig(local.options.conf)` for pattern config, `environment.scope` + `inspectorUrl` attached to the gina clone, `JSON.parse(JSON.stringify(...))` snapshot gated on `process.env.NODE_SCOPE === 'local'` before the redact pass, then `inspectorRedact.redact({compiledPatterns, replacement})`), serialises to `<script>window.__ginaData = ...</script>` with `</script>` → `<\/script>` and `<!--` → `<\!--` escapes matching swig at lines 1096-1097, and injects alongside a `<script>window.__ginaLogs</script>` console-hook script before the first `</body>` via case-insensitive regex replace. Gated on `displayInspector === false` → never; `displayInspector === true` → always; otherwise `self.isCacheless()` (dev-mode default, same as swig). Side effects: stashes redacted payload on `self.serverInstance._lastGinaData` (consumed by engine.io push + `/_gina/agent` SSE), stashes unredacted on `serverInstance._lastGinaDataUnredacted` when scope is local (null otherwise so `/_gina/reveal` has nothing to leak outside dev), and emits `process.emit('inspector#data', __gdPayload)`. The injection call is wrapped in try/catch in the render function body so an Inspector-side bug can never break the actual page render — it falls through to sending the un-injected HTML with a `[render-nunjucks] inspector injection skipped: ...` console warning. **Still deferred within the Inspector port:** (a) `statusbar.html` inclusion (the swig template uses `{% include %}` with swig-specific syntax; nunjucks-compatible version needed), (b) `data.page.flow` pipeline from `local._timeline` (render-swig.js:986-1040 shape; belongs in a shared helper before porting), (c) `data.page.queries` pipeline from `local._queryLog`. 18 new source-inspection tests in `test/lib/render-engine-dispatch.test.js` §05b — imports, helper declaration, both gates (`displayInspector` + `isCacheless`), `</body>` guard, payload construction, redact pipeline, unredacted-scope gate, `</script>`/`<!--` escapes, script tag text, console-hook script, serverInstance stashes, event emit, regex replace injection site, try/catch wrapping in the main render body, plus two "documentation" tests that lock in the within-Inspector deferred-features comment. Full suite 3316/3316. render-nunjucks.js header block updated to strike through the shipped deferred item and expand the remaining within-port deferred list.
* `controller.render-nunjucks.js` — HTTP/2 `stream.respond()` direct path shipped (closes N2 deferred item #2). `sendHtmlResponse` now implements the four-way branch documented in `the internal architecture docs §7b` and used by `render-swig.js:877-927`: (1) HEAD + HTTP/2 stream → `stream.respond({content-type, content-length, :status, ...merged})` + `stream.end()` with no body; (2) HEAD + HTTP/1.1 → `res.setHeader(content-length)` + `res.writeHead()` + `res.end()`; (3) body + HTTP/2 stream → `stream.respond({content-type, :status, ...merged})` + `stream.end(html)`; (4) body + HTTP/1.1 → `res.writeHead()` + `res.end(html)`. Both HTTP/2 branches guard against `stream.destroyed || stream.closed` (client disconnected before the async render callback completed — without the guard, `stream.respond()` throws `ERR_HTTP2_INVALID_STREAM`). Both HTTP/2 branches merge pipeline-set headers via `local.res.getHeaders()` because `stream.respond()` on the raw HTTP/2 stream does NOT include headers set via `response.setHeader()` — CORS, cache-control, cookies set earlier in the pipeline would otherwise be silently dropped on HTTP/2 responses. `local.res.headersSent = true` is flagged after successful `stream.respond()` to signal the HTTP/1.1 compat layer that the response was sent directly. Still deferred within the HTTP/2 track: Early Hints 103 auto-send from accumulated `h2Links` — that fires in `controller.js this.render()` before the delegate is called, so it's a controller-level change rather than a render-nunjucks change. 10 new source-inspection tests in `test/lib/render-engine-dispatch.test.js` §05a covering each of the four branches, the stream-destroyed guard (asserted to appear at least twice for both HEAD and body paths), the pipeline-header merge (getHeaders() called at least twice), the headersSent flag (set at least twice), and the HTTP/1.1 fallback patterns. Full suite 3326/3326.
* `controller.render-nunjucks.js` — error-page template routing shipped (closes the remaining "framework error template is swig-only" gap for nunjucks bundles). When a bundle has opted into `render.engine = "nunjucks"` and defines custom error templates under `templates/html/errors/<code>.html`, `controller.js renderCustomError()` sets `errOptions.file` to the absolute path of the matching error file from `bundleConf.content.templates._common.errorFiles[code]`. The render-nunjucks delegate now detects `localOptions.isRenderingCustomError === true` and branches: instead of going through `resolveTemplatePath` + `FileSystemLoader.render()` (the loader rejects absolute paths and cannot reach shared-path error templates that live outside the bundle template root), it reads the file with `fs.readFileSync(localOptions.file, 'utf8')` and renders the contents with `env.renderString(source, data)`. Every failure mode in the branch — absent path, missing file, read error, renderString error — falls back to a minimal inline HTML body served through `sendHtmlResponse`, never recursing via `self.throwError`. Recursion would re-enter the same branch and could loop infinitely; the inline fallback guarantees the client always gets a well-formed response. Defensive `localOptions.isRenderingCustomError = false` reset after render mirrors `render-swig.js` lines 804 and 1434 — prevents a downstream render that reuses the same `localOptions` reference from re-entering the branch. The shared `getEnvironment(nunjucks, templateRoot, ...)` call is now hoisted above the branch so both the custom-error path (uses `env.renderString`) and the normal path (uses `env.render`) reuse the same cached `nunjucks.Environment`. 14 new source-inspection tests in `test/lib/render-engine-dispatch.test.js` §05c — branch presence and ordering relative to `resolveTemplatePath`, `fs.readFileSync` on `localOptions.file`, `env.renderString(_errSource, data)` usage, missing-file fallback, read-error fallback, renderString-error fallback, the no-recursion negative invariant (`self.throwError` must never appear inside the branch body), flag reset after render, inline-HTML status-code derivation, `getEnvironment` ordering, normal-path regression guard (`env.render(templateRel, data)` still present), template-existence pre-flight guard, and the top-of-file header "shipped" lock. Full suite 3326 → 3340. Deferred-feature list in the render-nunjucks.js header block, `the internal architecture docs` §5 + §8, and `llms.txt` #67 all updated to mark the item as shipped.
### Removed
* Removed the unused vendored swig-client browser bundle (core/deps/swig-client/). The client-side Swig bundle is produced at build time from @rhinostone/swig (Phase 7) — the vendored copy had zero runtime or build-time references.
### Fixed
* Inspector payload redactor no longer false-positively redacts non-secret identifiers that happen to contain secret substrings (e.g. `companyName`, `lastCompanyUsedId`, `passportRequired`). The key matcher now tokenizes on camelCase and punctuation boundaries and compares anchored tokens, so only genuine secret-named fields are redacted.
* Inspector Reveal toggle now reliably shows the full HTML page payload instead of tiny session data from background JSON polls. The unredacted snapshot is only written by HTML renders — JSON polling endpoints no longer clobber the slot between page load and click.
* Inspector Reveal toggle now routes to the correct bundle in proxy-routed multi-bundle setups. `toggleReveal()` derives the `/_gina/reveal` URL from the opener window's pathname (or the standalone `?target=` param) so the request hits the bundle that rendered the current page, not the front proxy's default bundle.
* Bundle startup no longer crashes with `EINVAL: invalid argument, readlink` when loading config files in dev mode. The `require.cache` eviction calls in `config.js` `loadWithTemplate` are now wrapped in try/catch — `requireJSON` reads config via `fs.readFileSync` and evicts its own cache, so filesystem races (observed on OrbStack/Docker bind mounts after the bundles symlink is re-created) are non-fatal.
* Bundle no longer crashes with an uncaught `ECONNREFUSED` when an HTTP/2 client session fails to connect. The `client.on('error', ...)` listener in `handleHTTP2ClientRequest` had a property-access chain (`getContext('gina').ports[options.protocol][options.scheme.replace(...)][options.port]`) that threw `TypeError` when the caller passed a bare hostname (undefined `options.protocol`/`options.scheme`). A throw from inside an `'error'` listener becomes an `uncaughtException` and terminates the process. The whole listener body is now wrapped in try/catch, the port-enhancement lookup safe-navigates every key, and `proc.js` `uncaughtException` handler now filters `ECONNREFUSED` originating from TCP connect frames (matching the existing `ECONNRESET` behaviour).
* Inspector tab content now refreshes automatically when the opener page navigates. The client-side log poller (`pollLogs` in `inspector.js`) tracked `logsOff` against the opener's `__ginaLogs` array but never detected that the array itself is replaced on navigation — the new page's `statusbar.html` inline script re-runs and installs a fresh empty array, so the old `logsOff` offset would be larger than the new array length and the guard `src.length <= logsOff` returned early forever. New `_lastLogsRef` holds the last-polled array reference; when it changes (or the array length shrinks below the offset), `logsOff` is reset to 0 so the new page's entries are picked up on the next poll cycle.
* Detached daemon processes no longer spin at 100% CPU when their launching CLI exits and leaves an orphaned stdio pipe. Writing to the dead pipe triggered `EPIPE`, which escalated to `uncaughtException`; the existing handler then wrote the error back to the same dead stdout, producing another `EPIPE` — an infinite loop. `proc.js` now installs silent `error` listeners on `proc.stdout`/`proc.stderr` at init, guards the uncaughtException EPIPE branch with a re-entry flag, and wraps the diagnostic `stdout.write()` calls in `try/catch` so a subsequent write failure is swallowed instead of re-thrown.
* `gina framework:init` now re-heals project fields that have been saved as `null` in `projects.json`. The integrity loop in `checkIfProjects` used `typeof(project[prop]) != 'undefined'` to decide whether a field needed refreshing from `main.json`; because `typeof null === 'object'`, a field that had been written as `null` in a previous run (for example because its per-release entry was not yet defined) was permanently skipped. The guard now also requires `project[prop] !== null`, so `def_scope`, `local_scope`, `production_scope`, `dev_env` and similar namespaced fields recover on the next framework start.
* `gina bundle:start` no longer hangs indefinitely when the resolved bundle entry point does not exist on disk. The `isRealApp` callback in `lib/cmd/bundle/start.js` used to log the error via `terminal.error()` and then return without closing the CLI socket or calling `end()` — the connected `gina` CLI would wait forever for output that could not arrive. The error branch now mirrors the sibling `nodeModulesErr` path: `opt.msg` is set to the error message and `end(opt, cmd, isBulkStart, bundleIndex, true)` is called, so the CLI receives the error and the process exits cleanly.
* Inspector popup now stays in sync during SPA (pushState/XHR) navigation inside the inspected page. Opener polling alone could not pick up SPA renders because those paths do not rewrite `window.__ginaData`. The Inspector now opens a parallel `/_gina/agent` SSE subscription alongside the existing opener source and receives pushed `data`/`log` frames from `render-json.js` whenever `process.gina._inspectorActive` is true. Multi-bundle proxy setups are handled by deriving the stream URL from the opener pathname. Manual refresh after each page change is no longer required.
* `gina bundle:openapi` no longer crashes on `routing.json` files that contain `//` or `/* */` comments. The command used plain `require(routingPath)` which hands the file to Node's JSON loader, and that loader throws `SyntaxError` on any comment — including the `// bundle needs to be restarted on changes !!` header that real `routing.json` files routinely carry. Swapped to `requireJSON`, which strips comments before parsing. The new `bundle:mcp` command already used `requireJSON`; this aligns `bundle:openapi` with it.
* `gina project:add @<name> --path=<dir>` no longer hangs indefinitely when the target directory already contains a `package.json`. The existing-file branch now merges the existing package with the framework template and exits cleanly.
* CLI argument parsing no longer silently truncates `--<key>=<value>` flags whose value contains a literal `=` character. Both entry points in the argv pipeline — `utils/helper.js::filterArgs` (populates `process.gina.GINA_*`) and `lib/cmd/helper.js::getParams` (populates `cmd.params[<key>]`) — previously used `.split(/=/)` with no limit, chopping the value at every `=`. Real-world values affected: `--driver-version=">=5.3.0 <6.0.0"` (truncated to `>`), `--password=foo=bar` (truncated to `foo`), `--api-key=sk-=abc=` (truncated to `sk-`). Fix is an `indexOf('=')`-based substring that splits on the first `=` only; everything after is kept verbatim. Discovered during the `connector:add --install` smoke test (CN10 Session 5) when a semver range with a leading `>=` was silently chopped. Covered by 13 unit tests in `test/lib/cli-arg-parsing.test.js` (source-inspection on both sites + behavioural checks against `>=X.Y.Z`, caret ranges, multi-`=` passwords, base64-ish api keys, empty values, and flag-only tokens). Full suite 3044/3044.

## 0.3.6 - 2026-04-16
### Changed
* syncDocs now regenerates the docs-site package-lock.json after bumping devDependencies.gina — prevents CI / Vercel / Worker deploy failures on every stable release.
### Fixed
* Prevented spurious Whisper Error on the first CLI command after a fresh install (#B12). `gina --version` and `gina framework:*` commands no longer emit a red error stack trace before their output on a brand-new install.
* Hardening framework:init against missing or mistyped `def_*` keys in ~/.gina/main.json (#B13 — blast-radius follow-up to #B12). `main['def_prefix']`, `def_global_mode`, `def_arch`, `def_platform`, `def_env`, `def_scope`, `def_log_level` reads now short-circuit to undefined instead of throwing TypeError when the key is entirely absent.
* Surfacing the real cause when `prepare_version.js` is run against a stale `~/.gina/<release>/settings.json` `dir` field. Publish now fails fast with an actionable message instead of wedging at `pushChangesToGitIfNeeded` with a misleading "No branch selected" error.
* Preventing CORS preflight failure when a bundle's env.json `access-control-allow-headers` list omits a header the client actually sends. `completeHeaders()` no longer overwrites the echo that `checkPreflightRequest()` sets from the incoming `access-control-request-headers` (#B13).
### Security
* Added pre-commit hook (.githooks/pre-commit) that blocks local-tool configuration paths from entering git history. post_install.js installs core.hooksPath=.githooks for contributor clones only (gated on .git presence in gina repo root). #S5 follow-up to #S3.
* Added GitHub Actions workflow (.github/workflows/security.yml) that greps the git index on every push/PR to develop and master for local-tool configuration paths. Fails the build if any are tracked. Backs up the local pre-commit hook (#S5) and publish-boundary gate (#S3). #S6 follow-up to #S3.
* Added private-token leak gate (script/check_no_local_leak.js) wired into the npm prepack hook and the prepare_version.js self.* chain before any git add --all. Fails the publish if the tarball listing contains local-tool configuration paths OR if pack contents contain private-token patterns (phone, private email, private address, private domain, co-author legal name). #G1 follow-up to #S3/#S4.
* Added opt-in pre-commit hook (resources/git-hooks/pre-commit) that blocks commits authored under a private-domain git identity (.local hostnames, internal domains). Contributors install it once via cp resources/git-hooks/pre-commit .git/hooks/pre-commit. Complements the CI-side local-tool guard (#S5/#S6). #G5.
* Redacting secret-looking fields from the dev-mode Inspector feed before any sink (window.__ginaData, localStorage.__ginaData, /_gina/agent SSE, engine.io push, ginaToolbar forms/XHR overlays). New lib/inspector-redact deep-clones the Inspector payload and replaces values whose keys match the default regex (password, pwd, secret, token, apikey, cvv, ssn, authorization, credentials, private_key) with [redacted]. Two complementary carve-outs preserve validation metadata: (1) NON_SECRET_SUFFIX — keys ending in rule/rules/policy/policies/validator/config/configuration/settings/requirements/strength/constraint/options/schema/definition/spec pass through untouched (e.g. passwordRule, passwordPolicy describe form rules, not user input); (2) primitive-only redaction — when a matched secret key holds an object or array, the walker recurses into it instead of replacing it, because such values are always metadata (e.g. rules.account[password] = {isRequired: true, isString: 7} — an HTML form field name under a rules: section). Nested primitive secrets inside preserved objects are still caught on recursion. Client-side shim in statusbar.html mirrors both carve-outs and additionally redacts any DOM input whose type matches configured types (default: password). Configurable via settings.json inspector.redact.{patterns,types,replacement}. Dev-mode only — the actual HTTP response body is never touched. #R7.

## 0.3.5 - 2026-04-13
### Security
* Bumped @rhinostone/swig to 1.5.0 — extends CVE-2023-25345 path-traversal guards to bracket notation, set bracket assignment, for loop variables, macro names, and import aliases.
* Updated vendored client-side swig (core/deps/swig-client/) to the v1.5.0 browser build. Extends CVE-2023-25345 path-traversal guards to bracket-notation access, for-loop variables, macro names, and import aliases — completing the __proto__/constructor/prototype blocklist. Browser-side templating now matches the server-side @rhinostone/swig 1.5.0 protections.

## 0.3.4 - 2026-04-12
### Fixed
* bumpVersion and prepare_version now update gna.js framework paths on version changes — fixes CI test failures where gna.js referenced a stale framework directory
* Swig resolution test now skips on CI where framework/v*/node_modules does not exist (framework/package.json is gitignored)
* require('gina/gna') explicit exports now resolve correctly — v0.3.3 shipped with stale framework paths in gna.js that caused MODULE_NOT_FOUND errors

## 0.3.3 - 2026-04-11
### Added
* Live database index introspection for Inspector Query tab — SQL connectors (MySQL, PostgreSQL, SQLite) now report actual database indexes without requiring manual indexes.sql files
* Implement `framework:get` CLI command — read one or all keys from ~/.gina/settings.json (`gina get --key`, `gina get all`). Completes the `gina set` / `gina get` pair.
* Implement `port:set` CLI command — set or update a port number for a bundle/environment/protocol/scheme combination. Supports both positional syntax (`gina port:set http/1.1:3200 frontend @myproject/dev`) and flag syntax (`--protocol=`, `--scheme=`, `--port=`, `--env=`), with interactive prompts for missing values.
### Changed
* Improved popin loading and opening performance: parallel resource loading via DOM injection (replaces sequential XHR + eval), eliminated redundant DOM binding on open, reduced DOM scans with querySelectorAll, classList API, cached RegExp, lightweight ID counter, per-load XHR for concurrent popins
* Replacing uuid/crypto.randomUUID() with internal lib/uuid — lightweight base-62 ID generator using crypto.getRandomValues with bitmask bias avoidance, zero external dependencies
* Replaced vendored swig-1.4.2 with @rhinostone/swig npm dependency in framework/v*/package.json
### Fixed
* Fixed malformed regex in events.js XHR error handler, operator precedence bug in binding.js error logging, typo in events.js header loop variable, implemented popinDestroy (was a stub), and fixed registeredPopins duplicate-check array never being populated
* explainForIndexes crashes on bulkInsert path where conn._cluster is undefined (conn is a scope object, cluster lives at conn._scope._bucket._cluster). The crash occurs before self.emit(trigger), silently hanging the entity callback and causing a 504 timeout with no error in logs. Fixed by resolving the cluster from either conn shape, with early-return guard and internal try/catch so dev-mode instrumentation never blocks the emit path.
* Validator global validation pass no longer displays errors for untouched fields. On every field blur, the validator runs a full-form validation to determine submit button state (isFormValid). Previously, the global pass displayed errors for ALL invalid fields — untouched fields would suddenly show errors when the user interacted with a different field. Now only the field the user actually touched (event.target.name) shows errors. Two call sites fixed in validator/src/main.js (~line 3337 and ~line 4865).
* Vendoring streamsearch-1.1.0 to fix busboy MODULE_NOT_FOUND crash in Docker containers after framework directory rename
* Forwarding emerg message to CLI output and docker logs when a bundle aborts during startup
* Config loader checks MIDDLEWARE file existence before reading — prevents crash in containers or environments where the file is absent
* requireJSON now tolerates trailing commas in JSON config files — strips them and warns instead of calling emerg+exit, preventing bundle abort on common authoring mistakes

## 0.3.3-alpha.3 - 2026-04-09
### Added
* TypeScript declaration files (`types/index.d.ts`, `types/globals.d.ts`, `types/gna.d.ts`) — `.d.ts` declarations for the public surface: `SuperController`, `EntitySuper`, global helpers, connector config shapes, `routing.json` schema, all config file interfaces, `GinaRequest`/`GinaResponse` types, `PathObject`, `uuid`, and the `Gna` lifecycle module. Zero TS migration of internals — declarations only. Enables IDE autocomplete and static analysis for consumer projects. `package.json` wired with `"types"` and `"typesVersions"`.
* Explicit exports via `require('gina/gna')` — all global helpers (`getContext`, `setContext`, `_`, `requireJSON`, `onCompleteCall`, `getModel`, `uuid`, `ApiError`, etc.), `SuperController`, and `EntitySuper` available as named imports. Uses lazy getters so symbols resolve at access time (after framework boot). Enables IDE go-to-definition and `import { getContext } from 'gina/gna'`-style usage alongside the existing global injection.

## 0.3.3-alpha.2 - 2026-04-08
### Added
* `bundle:openapi` CLI command — generates an OpenAPI 3.1.0 specification from `routing.json` for one or more bundles. Alias: `bundle:oas`. Supports `--output` flag for custom output path. Zero manual spec writing — `url`, `method`, `param.control`, `namespace`, `requirements`, `_comment`, `_sample`, `middleware`, and `cache` fields all map to their OpenAPI equivalents. Makes any Gina app consumable by AI agents, API gateways, and testing tools.
* `framework:get` CLI command — read one or all keys from `~/.gina/settings.json`
* `port:set` CLI command — set or update a specific port for a bundle/env/protocol/scheme combination

## 0.3.2 - 2026-04-07
### Added
* JSON Schema files for all config files (app, connectors, manifest, routing, settings, watchers, crons) — published at gina.io/schema/*
* Entity short-name aliases — use self.getEntity('user') instead of self.getEntity('user/user')
### Changed
* Server engines — font MIME types (woff/woff2/otf/ttf) and Inspector endpoint improvements
* Inspector UI — tab layout presets (Balanced/Backend/Frontend/Custom with drag-to-reorder), query performance banners, missing-index banners, cross-bundle QI propagation, render-json Inspector data feed, _inspectorActive profiling gate
### Fixed
* Model loading without onInitialize hook — entities load correctly when the model has no onInitialize
* CLI stability and install path edge cases
* getConfig() proxy override — guards against undefined PROXY_HOSTNAME on same-origin POST requests with Origin header
* bundle:start crash on unregistered bundle — isDefined guard moved before property access

## 0.3.1 - 2026-04-06
### Added
* SQL index reporting Phase A (#QI1): connectors read indexes.sql at startup and populate query instrumentation index badges in the Inspector
### Changed
* HTML rendering now uses direct HTTP/2 stream.respond() + stream.end(), bypassing the HTTP/1.1 compatibility layer (matching JSON rendering behaviour)
* Removed ssl-checker, colors, and uuid dependencies — engine.io is now the sole runtime dependency

## 0.3.0 - 2026-04-06
### Added
* HTTP/2 client resilience: retry with backoff (up to 2 retries with 500ms delay), pre-flight PING validation on cached sessions, and session freshness tracking via _lastPongAt. Protects against OrbStack silent TCP drops and stale HTTP/2 sessions.
* Inspector UX: drag-to-select log rows, copy badge fade-out, left accent selection styling, logo watermark on content panes, window geometry and env panel height persistence
* Inspector View tab performance anomaly alerts: pulsing dot indicator on tab label, badge border override, and tooltip for metrics exceeding thresholds (load time, transfer size, FCP, query duration, query count)
* Inspector Query tab: index badges per query card showing which database indexes are used (green for secondary, amber for primary scan, red for no index, grey N/A for unsupported connectors)
* Inspector Query tab: per-card result size color-coding using the same weight thresholds as the total badge
* Inspector Query tab: search input debounce (200ms), filter persistence in localStorage, and pagination (20 cards default with Show all button)
* Couchbase connector: extractIndexes() helper and profile timings query option in dev mode for index reporting in the Inspector
* Inspector: manual connect form on the 'No source' overlay — type a bundle URL to connect without editing the address bar
* Inspector: tab layout presets (Balanced / Backend / Frontend / Custom) in settings panel — reorder tabs for different developer audiences, or drag to create a custom order
* Docker Compose starter: `docker compose up` in gina-starter sets up a Gina app container + Couchbase Community 7.6.4 with pre-seeded cluster, bucket, application user, and sample documents — zero local install required
### Changed
* Renamed legacy Inspector parameter and marker names across the render pipeline: `displayToolbar` → `displayInspector` (server.js, controller.js, render-swig.js, render-v1.js), `{# Gina Toolbar #}` → `{# Gina Inspector #}` comment markers and guard regexes, "Beemaster" → "Inspector" in server.isaac.js and scan.js comments. `render-v1.js` now includes `statusbar.html` instead of `toolbar.html`, completing the migration of all render paths to the modern Inspector statusbar.
### Fixed
* Fixed error page displaying `[object Object]` when upstream returned an error object instead of a string — render-swig now normalises error and message fields to strings before building the error response (#Q2)
* Couchbase session store v3/v4: touch() and destroy() passed MutationResult as error to express-session callback, causing defer(next, MutationResult) and a 500 response with the Couchbase CAS token as body on every authenticated read-only request (#CB-BUG-4)
* Static file server now sends ETag and Last-Modified headers in production, enabling conditional GET (304 Not Modified) responses for all static types on both HTTP/1.x and HTTP/2 paths
* Fixed method-based routing on shared URLs — the router now continues scanning after a URL match with a wrong method instead of immediately throwing 405. GET /notes and POST /notes can coexist as separate routing rules; a 405 is only emitted after the full scan finds no method match.
* Fixed crash in fitsWithRequirements when a URL param route has no requirements block — router now accepts any non-empty segment value and correctly binds it to req.params / req[method] instead of throwing TypeError.
* Fixed throwError(res, code, stringMsg) silently overriding an explicit 3-digit HTTP status code with res.status || 500 — throwError(res, 404, "Not found") now correctly sends 404 instead of 500.
* Removed dead buffer consumption code from entity.js Option B path (_arguments[trigger] was unreachable for both connector-generated and custom entity methods). Added defensive unconditional clear at Option B entry point to prevent stale buffer leakage across calls (#M5).
* Inspector Query tab: durationClass variable shadowing in renderQueryContent

## 0.3.0-alpha.1 - 2026-04-02
### Added
* `project:start` / `project:stop` / `project:restart` — start, stop, or restart all bundles in a project with one command. Delegates to `bundle:start/stop/restart @<project>` (bulk mode). Flags forwarded.
* Inspector Phase 2 — Inspector dev SPA embedded at `/_gina/inspector/` (engine-agnostic — works with both Isaac and Express). See CHANGELOG.md for the complete breakdown.
* Thin in-page status bar — `statusbar.html` (new file in `dist/vendor/gina/html/`) is included instead of `toolbar.html`. A 36-line vanilla JS IIFE creates a Shadow DOM host (`#__gina-statusbar`) fixed at bottom-right: status dot (green = ok, red = `data.error` set), `bundle@env` label, and an "Inspector" link to `/_gina/inspector/`. No RequireJS, no jQuery, no SASS. Shadow DOM isolates styles from the app. `gina.js` toolbar module silently skips init when `#gina-toolbar` is absent (`!$toolbar.length` guard at line 17752).
* Gina infrastructure port range 4100–4199 — reserved exclusively for Gina infrastructure; never assigned to bundle HTTP servers by `gina port:scan` (range skipped automatically). Layout: `4100` = socket server (future migration from 8124), `4101` = reserved, `4102` = engine.io internal transport, `4103–4199` = reserved for future Gina infrastructure. `settings.json` `engine.io.port` corrected from `8888` (conflicts with Jupyter Notebook) to `4102`. Port range documented in `framework/v0.3.0-alpha.1/lib/cmd/port/help.txt`.
* `window.__ginaData` — replaces the two `<pre id="gina-toolbar-*-json">` DOM nodes with a single `<script>window.__ginaData = { gina: {...}, user: {...} };</script>` tag injected before `</body>` in dev mode. Data is built in Node.js (`controller.render-swig.js`) with the same field masking as before (`view.assets = {}`, `view.scripts/stylesheets = "ignored-by-toolbar"`); `</script>` and `<!--` sequences are escaped before serialization. Toolbar JS (`gina.js` dist + `main.js` src) updated to read from `window.__ginaData` directly instead of scraping DOM text nodes; mock file-upload writes back to `window.__ginaData.user`/`.gina`. The Inspector will access this object via `window.opener` or `postMessage` when it opens as a separate tab. No change to the data structure or toolbar panel rendering logic.
* PATCH method support — `req.patch` is now populated with the parsed request body (JSON or form-encoded). Body parsing mirrors POST. `req.body` aliases `req.patch` for method-agnostic actions. URI params are merged into `req.patch` the same way as PUT. Use PATCH for partial updates (only the sent fields change); use PUT for full resource replacement. `case 'patch':` added to `getParams()` and `getParam()` in `controller.js`. `"method": "PATCH"` is now valid in `routing.json`.
* HEAD method support — `req.head` is populated with query-string and URI params (same semantics as GET). The framework runs the full controller action so all response headers are set correctly, then suppresses the body before writing to the wire. Both `render()` (Swig) and `renderJSON()` honour HEAD. Routes declared as `GET` automatically accept HEAD requests — no separate routing rule needed. Useful for cache validation, existence checks, and CDN probing without downloading a response body.
* Async controller actions — controller actions can be declared `async`. The router captures the return value of `controller[action]()` at both dispatch sites (with and without middleware) and attaches `.catch()` if the value is thenable. Rejections are routed to `throwError(response, 500, err.stack || err.message || String(err))`. Existing sync actions are unaffected.
* `onCompleteCall(emitter)` global helper — wraps any EventEmitter that exposes `.onComplete(cb)` into a native Promise. Enables `await onCompleteCall(_(path).mkdir())` and similar patterns for PathObject file operations (`mkdir`, `cp`, `mv`, `rm`) and `Shell` commands from async controller actions.
* Fixed empty `unhandledRejection` handler in `gna.js` — now logs `[ FRAMEWORK ] Unhandled promise rejection:` with the full stack so stray rejections are visible in framework logs instead of being silently swallowed.
* 13 new unit tests covering the async dispatch guard source structure and pure `dispatchAction` logic (test/core/router.test.js suites 06–07).
* `renderStream(asyncIterable, contentType)` on SuperController — streams any `AsyncIterable` as a chunked HTTP response without buffering. `contentType` defaults to `text/event-stream` (SSE); each yielded string/Buffer becomes `data: {chunk}\n\n`. For other content types, chunks are written raw. HTTP/2: `stream.respond()` + `stream.write()` + `stream.end()`. HTTP/1.1: automatic chunked transfer-encoding via `response.write()`. `x-accel-buffering: no` set automatically for SSE. Upstream response headers (CORS etc.) merged into the initial headers frame. Required for LLM token streaming with `ai.client` + `stream: true`. 32 unit tests (test/core/render-stream.test.js).
* Route radix trie (#R1) — `lib/routing/src/radix.js` builds a segment-level radix trie from `routing.json` once at startup. `Routing.buildTrie(routing, bundle)` is called in `onRoutesLoaded()`; `Routing.lookupTrie(pathname, bundle)` returns candidate route names in O(m) time (m = path segment count). On a cache miss, `handle()` builds a `Set` from the trie candidates and uses `Set.has()` to skip non-matching routes in the `for…in` loop — turning the worst-case O(n×m) scan into O(m) candidate selection + O(k×m) validation (k = candidates). Falls back transparently to the full scan when the trie is unavailable. Internal change — no user-facing API change. 46 unit tests (test/lib/radix.test.js).
* HTTP/2 configurable settings (#H3) — `maxConcurrentStreams` and `initialWindowSize` are now read from `settings.json` `http2Options` instead of being hardcoded. Defaults: `maxConcurrentStreams: 256`, `initialWindowSize: 655350` (10× TCP default). Security-critical settings (`maxHeaderListSize: 65536`, `enablePush: false`, `maxSessionRejectedStreams: 100`, `maxSessionInvalidFrames: 1000`) remain hardcoded and are not user-overridable. 16 unit tests in test/core/server.isaac.test.js (suites 03–03b).
* HTTP/2 session metrics (#H3) — `server._h2Metrics` counter object (`activeSessions`, `totalStreams`, `goawayCount`, `rstCount`) is instrumented via `server.on('session', …)` event handlers and exposed under the `"http2"` key in the `/_gina/info` JSON response. 18 unit tests in test/core/server.isaac.test.js (suites 04–04b).
* Security & CVE compliance docs page — `docs/security.md` lists all known HTTP/2 CVEs addressed by Gina: CVE-2023-44487 (Rapid Reset), CVE-2024-27316 / CVE-2024-27983 (CONTINUATION flood), CVE-2019-9514 (RST flood), HPACK bomb, and server push abuse. Each section includes attack description, Gina mitigation code snippet, and the Node.js version required. Docs only — no code changes.
* AI connector — declare any LLM provider in `connectors.json` via a named protocol (`anthropic://`, `openai://`, `deepseek://`, `qwen://`, `groq://`, `mistral://`, `gemini://`, `xai://`, `perplexity://`, `ollama://`). All OpenAI-compatible providers share the `openai` npm package; `baseURL` overrides let any endpoint work including Ollama (MiMo, Llama, Phi…) and self-hosted vLLM. Unified `.infer(messages, options)` normaliser returns `{ content, model, usage, raw }` across providers; raw SDK client exposed on `.client` for streaming and function-calling. No tokens spent at startup. 70 unit tests (test/core/ai-connector.test.js).
* `peerDependencies` for AI SDK clients — `openai` (>=4.0.0) and `@anthropic-ai/sdk` (>=0.27.0) added as optional peer dependencies alongside the existing database connector peers (`ioredis`, `mysql2`, `pg`, `mongodb`, `@scylladb/scylla-driver`, `couchbase`). npm/yarn will surface a compatibility warning when a user pins an untested client version. All clients remain loaded from the project's `node_modules` — zero framework runtime dependency.
* MySQL ORM connector — entity wiring + SQL method generation from `sql/` files, `?` positional placeholders, `mysql2` pool (loaded from project `node_modules` — zero framework dependency), native Promise + `.onComplete()` shim, `@param`/`@return` annotation support. Install `mysql2` in your project to activate. 68 unit tests (test/core/mysql-connector.test.js).
* PostgreSQL ORM connector — same entity/SQL architecture as MySQL; uses `$1`, `$2`, … placeholders, `pg.Pool` with idle-client error guard, `pool.query()` execution. Install `pg` in your project to activate. 72 unit tests (test/core/postgresql-connector.test.js).
* Per-bundle framework version pinning — declare `"gina_version": "<version>"` on any bundle entry in `manifest.json` to run that bundle under a specific installed gina version. The socket server keeps running its own version; only the spawned bundle process uses the declared version. Validated against the tracked version registry in `~/.gina/main.json` before start.
* `--gina-version=<version>` flag for `bundle:start` — CLI override that takes priority over `manifest.json`. Passes the version down to the spawned process via an isolated context clone so concurrent bundle starts do not interfere.
* `bundle:add` now writes `"gina_version"` to the new bundle entry in `manifest.json` — records the current framework version at scaffold time so the pin is explicit from day one.
* 33 new unit tests covering the resolution logic, validation, context isolation, and source structure (test/lib/bundle-gina-version.test.js).
* Auto-sync `def_framework` in `~/.gina/main.json` on `bundle|project start|restart` (`bin/cli`) and on `gina-container` startup — keeps the active framework version aligned with `package.json` automatically after a version bump or worktree branch switch, with no manual `main.json` edit required.
* Added `def_framework` sync in `post_install.js` — `main.json` is updated at install time so the CLI, cmd handlers, and `framework:init` all agree on the active version immediately after install.
* New unit tests for `def_framework` sync logic in `test/bin/cli.test.js` (9 tests) and `test/bin/gina-container.test.js` section 05 (10 tests).
* Hot-reload (#M6) — in dev mode `WatcherService` now starts automatically and registers watchers for `controller.js`, `controller.render-swig.js`, and the bundle's `controllers/` directory. `require.cache` is evicted only when a watched file has actually changed, eliminating per-request eviction overhead. Falls back transparently to per-request eviction when the watcher is not available (production, non-dev env). 15 new unit tests covering source-structure guards and pure dirty-flag logic (test/core/router.test.js suites 08–09).
* GOAWAY logging (#H5) ��� `client.on('goaway')` handler in `controller.js` now logs `errorCode`, `lastStreamID`, and session key via `console.warn`. Distinguishes clean server restarts (errorCode 0) from protocol errors. 2 tests in `http2-client.test.js` section 09.
* HTTP/2 session-level guards now configurable (#H7) — `maxSessionRejectedStreams` and `maxSessionInvalidFrames` are now read from `settings.json` `http2Options` (defaults 100 and 1000). Previously hardcoded. Bundle boilerplate `settings.server.json` documents all four `http2Options` keys. 4 new tests in `server.isaac.test.js`.
* `onCompleteCall` formalized as `lib/async` module (#M4) — the Promise adapter previously lived only as an implicit global in `helpers/path.js`. Now a proper library at `lib/async/src/main.js`, registered in `lib/index.js` as `lib.async`. Input validation throws `TypeError` for missing/invalid emitters. Global injection delegates to the lib module. 16 tests in `test/lib/async.test.js`.
### Fixed
* Fixed method-based routing on shared URLs — the router now continues scanning after a URL match with a wrong method instead of immediately throwing 405. GET `/notes` and POST `/notes` can coexist as separate routing rules; a 405 is only emitted after the full scan finds no method match. (`core/server.js`)
* Fixed crash in `fitsWithRequirements` when a URL param route has no `requirements` block — router now accepts any non-empty segment value and correctly binds it to `req.params` / `req[method]` instead of throwing `TypeError`. (`lib/routing/src/main.js`)
* Fixed `throwError(res, code, stringMsg)` silently overriding an explicit 3-digit HTTP status code with `res.status || 500` — `throwError(res, 404, 'Not found')` now correctly sends 404 instead of 500. (`core/controller/controller.js`)
* Fixed port scanner `opt.end` hardcoded window — `scan.js` previously used a fixed `start + 899` ceiling regardless of `limit`, so a `port:reset` scan requesting many ports could exhaust the window before finding them all. Window is now `Math.min(start + Math.max(899, limit + 99), maxEnd)`, giving at least 899 ports of headroom for small limits and scaling proportionally for large ones (e.g. `protocol:set` passes `protocols × schemes × envs × bundles`). Unit tests added in `test/unit/port-scan.test.js`.
* Fixed `query()` forwarding errors directly to `throwError()` on non-2xx upstream responses — the callback now receives the error object so controllers can implement graceful degradation instead of always surfacing a hard error. (#Q1)
* Fixed `throwError()` logging empty for plain-object errors from `query()` — `err.stack || err.message` was undefined; now serializes `errorObject.error` and captures the callsite stack when no stack is present. (#Q1)
* Fixed `controller.render-swig.js` error field priority — `statusCodes[status]` was evaluated first, burying the actual upstream error reason; `data.page.data.error` and `data.page.data.message` now take priority, `statusCodes[status]` is the fallback. Added `console.error` log before `throwError` so the upstream reason appears in bundle logs. (#Q1)
* Fixed `whisper()` error log showing `{key}` instead of `${key}` for missing dictionary keys.
* Fixed `bundle:start` (socket-server path) not initialising `ctx.envVars` — `gna.js defineDefault()` could not create GINA_* globals when the key was absent.
* Fixed `env.json` placeholder substitution — `${bundlesPath}`, `${homedir}`, `${cachePath}`, `${projectName}`, `${projectVersion}`, `${projectVersionMajor}` were missing from the `reps` dict in `CmdHelper`; whisper left them unresolved.
* Fixed `package.json` `"main"` field still pointing to `framework/v0.2.1-alpha.3/core/gna` after version bump — now `framework/v0.3.0-alpha.1/core/gna`.
* Fixed error page displaying `[object Object]` when upstream returned an error object instead of a string — `render-swig` now normalises `error` and `message` fields to strings before building the error response. (#Q2)

## 0.2.1-alpha.3 - 2026-03-29
Internal version bump — no user-facing changes. Repository URLs updated from Rhinostone to gina-io.

## 0.2.0 - 2026-03-29
### Added
* NODE_COMPILE_CACHE (#P1): set at startup in gna.js to ~/.gina/cache/v8 — Node.js >= 22.8 caches V8 bytecode to disk, reducing cold-start time by 30–60% on subsequent runs. No-op on older Node versions.
* JSON Schemas for routing.json, app.json, connectors.json, settings.json, app.crons.json — machine-readable schemas at schema/ in the package root. Scaffold templates now include "$schema" references to https://gina.io/schema/ for free editor validation and autocomplete in VS Code and any JSON Schema-aware IDE (#AI1)
* GitHub Discussions enabled — community Q&A and Show and Tell categories now available on the repo (#A10)
* Added V8 pointer compression detection at startup: sets GINA_V8_POINTER_COMPRESSED=true and logs heap limit when Node.js is built with --experimental-enable-pointer-compression (#P4)
* Added unit tests for #M2 entity._arguments buffer management — covers Promise path deletion and DISPATCH:BUFFER_CALLBACK deletion (3 tests, test/core/entity-arguments.test.js)
* Redis session store connector (CN1): ioredis-backed store with standalone, Cluster, and TLS support; configured via connectors.json — no credentials in application code
* SQLite session store connector (CN2 v1): node:sqlite-backed store (built-in since Node 22.5.0, zero npm deps) with WAL mode, prepared-statement caching, and background expiry cleanup; supports file-based and ':memory:' databases
* Added GitHub Actions CI pipeline — runs the full test suite (494+ tests) on Node.js 18, 20, and 22 on every push and PR to `develop` and `master` (#A1)
* Stable release now auto-tags, merges to master, and publishes the next alpha to npm
* SQLite ORM connector v2: entity wiring + SQL method generation from .sql files, Option-B Promise/.onComplete() pattern, connector.js (WAL, FK, synchronous open), 56 unit tests
* Added llms.txt — machine-readable framework reference following the llms.txt standard for IDE coding assistants
* Added peerDependencies (all optional) for connector client libraries: couchbase, ioredis, mysql2, pg, mongodb, @scylladb/scylla-driver
* Added GitHub issue templates (bug_report, feature_request) and PR template
* SQLite state storage for ~/.gina/ config files (CN2 v3): lib/state.js StateStore routes all state-file writes through node:sqlite (DatabaseSync) for atomic writes and concurrent access safety; JSON sidecars kept for backwards-compat read paths
* Added WatcherService (lib/watcher): fs.watch-based file-change registry. Reads watchers.json bundle config and starts native file watchers on server startup. Exposes gna.watcher for framework-internal registration (#M6). 9 new tests in test/core/watcher.test.js.
* bundle:add preserves existing bundle source when the src/ directory already exists and rewrite is false — cloned starter repos are no longer overwritten
### Changed
* README: added 3-bullet why-Gina summary, feature highlights table, and Quick start section at the top; added MIT license and Node.js >= 18 badges; updated npm keywords to be search-relevant (http2, multi-bundle, event-driven, rest, api, couchbase) (#A9, #V3)
### Fixed
* Removed dead singleton infrastructure from SuperController (SuperController.instance, SuperController.initialized, init()/getInstance() singleton path); removed 4 no-op freeMemory([], false) calls; fixed throwError() HTML error path missing local.req/res/next = null after res.end() (#M1)
* Added deprecation warning to Couchbase SDK v2 connector: logs on every connection when sdk.version is 2 (EOL since 2021, removal planned for 0.4.0). Adds a separate fatal error when GINA_V8_POINTER_COMPRESSED=true, as v2 NAN bindings are incompatible with pointer-compressed Node.js builds.
* Fixed DISPATCH:BUFFER_CALLBACK path (util.promisify path in entity method wrappers): entity._arguments[trigger] was consumed but not deleted after use, allowing a buffered result from one request to persist and resolve all subsequent callers with stale data. Now deletes the buffer after consuming, matching the Promise path behaviour (#M2).
* Retired freeMemory() helper in controller pipeline — replaced all call sites with explicit local.req/res/next nulling so intent is visible at each response exit point (#M3)
* Fixed `/_gina/info` HTTP/2 endpoint returning `[object Object]` — `stream.end(infoHeaders)` was passing the headers object instead of the JSON string `infoStatus` (#H1)
* Added stream-closed guard (`stream.destroyed || stream.closed`) to `throwError()` HTML error response path — prevents unhandled error on HTTP/2 when a stream closes between error trigger and response write (#H2)
* Fixed infinite recursion in connector ping() when keepAlive is false (CB-BUG-4): replaced unconditional self.ping() recursive call with return in v2/v3/v4
* Removed redundant modelUtil.setConnection() call in connector v2/v3/v4 onConnect() (CB-PERF-3): first unconditional call before the existsSync guard was a no-op duplicate
* Fixed gina.onError() handler accumulation on Couchbase reconnect (CB-BUG-1): guarded registration with _errorHandlerRegistered flag so only one handler is registered per connector instance across reconnections (v2/v3/v4)
* Fixed session-store.v3 get() always returning session-not-found (CB-BUG-2): converted to async/await so Promise resolves before checks run — every session lookup was failing silently, forcing re-authentication on every request
* Fixed session-store.v3 set() silently discarding writes (CB-BUG-3): converted to async/await so upsert is awaited before calling fn() — writes appeared to succeed but were never confirmed
* Fixed CB-PERF-1 (300ms startup delay), CB-PERF-2 (ping reconnect drops callback), CB-QUAL-2 (eval() for @options parsing), CB-QUAL-3 (bulkInsert returns plain object instead of Promise) in Couchbase connector
* Fixed CB-QUAL-4, CB-LOW-1–5, CB-LOW-7: sanitised bulkInsert key in N1QL, fixed uuid implicit global, re-enabled strict mode in index.js, removed dead os.version import, removed duplicate use-strict in session-store.v4, added exponential backoff for Couchbase reconnects, removed dead commented-out blocks in connector files
* Fixed bundle:start hang: JSON.parse(process.argv[3]) in gna.start() threw SyntaxError in CLI mode where argv[3] is the project name (plain string), causing every bundle to crash silently and wait the full 60-second timeout. Added JSON format guard. Also added immediate crash detection in the start.js child watcher for non-zero exits during startup (#B9).
* Made engine.io a lazy require in server.isaac.js — loaded only when options.ioServer is configured, so bundles without WebSocket support no longer crash at startup if engine.io is absent
* Made ssl-checker a lazy require in server.js — loaded only inside verifyCertificate(), so bundles not using HTTPS cert verification no longer crash at startup if ssl-checker is absent
* Fixed Couchbase entity methods called via util.promisify without .bind(): instead of crashing with a cryptic 'Cannot set properties of undefined (setting _isRegisteredFromProto)', the framework now transparently recovers the entity singleton so the call succeeds
* Fixed EventEmitter listener leak on entity singleton when using util.promisify without .bind() (intermittent 500 errors under repeated requests on the same entity method)
* Boilerplate templates installed by view:add now render correctly — doubled html/ prefix removed from extends and include paths
* cache:stats now falls back through all assigned port/scheme combinations instead of trying only the first — fixes ECONNREFUSED on bundles that run HTTP/2 only
* Fixed entity method concurrency: concurrent callers on the same method now each receive their own result (FIFO queue dispatch replaces single-slot _callbacks; DISPATCH:PREEMPTIVE_BUFFER and consume paths queue-aware)
* All user-customisable settings (port, debug_port, mq_port, host_v4, hostname, logdir, tmpdir, rundir, log_level) now survive a shortVersion bump (e.g. 0.1 → 0.2) — previously they were silently reset to defaults because post_install pre-creates ~/.gina/<shortVersion>/settings.json from the template before init.js runs, making the migration block unreachable; fixed by detecting the unresolved template and triggering migration in that case too
### Security
* Added four missing HTTP/2 server security settings in `isaac.js`: `maxHeaderListSize: 65536` (HPACK bomb defense), `enablePush: false` (deprecated in browsers), `maxSessionRejectedStreams: 100` (RST/rapid reset flood defense — CVE-2019-9514, CVE-2023-44487), `maxSessionInvalidFrames: 1000` (CONTINUATION flood defense — CVE-2024-27316, CVE-2024-27983) (#H3)
* Fixed Couchbase restQuery() credential exposure and shell injection (CB-SEC-1, CB-SEC-2): replaced exec(curl...) with http.request(), credentials now sent via Authorization header, no shell involved
* Removed err.stack from HTTP 500 JSON response body in Couchbase connector (CB-QUAL-1): stack traces with filesystem paths now logged server-side only, never sent to clients

## 0.1.8 - 2026-03-27
### Added
* Added .github/dependabot.yml — weekly npm dependency updates for root package.json; vendored framework deps excluded
* Added _debugLog / _isDebugLog to server.isaac.js (LOG_LEVEL=debug|trace outputs gray-coded timestamps with [gina:isaac] group tag, matching gna.js and server.js)
### Changed
* package.json description updated to: Node.js MVC framework with built-in HTTP/2, multi-bundle architecture, and scope-based data isolation — no Express dependency
### Fixed
* Removed swig devDependency declarations (package.json.dist) from git — eliminated 6 Dependabot false-positive alerts (lodash, express, phantomjs are swig test-only deps, never installed or reachable at runtime)
* Popin overlay stuck active after form submit: data-gina-popin-loading attribute was set on XHR readyState 1/3 but never removed on readyState 4 (response complete), leaving the overlay blocking subsequent interactions. Cleared in xhr.onreadystatechange readyState==4, popinClose, and popinReset.
* Couchbase connector: @include directives sent raw to Couchbase in dev mode — the dev-mode SQL re-read refreshed the query body without re-expanding @include tokens, causing N1QL syntax errors. Dev-mode re-read now applies the same @include expansion as the startup parser.

## 0.1.8-alpha.1 - 2026-03-26
### Added
* Config variable interpolation now accepts \${variable} syntax in addition to {variable}. Both forms work in all config files (routing.json, app.json, settings.json, etc.). The {variable} syntax continues to work unchanged — migration to \${variable} is optional and will be documented in a future release.
* GINA_LOG_STDOUT=true — stdout-only mode for containerised deployments: logs emit as JSON lines (ts, level, group, msg), MQ transport disabled; compatible with kubectl logs, Fluentd, Datadog
* gina-init: stateless ~/.gina/ bootstrap for Docker/K8s containers — generates projects.json, ports.json, ports.reverse.json, and settings.json from env vars or a mounted JSON config file, with no gina CLI ceremony required
* critical: false option on this.query(): fire-and-forget HTTP/2 calls can now pass { critical: false } to swallow errors silently (log-only) instead of propagating to throwError and terminating the user-facing response (#H3)
* critical: false option on this.query(): fire-and-forget HTTP/2 calls can now pass { critical: false } to swallow errors silently (log-only) instead of propagating to throwError and terminating the user-facing response (#H3)
* HTTP/2 startup warmup: bundles can now declare server.warmup: ['https://upstream:port'] in their server config to pre-establish HTTP/2 sessions at startup. Warmup fires after bundle start, sends an initial PING to verify connectivity, then starts the 5s keepalive cycle — so the first real request reuses a live session instead of cold-connecting (#H5)
* Couchbase connector: _scope property on entities (mirrors _collection), $scope placeholder substitution in N1QL queries, _scope stored in bulkInsert records. scope read from connectors.json config, falls back to NODE_SCOPE env var
* Mockable service locator (#R2): getConfig() and getModel() now check for a __mock__ context override before hitting the real implementation. Test setup calls setContext('__mock__', { config: fn, model: fn }) — zero breaking changes, zero business logic changes. Lifts testability from E2E-only to unit-testable models and helpers
* EntitySuper now accepts optional `injected` 3rd constructor param — `{connector, config}` — for unit testing entities without a running database or framework server (#R3). getConnection() returns injected.connector when set. New this.getConfig() instance method routes through injected.config, falling back to the global getConfig()/__mock__ chain (#R2).
* SuperController.createTestInstance(deps) — static factory for controller unit testing (#R4). Creates a fresh instance with isolated local closure, wires mock req/res/next/options via setOptions(), marks it with _isTestInstance=true. Production singleton is never modified. Normalises missing conf structure so setOptions doesn't crash on minimal test deps.
* Auto-migrate main.json and settings.json on first startup of a new short version (e.g. 0.1 → 0.2, 0.5 → 1.0): copies all namespaced keys from the previous release, seeds port/hostname from the previous settings.json. Downgrade is free — old keys are never removed.
### Changed
* whisper() now requires \${variable} syntax — bare {variable} placeholders are no longer replaced (migration: update config files and templates to use \${variable})
* bundle:add now auto-detects system locale and timezone when scaffolding settings.json (LANG/LC_ALL → Intl → en_CM / UTC fallbacks)
* conf/settings.json locale fallback now auto-detected from GINA_CULTURE at bundle config load (preferedLanguages, region, dateFormat.short, 24HourTimeFormat)
* conf/settings.json locale defaults now resolved from built-in locale database: currency.code (ISO 4217) and preferedLanguages pulled from core/locales/dist/region/ by country code
* conf/settings.json locale defaults fully auto-detected: measurementUnits (imperial for US/LR/MM), temperature (fahrenheit for US/BS/KY/PW), firstDayOfWeek from Intl.Locale.getWeekInfo()
### Fixed
* Fixed dev-mode resource churn (#B3): DNS resolver and cache wrapper were recreated on every HTTP request because controller.js is re-required each time by the hot-reload mechanism. Both are now persisted on process.gina and reused across re-requires. Eliminates silent abandonment of in-flight DNS lookups and loss of cache-invalidation listeners between requests.
* Fixed W2 migration regression: resource template placeholders were written as \${} (empty variable) instead of \${variableName}, causing gina CLI to crash with RangeError on container startup. Also added mq_port validation in MQSpeaker to guard against malformed settings values.
* Fixed W2 migration regression: {variable} → ${variable} in bin/cli, bin/gina-container, core/config.js, entityFactory.js, framework/init.js, bundle/add.js, and resources/locals.json (#B4)
* Fixed remaining W2 regression: {gina}/{version} in templates test, {Model} in entityFactory boilerplate, {projectPath} in merge test; updated stale framework version import in merge.test.js (#B4)
* Fixed HTTPS bundle abort on startup: verifyCertificate failure during dev startup (e.g. DNS failure or self-signed cert in containers) was logged at emerg severity, triggering start.js abort detection even though the server was already listening. Downgraded to warn.
* Fixed rootDomain placeholder detection in config.js to match both `{rootDomain}` (old format) and `${rootDomain}` (new format) after the W2 interpolation migration.
* getConfig() no-arg now exposes conf.settings — settings.json content is accessible as conf.settings on the bundle conf object, consistent with conf.security
* Fixed Couchbase connector path resolution in model loader: conf.connectorsPath was never set, causing a MODULE_NOT_FOUND crash on first Couchbase connect.
* Fixed entity loader in Couchbase connector crashing with TypeError when an entities directory contains helper modules that export plain objects instead of constructor functions.
* HTTP/2 timeout messages now display in seconds when the value exceeds 1000ms (e.g. 30s instead of 30000ms)
* HTTP/2 client now retries on GOAWAY mid-stream premature close and on 502 upstream responses. Previously both failures surfaced immediately to callers with no retry, requiring application-level degraded-mode fallbacks. The 502 retry also adds a req.on('response') handler that captures the :status pseudo-header — previously unread, making nginx-level errors indistinguishable from JSON parse failures.
* renderJSON no longer throws ERR_HTTP2_INVALID_STREAM when the client disconnects (nginx timeout, browser navigation) before an async callback (Couchbase, HTTP/2 upstream) completes. The ServerHttp2Stream destroyed/closed state is now checked before stream.respond() and the request is silently dropped with a warning log.
* entity.js: guard _resolver before .then() callbacks — prevents uncaught TypeError when a Promise-returning method has no registered trigger in _triggers, which corrupted the event loop
* Fixed iso_short derivation in framework init — was using country code (cm) instead of language code (en) from culture en_CM
* Fixed ReferenceError crash on startup: defIsoShort and defDate were referenced outside their declaring function in init.js

## 0.1.7 - 2026-03-20
### Added
* Cache: sliding-window expiration and absolute ceiling (maxAge) for HTML and JSON response caches
* Added gina cache:stats <bundle> @<project> CLI command. Fetches and prints a grouped table of in-memory cache entries from a running bundle. Requires the bundle to be running.
* Cached routes now emit `Cache-Control` headers synchronised with the server-side TTL. A new optional `visibility` field (`"public" | "private"`, default `"private"`) in the per-route cache config controls the directive sent to browsers and CDNs. The hit path uses the remaining TTL so downstream caches do not over-serve stale content.
* Per-route outgoing query timeout: add "queryTimeout": <ms> to any routing.json rule and self.query() will use it as the request timeout budget when options.requestTimeout is not explicitly set. The hardcoded 10 s default still applies when neither is set.
### Changed
* Cache FS writes in the JSON renderer are now asynchronous (fs.promises.writeFile), avoiding event-loop blocking on cache misses.
* routing.json queryTimeout field now accepts human-readable strings ("30s", "500ms", "2m", "1h") in addition to integer milliseconds (30000). The value is normalised to ms at parse time so req.routing.queryTimeout is always a number.
* app.json proxy target field "timeout" renamed to "requestTimeout" — unambiguous name for the per-request outgoing timeout. Update any app.json proxy target that declares a "timeout" to use "requestTimeout" instead. self.query() call sites that passed options.timeout must also be updated to options.requestTimeout.
* self.query() options field "timeout" renamed to "requestTimeout" — consistent with app.json::proxy.<service>.requestTimeout. The internal pipeline (both HTTP/1 and HTTP/2 handlers) and the defaultOptions fallback now use requestTimeout throughout
* Entity methods and Couchbase N1QL methods now return a native Promise (Option B). await entity.method(args) works directly without util.promisify. .onComplete(cb) remains fully backward-compatible. Removes the _isRegisteredFromProto guard that was silently dropping queries when a callback-path call preceded a Promise-path call on the same entity.
### Fixed
* Sub-second TTL and maxAge values (e.g. ttl: 0.5) now work correctly in the cache. Previously, fractional seconds were truncated to zero, causing immediate eviction.
* Completed sub-second maxAge fix (#C1): both renderers were pre-truncating maxAge with ~~ before passing it to lib/cache, bypassing the Math.round fix. Removed ~~ from render-swig.js and render-json.js. Also fixed Cache-Status header calculation in server.isaac.js.
* Cache keys now include the bundle name ("static:{bundle}:{url}" and "data:{bundle}:{url}") to prevent silent cache collisions when two bundles serve the same URL path on the same server instance.
* Cache entries are now capped via LRU eviction. Cache(options) accepts a `maxEntries` option (default 0 = unlimited); when reached, the least-recently-accessed entry is evicted before each insert. The shared server cache defaults to 1000 entries, configurable via `server.cache.maxEntries` in env.json. The routing cache is capped at 5000 entries (FIFO).
* Fixed handler files returning 404 when a catch-all root statics mapping (empty key) was defined: staticResources is now sorted by descending path length so specific prefixes like /handlers/ are matched before / in the prefix-regex loop
* CORS preflight (OPTIONS) requests are now handled before routing: the server responds with HTTP 204 and the correct Access-Control-Allow-Origin header immediately, instead of forwarding to the controller action which returned an error without CORS headers. CORS response headers are now also included on all HTTP/2 JSON responses.
* self.query() queryTimeout fallback now fires correctly: moved the req.routing.queryTimeout check to before the merge with defaultOptions, which was silently overwriting timeout with the "10s" framework default before the check could run
* controller.render-swig.js: isRenderingCustomError now detected from both userData and localOptions (set by renderCustomError() in controller.js). Adds null-guard on userData. localOptions is now resolved before the flag check.
### Security
* Removed dead framework version v0.1.1-alpha.1 from the repository. Its package.json declared sanitize-html ^2.5.0 (CVE-2021-26539, CVE-2021-26540 — XSS, high severity) and busboy ^0.2.14 (dicer ReDoS). Neither version nor its dependencies were in use.

## 0.1.6 - 2026-03-08
### Added
* Added JSDoc across core/controller/, core/model/, and helpers/: constructor @class/@constructor/@extends, @inner on private helpers, complete @param/@returns on all public methods, @global on the _ path helper, @module on controller/index.js
* Added JSDoc to lib/ runtime libraries: @module on all 13 files; @class/@constructor on Cache, Collection, ConfigUtil, Domain, Inherits, Merge, ModelUtil, Proc, SessionStore, Shell, Url, Validator; complete @param/@returns on all public methods; fixed @contructor typos and empty @returns stubs
* Added JSDoc to all lib/cmd/ command handlers (63 files) covering bundle, env, framework, minion, port, project, protocol, scope, view groups plus CmdHelper and the cmd dispatcher
* Added JSDoc to core/config.js, core/gna.js, core/server.js, core/server.isaac.js, and core/server.express.js
* post_publish now auto-increments the alpha version, renames the framework directory, updates ~/.gina config, and commits after each npm publish
* Added unit tests for V8 arm64 regression (const vs var object rest), routing cleanup, _isDebugLog gate, _debugLog format, and ANSI color codes in startup debug traces
* Added gina-container foreground bundle launcher for Docker/K8s containers (bin/gina-container)
### Changed
* Replaced synchronous fs I/O in render-swig.js (readFileSync, openSync/writeSync/closeSync), server.isaac.js (execSync for brotli/gzip), and config.js (readdirSync) with async equivalents (fs.promises, child_process.exec)
* Extracted try-catch config resolution block from Router::route() into module-level resolveRouteConfig() for V8 JIT optimization (#P25)
* Replaced string += in setOptions() routing param loop with Array.push()+join() for V8 optimization (#P26)
### Fixed
* Fixed HTTP/2 client requests crashing on retry when a request body was present, and corrected misleading undefined method/path in HTTP/2 stream error logs.
* Fixed HTTP/2 session cache growing without bound; sessions are now capped at 50 with FIFO eviction and properly removed on error, close, and goaway events
* Fixed query#complete listener accumulation: removeAllListeners() is now called before each once() registration in both HTTP/1 and HTTP/2 onComplete paths
* Fixed shared module-level EventEmitter in path helper: mkdir, cp, mv and rm now each create a per-call emitter, eliminating orphaned listener accumulation and cross-call event interference
* Fixed _maxListeners growing unbounded in entity.js: capped at 100 via Math.min to ensure Node's memory leak warning still fires if trigger accumulation goes beyond the realistic method count ceiling
* Fixed undrained http.IncomingMessage on 4xx in downloadFromURL: res.destroy() is now called before rejecting the Promise, releasing the TCP connection immediately instead of waiting for remote timeout
* Fixed HTTP/2 response chunk string concatenation: chunks are now collected as Buffers and assembled with Buffer.concat() in the end handler, halving peak memory usage for large responses
* Fixed HTTP/1 retry timer accumulation: capped maxRetry at 10 via Math.min to bound outstanding setTimeout closures under sustained connection failure regardless of user config
* Fixed req/res held in controller closure: all response-terminal exit paths in controller.js, controller.render-json.js, and controller.render-swig.js now explicitly null local.req/res/next immediately after the response is sent
* Fixed SuperController.instance._options written on every request: removed write in getInstance() and setOptions() — _options was never read (all reads go through per-request local.options/self._options), making the write a silent corruption trap for future code
* Fixed a silent ENOENT 500 on requests when the swig layout cache directory could not be created (e.g. permission denied). The renderer now clears the cache target, logs a warning on every affected request, and falls back to the original configured layout path so the page renders normally. The swig compiled-template cache and response cache are also suppressed while the failure persists, ensuring the warning remains visible until the underlying issue is resolved.
* prepare_version no longer attempts a stale recovery or a no-op rename when the working version is already in sync with the published one
* Fixed invalid JSDoc tags (@package, bare @returns) in lib/inherits and lib/logger that caused jsdoc-to-markdown parse errors
* Fixed ReferenceError: isLocalScope is not defined in SuperController.setOptions (controller.js:413) — bare call replaced with self.isLocalScope()
* Fixed post_install.js MIDDLEWARE update: .toString missing () caused comparison to always be true; added missing done() call in the already-in-sync branch to prevent hang
* Fixed HTTP/2 client stream premature close silently hanging requests: req.on('close') now emits an error when the stream closes before 'end' fires (GOAWAY race, network reset, or session timeout)
* Added stream-level timeout to HTTP/2 client requests (mirrors HTTP/1 path): streams that receive no events now time out instead of hanging indefinitely
* Added TCP keepalive and setNoDelay on HTTP/2 session socket to prevent OrbStack (ARM64) from silently dropping idle inter-container connections
* HTTP/2 stream timeout now auto-retries with a fresh session instead of surfacing a 500 error — transparent recovery when OrbStack silently drops idle inter-container connections
* Fixed null lib.generator crash in post_publish.js by initializing lib inside begin() to avoid circular dependency at module load time
* Fixed bundle crash on startup when verifyCertificate DNS lookup fails inside containers (unhandled rejection from async setTimeout callback now logs at emerg level instead of aborting the process; added 5 s timeout to ssl-checker to prevent indefinite hangs)
* Fixed credentials.ca path not expanding ~/ before fs.readFileSync in verifyCertificate (server.js) and http2Options (server.isaac.js); credentials paths use ~/ which fs.readFileSync cannot resolve — now uses _() for tilde expansion
* Fixed logger silently swallowing all log output when LOG_LEVEL is set to a level name with no hierarchy entry (e.g. 'notice', 'alert', 'crit'): init now falls back to 'info' with a warning, matching the existing setLevel() guard
* Fixed bundle crash when HTTP/2 client abruptly disconnects (ECONNRESET): added session error handler in server.isaac.js and restored ECONNRESET guard in proc.js uncaughtException handler
* Fixed bundle being SIGKILL'd by start.js stdout watchdog when a runtime uncaughtException triggered an emerg log — the emerg-detection guard in child.stdout.on('data') is now skipped once the bundle is running, letting proc.js handle the lifecycle cleanly and allowing the actual error to reach the logs
* Replaced per-request regex /^(\.|\/|\\)/.test(file) with charAt(0) checks in render-swig.js and render-v1.js
* Added bundle startup integration test validating the async render pipeline (P28-P31) works end-to-end; test skips gracefully when gina socket is not running
* Reverted async readdirSync conversion in config.js (loadWithTemplate, loadBundleConfig) — async I/O in these functions breaks the synchronous Config initialisation contract, causing bundle startup crashes with TypeError: Cannot read properties of undefined (reading 'bundlesConfiguration')
* Fixed {{ page.environment.* }} placeholders not substituted in ginaLoader (gina.onload.min.js) — ginaLoader is injected after Swig compilation so whisper() must resolve them; dic was missing page.environment.key flattened entries in render-swig.js and render-v1.js
* Fixed %s placeholders not substituted in session-store v3/v4 debug log calls (gina logger does not support printf-style formatting)
* Eliminated [DOMAIN] PSL Loaded ×2 noise per request in dev mode: removed unused Domain import and instantiation from controller.js (dead code), and removed redundant direct pre-load of controller.js from refreshCoreDependencies() in router.js
* Added graceful HTTP shutdown on SIGTERM: server.close() drains in-flight requests before exit, with configurable timeout (GINA_SHUTDOWN_TIMEOUT env var, default 10s) and closeIdleConnections() for keep-alive connections (Node 18.2+ http.Server)
* Fixed per-request "Logger instance already exists: reusing it ;)" debug noise in dev mode. Root cause: refreshCore() (server.isaac.js) deleted and re-required lib/index.js on every HTTP request, which re-ran Lib() and called _require('./logger'), evicting and re-loading the logger singleton on every request. Logger is persisted via getContext('loggerInstance') and does not benefit from hot-reload. Changed lib/index.js to use plain require('./logger') instead of _require('./logger').
### Security
* Fixed directory traversal vulnerability (CVE-2023-25345) in the swig template engine: a malicious template using `{% extends %}` or a path starting with `./`, `/`, or `\` could read arbitrary files outside the template root.
* Removed dead framework version v0.1.1-alpha.1 from the repository. Its package.json declared sanitize-html ^2.5.0 (CVE-2021-26539, CVE-2021-26540 — XSS, high severity) and busboy ^0.2.14 (dicer ReDoS). Neither version nor its dependencies were in use.
* Removed internal development files from the npm package. The published tarball no longer includes .env, local IDE and tooling configuration files, .changes/ (changie entries), .changie.yaml, services/ (WIP bundles), vendor benchmark and CI files, library example and test files, locale build sources, and stray backup files. Previously .npmignore did not cover these paths.
