Vex · README.md

This is README.md as @niscorp/vex 0.4.2 ships it. A coding agent reads it from node_modules. The file itself:

nisc.dev/vex/README.md

@niscorp/vex

Declarative query synthesis. Describe the data you want in plain English and the shape you want it in; Vex generates a constrained query DSL, compiles it to parameterized SQL, runs it, reshapes the result, and caches every generation under a fingerprint so replaying it costs zero LLM calls.

The LLM never writes SQL. It fills in a validated JSON DSL; a deterministic pipeline does the rest. Server-side scope policies enforce access control the model can't see or bypass.

Install

pnpm add @niscorp/vex @niscorp/prism @niscorp/strata zod
# prism (mappings), strata (the cache table's ledger) and zod are required
# peers. Add the others for the paths you use:
pnpm add pg                               # a Postgres pool for the adapter and the cache
pnpm add hono                             # the Hono adapter (or: express)
pnpm add @niscorp/cortex @niscorp/signal  # only if you wire the bundled LLM agents

Quick Example

import pg from 'pg';
import { createQueryEngine, createPostgresAdapter } from '@niscorp/vex';

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const adapter = createPostgresAdapter({ pool });
const engine = createQueryEngine({ adapter });

await engine.introspect(); // discover tables, columns, relations, indexes — and their
                           // COMMENTs, as descriptions the query writer reads

// Generate once — the intent + the shape drive the DSL and the mapping.
// No fingerprint given, so the engine mints one and returns it.
const first = await engine.execute({
  intent: 'a customer\'s orders, newest first',
  shape: [{ id: '', total: 0, createdAt: '' }],
  context: { customerId: 'cust-42' },
});
const fp = first.meta.cache.fingerprint; // 'fp_…' — the replayable identity

// Replay by fingerprint — same DSL + mapping, zero LLM, context varies per call.
const again = await engine.execute({ fingerprint: fp, context: { customerId: 'cust-99' } });
// again.result (array | object | scalar), again.meta.cache.hit === true

Generating needs an LLM-backed generateDsl hook — see DOCS.md. Without one, the engine still serves any request that replays a fingerprint (a named slot or a seeded read), and the compile/test APIs work with no model: both need the introspected schema, and test runs the query against the database.

The production shape

Most apps use Vex in two moves, then leave one door open:

  1. Author the API as fingerprints. Seed each read and write as a named cache entry (orders/recent, tasks/setDone) — generation happens once, at build time, and the fingerprint is the whole wire contract. The wire carries { fingerprint, context } and nothing else.
  2. Lock the endpoint. locked: true makes it replay-only: a seeded fingerprint replays with zero LLM; an unknown or changed one is refused (locked). The model cannot generate ad-hoc SQL against your data.

Then keep one unlocked endpoint (wired with generateDsl) for genuine ad-hoc queries — an agent, an admin console — under its own scope policy. Built surfaces are locked; exploration is open and scoped.

Documentation

  • DOCS.md — Full usage guide: configuration, DSL reference, scope, caching, framework adapters, error handling, local dev.
  • DESIGN.md — Architecture, the pipeline, and the reasoning behind each decision.

API

const engine = createQueryEngine(config);

engine.introspect();              // Promise<DatabaseSchema> — load + cache schema
engine.execute(request, options); // Promise<QueryResponse> — the full pipeline
engine.compile(dsl);             // CompiledQuery — DSL → SQL, no LLM, no execution
engine.test(dsl, scope?);         // Promise<TestResult> — compile + run with synthetic params (LIMIT 5)
engine.getDslSchema();            // object — the DSL as JSON Schema (draft-7)
engine.getSchema();               // DatabaseSchema | undefined — last introspected schema
engine.cache;                     // CacheBackend — the live cache

What's in the box

Area Provided
Database adapters PostgreSQL (createPostgresAdapter) — pluggable DatabaseAdapter interface for others. createPglitePool (@niscorp/vex/pglite) shapes a PGlite database as the pool it takes, createPgPool (@niscorp/vex/pg) a node-postgres pg.Pool
Cache backends In-memory, Postgres (durable), tiered L1/L2 with warm-up
Framework adapters Hono (@niscorp/vex/hono), Express (@niscorp/vex/express) — discovery + query endpoints
LLM integration generateDsl / mapToShape hooks; reference agents built on @niscorp/cortex (@niscorp/vex/agent: createQueryDsl, createShapeMapper)
Writes Mutation entries in the same cache — a closed grammar, replay-only by fingerprint, never generated; lintMutation checks a statement at seed
Seeding and freshness seedCache turns authored SeedEntry / SeedMutation rows into protected cache entries; an entry's refresh is 'snapshot' or 'reactive'
Safety Server-side scope policies, query analyzer (cartesian/nesting/index checks), parameterized SQL only
Its own table The Postgres cache's table is a strata sequence (cache.sequence, nisc.vex.cache): cache.init() applies it through the ledger once, instead of IF NOT EXISTS on every boot. Requires a pool with transaction (the PGlite pool and createPgPool's have one; a bare pg.Pool does not).

License

Apache-2.0