Giving an LLM access to your database is easy. Giving it safe access is the hard part. Carte is a typed query carte that bounds what the LLM can ask: it picks from named, parameterised queries you define, never sees your schema, and never writes SQL.
Carte fits bounded use cases — internal dashboards, customer-facing reports, structured Q&A over a known domain. It deliberately doesn't fit "let your users ask anything about our data": if you want open-ended exploration over an unconstrained schema, reach for a text-to-SQL tool instead.
Not yet on npm. To try it locally: clone the repo, run pnpm install, then either pnpm --filter @usecarte/sqlite-quickstart-example runner for the 60-second tour against in-memory SQLite (no Postgres, no Docker, no API key needed), or follow the Demo below. The Demo wires Carte through Next.js + json-render; the core is framework-agnostic — jump to Without a framework if you're on Remix, Svelte, a CLI, or just want the renderer-free mechanics.
A live deploy is at https://clinic-ops-astro.jacobcable94.workers.dev/ — Astro on Cloudflare Workers, twenty-one queries across five RBAC tiers, with provenance receipts on every panel. Switch the role to see how the prompt's bounded surface changes, then click the ⓘ receipt button on a chart to see the lineage end-to-end.
The bounded-surface design is a sharp wedge, not a general-purpose tool. If any of these describe your problem, reach for something else:
- Open-ended ad-hoc analytics. If users phrase questions you didn't anticipate and every novel question must work, reach for text-to-SQL. Carte requires queries to exist before the question is asked.
- Highly relational dynamic composition. Joins computed at runtime, filters layered conversationally — Carte's queries are typed and named, so composition happens at authoring time, not at query time.
- Result-aware narration or anomaly detection. "Look at this and explain what's interesting" requires the model to see runtime data. That's the security commitment Carte refuses to make. If you need it, this is the wrong architecture and a different threat model — handle it in deterministic code, in the renderer, or in a separate sandboxed model call where the threat model is explicit.
- Small-scope internal tooling with no audit needs. Carte's bounded-surface and provenance story is for regulated, multi-tenant, or security-conscious deployments. For a five-person internal CSV explorer, this is overkill — wire an LLM to your DB the simple way.
The longer-form "What this isn't" section near the bottom expands on each. If you're still here after reading those bullets, you're the audience.
A Carte plan is a JSON object the LLM emits in response to a user question. It contains exactly three things:
- Entry selection — a
query.idfrom the carte's fixed list. - Parameter values — typed by the entry's Zod schema and validated before execution.
- Component selection + prop bindings — a
component.idfrom the catalog plus literal-or-{ $bind }values for each prop, also validated.
The LLM never produces SQL, never authors display strings (titles come from author-written titleTemplates with typed slot interpolation against params), never sees query results, and never sees database schema. The blast radius of a bad LLM response is whatever you put in the carte.
pnpm add @usecarte/core @usecarte/server @usecarte/components @usecarte/json-renderUser types a question. LLM picks a typed query and a typed component from your carte. Validator checks the plan. Executor runs the SQL. The default styled components (Tailwind + Recharts, with mandatory provenance receipts) draw the result. Four files:
// carte.ts — what the LLM is allowed to ask
import { defineCarte, defineEntry } from "@usecarte/core";
import { z } from "zod";
export const carte = defineCarte([
defineEntry({
id: "failure_rate_by_type",
description: "Failure rate per job type over a recent window.",
// Author-written template; the LLM fills `{hours}` from params, not free
// text. The framework injects the rendered title into every component
// — components have no `title` prop in the catalog.
titleTemplate: "Failure rate by job type (last {hours}h)",
params: z.object({ hours: z.number().int().min(1).max(168).default(24) }),
returns: z.array(z.object({
jobType: z.string(), failed: z.number(), failureRate: z.number(),
})),
staticHints: () => ({ windowHoursMin: 1, windowHoursMax: 168 }),
query: async ({ hours }) => db.select(...).from(jobs)...,
}),
]);// ui.ts — what the LLM is allowed to render with
import { defaultCatalog, defaultRegistry } from "@usecarte/components";
import { toUIAdapter } from "@usecarte/json-render";
// `defaultCatalog` ships bar / line / table / metric / layout components
// with the canonical prop schemas. The catalog deliberately excludes
// `title` from every component's prop set; the framework injects each
// panel's title from its entry's `titleTemplate`. Override individual
// components by passing your own `defineRegistry(defaultCatalog, {...})`,
// or replace the catalog entirely if you want a different LLM-facing
// surface.
export const uiAdapter = toUIAdapter(defaultCatalog);
export const registry = defaultRegistry;// app/api/carte/route.ts — server: LLM call, validate, execute
import { createCarteHandler } from "@usecarte/server";
import { carte } from "@/lib/carte";
import { uiAdapter } from "@/lib/ui";
export const POST = createCarteHandler({
carte,
uiAdapter,
context: () => ({ role: "admin" }),
llm: async (systemPrompt, userMessage) => /* call any LLM, return JSON string */,
});// app/page.tsx — client: ask, render
"use client";
import { useCarte } from "@usecarte/server/client";
import { CarteRenderer } from "@usecarte/json-render/client";
import { carte } from "@/lib/carte";
import { registry } from "@/lib/ui";
export default function Page() {
const { submit, plan, results, executedAt, isLoading } = useCarte({
endpoint: "/api/carte",
});
return (
<>
<input onKeyDown={(e) => e.key === "Enter" && submit(e.currentTarget.value)} />
{isLoading && <p>Thinking…</p>}
<CarteRenderer
plan={plan}
results={results}
registry={registry}
carte={carte}
role="admin"
executedAt={executedAt}
/>
</>
);
}That's the whole stack. Components are declared once and feed both rendering (defaultRegistry) and plan validation (toUIAdapter); <CarteRenderer> wraps the renderer with a provenance context so every panel surfaces a receipt (which query, which params, which formatters were applied) the user can audit.
The literal context: () => ({ role: "admin" }) above is a stub. For a real session-backed bridge, install @usecarte/better-auth and replace it with context: betterAuthContext({ auth, map: ({ user, session }) => ({ ... }) }); missing sessions surface as 401 automatically. Server Actions get a sibling betterAuthActionContext that takes getHeaders (e.g. async () => await headers() on Next 15+).
Authoring carte entries with an LLM agent? The repo ships an agent skill at skills/carte/ you can install with npx skills add <your-org>/carte. Works across Claude Code, Cursor, Codex, Windsurf, Aider, and anything that reads SKILL.md. See Authoring entries with an agent skill.
The default way to put an LLM in front of a database is to give it a tool that runs SQL. It works for demos. It also gives the model unlimited surface area: any query against any table, any join, any subquery. You spend most of your time writing prompts that defend against everything the LLM might do (drop a table, join PII into the response, run a 30-minute aggregate). The model sees your schema, which means a prompt-injection attack sees it too. And the LLM is allowed to invent SQL it has never been tested with, which means broken queries in production look like LLM hallucinations rather than the schema-vs-query mismatches they actually are.
Carte inverts the relationship. You define a small set of named, parameterised queries up front: each one has a Zod-typed params schema, a Zod-typed return schema, an optional access predicate, and a query function (typically Drizzle, but anything that returns a Promise works). The LLM sees a generated description of what queries exist and what they return; it picks one and fills in the params. A validator confirms the choice is well-typed and allowed under the current user's role. An executor runs it. The LLM never sees raw schema, never writes SQL, never picks columns it doesn't know exist, and never sees query results — not in the same exchange, not in conversation history, not in error messages, not as derived statistics. The blast radius of a bad LLM response is bounded by what you put in the carte.
The Demo above wires Carte through @usecarte/server and @usecarte/json-render, but the core is framework-agnostic. Same flow, no handler, no json-render:
import {
bindable,
defineCarte,
defineEntry,
defineParams,
defineUIAdapter,
executePlan,
generatePrompt,
parsePlan,
} from "@usecarte/core";
import { z } from "zod";
// 1. Define what queries exist. Each entry is a complete contract:
// typed params, typed return shape, optional RBAC, and the query function.
const carte = defineCarte([
defineEntry({
id: "top_users",
description: "Top users by event count over a recent window.",
params: defineParams({
schema: z.object({ hours: z.number().int().min(1).max(168).default(24) }),
}),
returns: z.array(z.object({
userId: z.string(), name: z.string(), events: z.number(),
})),
access: (ctx) => ctx.role !== "guest",
query: async ({ hours }) => db.select(...).from(events)...,
}),
]);
// 2. Define what UI components exist. `bindable(schema)` marks a slot that
// can either receive a literal value or bind to a query result field.
const uiAdapter = defineUIAdapter([
{
id: "bar",
description: "Bar chart for categorical aggregations.",
props: z.object({
title: z.string(),
xField: bindable(z.string()),
yField: bindable(z.string()),
}),
},
]);
// 3. Generate the system prompt; send it to your LLM with the user's question.
const systemPrompt = await generatePrompt(carte, uiAdapter, ctx);
// Call any LLM (Anthropic SDK, OpenAI SDK, your own router, your local `claude` CLI).
const planJson = await callYourLLM(systemPrompt, userQuestion);
// 4. Validate the plan: query id exists, params type-check, $bind targets
// are real fields on the declared return shape, RBAC passes.
const parsed = parsePlan(planJson, carte, uiAdapter, ctx);
if (!parsed.ok) handleErrors(parsed.errors);
// 5. Execute. Returns rows indexed by panel index, each one validated
// against its carte entry's `returns` schema.
const results = await executePlan(parsed.plan, carte);That is the whole framework. The LLM call is yours (Anthropic SDK, OpenAI SDK, your own router). The renderer is yours (json-render, your React components, an HTML report, anything that consumes typed rows). @usecarte/json-render is the bridge to json-render specifically: toUIAdapter(catalog) derives a UIAdapter from a json-render Catalog (defaults to skipping "layout" so panel-only components reach the LLM); hydrate(plan, results) turns a validated plan + executor rows into a json-render Spec by resolving every { $bind } reference to a literal; <CarteRenderer> wraps json-render's <Renderer> with <ProvenanceProvider> from @usecarte/components-core so every panel renders with audit-correct receipts.
@usecarte/json-render is one renderer integration; the same pattern works for any output. A renderer adapter is two functions plus optionally a component:
- A function that produces a
UIAdapterfrom whatever shape the renderer uses to declare its components.toUIAdapter(in@usecarte/json-render) takes a json-renderCatalog. A markdown adapter would take a list of{ id, description, props }; a Slack adapter would take a registry of Block Kit templates. The output is always the sameUIAdapterinterface (hasComponent,getComponentIds,getDescription,getProps) — that's whatvalidatePlanandgeneratePromptconsume. - A function that turns a validated plan + executor results into the renderer's IR.
hydrateproduces a json-renderSpec; a markdown adapter would produce a string; a Slack adapter would produce a Block Kit JSON payload. The renderer-agnostic kernel — resolving every{ $bind }reference against the panel's result — lives in@usecarte/coreasresolveBindings(props, result). Each adapter calls it before laying out its IR.
Components are declared once. @usecarte/core knows nothing about UI. The same Carte powers a json-render dashboard, a markdown report, and a Slack message — any future @usecarte/markdown-render or @usecarte/slack-render is a small adapter, not a fork.
The repo includes several working examples. examples/sqlite-quickstart/ is the 60-second tour: in-memory SQLite, three carte entries, one command (pnpm runner), no external setup. It's the right place to start if you just want to see the validate→execute pipeline run against real data. Skip ahead to the rich demo if you want the full story.
examples/job-queue-dashboard/ is a complete working carte against a real Postgres database. Six queries covering snapshot, aggregated, row-level, time-series, performance, and RBAC-gated shapes. Seeded with 2,500 jobs across five job types whose data deliberately tells a story: email_sender is broken, pdf_generator is slow, image_resizer has a recent pile-up.
pnpm install
cd examples/job-queue-dashboard
pnpm db:up # docker compose up -d (Postgres 16 on :5433)
pnpm db:push # apply the jobs schema via drizzle-kit
pnpm db:seed # generate the storytelling data
DATABASE_URL='postgres://carte:carte@localhost:5433/carte_demo' pnpm runnerThe runner sends "show me which job types are failing most in the last 6 hours" to Claude Opus 4.7 (via your local claude CLI, no API key needed). Plain prompt, plain JSON, no tool-calling, no special structured output mode. The LLM returns:
{
"layout": "grid",
"panels": [{
"query": { "id": "failure_rate_by_type", "params": { "hours": 6 } },
"component": {
"id": "bar",
"props": {
"title": "Failure rate by job type (last 6 hours)",
"xField": { "$bind": "jobType" },
"yField": { "$bind": "failureRate" }
}
}
}]
}validatePlan accepts it. executePlan runs the underlying Drizzle query against Postgres and returns:
[[
{ "jobType": "email_sender", "total": 208, "failed": 72, "failureRate": 34.6 },
{ "jobType": "webhook_delivery", "total": 175, "failed": 21, "failureRate": 12.0 },
{ "jobType": "pdf_generator", "total": 144, "failed": 8, "failureRate": 5.5 },
{ "jobType": "image_resizer", "total": 466, "failed": 14, "failureRate": 3.0 },
{ "jobType": "data_export", "total": 61, "failed": 1, "failureRate": 1.6 }
]]email_sender is the offender, by a wide margin. The carte's descriptions disambiguated the right query (failure_rate_by_type aggregated, vs top_failing_jobs which returns row-level data). Param parsing turned "last 6 hours" into hours: 6. The component selection chose bar for categorical aggregation. Both $bind targets resolved against the declared returns shape.
The example also includes pnpm tsx scripts/check-queries.ts which exercises every carte entry against the seeded database, useful when you change the carte and want to know whether anything broke at the SQL layer.
apps/job-queue-dashboard/ wires the same carte into a Next.js App Router demo: useCarte calls a route handler from @usecarte/server, the validated plan + executor rows flow into <CarteRenderer> from @usecarte/json-render, and json-render draws the result. pnpm --filter @usecarte/job-queue-dashboard-app dev runs it on :3001 against the same seeded Postgres.
The repo ships a cross-tool agent skill at skills/carte/ that turns "I have a Drizzle schema and want a carte" into "I have a verified carte entry." It runs as a two-phase workflow: the agent reads your schema, proposes a carte shape (which entries need built-in filters/sorts/limits via defineParams(...), which should become their own entry, which need RBAC, which need staticHints), waits for your approval, then generates the entries and runs your verification script — fixing failures up to three rounds per entry.
Install via the Vercel Labs cross-tool installer:
npx skills add <your-org>/carteThe installer reads skills/carte/ from this repo and drops it into the right per-agent location (.claude/skills/, .cursor/skills/, .agents/skills/, etc.) so it works across Claude Code, Cursor, Codex, Windsurf, Aider, and anything that reads SKILL.md. If you use a different cross-tool installer (add-skill, openskills, etc.) it should auto-discover the same folder.
The skill bundle includes:
SKILL.md— workflow, decision rules, boundaries.references/*.md— operator vocabulary,defineParams(...)mechanics,$bindrules, RBAC patterns,staticHintsguidance, prompt-format details. Loaded on demand.examples/*.ts— seven archetype templates (snapshot, windowed-aggregate, row-level-filtered, mode-switched, timeseries, rbac-detail, single-metric) lifted from the worked example.
The skill explicitly does not author UI components from scratch, modify your migrations, or decide which columns are safe to expose — those decisions stay with you.
- No raw schema exposure. The LLM sees only the carte: query ids, descriptions, params schemas, return schemas. Tables, columns, indexes, joins, foreign keys never reach the model.
- No row data in model context, ever. Query results flow from the executor to the renderer; they never enter the LLM's context window. Not in the same exchange that produced them, not in retry prompts (the repair channel feeds back only structured validation errors and the model's own previous JSON), not in conversation history, not as derived statistics, not in error messages. A successful prompt injection has nothing to exfiltrate because the model has no surface through which to request or receive data. This is enforced structurally — see the security model for the three named architectural invariants and what each rules out, and docs/provenance.md for the user-visible audit trail (every panel ships a receipt that traces each cell back to a closed-enum formatter applied to certified data).
- RBAC is a predicate on the carte. Each entry's
access(ctx)decides whether a given context can see and use that entry. Filtered entries are removed from the prompt before generation, so the LLM cannot even reference them.validatePlanre-checks at runtime as defense in depth. - Any LLM.
generatePromptreturns a string. The plan is JSON. There is no SDK lock-in: Claude, GPT, Gemini, a local model, or your own router all work the same way. Three prompt formats are supported:markdown(default),xml(which Claude tends to reason about better for structured content), andplain. - Any data source. A carte entry's
queryis just(params) => Promise<rows>. Drizzle against Postgres is the natural fit and the example uses it, but a GitHub API call, a BigQuery query, a Supabase select, an internal gRPC service, or an in-memory mock all satisfy the contract. Mix sources freely within a single carte — the LLM doesn't know or care which entry hits which backend. - Any renderer.
executePlanreturns rows indexed by panel index. What you do with them is your concern: pass to json-render, plug into React components, render to PDF, or use the carte as a typed Q&A backend with no UI layer at all. - Bindable component props are structural. A prop that accepts query data declares it via
bindable(schema), which is sugar forz.union([schema, bindRefSchema]). No registry, no metadata, no special types: detection is just "does this Zod schema's union contain a{ $bind: string }branch." Adapters wrapping a different renderer's catalog format implement a four-method interface and the rest of the framework just works.
Carte is a bounded system by design, not by limitation. Worth being clear about what falls outside it.
Not text-to-SQL. The LLM never writes a query. It picks an id from a fixed list you authored and fills in typed params. Nothing the model emits reaches your database; only your query functions do.
Not an agent loop. There is no result inspection, no follow-up query, no self-directed multi-step reasoning. The model picks a plan, the executor runs it, the result renders. Done.
The @usecarte/server handler ships one deliberate exception: bounded one-shot repair on validation failure. If validatePlan rejects the LLM's first plan, the structured validationErrors (panel index, dotted prop path, the specific reason — wrong shape for a bindable, hallucinated query id, missing required prop) are fed back into the same prompt and the model gets exactly one chance to correct itself. That's the bounded middle ground between zero-repair (fail loudly on the first try) and full agent loops (let the model retry until it works, including dead-ending into nonsense). Configurable via maxRetries on the handler — defaults to 1 (one initial attempt + one repair); set 0 to opt out.
The repair channel feeds back only the framework-controlled validationErrors (panel index, dotted prop path, an error message keyed off a closed enum) and the model's own previous JSON. It never feeds back executor errors, query results, driver-error messages, or ZodError values from runtime data — those bypass the retry loop entirely and surface as structured failures.
Not a result-aware renderer. Carte deliberately does not feed query results back to the LLM. The model produces a plan; the executor produces results; the renderer binds results to components. Results never enter the model's context window.
This is a security commitment, not a limitation. Any framework that lets an LLM make decisions based on row data is one stored-injection away from arbitrary exfiltration: an attacker stores a payload in a display_name field, a legitimate query reads it, and if the LLM ever sees that row to "make a better rendering decision" the payload is now in model context. Carte's bounded surface only works if the boundary holds in both directions — the model can't pick queries it doesn't know about, and it can't react to data it shouldn't see.
If you need result-aware behaviour — chart-type-by-cardinality, conditional drill-downs, narrative summaries — do it in deterministic code, in the renderer, or in a separate sandboxed model call where the threat model is explicit.
Invariant: returns is statically declared. Each carte entry's returns schema must be a Zod schema known at authoring time, not derived from query output. The model can safely see the shape of what each query returns precisely because the shape is declared by you, not inferred from data. defineEntry rejects schemas built from runtime values (z.lazy(() => freshSchemaEachCall), dynamically-constructed unions, etc.) at registration time. This is a security invariant, not a stylistic check.
Conversational use (out of scope for v0.1). Carte v0.1 is single-exchange. The framework doesn't ship a conversation manager, but the rule for building one safely is simple: plan history and result schemas are safe to keep across turns; row data, derived statistics, and anything containing values from prior results are not. A safe conversation state shape is { messages, planHistory, resultSchemas } — never { ..., previousRows }. A @usecarte/conversation package may ship later; until then, building one yourself with the existing API is straightforward.
Integration contract: do not feed Carte errors back to the model. Carte's HTTP response surface returns a closed-enum error projection ({ category, panelIndex, queryId, correlationId }) so consumers can handle errors without surfacing them to the LLM. The only field of CarteExecutionError safe to feed to a model is category — an enum of values declared by the framework. Never feed the cause field, the underlying driver error, or a raw Error.message string back into a prompt. This pattern is encouraged by agentic frameworks (LangChain, the Vercel AI SDK, Mastra, and others all have ergonomics for "feed errors back to the model for retry"), and a naive integration weaponizes it without anyone realizing — a Postgres unique-constraint violation includes the conflicting value in its DETAIL string, and a ZodError on a returns mismatch contains row values verbatim. Pass the category enum and let the model ask the user a question; never pass it a string derived from execution. The framework cannot enforce this if you bypass toModelSafeJSON(); treat it as a contract, the same way you treat "don't log secrets."
Not a semantic layer (but adjacent). Cube, dbt Semantic Layer, and Metabase's models live in the same neighbourhood — typed contracts between human concepts and underlying data, usually paired with BI tooling for dashboards. The differences: Carte's modelling language is TypeScript + Zod (no separate DSL), the core has no opinions about rendering (no BI tooling assumed), entries are arbitrary (params) => Promise<rows> (not just SQL views over warehouse tables), and LLM affordance is built in rather than bolted on. Choose the semantic-layer tools if you have a warehouse and want batteries-included BI; choose this if you want a typed, bounded interface to give LLMs wherever your data lives.
Carte authoring is the cost. You trade prompt-engineering-against-SQL-abuse for hand-curating queries up front. A new question that doesn't fit any existing carte entry needs a new carte entry, not a smarter prompt — that's the deliberate boundary the audience cut at the top is drawing.
First-party packages, all built and end-to-end validated against real Postgres + SQLite with a real LLM (still pre-npm — see top):
@usecarte/core— carte, UI adapter interface, plan schema, validator, executor, prompt generator, error types. Framework-agnostic.@usecarte/server— HTTP route handler, server action factory, clientuseCartehook. Web-standardRequest/Response(no Next-specific imports), so the same handlers run unmodified on Cloudflare Workers, Astro, Bun, Deno, or plain Node 18+. One-shot repair on validation failure.@usecarte/components-core— headless behavior, provenance context, closed-enum formatter/aggregation registries, mandatory receipts, strict-throw outside<CarteRenderer>. The auditability layer.@usecarte/components— styled React components (Tailwind + Recharts) implementing the default catalog. Designed as a good json-render registry citizen.@usecarte/json-render— bridge to json-render's renderer ecosystem (web, native, PDF, email, terminal, video, Vue, Svelte, Solid). Wraps<Renderer>with<ProvenanceProvider>.@usecarte/better-auth— bridges a better-auth session into Carte'sContext. Two factories —betterAuthContextfor handler routes (createCarteHandler) andbetterAuthActionContextfor Server Actions (where headers come fromnext/headersor equivalent).nullsession throwsCarteExecutionError({ category: "context_unauthorized" })which the default handler maps to 401 — noonErrorneeded for the common case. End-to-end verified against real better-auth viaexamples/better-auth/(sign-up → sign-in → role flow-through → 401 on no-session).
What works today:
- Carte definition with full Zod typing, including
defineParams(...)for built-in filters/sorts/limit andstaticHints()for authoring-time prompt metadata (deterministic, runtime-data-free; opt-inCARTE_VERIFY_STATIC_HINTSstrict mode catches drift in CI). - Plan validation via
parsePlan(...)returning a brandedValidatedPlan, including RBAC and bindable-prop checks. - Prompt generation in markdown, xml, or plain format with per-context RBAC filtering and a compact two-stage variant for large cartes.
- Executor with closed-enum typed errors (
CarteExecutionError.toModelSafeJSON()stripscause); discriminatedCarteResponsecarriesexecutedAtfor audit-correct receipts. - Next.js App Router handler with one-shot repair when
parsePlanrejects the first plan; runtime-agnostic. - First-party styled component layer with mandatory receipts, framework-injected titles via
titleTemplate, and a closed-enum formatter set (percent / currency / date / duration / bytes / boolean / truncate / null / raw). - Worked examples: job queue dashboard (Postgres, six queries with storytelling seed data) and clinic operations (SQLite, twenty-one queries across five RBAC tiers).
- Live demos: Next.js + better-sqlite3 (
apps/clinic-ops-dashboard/), Astro + Cloudflare Workers + D1 + Workers AI (apps/clinic-ops-astro/), Next.js + Postgres (apps/job-queue-dashboard/). - Verified end-to-end: Claude Opus 4.7 produces a valid plan on the first try for the example questions.
- SQLite quickstart at
examples/sqlite-quickstart/: in-memory database, three carte entries, one command, no external setup. - Cross-tool agent skill at
skills/carte/— installs vianpx skills addinto Claude Code, Cursor, Codex, Windsurf, Aider, and anything else that readsSKILL.md. - Chart-selection harness at
examples/job-queue-dashboard/scripts/chart-selection-eval.ts: pressure-tests the no-result-feedback commitment by comparing blind / augmented / result-aware-oracle chart picks under the same model. - First-party authentication bridge:
@usecarte/better-authderives a typed CarteContextfrom a better-auth session for both handler routes and Server Actions, with named context-phase sentinels (CONTEXT_PHASE_PANEL_INDEX,CONTEXT_PHASE_QUERY_ID) exported from@usecarte/coreand a literal-key-set regression test guarding thetoModelSafeJSON()projection shape.
What is not built yet:
- Published npm release
$state-based binding (resolving$bindto client-side reactive paths instead of literals)- Example cartes in domains other than job queues
- Genkit plugin
If you want to use this for something specific, or you want to contribute one of the missing pieces, open an issue.