Skip to content

feat(studio): schema history, SQL diagnostics, AI assistant, query insights, operation console, grid parity - #229

Merged
prisis merged 47 commits into
alphafrom
worktree-studio-prisma-parity-plans
Jul 29, 2026
Merged

feat(studio): schema history, SQL diagnostics, AI assistant, query insights, operation console, grid parity#229
prisis merged 47 commits into
alphafrom
worktree-studio-prisma-parity-plans

Conversation

@prisis

@prisis prisis commented Jul 29, 2026

Copy link
Copy Markdown
Member

Closes the Prisma Studio parity gap identified in plans/README.md Wave 15. Six plans (200–205), grounded against prisma/studio's source and its normative Architecture/*.md docs rather than its README.

What ships

Plan Feature
200 Schema-version visualizer — a per-shard __lunora_schema_history ledger, a timeline, and a visual diff on the existing React Flow canvas
201 SQL editor diagnostics — inline lint before Run: rejected verbs, unknown tables/columns, syntax errors, full-scan plans
202 AI assistant — NL→SQL with error repair, NL→filter, and chart inference, on the app's own Workers AI binding
203 Time-ranged query insights — 1m/5m/15m/1h, throughput + latency charts, p95 beside the mean
204 Operation console — a tape of every admin RPC the Studio itself issued
205 Data-grid parity — column pinning, match highlighting, typed date search, column windowing, reverse-relation counts, URL state

Design decisions worth reviewing

The diff engine moved to shared/. The Studio's schema history and lunora deploy's drift gate must classify a change identically, or the UI says "safe" while the gate refuses to ship. One implementation, bundler-inlined into both, with a scope discriminant stamped at construction so a new change variant cannot silently render a table as untouched.

The read-only SQL gate also moved to shared/. The editor now lints with the exact function @lunora/do enforces with, so the warning while you type and the rejection on Run cannot disagree.

AI output is never privileged. Generated SQL passes the same classifyStatement gate as hand-typed SQL and lands in the editor unexecuted. A response that fails is retried once, then discarded — not returned labelled. Filter clauses and chart axes are validated against the table's real columns before use.

No row values leave the machine. Chart inference sends column names, inferred types, and the row count — that is enough to choose an axis. Asserted by a test.

Two decisions recorded rather than defaulted. Infinite scroll: not adopting — pagination gives a stable position, an exact count, and export semantics that mean "these rows". The perf motive behind Prisma's choice was the vertical axis, which row virtualization already answers.

Review history

Two full adversarial passes (/thermos), both fully addressed. Findings worth knowing about, because the tests were green throughout:

  • The operation console recorded almost nothing it claimed to — the recording helper had one caller; ~53 imperative dispatches, including every write, bypassed it. Recording moved to a Proxy over the client, so coverage is true by construction.
  • Column virtualization never ran — the measurement effect attached to a ref whose node did not exist yet, and with [] deps never retried. Fixed with a callback ref.
  • The studio bundle had not built since the first commit — a generic arrow in a .ts file parses as JSX under packem's Babel. Only build:packages catches this.
  • All 8 examples silently lost their schema advisories in a regeneration against a stale advisor dist — the entire −1272-line half of an earlier diff.
  • Two docblocks asserted invariants the code violated. Both are fixed by making the code true, not by softening the comment.

Testing it

pnpm --filter lunora-playground run dev     # Studio at <dev-url>/__lunora
cd apps/playground && lunora seed --table demoRecords --count 250 --seed 7

No --url, no --token, no sign-in: the seeder reads the running dev server's
URL from .lunora/dev.json and the admin bearer from .dev.vars, seeds the FK
parents (users, channels) first, and wires the refs to real ids. --seed 7
makes it reproducible.

That gives you 250 rows across 24 columns with createdAt/updatedAt spread
over the last six months — enough for pinning, column windowing, match
highlighting, 2026-07-style date search, and reverse-relation counts.

Gate

do 1364 · codegen 922 · studio 922 · cli 864 — all passing. tsc --noEmit, ESLint --max-warnings=0, Prettier, package.json order, and api:check clean across all five packages; build:packages green across 49 projects.

Branch is merged up to date with alpha.

React Doctor sweep (added after review)

Review feedback pulled a second thread into this PR: packages/studio reported
232 react-doctor diagnostics, 50 of them error-severity. It now reports 0.

188 were fixed rather than silenced. The ones worth knowing about:

  • 31 render-phase ref writes moved into mirror effects (or a lazy useState
    where a ref was seeded and read mid-render). React can render without
    committing, so a render-phase write publishes a value that never became the UI.
  • 24 try/catch/finally blocks flattened. React Compiler cannot lower
    finally, and one unsupported statement bails the whole component out of the
    auto-memoization this package depends on. Applied only where the catch swallows
    and the try has no early return; the other 7 keep their finally.
  • 30 manual memos removed — 11 useCallback wrappers deleted, 19 useMemo
    bodies hoisted to module-scope pure functions. The 9 that stay are the ones
    where identity is behaviour (the client, values effects key on, the arrays
    react-table and react-flow re-seed from), each with the reason in the code.
  • Two window listeners were re-registering on every render — the sidebar
    shortcut and the insights visibility-refresh — now useEffectEvent.
  • A11y: the row-form fields and the grid's inline cell editor had no
    accessible name; the chart legend filtered a series on click with no keyboard
    path at all.
  • Quadratic lookups in the RLS/permissions folds and KV row selection, and a
    per-keystroke double walk in SQL autocomplete.

The remaining 44 are suppressed in place with the reason in the code, because
the fix is a refactor rather than a lint change: only-export-components (20
files, an HMR-only gain), no-giant-component (10 panels), and a few
false positives. Those two rules are the ones genuinely worth revisiting — they
point at structure, not at a lint nit.

Gotcha for anyone adding a suppression: react-doctor rules are
plugin-namespaced (react-doctor/… vs react-hooks-js/…), the directive must be
adjacent to the offending line, and the comment form depends on position —
{/* … */} in JSX children, // in an attribute list or arrow body. A // in
JSX children is literal text: it broke a file's parse and silently dropped 38
tests from the run without failing it.

Not in scope

plans/README.md records what was deliberately left: infinite scroll (decided against), and the AI Query-Insights recommendations follow-on.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added AI-assisted SQL generation/repair, structured filter suggestions, chart suggestions, and in-editor SQL diagnostics with read-only safety checks.
    • Introduced an operation console drawer with global shortcut, plus “show in console” error links.
    • Added schema history/version timeline with visual diffs and a dedicated migrations route.
    • Enhanced data browsing with pinned columns, in-cell highlighting, opt-in reverse-relation counts (URL-seeded pins), and improved column windowing.
    • Added query insights with selectable ranges and percentile latency (p50/p95), plus a deterministic playground demo seeding command.
  • Bug Fixes
    • Improved date-prefixed search behavior and corrected SQL alias/qualifier resolution for qualified references.

prisis and others added 14 commits July 28, 2026 21:18
Plans 200 and 201 of the Prisma Studio parity wave.

Plan 200 — schema-version visualizer. Studio had no migration history: the
Migrations page was a run-state table for data migrations. Adds a per-shard
`__lunora_schema_history` ledger appended by `runShardMigrations`, keyed by the
content hash of the structural snapshot codegen already computes for the
pre-deploy drift gate, so the ledger and the gate can never describe different
shapes. The snapshot format and its `safe`/`breaking` diff move to
`shared/schema-snapshot.ts` (bundler-inlined, no dependency edge) so the Studio
renders the exact verdict `lunora deploy` blocks on. The diff draws on the
existing React Flow canvas via a new `nodeClasses` prop rather than a fork, and
the selected version lives in the `/migrations` route's search params so a diff
is a shareable link.

