docs: address codex review in esmole spec

This commit is contained in:
2026-06-16 18:51:23 +05:00
parent bbad46ddcf
commit 40280a3630
@@ -74,24 +74,51 @@ storePath/envVar/logPrefix as dependencies.
- `createRegistry({ storePath, configPath, env, schema, logPrefix, envVar })`
schema injected; no direct import of any concrete schema.
- `createManager<TBackend extends { dispose(): Promise<void> }>(registry,
{ createBackend, createTunnel })` — generic over the backend; does not know
`Driver`.
- `baseConnectionShape` — a zod raw shape **without** `type`. Each package spreads
it, adds its own `type` enum and engine-specific fields, then calls `.strict()`.
{ createBackend, createTunnel, resolvePort })` — generic over the backend; does
not know `Driver`. `resolvePort(config)` is injected because the manager itself
calls `defaultPort(config.type)` today (`manager.ts:67`); ES needs 9200, SQL
5432/3306.
- `baseConnectionShape` — a zod raw shape **without** `type`, **including** the
`ssh` field (`sshConfigSchema` moves to core; the tunnel is already SQL-free
except for the `SshConfig` type at `tunnel.ts:5`). Each package spreads it, adds
its own `type` enum and engine-specific fields, then calls `.strict()`.
- `openTunnel` / `Tunnel` — unchanged (pure TCP).
- `withManaged`, `respond` — unchanged, already generic.
- `registerConnectionTools(server, { manager, registry, schema, ping })` — generic
connection CRUD (list / add / remove / update / test_connection); `ping(backend)`
is the per-backend hook backing `test_connection`.
- format primitives: `clampLimit`, `truncateRows`, `truncateJsonBudget`.
- `respond` — unchanged, already generic.
- `withManaged<TBackend, TConfig>(manager, name, fn, { isStaleError, formatError })`
— generic. It is **not** unchanged: today it imports SQL `DriverDisposedError`,
`ManagedConnection`, and `formatDbError(config.type, …)` (`managed.ts:25,34`).
Core exports a backend-neutral stale-error class; stale detection and error
formatting are injected by each package (or stale-retry moves into the manager
behind that neutral error).
- `registerConnectionTools(server, { manager, registry, fullSchema, patchSchema,
publicView, descriptions, ping, formatError })` — generic connection CRUD
(list / add / remove / update / test_connection). The current tools bake in SQL
patch fields, the SQL public view (`database`), SQL default-port rendering, SQL
descriptions, `serverVersion()`, and SQL error formatting
(`connections.ts:21,61,131`); all of these are package-owned and injected. Core
only orchestrates registry + manager calls. `ping(backend)` backs
`test_connection` (SQL `serverVersion()` vs ES `GET /`).
- format split: only truncation / token-budget helpers go to core (`clampLimit`,
`truncateRows`, `truncateJsonBudget`). SQL-shaped `normalizeCell` /
`formatDbError` (`format.ts:16,36`) stay in dbmole-mcp.
### §3. Manager generalization
The manager needs only `dispose()` from a backend. All the hard logic — cache,
rotation, dispose-race handling, tunnel guards, retry-on-stale (`manager.ts:82-129`)
— moves verbatim into core. The only change: `defaultCreateDriver` becomes the
injected `createBackend(target)`. `DriverTarget` → `BackendTarget { config, host,
port }` (generic config type parameter). The `tunnel?.isClosed()` recheck stays.
— moves verbatim into core. Changes: `defaultCreateDriver` becomes the injected
`createBackend(target)`; the internal `defaultPort(config.type)` call
(`manager.ts:67`) becomes the injected `resolvePort(config)`. The
`tunnel?.isClosed()` recheck stays.
`DriverTarget` → `BackendTarget { config, connectHost, connectPort, serverName }`
(generic config type parameter). `connectHost`/`connectPort` are where the client
actually dials — the tunnel's `127.0.0.1:localPort` when tunneled, else the real
host/port. `serverName` is the original `config.host`, carried through for TLS SNI
/ certificate hostname verification. Without this split, HTTPS Elasticsearch over
an SSH tunnel fails `verifyTls`, because the cert covers the real host, not
`127.0.0.1` (`tunnel.ts:170`). SQL drivers ignore `serverName`; the ES client sets
it as the TLS servername.
The SQL `Driver` interface stays in `dbmole-mcp`. ES implements its own `Backend`.
Both satisfy `{ dispose(): Promise<void> }`, so both ride the same manager — ES
@@ -116,17 +143,24 @@ and the store for free (an upgrade over the single-connection reference).
### §5. esmole backend + tools
**Backend** = an HTTP client (undici, keep-alive + `dispose()`) bound to
`scheme://tunnelHost:tunnelPort`, with auth (basic or apiKey) and `verifyTls`.
**Backend** = an HTTP client (undici, keep-alive + `dispose()`) dialing
`scheme://connectHost:connectPort` (the tunnel endpoint when tunneled), with auth
(basic or apiKey) and `verifyTls`. When `verifyTls` is on and the connection is
tunneled, the client sets the TLS servername to `BackendTarget.serverName` (the
real ES host) so certificate hostname verification passes (see §3).
`request(method, path, { body, params })` → `{status, body}`, never throwing on
4xx/5xx; body parsed as JSON when possible, else text. A `string` body is sent
as-is (for NDJSON `_bulk`); dict/list is JSON-serialized.
**readonly guard** (`es/guard.ts`, role analogous to `sqlGuard`): when the
connection is `readonly`, allow only GET/HEAD plus POST to a read-suffix allowlist
(`_search`, `_msearch`, `_count`, `_field_caps`, `_cat`, `_mapping`, `scroll`).
Block all PUT/DELETE and any other POST. Allowlist (not blocklist) so unknown
endpoints fail safe.
**readonly guard** (`es/guard.ts`, role analogous to `sqlGuard`): a method+path
boundary. When the connection is `readonly`, allow GET/HEAD plus POST to a
read-suffix allowlist (`_search`, `_msearch`, `_count`, `_field_caps`, `_cat`,
`_mapping`, `_search/scroll`, `_pit`) plus DELETE limited to `_pit` and
`_search/scroll` (point-in-time / scroll cleanup — read-session teardown, not data
mutation). Block all other PUT/DELETE and any other POST. `_sql` is blocked by
absence from the allowlist (it can write). Allowlist (not blocklist) so unknown
endpoints fail safe. Script content inside a `_search` body is content-level, not
method-level, and is out of scope for this guard.
**Tools (5 ES-specific + connection CRUD from core):**
@@ -159,6 +193,9 @@ mirroring dbmole's row truncation.
esmole integration runs against ES 7.x **and** 8.x containers. Coverage ≥90%
lines/functions per package — thresholds never lowered.
- Manager concurrency tests move into `core` alongside the manager.
- Remap / alias test import paths **before** moving files, not after. Integration
tests reference the old `src/...` paths; if files move first, the ≥90% gate
breaks mid-migration.
## Defaults