# Gina — LLM Reference Node.js MVC framework. No Express dependency. Built-in HTTP/2. Multi-bundle architecture. Scope-based data isolation. CommonJS throughout (no ESM). Single runtime dependency (`engine.io`) — all other functionality uses Node.js built-ins. --- ## Key concepts **Bundle** — Gina's unit of deployment. A project contains one or more bundles. Each bundle has its own controllers, models, config, templates, and port. Bundles start independently (`gina bundle:start @`). **Per-bundle framework version** — Each bundle can run under a specific installed gina version, independent of the socket server. Declared as `"gina_version": "0.1.8"` in the bundle's `manifest.json` entry, or overridden at start time with `--gina-version=0.1.8`. The declared version is validated against `~/.gina/main.json` before the bundle process is spawned. **Scope** — Runtime environment label: `local`, `beta`, `production`, `testing`. Set per-process via `NODE_SCOPE`. Stored on every entity instance as `_scope`. Used to isolate data in shared databases (Couchbase N1QL `AND t._scope = $scope`). **Short version** — The `major.minor` portion of the Gina semver (e.g. `0.1` from `0.1.9-alpha.2`). Per-version config lives at `~/.gina/${shortVersion}/settings.json`. --- ## Directory layout ``` framework/v${version}/ core/ config.js Config singleton — synchronous, must complete before returning gna.js Bootstrap — injects globals (_/getContext/setContext/requireJSON/getPath) server.js HTTP server lifecycle server.isaac.js Isaac built-in HTTP/HTTP2 engine router.js URL routing (matches routing.json rules) controller/ controller.js SuperController base class (per-request instances via inherits) controller.render-swig.js HTML rendering (delegate file, hot-reloaded in dev). HTTP/2: uses stream.respond() + stream.end() directly (#H8). Tests: test/core/render-swig.test.js (77 tests — async I/O, error normalization, exit paths, guard patterns, HTTP/2 stream implementation) controller.render-json.js JSON rendering (delegate file) model/entity.js EntitySuper — EventEmitter base for all ORM entities connectors/ Couchbase, Redis, SQLite connectors lib/ index.js Exports all framework libraries merge/ Deep object merge inherits/ Prototype-based inheritance cache/ Memory cache with TTL logger/ Multi-stream logging ``` --- ## Vendored dependencies (`core/deps/`) | Package | Version | Notes | |---|---|---| | `optimist` | 0.6.1 | CLI arg parsing. Uses `minimist ~1.2.8` and `wordwrap ~1.0.0`. | | `busboy` | 1.6.0 | Multipart form parsing. Requires `streamsearch` (vendored alongside). | | `streamsearch` | 1.1.0 | Boyer-Moore-Horspool stream searching. Dependency of `busboy`. | | `swig-client` | custom | Client-side Swig for browser template rendering. | ### Swig — `@rhinostone/swig` (npm dependency) Maintained fork of the abandoned swig 1.4.2 template engine. Published to npm as `@rhinostone/swig@1.5.0` (npm org: `rhinostone`, maintainer: `beemaster`). Repo: `https://github.com/gina-io/swig`. Declared in `framework/v*/package.json` — installed via `npm install` inside the framework directory. Requires `npm install` after clone, merge, or worktree switch (the framework `node_modules/` is gitignored). Previously vendored at `core/deps/swig-1.4.2/` — removed in the migration to the npm dep. CVE-2023-25345 parse-time blocklist (`__proto__`/`constructor`/`prototype`) is patched in the fork's `parser.js` and `tags/set.js`. The path traversal boundary check in `controller.render-swig.js` is Gina's own code and is unaffected by this migration. --- ## TypeScript declarations & explicit exports **TypeScript declarations** — `types/index.d.ts`, `types/globals.d.ts`, `types/gna.d.ts`. Covers `SuperController`, `EntitySuper`, all global helpers, `PathObject`, `uuid`, `ApiError`, config file interfaces (`RoutingConfig`, `ConnectorsConfig`, `AppConfig`, `SettingsConfig`, `ManifestConfig`, `WatchersConfig`, `CronsConfig`), `GinaRequest`/`GinaResponse`, and the `Gna` lifecycle module. `package.json` has `"types": "./types/index.d.ts"` and `"typesVersions"` for `gina/gna`. **Explicit exports** — `require('gina/gna')` returns an object with all global helpers as named properties: ```javascript const { getContext, _, onCompleteCall, uuid, SuperController } = require('gina/gna'); ``` Uses lazy getters — symbols resolve at access time (after framework boot). The globals are still injected as before; this adds an importable path for IDE navigation and static analysis. Entry point: `gna.js` at package root. --- ## Global helpers (injected by gna.js — no require needed) ```javascript _(path, isAbsolute) // PathObject — resolves paths, normalises separators requireJSON(path) // JSON require with caching getPath(name) // Get named path ('gina', 'bundle', 'project', ...) setPath(name, pathObj) // Register named path getContext(key) // Global key-value store (survives require.cache eviction) setContext(key, value) // Write to global store define(key, value) // Property definition helper getEnvVar(key) // Read env variable setEnvVar(key, val, protect) // Write env variable onCompleteCall(emitter) // Wraps an EventEmitter .onComplete(cb) into a native Promise — lib/async/src/main.js, also available as global and via lib.async ``` ## Bare-module require — `require('lib/')` Bundle entities, controllers, and middleware can `require()` framework libraries as bare modules from any depth: ```javascript var uuid = require('lib/uuid'); // → framework/v/lib/uuid/ var merge = require('lib/merge'); // → framework/v/lib/merge/ var routing = require('lib/routing'); // → framework/v/lib/routing/ ``` `gna.js` injects the framework path into `process.env.NODE_PATH` and calls `require('module').Module._initPaths()` at bootstrap. This mirrors the frontend RequireJS path alias convention and removes the need for relative paths like `require('../../../../framework/v/lib/uuid')`. Every `lib//` directory must have a `package.json` with `"main": "src/main"` (see `lib/merge/package.json` for the canonical pattern). --- ## Controller pattern ```javascript // controllers/controller.content.js // NO require, NO inherits — the router (router.js) calls inherits(Controller, SuperController) // automatically before dispatch. All SuperController methods are available via `this` / `self`. function ApiContentController() { var self = this; this.home = function(req, res) { self.render({ title: 'Home' }); // HTML via Swig // self.renderJSON({ ok: true }); // JSON }; } module.exports = ApiContentController; ``` **Rules:** - Controller files are **plain constructor functions** — no `require`, no `inherits`, no `prototype` assignments. The router does `inherits(Controller, SuperController)` before every dispatch (see `router.js:557-560`). Each request gets a fresh instance with its own `local` closure. - Name the constructor `${Bundle}${Namespace}Controller` (e.g. `ApiContentController` for bundle `api`, namespace `content`). The name is not enforced but matches the boilerplate convention. - `inherits` is a Gina global — it is NOT available inside controller files via a require, only as an injected global in other framework files. - Auth is at `req.session.user` (NOT `req.user`). - Request data: `req.get` (GET query), `req.post` (POST body), `req.put` (PUT body), `req.patch` (PATCH body — partial update), `req.delete` (DELETE query), `req.head` (HEAD query — body suppressed). `req.body` aliases `req.post`/`req.put`/`req.patch`. Routes declared as GET automatically accept HEAD. PATCH vs PUT: PATCH sends only changed fields; PUT replaces the full resource. OPTIONS is CORS-only, never dispatched to a controller. - Routes declared in `routing.json`, not in code. - Always null `local.req/res/next` at response exit (done automatically in render path). - `createTestInstance(deps)` creates an isolated instance for unit tests without touching production state. - `async` controller actions are fully supported — the router attaches `.catch()` to any thenable returned by an action and routes rejections to `throwError(response, 500, ...)`. Use `await entity.method()` directly. For PathObject ops and Shell use `await onCompleteCall(_(path).mkdir())`. - `self.setEarlyHints(links)` — send a 103 Early Hints informational response. Call before the terminal method. `links` is a string or array of `Link` header values. HTTP/2: `stream.additionalHeaders({ ':status': 103 })`; HTTP/1.1: `res.writeEarlyHints()` (Node.js 18.11+). Silent no-op when unsupported. Returns `self`. **Also automatic**: `render()` auto-sends 103 for the bundle's CSS/JS preloads (from `h2Links`) before Swig compilation in HTTP/2 production mode — zero config required. - `self.renderStream(asyncIterable, contentType)` — stream an `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 SSE; raw for other content types. HTTP/2: `stream.respond()` + `stream.write()` + `stream.end()`. HTTP/1.1: automatic chunked transfer-encoding. `x-accel-buffering: no` set automatically for SSE. Fire-and-forget — do not `await`. Required for LLM token streaming via `ai.client` with `stream: true`. ```javascript // async action example Controller.prototype.upload = async function(req, res, next) { var self = this; var user = await self.UserEntity.getById(req.params.id); await onCompleteCall( _(self.uploadDir).mkdir() ); self.renderJSON({ ok: true, user: user }); }; ``` ```javascript // 103 Early Hints example Controller.prototype.home = function(req, res, next) { var self = this; self.setEarlyHints([ '; rel=preload; as=style', '; rel=preload; as=script' ]); // ... fetch data ... self.render({ title: 'Home' }); }; ``` --- ## Views / Templates Add HTML template support to a bundle: ```bash gina view:add @ ``` This scaffolds `src//templates/` (Swig layout + example content page) and `src//public/` (CSS/JS). There is **no `views/` directory** in Gina. **Template file resolution** — `self.render(data)` resolves: ``` src//templates/html/content//.html ``` - `` = the `namespace` field in `routing.json` - `` = the route **key** in `routing.json` (not the action name) Controlled by `"routeNameAsFilenameEnabled": true` in `templates.json`. Override per-route with `"param": { "file": "custom-name" }` in `routing.json`. **Template data** — data passed to `self.render({ key: value })` is available as `page.data`: ```html {% extends 'layouts/main.html' %} {% set data = page.data %} {% block content %} {{ data.key }} {% endblock %} ``` `{{ key }}` directly does NOT work — always access via `page.data`. --- ## Entity (ORM) pattern ```javascript // models//entities/UserEntity.js function UserEntity() {} UserEntity.prototype.getById = function(id) { // body injected by connector from sql/User/getById.sql }; module.exports = UserEntity; ``` **Rules:** - Entities extend EventEmitter (via EntitySuper). - Entity methods return a native Promise with `.onComplete(cb)` shim for backward compat. - `_scope` is set on every entity prototype by the connector from `connectors.json`. - `_arguments[trigger]` buffers results for late-binding listeners — cleared in dev mode per call. Buffer is unreachable in Option B (Promise path): connector trigger naming mismatch + `_callbacks` array check prevent `setListener` from firing. Dead consumption code removed (#M5); defensive unconditional clear at Option B entry. - Dependency injection: `new MyEntity(conn, caller, { connector: mockConn })` for tests. --- ## SQL file format All relational connectors (SQLite, MySQL, PostgreSQL) share the same `sql/` directory layout and annotation format. Couchbase uses `n1ql/`. ```sql /* * @param {string} $1 user id ← $1/$2/... for Couchbase/PostgreSQL * @param {string} ? user id ← ? for SQLite/MySQL * @return {object} */ SELECT * FROM users WHERE id = $1 AND t._scope = $scope ``` - `@return {object}` → single row (first result or `null`) - `@return {Array}` → all rows (default for SELECT) - `@return {boolean}` → `changes > 0` / `affectedRows > 0` / `rowCount > 0` (write) or `rows.length > 0` (SELECT) - `@return {number}` → first key of first row (COUNT queries) - `$scope` — Couchbase only: substituted as a string literal, not a positional parameter - Placeholders: SQLite `?`, MySQL `?`, PostgreSQL `$1 $2 …`, Couchbase `$1 $2 …` --- ## Connector config (connectors.json) ```json { "mydb": { "connector": "sqlite", "database": "mydb", "file": ":memory:" }, "mysqldb": { "connector": "mysql", "host": "127.0.0.1", "port": 3306, "database": "myapp", "username": "root", "password": "secret", "connectionLimit": 10 }, "pgdb": { "connector": "postgresql", "host": "127.0.0.1", "port": 5432, "database": "myapp", "username": "postgres", "password": "secret", "connectionLimit": 10 }, "cache": { "connector": "redis", "host": "localhost", "port": 6379 } } ``` Connectors supported: `couchbase` (v3/v4), `mysql` (mysql2), `postgresql` (pg), `redis`, `sqlite` (v2 ORM + session store), `ai` (LLM providers). All connector clients (`mysql2`, `pg`, `ioredis`, `couchbase`, `mongodb`, `@scylladb/scylla-driver`, `openai`, `@anthropic-ai/sdk`) are loaded from the project's `node_modules` — zero framework runtime dependency. All are declared as optional `peerDependencies` in `package.json` so npm/yarn surfaces a compatibility warning when an untested version is pinned. ## AI connector Declare any LLM provider in `connectors.json` via `"connector": "ai"`. Named protocol shortcuts resolve the SDK and base URL automatically. ```json { "claude": { "connector": "ai", "protocol": "anthropic://", "apiKey": "${ANTHROPIC_API_KEY}", "model": "claude-opus-4-6" }, "deepseek": { "connector": "ai", "protocol": "deepseek://", "apiKey": "${DEEPSEEK_API_KEY}", "model": "deepseek-chat" }, "local": { "connector": "ai", "protocol": "ollama://", "model": "mimo" } } ``` | Protocol | SDK | Notes | |---|---|---| | `anthropic://` | `@anthropic-ai/sdk` | Claude models | | `openai://` | `openai` | GPT / o-series | | `deepseek://` | `openai` | DeepSeek-V3, DeepSeek-R1 — OpenAI-compat by design | | `qwen://` | `openai` | Alibaba Qwen via DashScope | | `groq://` | `openai` | Groq LPU inference | | `mistral://` | `openai` | Mistral AI | | `gemini://` | `openai` | Google Gemini OpenAI-compat endpoint | | `xai://` | `openai` | xAI Grok | | `perplexity://` | `openai` | Perplexity | | `ollama://` | `openai` | Local models: MiMo, Llama, Phi, Qwen-local, Gemma… | | `openai://` + `baseURL` | `openai` | Any OpenAI-compatible endpoint / self-hosted vLLM | `getModel('connectorName')` returns `{ client, provider, model, infer(messages, options) }`. ```javascript var ai = getModel('deepseek'); var result = await ai.infer([{ role: 'user', content: 'Hello' }]); // result: { content, model, usage: { inputTokens, outputTokens }, raw } // .onComplete() for backward compat ai.infer(messages).onComplete(function(err, result) { ... }); // raw SDK for streaming, embeddings, function calling ai.client.chat.completions.create({ stream: true, ... }); ``` - System message: pass `{ role: 'system', content: '...' }` in the array OR use `options.system` - For Anthropic: system message is extracted and passed as the `system` parameter automatically - No live API ping at startup — zero tokens spent on connector init --- ## Route radix trie `lib/routing/src/radix.js` — built once at startup from `routing.json`. Never mutated at runtime. - `createNode()` — `{ static: {}, param: null, names: [] }` - `insert(root, url, name)` — splits URL on `/`, static segments go to `node.static[s]`, `:param` segments to `node.param` - `lookup(root, pathname)` — O(m) candidate lookup (m = segment count); strips query string before matching; returns `string[]` of route name candidates - `_match()` — static child has priority over param (more specific), but param is always tried when present; both may appear in results - **Candidate set**: `lookup()` returns structural matches only; semantic validation (HTTP method, `requirements`, param extraction) remains in `compareUrls()` and `parseRouting()` - `Routing.buildTrie(routing, bundle)` called in `onRoutesLoaded()` after `config.setRouting()` - `Routing.lookupTrie(pathname, bundle)` returns `null` when no trie is available (safe full-scan fallback) - In `handle()`: `_trieCandidateSet = new Set(trieHits)` used with `Set.has(name)` to skip non-candidates in the `for…in` loop - Falls back silently to the full linear scan when trie is absent or lookup returns no hits --- ## HTTP/2 settings & metrics **Configurable settings** (via `settings.json` `http2Options` or `settings.server.json` `http2Options`): ```json { "http2Options": { "maxConcurrentStreams": 256, "initialWindowSize": 655350, "maxSessionRejectedStreams": 100, "maxSessionInvalidFrames": 1000 } } ``` Security-critical settings that are hardcoded and not user-overridable: `maxHeaderListSize: 65536` (HPACK bomb), `enablePush: false`. **Session metrics** — exposed at `/_gina/info` under `"http2"` key: ```json { "http2": { "activeSessions": 3, "totalStreams": 142, "goawayCount": 0, "rstCount": 5 } } ``` Counters are live — no restart required. `activeSessions` is bounded by ≥ 0. `rstCount` counts only non-zero RST codes (normal stream close = code 0 = not counted). **CVE coverage** — see `docs/security.md` for the full table. All mitigations are on by default: | CVE | Name | Mitigation | |---|---|---| | CVE-2023-44487 | Rapid Reset | `maxSessionRejectedStreams: 100` + Node ≥ 20.12.1 | | CVE-2024-27316 / CVE-2024-27983 | CONTINUATION flood | `maxSessionInvalidFrames: 1000` + Node ≥ 20.12.1 | | CVE-2019-9514 | RST flood | `maxSessionRejectedStreams: 100` | | — | HPACK bomb | `maxHeaderListSize: 65536` | | — | Server push abuse | `enablePush: false` | **HTTP/2 client resilience** (inter-bundle `self.query()` calls): `handleHTTP2ClientRequest` retries failed requests up to 2 times with 500ms backoff on 2nd+ retry. Before sending on a cached session, validates freshness via a pre-flight PING (if no PONG received in 3s, evicts session and retries). Protects against silent TCP drops (observed with OrbStack Docker networking) and stale HTTP/2 sessions. | Constant | Value | Purpose | |---|---|---| | `HTTP2_MAX_RETRIES` | 2 | Max retry attempts (3 total tries) | | `HTTP2_RETRY_DELAY_MS` | 500 | Backoff delay on 2nd+ retry | | `HTTP2_PREFLIGHT_STALE_MS` | 3000 | Session age threshold before pre-flight PING | | `HTTP2_PREFLIGHT_DEADLINE_MS` | 1500 | Pre-flight PING timeout | `GinaHttp2Error` codes: `TIMEOUT`, `PREMATURE_CLOSE`, `STREAM_ERROR`, `ECONNRESET`, `ECONNREFUSED`, `PREFLIGHT_TIMEOUT`, `PREFLIGHT_FAILED`. `ECONNREFUSED` is never retried. `retryCount` tracks attempts; `retriedOnce` is derived from `retryCount > 0`. **GOAWAY logging** — when a cached HTTP/2 session receives a GOAWAY frame, the handler logs `console.warn('[http2] GOAWAY received — errorCode: X, lastStreamID: Y, session: Z')`. `errorCode` distinguishes clean server restarts (0 = NO_ERROR) from protocol errors (e.g. 1 = PROTOCOL_ERROR, 11 = ENHANCE_YOUR_CALM). `lastStreamID` indicates which streams were processed before the GOAWAY. --- ## Routing (routing.json) ```json { "home": { "method": "GET", "url": "/", "param": { "control": "home" } }, "user-get": { "method": "GET", "url": "/users/:id", "param": { "control": "getUser" } } } ``` --- ## OpenAPI spec generation `gina bundle:openapi @` reads `routing.json` and emits an OpenAPI 3.1.0 `openapi.json` in the bundle's `config/` directory. No manual spec writing required. Mapping from routing.json to OpenAPI: | routing.json field | OpenAPI equivalent | | --- | --- | | `url` (`:param` syntax) | `paths` (`{param}` syntax) | | `method` | HTTP operations under each path | | `param.control` | `operationId` | | `namespace` | `tags` | | `requirements` (regex) | `parameters[].schema.pattern` | | `requirements` (pipe-separated) | `parameters[].schema.enum` | | `_comment` | operation `description` | | `_sample` | `x-sample-url` extension | | `param.title` | operation `summary` | | `middleware` | `x-middleware` extension | | `cache` | `Cache-Control` response header docs | | `param.code` + `param.path` (redirects) | 3xx response with `Location` header | To enrich the generated spec, add `_comment` (description) and `_sample` (example URL) fields to your routes. The `namespace` field groups operations into tags. --- ## Project config files | File | Location | Purpose | | --- | --- | --- | | `manifest.json` | project root | Bundle registry — lists all bundles, their versions, src paths, and optional `gina_version` pins | ```json { "name": "myproject", "version": "1.0.0", "scope": "local", "rootDomain": "localhost", "bundles": { "api": { "version": "0.0.1", "gina_version": "0.2.1-alpha.3", "src": "src/api", "link": "bundles/api", "releases": {} } } } ``` `gina_version` is optional. When absent the bundle uses whatever version the socket server is running. `bundle:add` writes the current version automatically. --- ## Bundle config files | File | Purpose | | --- | --- | | `app.json` | Bundle settings (port, region, encoding, session, middleware) | | `routing.json` | URL route definitions | | `settings.json` | Framework settings (locale, timezone) | | `connectors.json` | Database connector declarations | | `app.crons.json` | Scheduled tasks | | `templates.json` / `statics.json` | Template and static file mappings | All config files support `$schema` references for editor validation. --- ## CLI entry points ```bash gina bundle:start @ # Start a bundle gina bundle:start @ --gina-version=0.1.8 # Start with a pinned framework version gina bundle:stop @ # Stop a bundle gina bundle:restart @ # Restart a bundle gina project:start @ --env=dev # Start all bundles in a project gina project:stop @ # Stop all bundles in a project gina project:restart @ # Restart all bundles in a project gina bundle:add @ # Scaffold a new bundle (overwrites existing src) gina bundle:add @ --import # Register existing bundle (preserves src) gina bundle:remove @ # Remove a bundle gina bundle:list @ # List bundles gina bundle:openapi @ # Generate OpenAPI 3.1.0 spec from routing.json gina bundle:oas @ # Alias for bundle:openapi gina bundle:openapi @ --output=/path/to/spec.json # Custom output path gina view:add @ # Add HTML templates to a bundle gina tail --follow # Follow logs gina framework:set --env=dev # Set framework setting gina env:get -- @ # Read a bundle/project setting gina env:set --= @ # Write a bundle/project setting gina port:list @ # List allocated ports gina port:reset @ --start-port-from=4200 # Re-scan ports from a new base gina-container # Foreground launcher for Docker/K8s gina-init # Bootstrap ~/.gina/ from env vars (containers) ``` **CLI stubs — commands that appear in docs or help.txt but are NOT implemented (handler files are empty or missing):** - `bundle:status` / `bundle:rename` / `bundle:copy` — comments-only handler files - `project:status` / `project:move` / `project:backup` / `project:restore` — empty or missing - `protocol:remove` — no handler file (also has a typo "remouve" in help.txt) - `minion:kill` / `minion:list` — no handler files - `framework:update` — empty handler - `gina --status` / `gina -t` — not in aliases.json, no handler All stubs are tracked in `ROADMAP.md § CLI` with target versions. --- ## Dev mode behaviour - `NODE_ENV_IS_DEV=true` enables hot-reload. - `WatcherService` starts automatically in dev mode and watches `controller.js`, `controller.render-swig.js`, and the bundle's `controllers/` directory. - `require.cache` is evicted only when a watched file has actually changed (file-change-triggered eviction, not per-request). - Falls back to per-request eviction when the watcher context is unavailable (production/non-dev env). - SQL files are re-read from disk on every entity call (Couchbase/SQLite connectors). - Static files are served with `cache-control: no-cache, no-store, must-revalidate` — 304 is never sent; browser always re-fetches. - Do NOT rely on module-level state in files that are hot-reloaded. --- ## Ports | Port | Role | | --- | --- | | 8124 | Framework socket server (online commands) | | 8125 | MQ listener (log tail) | **Gina infrastructure reserved range: 4100–4199** — never assigned to bundle HTTP servers; `gina port:scan` skips this range automatically (RFC 6335). | Port | Role | | --- | --- | | 4100 | Reserved — future socket server migration (currently 8124) | | 4101 | Reserved (Inspector moved to `/_gina/inspector/` built-in endpoint) | | 4102 | engine.io internal transport (moved from 8888, Jupyter conflict) | | 4103–4199 | Reserved for future Gina infrastructure | Bundle HTTP ports are allocated per-project in `~/.gina/ports.json`. **Port scanner window** — the scanner searches `start + max(899, limit + 99)` ports (capped at `maxEnd = 49151`). Default start is `3100`, giving a window of `3100–3999`. If all ports in the window are in use, the scanner fails with "Maximum port number reached". Fix: `gina port:reset @ --start-port-from=4200` (not 4100 — that's reserved). --- ## Inspector (formerly "Beemaster") The Inspector is a dev-mode SPA embedded in every bundle at `/_gina/inspector/`. Served by the bundle's own HTTP server — no separate port, process, or project registration. Same origin as the monitored bundle, so `window.opener.__ginaData` is always accessible. **Data channels:** 0. **`/_gina/agent` SSE** — remote/standalone mode. Activated by `?target=` query param. Streams `event: data` (ginaData updates) + `event: log` (server log entries) over a single SSE connection. When active, all other channels are skipped. The "No source" overlay provides a manual connect form that navigates to `?target=` — users can type a bundle URL (scheme auto-prefixed) instead of editing the address bar. 1. **`window.opener.__ginaData`** — same-origin polling every 2 s (primary). Always works since the Inspector and the monitored page share the same origin. 2. **engine.io** — real-time push when the monitored bundle has `ioServer` configured. The Inspector connects to the bundle's own port via `window.location.port`. **Log channels:** - **Client-side:** `window.opener.__ginaLogs` — array filled by the `__logsScript` capture script injected in dev mode. - **Server-side (SSE):** `/_gina/logs` endpoint streams log entries via SSE. Taps `process.on('logger#default')` — no logger modification needed. Dev mode only. - **Server-side (SSE agent):** `/_gina/agent` endpoint `event: log` — combined stream in standalone mode. Same log format as `/_gina/logs`. - **Server-side (engine.io):** `{ type: 'log', data: {...} }` WebSocket messages pushed from the `ioServer` connection handler when engine.io is configured. **Lazy activation** — `process.gina._inspectorActive` is `false` at startup. Set to `true` when the Inspector SPA, `/_gina/logs`, or `/_gina/agent` is first accessed. All profiling infrastructure (timeline init, query log wiring, Inspector payload emission) is gated on this flag. JSON responses stay clean until a developer actually opens the Inspector. **Dev mode injections** (both added to user pages in dev mode, before ``): - **`__logsScript`** — patches `console.log/info/warn/error/debug` to push `{ t, l, b, s }` entries to `window.__ginaLogs`. The Inspector reads this via `window.opener.__ginaLogs`. - **`__gdScript`** — `window.__ginaData = { gina: {...}, user: {...} }`. `__gdPayload` also stored on `self.serverInstance._lastGinaData` for engine.io. Also emits `process.emit('inspector#data', __gdPayload)` for `/_gina/agent` SSE clients. **JSON API coverage** — `render-json.js` emits the same `process.emit('inspector#data')` event and stores `_lastGinaData` when `_inspectorActive` is true. Environment is built from `getContext('gina')` + `local.options.conf` (since `data.page` doesn't exist in the JSON path). The JSON response body itself is NOT modified — Inspector data travels only via the SSE/engine.io channel. **`statusbar.html` link** — simple relative `/_gina/inspector/` (opens in new tab, same origin). **Serving** — `server.js` `onRequest()` matches `GET /_gina/inspector/*` in dev mode (engine-agnostic — works with both Isaac and Express). Files served from `core/asset/plugin/dist/vendor/gina/inspector/` with `no-cache` headers. Requests for `/_gina/inspector/` or `/_gina/inspector` serve `index.html`. MIME types: `html` → `text/html`, `js` → `application/javascript`, `css` → `text/css`, `svg` → `image/svg+xml`. **Source** (`core/asset/plugin/src/vendor/gina/inspector/`): organized by type — `html/` (index.html, statusbar.html), `js/` (inspector.js), `sass/` (inspector.scss), `css/` (compiled intermediate), `img/` (logo.svg). SCSS source uses nesting and CSS custom properties for theming. Build script Phase 3 compiles SCSS and copies to flat dist. Inspector SASS is excluded from Phase 2 auto-discovery — CSS is served separately at `/_gina/inspector/inspector.css`, not concatenated into `gina.min.css`. **Dist** (`core/asset/plugin/dist/vendor/gina/inspector/`): flat layout — `index.html`, `inspector.js`, `inspector.css`, `logo.svg`. **Unit tests:** `test/core/inspector.test.js` (renamed from `beemaster.test.js`). **Query Instrumentation (QI):** Dev-mode query instrumentation captures every database query tied to the current HTTP request and surfaces them in the Inspector "Query" tab. - **Per-request isolation** — `process.gina._queryALS` (`AsyncLocalStorage`) binds the query log to the request's async context via `enterWith()` in `setOptions()`. Created once in `controller.js`, survives dev-mode cache busts. - **Connector-level interception** — instrumented in each connector's query execution path, not in `entity.js`. Connector-generated methods use closure variables — entity wrappers can't see them. Query entry shape: `{ type, trigger, statement, params, durationMs, resultCount, resultSize, error, source, origin, connector, indexes }`. - **Supported connectors** — Couchbase (`type: 'N1QL'`, `connector: 'couchbase'`), MySQL (`type: 'MySQL'`, `connector: 'mysql'`), PostgreSQL (`type: 'PG'`, `connector: 'postgresql'`), SQLite (`type: 'SQL'`, `connector: 'sqlite'`). SQLite uses synchronous `execute()` (try/catch) instead of async callbacks. - **`origin` tagging** — `infos.bundle` (set on `Entity.prototype.bundle` at registration). **`connector` tagging** — hardcoded string per connector. - **Cross-bundle propagation** — when bundle A calls B via `self.query()`, B's queries travel back as `__ginaQueries` in the JSON response (embedded by `render-json.js`). A's `query()` callback extracts, merges into its own `local._queryLog`, and deletes the field before data reaches the controller action. - **Index reporting (Couchbase)** — `extractIndexes(profile)` walks `meta.profile.executionTimings` tree (`~child`/`~children` recursion) to find `IndexScan3`/`PrimaryScan3`/`ExpressionScan`/`KeyScan` operators and extract index names. Two extraction paths: `meta.profile` (fast, SDK v3) and EXPLAIN fallback (SDK v4 — C++ binding doesn't surface `meta.profile`). `_explainCache` Map caches per unique statement. `USE KEYS` queries detected via `ExpressionScan`/`KeyScan` operators. Enabled by `queryOptions.profile = 'timings'` in dev mode for SDK v3+. Three visual states: green (secondary), amber (primary scan), red (no index). Grey N/A badge for unsupported connectors. - **Index reporting (SQL connectors — #QI1 Phase A)** — `sql-parser.js` provides `parseCreateIndexes(src)` (parses `CREATE [UNIQUE] INDEX` statements → `{ tableName: [{ name, primary }] }` map) and `extractTargetTable(queryString)` (extracts primary table from SELECT/INSERT/UPDATE/DELETE). Each SQL connector reads an optional `indexes.sql` from the bundle's `sql/` directory at init time, builds an in-memory `_knownIndexes` map. QI resolves `_queryEntry.indexes` per query: `null` when no `indexes.sql` (N/A badge), `[]` when file exists but no index covers the table (red badge), `[{name, primary}]` when matched (green badge). - **Live index introspection (#QI1 Phase B)** — `/_gina/indexes` endpoint (dev-mode only, both `server.js` and `server.isaac.js`) queries actual databases for their indexes. Emits `process.emit('inspector#indexes', callback)` — each SQL connector registers a listener inside its constructor closure and responds with live index data. MySQL queries `INFORMATION_SCHEMA.STATISTICS`, PostgreSQL queries `pg_indexes`, SQLite uses `sqlite_master` + `PRAGMA index_list`. Collector pattern with 2-second timeout. `_liveIntrospected` flag prevents re-querying. Inspector SPA calls the endpoint lazily (only when N/A badges are present), caches result, and re-renders Query tab with resolved indexes. Eliminates the need for manual `indexes.sql` files. - **Inspector UI** — split trigger badge (`entity` | `method`), SQL syntax highlighting, params table (`$1`/`$2` → value), free-text search bar with 200ms debounce, per-card weight color-coding (`bm-stat-*`), filter persistence (lang/connector/bundle in localStorage), pagination (20 cards default, "Show all" button). **UX enhancements:** - **Logo watermark** — Gina logo displayed as a fixed-position watermark (bottom-right, 0.045 opacity) on content panes. `filter: invert(1)` for dark theme. SVG served at `/_gina/inspector/logo.svg`. - **Window geometry persistence** — Inspector popup position/size saved to `localStorage.__gina_inspector_geometry` on resize (debounced) and `beforeunload`. `statusbar.html` restores geometry on next open via `window.open()` features string. - **Env panel height persistence** — resizable environment panel height saved to `localStorage.__gina_inspector_env_height`. - **Drag-to-select log rows** — `mousedown` → `mousemove` → `mouseup` builds a range selection in real time. Plain click (no movement) copies the single row with a green flash + left accent feedback. - **Copy badge fade-out** — after copy, badge shows "Copied", fades out (400ms opacity transition via `setTimeout`), then clears the selection. `transitionend` was unreliable — replaced with `setTimeout`. - **Selection styling** — 3px left amber accent (`::before` pseudo-element), subtle amber background, rounded corners (6px) on first/last rows of contiguous groups. CSS `:has()` and `+` sibling combinators detect group boundaries. - **Tab layout presets** — segmented control (joined buttons with SVG icons) in settings panel, split from other settings by a thin divider. Four presets: Balanced (default: Data, View, Logs, Forms, Query, Flow), Backend (Data, Query, Flow, Logs, View, Forms), Frontend (View, Data, Forms, Logs, Query, Flow), Custom (user-defined drag-to-reorder). Color-coded preview pills below the control show the active order at a glance. `applyTabLayout()` reorders DOM nodes (preserving listeners). `renderLayoutPreview()` rebuilds the pill row. Persisted in `localStorage.__gina_inspector_tab_layout`. Custom order persisted separately in `localStorage.__gina_inspector_tab_layout_custom` as a JSON array. Custom mode adds `.bm-drag-mode` to the tab bar — tabs become draggable with grab cursor, 2px amber drop indicator, and 4px drag threshold. - **Performance anomaly alerts** — View tab dot indicator (8px circle, heartbeat animation) activates when page metrics exceed thresholds (load > 3s warn / 10s critical, transfer > 1MB / 5MB, FCP > 2.5s / 4s, query total > 500ms / 2s, query count > 20 / 50). Affected badges get `bm-perf-warn` (amber border) or `bm-perf-critical` (red border) class. Tooltip shows threshold details. `checkPerfAnomalies(metrics, queries)` runs on every poll cycle. --- ## Client-side plugins (`core/asset/plugin/`) **Source:** `src/vendor/gina/` — AMD modules bundled into `dist/vendor/gina/js/gina.min.js` via RequireJS + Closure Compiler. **Popin** (`src/vendor/gina/popin/main.js`) — client-side dialog/modal component. Manages popup lifecycle (register, load, open, close, destroy), XHR content loading, event wiring, and `` element support. - **Performance:** `_nextId()` counter replaces `crypto.randomUUID()`; `querySelectorAll` replaces `getElementsByAttribute`/`getElementsByTagName` for DOM scanning; `classList` API replaces `className` string manipulation; cached `RegExp` for click handlers; DOM-injected `` tags in the rendered HTML, including inline ones. Inline scripts have no `src` attribute, so the `urlArr` match at line 927 returns `null`, causing `urlArr.length` to throw into the catch block that prints `"Problem with this asset"`. Fix: skip `