Schema versions and data migrations stay two sections on one page: schema is
applied at runtime from defineSchema, `defineMigration` is hand-written data
movement, and merging them into one timeline would imply a link the data does
not carry.

Plan 201 — SQL editor diagnostics. Feedback was run-then-see. The read-only gate
moves to `shared/sql-readonly.ts` so the editor lints with the exact function the
DO enforces with, and a new `lintSql` admin RPC plans (never executes) a
statement for syntax errors and full-scan warnings, gated identically to
`runSql`. Diagnostics render as a transparent underline overlay plus an
always-present problems row — no CodeMirror, so the embedded bundle is unchanged
and the existing gutter and autocomplete survive.

Plan 201 Phase 3 (filter-expression lint) is dropped: the data browser's filter
is a structured builder, not an expression, so there is nothing to lint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plan 204. Studio had three log surfaces and none of them answered "what did this
UI just do": getLogs is the application's durable logs, getAuditLog is the
server's record of privileged writes, getRequestLog is request-level traffic.
None carries the client's view — which RPC was called, with which shard and
arguments, how long it took, whether it failed before reaching the server, and
in what order relative to the other calls one click fanned out.

Adds a bounded in-memory tape recorded at the single choke point every admin call
already funnels through (`recordedCall` in lib/internal.ts, used directly and via
useAdminQuery's fetcher), rendered as a ⌘/Ctrl+` drawer that docks under the
current panel.

Shapes, never payloads: arguments go through an explicit per-function summariser
map, and the fallback for an unmapped function records argument KEYS only — a
blanket JSON.stringify is exactly how row data would leak in. The search term of
a table read and the text of a SQL statement are both recorded as size/presence
only, asserted by tests.

Sequence numbers are assigned at dispatch, so a slow call that started first
cannot appear after the fast one it raced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Completes plan 204's two open items.

Live subscriptions are now recorded as ONE entry per channel rather than being
invisible: startSubscription on open, a push counter, failSubscription on
channel error, endSubscription on teardown. Teardown deliberately does not
overwrite an error status — the close of something already reported broken must
not erase the diagnosis. A channel sitting at "live" now reads as healthy where
a call stuck at "pending" reads as hung.

Phase 3 wires failures to the tape. The drawer's open/focus state moved into a
provider above the shell, and `recordedCall` tags each rejection with its tape
sequence under a Symbol key — invisible to JSON.stringify and to the existing
errorMessage/errorHint readers — so ErrorAlert opens the console on the exact
entry that failed instead of making the operator hunt. LiveError gets the same
affordance, opening the errors-only view since it receives a message rather than
the error object.

The provider's default value is inert rather than throwing: ErrorAlert is mounted
standalone by other suites, and a debugging affordance must never be the reason
an error component crashes. Asserted by a test.

Five existing assertions relaxed from toBe to toContain: they pinned an alert's
entire textContent to the error code, which the new affordance appends to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two independent review passes converged on the same P1, plus a build break
neither caught.

**The operation console recorded almost nothing it claimed to.** recordedCall
was documented as "THE choke point ... no second path to miss" and had exactly
one caller. ~53 imperative dispatches bypassed it — every write among them:
writeRow, deleteRows, runMigration, pitrRestore, importShard, runSql — so the
console recorded reads only and six of eight argument summarisers were
unreachable. The operations an operator most needs after a failure were the ones
missing, and the comment would have told the next contributor to do nothing.
Recording moved to the client itself: one Proxy applied where app.tsx builds the
LunoraClient, so coverage is true by construction and dispatchByKind is covered
for free.

**The studio bundle had not built since the first commit.** sortKeys is a generic
arrow, which packem's Babel parses as JSX in a .ts file. It was fine inside
@lunora/codegen; moving it to shared/ put it in the studio bundle. tsc, ESLint
and 4000+ tests all passed regardless — only build:packages catches this.

**The examples lost every schema advisory.** All 8 _generated/shard.ts were
regenerated against a stale advisor dist and emitted an empty LUNORA_ADVISORIES,
blanking their Advisors pages — the entire -1272-line half of the diff. Rebuilt
and regenerated; the net per-example diff is now +4/-1.

Also fixed: openConsole({errorsOnly}) did nothing on an already-open drawer (the
filter was seeded into local state; now single-owned, with a regression test);
the affordance rendered dead under the inert default context (context is now
undefined outside a provider, which let the five toBe assertions be restored);
Object.defineProperty could throw from inside the catch and replace the real
error; recordPush was O(300) per WS push with the console closed; toSpans
compared against the unfiltered previous span, letting a nested span desync the
overlay; Ctrl+` no longer collides with the macOS window shortcut or fires while
typing.

Structural: TABLE_ANCHORED moved next to the union it tracks as a scope
discriminant stamped by the diff engine, so a new change variant cannot silently
render a table as untouched; the schema-drift re-export shim is gone (it had
already caused two import paths inside one package); the three new admin reads
left shard-do.ts's 64-arm chain for a lookup table; the untranslated migrations
headings moved into a real component that can call useT(); dead exports removed
and the false nav-gate, choke-point and FNV-collision comments corrected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…items

The editor had grown two incompatible answers to "what table does this
qualifier mean". sql-autocomplete resolved a `tbl.` prefix by reading the word
behind the caret, which cannot see aliases at all — `SELECT m.| FROM messages m`
completed nothing — while the new linter built a correct alias map to check
qualified columns. Two modules in one directory disagreeing about `m.` is how you
get "the linter says the column is wrong but autocomplete won't offer the right
one".

Both now read features/sql/sql-context.ts: one masking scanner, one CTE set, one
alias map. The autocomplete gains alias-aware column completion as a side effect,
pinned by a test that would have failed before.

shared/sql-readonly.ts keeps its own comment scanner deliberately — it is inlined
into @lunora/do, which must stay free of studio feature modules, and shared/ may
only import other zero-dependency shared/ files. Recorded at the module so the
duplication reads as a decision rather than an oversight.

Also clears the remaining review items: the dead useOperationLog export;
SnapshotParseOutcome's two-optional bag became a three-state union so the CLI's
fatal policy and the Studio's tolerant one both read as exhaustive switches; the
console provider's actions moved into a ref so `toggle` keeps its identity and no
longer re-registers the window keydown listener on every open/close; the
"entry per push" rationale is stated once instead of three times; and the SQL
draft egress (the editor now sends drafts to be planned ~600ms after a typing
pause) is documented in plan 201 rather than left invisible.

Adds the two missing test surfaces: use-sql-diagnostics (gate diagnostics are
synchronous, a refused statement is never sent to the server, older workers
without lintSql degrade to client-only) and schema-history (derived selection
defaults to newest and survives a pruned ?version=, first version reads as
all-new, an unreachable RPC surfaces as an error rather than "no versions").

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plan 203. The slow-query leaderboard answered "what has been expensive since
this shard was created" — cumulative counters with a mean and no time axis. That
is not the question an operator has during an incident, where a statement
hammered in the last minute matters more than one that was hot last week, and a
mean hides the tail being chased.

Adds a time-bucketed companion to __lunora_metrics_queries, keyed by an 8-char
hash of the normalised statement (the table holds one row per statement per
window, so the text stays in the lifetime table and is joined back on read). The
bucket width is the same 60s window function-metrics already uses, so both
series chart on one axis without resampling.

Percentiles come from a fixed logarithmic latency histogram interpolated on
read, rather than stored samples — unbounded hot-path growth for a precision
nobody needs at this altitude. Accuracy is a bucket's width and says so.

The Studio tab gains 1m/5m/15m/1h ranges, throughput and latency charts, and p95
beside the mean. The tracked-statement cap is reported and badged, so the view
never implies totality while statements past the cap are being dropped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The maintainer answered Q1: use @lunora/ai, i.e. Workers AI through the app's own
AI binding.

That answer largely dissolves Q2. Workers AI runs inference on the user's own
Cloudflare account, inside the trust boundary the app already runs in, so schema
does not leave their infrastructure for a third party and can go by default.
Result row VALUES still stay opt-in for chart inference — "same account" is not
the same as "the operator expected a model to read this row", and the opt-in
costs one click.

It also supersedes the original design. The repo already solves this exactly
once, in issue-explainer.ts behind __lunora_admin__:explainIssue: an engine
outside shard-do.ts, pure over an injected AI binding, with input caps, a fencing
delimiter, a timeout (the DO's admin dispatch is single-threaded), a pinned model
id, and graceful degradation to the non-AI answer when the binding is absent or
the model errors — and no dependency edge from @lunora/do onto @lunora/ai.

So plan 202 no longer adds an `llm` prop to StudioProps. It adds a sql-assistant
engine beside issue-explainer and an aiGenerateSql RPC that mirrors it, and the
Studio calls it like any other admin read, hiding the affordance when no binding
is reported. The browser bundle never sees a model or a key, and there is no new
credential path.

Phases 1-4 rewritten against that design. The security rule is unchanged:
generated SQL passes shared/sql-readonly.ts and is shown before it runs, never
auto-executed.

Also marks 200/201/203/204 as shipped in the wave index.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… column windowing

Plan 205, phases 1-3 and 5.

Column pinning is now a SET with cumulative offsets rather than a boolean on
column 0. The previous `left: PINNED_DATA_LEFT` was correct only while exactly
one column could be pinned — a second would have stacked on top of the first.
Offsets derive from the visible order each render, so hiding or reordering a
pinned column re-flows the rest instead of leaving a gap. Pins persist per table
and the URL wins over the stored default, so a shared link to a wide table
arrives with the same columns frozen.

Search now shows its work: matched substrings are marked in the cells that
matched, which on a wide table is the difference between a result set you can
scan and one you have to read. A masked cell is never highlighted — the
highlight would reveal where in the redacted value the match landed, which is
exactly the position the mask exists to hide.

Server-side search gains a typed date-range predicate. `2026-07` against an
epoch-millis timestamp previously matched by substring accident or not at all;
it now also matches the month by half-open range. Deliberately date-only:
numeric/boolean/UUID equality would be surface without a bug behind it, since
the existing LIKE already finds those in doc-stored columns.

Column windowing bounds the horizontal axis, which row virtualization never did
— a 200-column table mounted every cell of every visible row. Pinned columns are
always mounted regardless of scroll (they are position: sticky and would
otherwise vanish once their span scrolled away), spacers preserve total width so
alignment and the scrollbar are unchanged, and a zero viewport yields every
column so nothing is blank in jsdom or on first paint.

Records two decisions the plan asked for. Infinite scroll: NOT adopting —
pagination gives a stable position, an exact count, and export semantics that
mean "these rows", and the performance motive behind Prisma's choice was the
vertical axis, which row virtualization already answers. Back-relation columns:
deferred — a reverse relation needs a per-row aggregate the read path does not
produce, so it is a query feature, not a rendering one, and bolting it onto a
grid pass would have been the wrong seam.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A row's forward relations are already visible — a v.id("users") column renders as
a link. The reverse direction ("how many messages does this user have") is not
derivable from the row, because the foreign key lives on the other table.

The deferral note said this needed a server-side per-row aggregate. That was half
right: finding the EDGES needs no server at all, because describeTables already
returns every table's columns with their ref target, so "who points at me" is a
scan of metadata the Studio fetches once. Only the counts need the database.

Those are one grouped query per relation, never one per row — GROUP BY :fk WHERE
:fk IN (ids), the same shape relations.ts uses for the forward fan-out and for
the same N+1 reason. A 50-row page with two relations costs 2 queries, not 100.
The FK resolves through the same physical-vs-__doc__ path the filter builder
uses, so a doc-stored key works identically and the JSON path is bound.

Off by default, toggled per table from the columns menu's Related group:
resolving these is proportional to relations × page size and most sessions never
open them, so only switched-on edges are requested.

Two failure modes handled deliberately. An unresolvable edge — a table dropped
since the page loaded — is skipped rather than thrown, because the Studio derives
these from metadata that can lag the live database and one optional column must
not fail the page. And childless parents are omitted rather than returned as
zero, so the payload does not grow with the page; the client renders 0 for an
absent id and a dash while loading, so "no children" reads differently from "not
asked yet".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ases 1-2)

Wires the Studio SQL editor to the app's own Workers AI binding, per the Phase 0
decision: the model runs on the user's own Cloudflare account, the browser bundle
never sees a model or a key, and there is no new credential path.

The engine mirrors issue-explainer.ts point for point rather than inventing a
second AI surface — injected binding, input caps, untrusted fence, a 15s timeout
raced against the inference (the DO's admin dispatch is single-threaded, so a
hung model would hold it open), a pinned model id, and a closed degraded union.
Those conventions are load-bearing, and a second surface reinventing them is how
one of them ends up missing.

The security rule is absolute: a response that fails shared/sql-readonly.ts —
the same gate runSql enforces — is retried once and then DISCARDED, not returned
labelled. A model is a drafting aid inside the existing boundary, never a way
around it. What reaches the editor has passed the gate, and lands there
unexecuted for the operator to read and run.

Fence extraction is indexOf, not a regex: the obvious pattern backtracks
super-linearly and a model response is the one string in this flow that is
neither produced nor length-capped by us.

The prompt is grounded in the shard's real tables and columns via the same
tableColumns reader describeTables serves the Studio from, so the model names
things that exist. In the client, no-ai-binding is sticky — an app without a
binding answers that way every time, so the first such reply latches and the
affordance disappears rather than staying as a button that always fails.

Phases 3-4 (NL→filter, chart config) deferred: they are additional tasks riding
this contract rather than additional architecture, and chart inference carries
the one product question Phase 0 deliberately left open — it is the only task
that would want row VALUES, and that opt-in is a design choice, not a mechanical
one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Completes plan 202.

Both tasks ride the contract phases 1-2 established — engine, read-only gate,
untrusted fence, timeout, retry, degrade arms — through a task discriminator
rather than three copies of that scaffolding. Three copies is three places for
one of them to go missing.

Both return STRUCTURED output validated against reality before it is used.
Filter clauses are checked against the table's real columns and the seven
operators the builder accepts, so they pass through the data browser's existing
validation and parameter binding untouched and a hallucinated column is dropped
here rather than reaching the query builder. Chart axes are checked against the
result's real columns and the three kinds the editor can render, so a
hallucinated name degrades to "could not infer a chart" instead of rendering
empty.

The row-values question that phase 0 deliberately left open resolved itself.
Chart inference was the one task that might have wanted them, and it does not:
column names, inferred types and the row count are enough to choose an axis. So
no row values are sent at all, no opt-in control is needed, and the egress line
holds without one. A test asserts the prompt carries the shape and not the data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merges origin/alpha (9 commits) and updates the three drifted public-API
snapshots — codegen, do, studio — for the surface this branch adds.

The api-snapshot job is a CI gate that lint, types and 4000+ tests are all blind
to, so it is checked here rather than discovered on the PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both review passes ran against the final merged branch. The most serious finding
is one the tests could not see.

**Column virtualization never ran in production.** The measurement effect lived
in `useDataBrowserTable` (always mounted) but `scrollRef` attaches inside
`DataBrowserTableView`, which mounts only once a page has rows. On a cold load
the effect ran with a null ref, returned early, and with [] deps never ran
again — so the viewport measured 0px forever and the window fell back to
rendering every column. The whole phase-3 perf change was inert. Replaced with a
callback ref, which fires exactly when the node appears.

**`?pins=` permanently disabled the pin toggle.** URL pins took precedence over
storage unconditionally, and the toggle wrote only to storage — so on any link
carrying pins, every pin/unpin click was a no-op for the rest of the session.
Precedence flipped: storage wins, the URL seeds a table nobody has pinned yet.

**The sql-assistant docblock asserted an invariant the code violated** — it
claimed one RPC with a task discriminator "because three copies of that
scaffolding is three places for one of them to go missing", while shipping three
RPCs, two timeout races and three retry loops. Same failure mode as the
`recordedCall` "choke point" comment the first review caught. Built the
consolidation the comment promised: one `runPrompt` (the only `binding.run`, the
only deadline) and one generic `attempt`, so the caps, the fence, the deadline
and both degrade arms exist exactly once.

**Two AI RPCs shipped with no client surface.** `aiTableFilter` and
`aiChartConfig` were admin-reachable but uninvokable — an unused inference and
cost surface. Wired both: a natural-language filter bar that lands STRUCTURED
clauses in the visible filter rows for review, and a chart suggestion that
overrides the heuristic axes. Both hidden without an AI binding.

Also fixed: uncapped, unfenced caller-controlled column names in the chart
prompt (the one input on that surface without a cap); prototype-chain lookup in
the admin read dispatch (`__lunora_admin__:toString` resolved to
`Object.prototype.toString` and was returned as an outcome); the bucket prune
firing ~0.08% of dispatches rather than once per window, which made the
documented row bound false; `datePrefixRange` remapping years 0-99 to 1900+ and
accepting 2026-02-31; double-quoted identifiers masked as string literals, which
made the linter report `unknown table WHERE` on valid SQL and cost the
autocomplete its aliases; "Fix this" repairing the live draft rather than the
statement that actually failed; three interfaces declared twice in admin.ts
(interface merging swallowed it); and eight dead exports.

Two weak tests replaced with real ones: the bucket-failure test used a healthy
handle and passed whether or not the try/catch existed, and the chart-egress
test asserted the absence of a string the fixture never contained.

Adds playground demo data so the new Studio surfaces are testable: a deliberately
wide 24-column `demoRecords` table (past the point where windowing and pinning
matter), 250 rows spread over six months so date-range search selects a real
subset, and foreign keys to `users`/`channels` so reverse-relation counts have
something to count. `now` is a mutation ARGUMENT — the advisor correctly rejected
`Date.now()` in a handler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@netlify

netlify Bot commented Jul 29, 2026

Copy link
Copy Markdown

Deploy Preview for lunorash ready!

Name Link
🔨 Latest commit bf3a62f
🔍 Latest deploy log https://app.netlify.com/projects/lunorash/deploys/6a6a6553146fb100084e8714
😎 Deploy Preview https://deploy-preview-229--lunorash.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions

Copy link
Copy Markdown
Contributor

Thank you for following the naming conventions! 🙏

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR adds deterministic playground seeding, schema snapshot history, admin analytics and AI RPCs, reverse-relation data browsing, SQL diagnostics and assistance, and a Studio operation console with recorded admin activity.

Changes

Platform and playground workflows

Layer / File(s) Summary
Schema snapshots and demo data
apps/playground/*, packages/codegen/src/*, packages/do/src/schema-history.ts, packages/do/src/ctx-db-migrations.ts
Adds deterministic demo records, generated schema snapshots, bounded per-shard schema history, and notification capability wiring.
Admin services and analytics
packages/do/src/back-relations.ts, packages/do/src/query-metrics.ts, packages/do/src/schema-history-reads.ts, packages/do/src/introspect.ts
Adds reverse-relation counts, query insight buckets, date-prefix search, and admin read routing.
AI and SQL services
packages/do/src/sql-assistant.ts, packages/do/src/sql-console.ts, packages/do/src/shard-do.ts
Adds grounded SQL, filter, and chart generation plus read-only SQL linting and admin dispatch.

Studio features

Layer / File(s) Summary
Data browser enhancements
packages/studio/src/features/data/*
Adds reverse-relation columns and counts, persistent pinning, horizontal column windowing, search highlighting, and AI filter suggestions.
SQL editor and reports
packages/studio/src/features/sql/*, packages/studio/src/features/reports/*, packages/studio/src/features/database/*
Adds SQL context analysis, diagnostics, AI assistance, inferred chart axes, query insight ranges, and schema-history visualization.
Operation console and Studio shell
packages/studio/src/lib/*, packages/studio/src/app/*, packages/studio/src/features/logs/*, packages/studio/src/features/schema/*
Records admin calls and subscriptions, exposes a searchable console, updates routing and navigation, and adds schema diagram controls.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • anolilab/lunora#188: Both modify schema snapshot generation and deterministic ordering.
  • anolilab/lunora#215: Both extend reserved admin RPC wiring and shard admin dispatch for new administrative endpoints.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description only provides a high-level summary; it is missing the template sections for linked issues, test plan, checklist, notes, and CLA. Add the required sections from the template, especially linked issues, test plan, checklist, reviewer notes, and the CLA statement.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is conventional-commit style and captures the main Studio feature bundle in the PR.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-studio-prisma-parity-plans

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Thank you for confirming the Contributor License Agreement! 🙏

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit bf3a62f.

Comment thread packages/studio/src/features/data/data-browser-grid.tsx Fixed
@codspeed-hq

codspeed-hq Bot commented Jul 29, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 97.54%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 250 untouched benchmarks
⏩ 2 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
rls(true): wrapper installs, no baseWhere merged 413.6 µs 209.4 µs +97.54%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing worktree-studio-prisma-parity-plans (bf3a62f) with alpha (1ee1e0a)2

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on alpha (b0cb8df) during the generation of this report, so 1ee1e0a was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (8)
packages/do/src/shard-do.ts (2)

6737-6749: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Successful aiTableFilter / aiChartConfig calls are not audited, unlike aiGenerateSql and explainIssue.

Both only record an audit entry on the degraded path, so a successful (billed) model invocation leaves no trace — the same spend/abuse accountability gap handleExplainIssue explicitly avoids by auditing both arms.

📝 Audit the success arm too
-        if (result.degraded && result.reason !== "no-ai-binding") {
+        if (!result.degraded) {
+            this.recordAudit("aiTableFilter", { detail: { clauses: result.clauses.length, table } });
+        } else if (result.reason !== "no-ai-binding") {
             this.recordAudit("aiTableFilter", { detail: { reason: result.reason, table } });
         }

Also applies to: 6759-6779

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/do/src/shard-do.ts` around lines 6737 - 6749, Update
handleAiTableFilter and the corresponding aiChartConfig handler to record an
audit entry for successful model invocations as well as degraded results,
matching the both-branch auditing behavior in handleExplainIssue and
aiGenerateSql. Preserve the existing degraded reason and table/config detail
while adding the success audit path.

6700-6706: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Ground the prompt with table names instead of COUNT(*) rows

listTables runs a separate row count for every internal-table-filtered table, but handleGenerateSql only uses table.name in the grounding schema. Use a name-only list from sqlite_master for this prompt-building path so AI query drafting does not count every table before the model is called.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/do/src/shard-do.ts` around lines 6700 - 6706, Update the
schema-building path in handleGenerateSql to obtain only table names from
sqlite_master instead of calling listTables, which performs per-table row
counts. Preserve the existing tableColumns lookup and schema shape, using the
name-only query results to ground generateSql without counting rows.
packages/do/src/back-relations.ts (1)

115-141: 🚀 Performance & Scalability | 🔵 Trivial

Each relation is an unindexed scan of the child table.

WHERE json_extract(__doc__, ?) IN (…) (and the physical-column form without a declared index) can't use an index, so a page with 8 enabled relations costs 8 full scans of the child tables — on the Studio's request path. Worth watching in the query-insights leaderboard, and possibly capping the child-table size or documenting that reverse counts are opt-in for this reason (the module comment already frames it as opt-in for fan-out, not scan cost).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/do/src/back-relations.ts` around lines 115 - 141, Update the
reverse-relation count flow around the relation query and its caller to avoid
issuing one full child-table scan per enabled relation on the Studio request
path. Add an explicit safeguard or opt-in policy for unindexed scans—such as
capping the child-table size or disabling reverse counts unless enabled—while
preserving indexed relation counts and the existing resolved-count behavior.
packages/studio/src/lib/operation-log.ts (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated ADMIN_PREFIX constant across two files. Both files independently declare the identical literal "__lunora_admin__:", risking drift if the reserved prefix ever changes.

  • packages/studio/src/lib/operation-log.ts#L123-125: import the prefix from a single shared module instead of redeclaring it here.
  • packages/studio/src/lib/recording-client.ts#L28-30: import the same shared constant instead of its own local declaration.

As per path instructions for packages/**/*.ts, "Follow DRY principles."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/studio/src/lib/operation-log.ts` at line 1, Consolidate the
duplicated ADMIN_PREFIX declarations in operation-log.ts and recording-client.ts
by defining the literal once in a shared module, then import and reuse that
exported constant in both files. Remove each local declaration while preserving
existing prefix usage.

Source: Path instructions

packages/studio/src/features/data/data-browser-grid.tsx (1)

899-911: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate getVisibleLeafColumns() mapping.

visibleColumns is computed for columnWindow, then the same {getSize, id} shape is recomputed from scratch for pinnedOffsets a few lines later. Reuse visibleColumns for both calls.

♻️ Proposed fix
     const visibleColumns = table.getVisibleLeafColumns().map((column) => {
         return { getSize: () => column.getSize(), id: column.id };
     });
     const columnSlice = columnWindow(visibleColumns, pinnedColumns, scrollLeft, viewportWidth);

     // Derived from the VISIBLE column order each render, so reordering or hiding a
     // pinned column re-flows the rest instead of leaving a gap.
-    const pinOffsets = pinnedOffsets(
-        table.getVisibleLeafColumns().map((column) => {
-            return { getSize: () => column.getSize(), id: column.id };
-        }),
-        pinnedColumns,
-    );
+    const pinOffsets = pinnedOffsets(visibleColumns, pinnedColumns);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/studio/src/features/data/data-browser-grid.tsx` around lines 899 -
911, Reuse the existing visibleColumns collection when calling pinnedOffsets
instead of mapping table.getVisibleLeafColumns() a second time. Keep the current
derived visible-column ordering and pinnedColumns arguments unchanged.
packages/studio/src/features/data/data-browser.tsx (2)

394-416: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

AI filter suggestion fails silently on error.

apply() has no try/catch, so a rejected assistant.suggestFilter call is swallowed by fireAndForget's default no-op onError. Unlike the rest of this file (writes surface failures via writeError), a failed "Suggest" click gives the operator no feedback at all — the button just does nothing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/studio/src/features/data/data-browser.tsx` around lines 394 - 416,
Update the apply function inside askAiFilter to catch rejected
assistant.suggestFilter calls and surface them through the existing writeError
mechanism used elsewhere in the file. Keep the successful suggestion mapping and
onFiltersChange flow unchanged, and ensure fireAndForget receives the handled
operation.

425-435: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate row-id extraction instead of the shared rowId helper.

pageIds reimplements row._id ?? row.id + string-check inline, duplicating logic already centralized in the exported rowId() helper (used elsewhere for getRowId, renderRow, etc.). If rowId()'s handling ever changes (e.g. coercing non-string ids), this inline copy will silently diverge and back-relation counts could stop matching rows.

♻️ Suggested fix
+import { rowId } from "./data-browser-grid";
...
     const pageIds = useMemo(
         () =>
             (page?.rows ?? [])
-                .map((row) => {
-                    const id = row._id ?? row.id;
-
-                    return typeof id === "string" ? id : "";
-                })
-                .filter((id) => id !== ""),
+                .map((row) => rowId(row) ?? "")
+                .filter((id) => id !== ""),
         [page],
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/studio/src/features/data/data-browser.tsx` around lines 425 - 435,
Update the pageIds useMemo to derive each row’s identifier through the shared
rowId() helper instead of duplicating row._id ?? row.id and the inline string
check. Preserve filtering of invalid or empty identifiers, and keep the existing
dependency and pageIds behavior unchanged.
packages/studio/src/features/data/grid-features.tsx (1)

292-308: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse backRelationKey instead of reconstructing the key format inline.

This duplicates the ${table}.${column} format already centralized in backRelationKey() (back-relations.ts). Currently in sync, but a future change to that helper's format would silently desync checkbox state here.

♻️ Proposed fix
+import { backRelationKey } from "./back-relations";
...
         {backRelations.map((relation) => {
-            const key = `${relation.table}.${relation.column}`;
+            const key = backRelationKey(relation);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/studio/src/features/data/grid-features.tsx` around lines 292 - 308,
Update the backRelations mapping in the grid feature to use the existing
backRelationKey helper for the relation key instead of reconstructing
`${relation.table}.${relation.column}` inline. Use that helper-derived key
consistently for checkbox state, test IDs, React keys, toggle callbacks, and
displayed relation text only where appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/playground/lunora/demo.ts`:
- Around line 34-36: Update the mutation around the target calculation to
validate the provided count with Number.isInteger before deriving target,
rejecting fractional seed counts while preserving the existing default and range
clamping for valid values.
- Line 1: Update the handwritten import of internalMutation and v in demo.ts to
remove the “.js” extension, preserving the existing module path and imported
symbols.

In `@apps/playground/scripts/seed.mjs`:
- Around line 16-18: Replace the Date.now() assignment in the seed script with a
documented fixed demo epoch, while allowing an explicit --now override when
provided. Keep the timestamp computed before the mutation so replayed mutations
remain deterministic, and preserve the documented 2026-07 search behavior by
default.

In `@packages/do/src/query-metrics.ts`:
- Around line 146-151: Replace the module-scoped lastPrunedBucket state with
per-SQL-handle bookkeeping, using the sql handle as the key so each Durable
Object shard independently tracks its most recently pruned bucket. Update the
pruning logic and all related references, including the additional metrics-write
path, while preserving the once-per-window behavior for each handle.

In `@packages/do/src/schema-history-reads.ts`:
- Around line 63-68: Validate each element of args.relations in
backRelationCounts before passing it to readBackRelationCounts, retaining only
entries whose table and column fields are strings. Exclude malformed values such
as numeric columns so columnExpression cannot throw, while preserving the
existing behavior for valid relations.
- Line 30: Update stringArgument to store args[name] in a local variable before
the conditional, then return that narrowed local when it is a string and return
the empty-string fallback otherwise. Do not perform a second indexed lookup in
the true branch.

In `@packages/do/src/sql-console.ts`:
- Around line 91-92: Update PLAN_SCAN and the scan-warning handling to recognize
only actual table scans, excluding entries such as “SCAN CONSTANT ROW” and “SCAN
SUBQUERY 1”. In the relevant linting flow, deduplicate warnings for repeated
scans of the same table so each table produces at most one full-table-scan
warning.

In `@packages/studio/src/components/result-chart.tsx`:
- Around line 33-40: The SqlResultChart component currently ignores the inferred
chart kind and always renders the bar variant. Preserve the existing validated
axes and column-selection logic, then use axes.kind when choosing the chart
container and matching series element so "area", "line", and "bar" render
through their respective chart variants, with bar remaining the fallback when
kind is absent.

In `@packages/studio/src/features/data/data-browser.tsx`:
- Around line 66-67: Update the documentation for the initialPins prop in the
data browser props definition to describe the corrected precedence: persisted
storage wins, while the URL value is used only as an initial seed when no stored
pins exist. Remove the claim that the URL overrides the per-browser default.
- Around line 420-436: Expose the debounced shard value from the useDataBrowser
return object, then pass that value to useBackRelations instead of the raw
shardKey in the backRelationCounts setup. Keep shardKey available for existing
consumers while ensuring back-relation queries only react after debouncing.

In `@packages/studio/src/features/logs/operation-console.tsx`:
- Around line 145-153: Update the Errors filter button in the operation console,
identified by data-testid="oc-filter-errors", to include an aria-pressed
attribute bound to whether shownFilter equals "errors", matching its existing
visual active state and toggle behavior.

In `@packages/studio/src/features/sql/hooks/use-sql-assistant.ts`:
- Around line 53-153: Split the shared pending/reason state in useSqlAssistant
so generate, suggestFilter, and inferChart each maintain and expose their own
operation-specific status fields. Return the corresponding
generatePending/generateReason, filterPending/filterReason, and
chartPending/chartReason values, then update sql-assistant-bar.tsx to use the
SQL fields and the Suggest chart control in sql-editor-panel.tsx to use the
chart fields, preserving existing unavailable handling.

In `@packages/studio/src/features/sql/sql-editor-panel.tsx`:
- Around line 183-208: Move failedRun and inferredChart from panel-wide state
into the per-tab outputs/TabOutput data keyed by activeTab.id, and update run(),
SqlAssistantBar, and chart rendering to read and write the active tab’s values.
Clear or replace both values on every run success or failure, and prune them
when commitTabs closes tabs, matching the existing outputs lifecycle so tab
switches cannot reuse another tab’s failure or chart.

---

Nitpick comments:
In `@packages/do/src/back-relations.ts`:
- Around line 115-141: Update the reverse-relation count flow around the
relation query and its caller to avoid issuing one full child-table scan per
enabled relation on the Studio request path. Add an explicit safeguard or opt-in
policy for unindexed scans—such as capping the child-table size or disabling
reverse counts unless enabled—while preserving indexed relation counts and the
existing resolved-count behavior.

In `@packages/do/src/shard-do.ts`:
- Around line 6737-6749: Update handleAiTableFilter and the corresponding
aiChartConfig handler to record an audit entry for successful model invocations
as well as degraded results, matching the both-branch auditing behavior in
handleExplainIssue and aiGenerateSql. Preserve the existing degraded reason and
table/config detail while adding the success audit path.
- Around line 6700-6706: Update the schema-building path in handleGenerateSql to
obtain only table names from sqlite_master instead of calling listTables, which
performs per-table row counts. Preserve the existing tableColumns lookup and
schema shape, using the name-only query results to ground generateSql without
counting rows.

In `@packages/studio/src/features/data/data-browser-grid.tsx`:
- Around line 899-911: Reuse the existing visibleColumns collection when calling
pinnedOffsets instead of mapping table.getVisibleLeafColumns() a second time.
Keep the current derived visible-column ordering and pinnedColumns arguments
unchanged.

In `@packages/studio/src/features/data/data-browser.tsx`:
- Around line 394-416: Update the apply function inside askAiFilter to catch
rejected assistant.suggestFilter calls and surface them through the existing
writeError mechanism used elsewhere in the file. Keep the successful suggestion
mapping and onFiltersChange flow unchanged, and ensure fireAndForget receives
the handled operation.
- Around line 425-435: Update the pageIds useMemo to derive each row’s
identifier through the shared rowId() helper instead of duplicating row._id ??
row.id and the inline string check. Preserve filtering of invalid or empty
identifiers, and keep the existing dependency and pageIds behavior unchanged.

In `@packages/studio/src/features/data/grid-features.tsx`:
- Around line 292-308: Update the backRelations mapping in the grid feature to
use the existing backRelationKey helper for the relation key instead of
reconstructing `${relation.table}.${relation.column}` inline. Use that
helper-derived key consistently for checkbox state, test IDs, React keys, toggle
callbacks, and displayed relation text only where appropriate.

In `@packages/studio/src/lib/operation-log.ts`:
- Line 1: Consolidate the duplicated ADMIN_PREFIX declarations in
operation-log.ts and recording-client.ts by defining the literal once in a
shared module, then import and reuse that exported constant in both files.
Remove each local declaration while preserving existing prefix usage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 40d1218e-2d40-4d38-afce-979bd6254fc2

📥 Commits

Reviewing files that changed from the base of the PR and between 2e35427 and e419215.

⛔ Files ignored due to path filters (39)
  • api-snapshots/codegen.api.md is excluded by none and included by none
  • api-snapshots/do.api.md is excluded by none and included by none
  • api-snapshots/studio.api.md is excluded by none and included by none
  • examples/auth-playground/lunora/_generated/shard.ts is excluded by !**/_generated/** and included by none
  • examples/blog/lunora/_generated/shard.ts is excluded by !**/_generated/** and included by none
  • examples/expo/lunora/_generated/shard.ts is excluded by !**/_generated/** and included by none
  • examples/notify-demo/lunora/_generated/shard.ts is excluded by !**/_generated/** and included by none
  • examples/offline-rejections/lunora/_generated/shard.ts is excluded by !**/_generated/** and included by none
  • examples/payment-demo/lunora/_generated/shard.ts is excluded by !**/_generated/** and included by none
  • examples/realtime-cursors/lunora/_generated/shard.ts is excluded by !**/_generated/** and included by none
  • examples/todo-app/lunora/_generated/shard.ts is excluded by !**/_generated/** and included by none
  • packages/codegen/__tests__/fixtures/simple/expected/_generated/shard.ts is excluded by !**/_generated/**, !**/__tests__/** and included by packages/**
  • packages/codegen/__tests__/schema-drift.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/do/__tests__/back-relations.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/do/__tests__/introspect.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/do/__tests__/query-metrics.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/do/__tests__/schema-history.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/do/__tests__/sql-assistant.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/do/__tests__/sql-console.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/studio/__tests__/components/operation-console-wiring.test.tsx is excluded by !**/__tests__/** and included by packages/**
  • packages/studio/__tests__/features/data/back-relations.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/studio/__tests__/features/data/column-window.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/studio/__tests__/features/data/highlight-segments.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/studio/__tests__/features/database/schema-diff-model.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/studio/__tests__/features/database/schema-history.test.tsx is excluded by !**/__tests__/** and included by packages/**
  • packages/studio/__tests__/features/schema/schema-diagram.test.tsx is excluded by !**/__tests__/** and included by packages/**
  • packages/studio/__tests__/features/sql/sql-diagnostics.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/studio/__tests__/features/sql/use-sql-diagnostics.test.tsx is excluded by !**/__tests__/** and included by packages/**
  • packages/studio/__tests__/lib/operation-log.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/studio/__tests__/setup-reactflow.ts is excluded by !**/__tests__/** and included by packages/**
  • plans/200-studio-migration-visualizer.md is excluded by none and included by none
  • plans/201-studio-sql-diagnostics.md is excluded by none and included by none
  • plans/202-studio-ai-layer.md is excluded by none and included by none
  • plans/203-query-insights-time-series.md is excluded by none and included by none
  • plans/204-studio-operation-console.md is excluded by none and included by none
  • plans/205-studio-grid-parity.md is excluded by none and included by none
  • plans/README.md is excluded by none and included by none
  • shared/schema-snapshot.ts is excluded by none and included by none
  • shared/sql-readonly.ts is excluded by none and included by none
📒 Files selected for processing (55)
  • apps/playground/lunora/demo.ts
  • apps/playground/lunora/schema.ts
  • apps/playground/package.json
  • apps/playground/scripts/seed.mjs
  • packages/codegen/src/emit.ts
  • packages/codegen/src/index.ts
  • packages/codegen/src/run-codegen.ts
  • packages/codegen/src/schema-drift.ts
  • packages/do/src/back-relations.ts
  • packages/do/src/ctx-db-migrations.ts
  • packages/do/src/introspect.ts
  • packages/do/src/query-metrics.ts
  • packages/do/src/schema-history-reads.ts
  • packages/do/src/schema-history.ts
  • packages/do/src/shard-do.ts
  • packages/do/src/sql-assistant.ts
  • packages/do/src/sql-console.ts
  • packages/studio/src/app/app.tsx
  • packages/studio/src/app/studio.tsx
  • packages/studio/src/components/error-alert.tsx
  • packages/studio/src/components/live-status.tsx
  • packages/studio/src/components/operation-console-provider.tsx
  • packages/studio/src/components/result-chart.tsx
  • packages/studio/src/features/data/back-relations.ts
  • packages/studio/src/features/data/data-browser-grid.tsx
  • packages/studio/src/features/data/data-browser.tsx
  • packages/studio/src/features/data/data-filters.tsx
  • packages/studio/src/features/data/data-grid.tsx
  • packages/studio/src/features/data/grid-features.tsx
  • packages/studio/src/features/data/hooks/use-back-relations.ts
  • packages/studio/src/features/data/hooks/use-data-browser.tsx
  • packages/studio/src/features/data/table-editor.tsx
  • packages/studio/src/features/database/migrations-route.tsx
  • packages/studio/src/features/database/schema-diff-model.ts
  • packages/studio/src/features/database/schema-history.tsx
  • packages/studio/src/features/logs/operation-console.tsx
  • packages/studio/src/features/reports/metrics-panel.tsx
  • packages/studio/src/features/reports/query-insights-range.tsx
  • packages/studio/src/features/reports/query-insights.tsx
  • packages/studio/src/features/schema/schema-diagram.tsx
  • packages/studio/src/features/sql/hooks/use-sql-assistant.ts
  • packages/studio/src/features/sql/hooks/use-sql-diagnostics.ts
  • packages/studio/src/features/sql/sql-assistant-bar.tsx
  • packages/studio/src/features/sql/sql-autocomplete.ts
  • packages/studio/src/features/sql/sql-context.ts
  • packages/studio/src/features/sql/sql-diagnostics-ui.tsx
  • packages/studio/src/features/sql/sql-diagnostics.ts
  • packages/studio/src/features/sql/sql-editor-panel.tsx
  • packages/studio/src/hooks/use-admin-query.ts
  • packages/studio/src/lib/admin.ts
  • packages/studio/src/lib/data-view-params.ts
  • packages/studio/src/lib/operation-log.ts
  • packages/studio/src/lib/recording-client.ts
  • packages/studio/src/locales/en.ts
  • packages/studio/vitest.config.ts

Comment thread apps/playground/lunora/demo.ts Outdated
Comment thread apps/playground/lunora/demo.ts Outdated
Comment thread apps/playground/scripts/seed.mjs Outdated
Comment thread packages/do/src/query-metrics.ts Outdated
Comment thread packages/do/src/schema-history-reads.ts
Comment thread packages/studio/src/features/data/data-browser.tsx
Comment thread packages/studio/src/features/data/data-browser.tsx Outdated
Comment thread packages/studio/src/features/logs/operation-console.tsx
Comment thread packages/studio/src/features/sql/hooks/use-sql-assistant.ts Outdated
Comment thread packages/studio/src/features/sql/sql-editor-panel.tsx
@prisis prisis changed the title feat(studio): Prisma Studio parity — schema history, SQL diagnostics, AI assistant, query insights, operation console, grid parity feat(studio): schema history, SQL diagnostics, AI assistant, query insights, operation console, grid parity Jul 29, 2026
Per-task AI assistant status, per-tab editor output, and the review's
correctness/perf findings across the studio grid and the DO admin reads.

- assistant status is keyed by task, so a chart inference no longer spins
  the SQL bar or prints its error there
- failedRun/inferredChart move into TabOutput: a failure in one tab no
  longer arms "Fix this" in another against a statement it never ran
- back-relation counts key on the DEBOUNCED shard, matching the page the
  ids came from
- callback ref returns its cleanup instead of parking a teardown in a ref
- aria-pressed on the console's Errors toggle
- Set lookups in the chart axis scan; single-pass filters in the console
  tape and the page-id derivation
- pure helpers split out of component modules (column-window,
  highlight-segments, editor-spans) so Fast Refresh survives
- playground seed accepts LUNORA_SEED_NOW for a reproducible epoch, and
  the demo row count is floored

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
react-doctor deletes and re-posts its PR comments on every run, so a resolved
thread comes back. `react-doctor-disable-next-line <rule> -- <reason>` is the
tool's own mechanism (it names it in `react-doctor why`) and is respected by
default, so the nine deliberate sites now carry the reason in the code instead.

Every one is load-bearing identity or a verified false positive: the memos
feeding react-table's `columns` and SchemaDiagram's identity-keyed re-seed
effects, the scroll ref callback, the two exhaustive-deps hits that list
locals rather than the objects they came from, and the callback ref that does
return its cleanup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
prisis and others added 6 commits July 29, 2026 17:05
Sweep of packages/studio, errors first.

Fixed, not suppressed:
- 31 render-phase ref writes (`refs` / `no-ref-current-in-render`) moved into
  mirror effects, or replaced by a lazy `useState` where the ref was a
  write-then-read-during-render seed. React may render without committing, so a
  render-phase write can publish a value that never became the UI.
- 24 `try/catch/finally` blocks flattened. React Compiler cannot lower
  `finally`, and one unsupported statement bails the WHOLE component out of
  auto-memoization — which this package relies on. Safe only where the catch
  swallows and the try has no early return; the other 7 keep their `finally`.
- `??=` in the health fan-out, the sidebar's per-render keydown re-registration
  (now an effect event), and the flags panel's unpreservable memo.

Suppressed with a recorded reason where the finding is a false positive or the
fix would cost correctness: async-load and subscription effects, the two
compiler-incompatible TanStack call sites, and the `try` blocks whose cleanup
has to run on the throw path.

0 error-severity findings remain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removes 30 of the 47 `react-compiler-no-manual-memoization` findings by
deleting the wrapper (11 `useCallback`s the compiler already caches) or
hoisting the body to a module-scope pure function (19 `useMemo`s), which also
clears the matching prefer-module-scope-pure-function findings.

`useFileItem` became `fileItemBindings`: with its four `useCallback`s gone it
calls no hooks, so the `use` prefix was a lie.

The 9 that stay are the ones where identity is behaviour, each with the reason
in the code: the LunoraClient, the values effects key on, the arrays react-table
and react-flow reset from, and the nav-label map the document-title effect
depends on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- `.map().filter(Boolean)` -> `flatMap` in the cascade preview and the FK-pool
  loader; besides the extra walk, `filter(Boolean)` never narrowed the type.
- Quadratic `includes` inside the RLS and permissions fold loops -> Set lookups.
- The SQL autocomplete's keyword scan is one pass; it runs per keystroke.
- 8 pure helpers rebuilt per render moved to module scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…board-operable

The row-form fields and the grid's inline cell editor had no accessible name —
a screen reader announced five identical "edit text" controls. Each now carries
its column name.

The chart legend filtered a series on click with no keyboard path at all; it now
answers Enter/Space, exposes `role="button"`/`tabIndex` while interactive, and
reports its pressed state.

The three schema-editor selects are labelled by an adjacent `<Label htmlFor>`
the rule cannot see — suppressed with that reason. Note the suppression there
had to be a JSX comment: `//` inside JSX children is literal text, which broke
the parse (and silently dropped 38 tests) until it was fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…, Set lookups

The insights panel re-registered its visibilitychange listener on every render
(five churning values in the deps); it is an effect event now, bound once.
`useState(formatCell(...))` re-ran the format on every render and threw it away.
KV bulk selection asked `bulk.includes` once per rendered row.

The rest of the small findings are suppressed with the reason in the code:
deliberately sequential awaits, the controlled/uncontrolled brush sync, the
scroll-sync effect, and the two panels whose independent query results would be
serialised by a reducer.

Note on directive placement: react-doctor and eslint both require their comment
adjacent to the offending line, so where they overlap eslint uses its block
form. Inside JSX children the comment must be {/* ... */} — a // line renders as
visible text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ding

Closes the sweep: packages/studio now reports 0 react-doctor diagnostics, down
from 232.

The last 44 are suppressed in place with the reason in the code, because the
fix is a refactor rather than a lint change:

- only-export-components (20 files) wants every panel split from the helpers and
  types it owns, purely so Fast Refresh keeps state during dev.
- no-giant-component (10) wants ten panels decomposed — real work with its own
  review, not something to do blind inside an unrelated change.
- js-combine-iterations (9) is two passes over bounded lists (chart series, log
  levels, nav tabs), where the one-pass rewrite reads worse.
- no-array-index-as-key (4) is positional rows with no domain id.
- rerender-memo-before-early-return: the suggested fix would make a hook
  conditional.

Both linters demand their directive adjacent to the offending line, so where
they overlap eslint uses its block form; in JSX children the comment is
{/* … */}, and inside an attribute list it is //.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…utomatically

`pnpm run seed` in the playground could never work: it shelled `lunora run` at
wrangler's 8787 while Vite serves on 5173+ (and bumps when taken), passed its
args positionally instead of via --args, targeted an internalMutation the public
RPC refuses to dispatch, and would have been rejected by the app's authorizeShard
anyway. Deleted, along with the demo:seedDemo mutation it called — `lunora seed`
is the supported path and goes through the admin endpoint that clears all four.

Making that path actually usable took four fixes:

- **URL**: with no --url, admin commands now read the running dev server's
  recorded URL from .lunora/dev.json (the record `lunora status`/`stop` and the
  MCP server already keep) instead of guessing 8787. Only when live — the reader
  drops a record whose pid is gone.
- **Token**: resolved from .dev.vars when --token and LUNORA_ADMIN_TOKEN are
  absent, so a local run needs no exports. Loopback targets only: a dev secret
  must not leave the machine because a command was pointed at prod.
- **Import gate**: /_lunora/admin/import demanded a `queryCoordinator` it never
  used — `streamingImport` ignores it — which failed every builder-produced app,
  since the builder cannot configure one. Now admin-gated without the phantom
  requirement.
- **Timestamps**: `createdAt: v.number()` seeded as `641`, because the name
  heuristics were string-only. A number column whose last word is at/date/time/…
  now gets epoch-ms spread over six months, which is what makes date filters and
  the studio's YYYY-MM search demoable. Matching is on the last word, so
  `format`, `timeout`, `candidateId` and `latitude` stay numbers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`lunora run` sent no identity and no bearer, so every app that configures
`authorizeShard` — the posture the docs recommend — answered FORBIDDEN_SHARD.
The command looked general-purpose and was in practice unusable against any
non-trivial app.

`--as <userId>` dispatches through the existing admin-gated `__lunora_admin__:runAs`
op instead of calling the function directly: it forges the named identity for
that one dispatch, so the function and any RLS middleware observe that user.
That op already refuses to target reserved admin paths and records an audit
entry, and the runtime already exempts single-shard admin envelopes from the
tenant gate, so the call reaches the DO's own bearer check. `--claims` adds
extra identity claims alongside the user id.

The bearer resolves from --token, then LUNORA_ADMIN_TOKEN, then .dev.vars
(loopback only), so against your own dev server this needs no flags. A reserved
admin path now carries the bearer too, and a plain call that gets FORBIDDEN_SHARD
prints what to do about it instead of leaving a bare 403.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
prisis and others added 2 commits July 29, 2026 21:18
plans/README.md's own rule: completed plans are removed once shipped, with the
record living in the tables and git history. All six Wave 15 plans (200-205)
shipped in this branch, so the files go and their status rows carry the outcome.

Two decisions were carried into the README rather than lost with the files:
202's product decision (the model runs server-side on the app's own Workers AI
binding; no key in the browser, no row values off the machine), and 205's
infinite-scroll decision (against — pagination gives a stable position, an exact
count, and export semantics that mean "these rows").

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ocomplete regression

Two blockers and a live regression, from the parallel bug/security and
maintainability passes.

FIXED — correctness:
- Deleting `useSqlSchema`'s memo broke Escape in the SQL editor. `schema` is a
  dep of the autocomplete's `refresh`, so a fresh object per render re-fired the
  probe effect and the popover reopened on the render that closing it caused.
  Restored, with a hook-level test that fails without it. The neighbouring
  `useCallback` already documented this exact reasoning.
- `@lunora/seed` promised determinism it no longer delivered: `generateValue`
  took `now = Date.now()` and nothing passed it. `now` is threaded from
  `SeedOptions` with NO default, exposed as `lunora seed --now` (replacing the
  `LUNORA_SEED_NOW` the deleted script had), and covered by a reproducibility
  test. Verified byte-identical across runs.
- `lunora run --as` sent a full-access bearer without the https gate every other
  bearer-carrying command uses, and inherited a `lunora link` prod target with no
  `--prod` — so a forged-identity mutation could hit production unflagged. Both
  closed; a test asserts the cleartext refusal sends nothing.
- `export`/`import` evaluated the `.dev.vars` loopback gate against the raw
  `--url` while sending to the resolved target. Resolved target first now.
- `*Time` columns (`responseTime`, `loadTime`) are durations; they no longer
  seed as epoch-ms.
- `evil-brush`'s state updater is pure — the previous range comes from the ref
  that already tracks it, so no ref write inside an updater React may re-run.

FIXED — honesty and structure:
- 25 hoisted helpers had been spliced BETWEEN a docblock and the symbol it
  documents. Moved above.
- `no-giant-component` claimed it was "tracked separately" when nothing tracked
  it. Each now states its line count and points at the Wave 15 deferral.
- One `js-combine-iterations` reason was pasted across 9 sites naming collections
  four of them do not touch. Rewritten per site.
- `only-export-components` moves from 20 copies to one `doctor.config.mjs` entry,
  the same call `eslint.config.js` already makes for the compiler-redundant rules.
- `useMirroredRef` replaces six hand-written ref+effect pairs, so the ordering
  contract cannot drift, and documents that it holds within one component only.
- `toSchema` identity wrapper deleted; parallel dedup maps collapsed onto the
  Set the record already had; `toPageArgs`/`buildTabs` take options objects;
  `buildRequestHeaders` inlined and the target/envelope seams extracted honestly;
  the shard-denial hint reads the parsed error code, not a substring.
- `demoRecords` is `.externallyManaged()` — it is seeded through the admin import
  endpoint, so the insert-path advisory was noise (same as `rateLimits`/`users`).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant