feat(studio): schema history, SQL diagnostics, AI assistant, query insights, operation console, grid parity - #229
Conversation
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>
✅ Deploy Preview for lunorash ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Thank you for following the naming conventions! 🙏 |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis 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. ChangesPlatform and playground workflows
Studio features
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
Thank you for confirming the Contributor License Agreement! 🙏 |
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
Merging this PR will improve performance by 97.54%
|
| 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
Footnotes
-
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. ↩
-
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. ↩
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (8)
packages/do/src/shard-do.ts (2)
6737-6749: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winSuccessful
aiTableFilter/aiChartConfigcalls are not audited, unlikeaiGenerateSqlandexplainIssue.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
handleExplainIssueexplicitly 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 winGround the prompt with table names instead of
COUNT(*)rows
listTablesruns a separate row count for every internal-table-filtered table, buthandleGenerateSqlonly usestable.namein the grounding schema. Use a name-only list fromsqlite_masterfor 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 | 🔵 TrivialEach 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 winDuplicated
ADMIN_PREFIXconstant 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 winDuplicate
getVisibleLeafColumns()mapping.
visibleColumnsis computed forcolumnWindow, then the same{getSize, id}shape is recomputed from scratch forpinnedOffsetsa few lines later. ReusevisibleColumnsfor 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 winAI filter suggestion fails silently on error.
apply()has no try/catch, so a rejectedassistant.suggestFiltercall is swallowed byfireAndForget's default no-oponError. Unlike the rest of this file (writes surface failures viawriteError), 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 winDuplicate row-id extraction instead of the shared
rowIdhelper.
pageIdsreimplementsrow._id ?? row.id+ string-check inline, duplicating logic already centralized in the exportedrowId()helper (used elsewhere forgetRowId,renderRow, etc.). IfrowId()'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 winReuse
backRelationKeyinstead of reconstructing the key format inline.This duplicates the
${table}.${column}format already centralized inbackRelationKey()(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
⛔ Files ignored due to path filters (39)
api-snapshots/codegen.api.mdis excluded by none and included by noneapi-snapshots/do.api.mdis excluded by none and included by noneapi-snapshots/studio.api.mdis excluded by none and included by noneexamples/auth-playground/lunora/_generated/shard.tsis excluded by!**/_generated/**and included by noneexamples/blog/lunora/_generated/shard.tsis excluded by!**/_generated/**and included by noneexamples/expo/lunora/_generated/shard.tsis excluded by!**/_generated/**and included by noneexamples/notify-demo/lunora/_generated/shard.tsis excluded by!**/_generated/**and included by noneexamples/offline-rejections/lunora/_generated/shard.tsis excluded by!**/_generated/**and included by noneexamples/payment-demo/lunora/_generated/shard.tsis excluded by!**/_generated/**and included by noneexamples/realtime-cursors/lunora/_generated/shard.tsis excluded by!**/_generated/**and included by noneexamples/todo-app/lunora/_generated/shard.tsis excluded by!**/_generated/**and included by nonepackages/codegen/__tests__/fixtures/simple/expected/_generated/shard.tsis excluded by!**/_generated/**,!**/__tests__/**and included bypackages/**packages/codegen/__tests__/schema-drift.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/do/__tests__/back-relations.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/do/__tests__/introspect.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/do/__tests__/query-metrics.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/do/__tests__/schema-history.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/do/__tests__/sql-assistant.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/do/__tests__/sql-console.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/studio/__tests__/components/operation-console-wiring.test.tsxis excluded by!**/__tests__/**and included bypackages/**packages/studio/__tests__/features/data/back-relations.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/studio/__tests__/features/data/column-window.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/studio/__tests__/features/data/highlight-segments.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/studio/__tests__/features/database/schema-diff-model.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/studio/__tests__/features/database/schema-history.test.tsxis excluded by!**/__tests__/**and included bypackages/**packages/studio/__tests__/features/schema/schema-diagram.test.tsxis excluded by!**/__tests__/**and included bypackages/**packages/studio/__tests__/features/sql/sql-diagnostics.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/studio/__tests__/features/sql/use-sql-diagnostics.test.tsxis excluded by!**/__tests__/**and included bypackages/**packages/studio/__tests__/lib/operation-log.test.tsis excluded by!**/__tests__/**,!**/*.test.tsand included bypackages/**packages/studio/__tests__/setup-reactflow.tsis excluded by!**/__tests__/**and included bypackages/**plans/200-studio-migration-visualizer.mdis excluded by none and included by noneplans/201-studio-sql-diagnostics.mdis excluded by none and included by noneplans/202-studio-ai-layer.mdis excluded by none and included by noneplans/203-query-insights-time-series.mdis excluded by none and included by noneplans/204-studio-operation-console.mdis excluded by none and included by noneplans/205-studio-grid-parity.mdis excluded by none and included by noneplans/README.mdis excluded by none and included by noneshared/schema-snapshot.tsis excluded by none and included by noneshared/sql-readonly.tsis excluded by none and included by none
📒 Files selected for processing (55)
apps/playground/lunora/demo.tsapps/playground/lunora/schema.tsapps/playground/package.jsonapps/playground/scripts/seed.mjspackages/codegen/src/emit.tspackages/codegen/src/index.tspackages/codegen/src/run-codegen.tspackages/codegen/src/schema-drift.tspackages/do/src/back-relations.tspackages/do/src/ctx-db-migrations.tspackages/do/src/introspect.tspackages/do/src/query-metrics.tspackages/do/src/schema-history-reads.tspackages/do/src/schema-history.tspackages/do/src/shard-do.tspackages/do/src/sql-assistant.tspackages/do/src/sql-console.tspackages/studio/src/app/app.tsxpackages/studio/src/app/studio.tsxpackages/studio/src/components/error-alert.tsxpackages/studio/src/components/live-status.tsxpackages/studio/src/components/operation-console-provider.tsxpackages/studio/src/components/result-chart.tsxpackages/studio/src/features/data/back-relations.tspackages/studio/src/features/data/data-browser-grid.tsxpackages/studio/src/features/data/data-browser.tsxpackages/studio/src/features/data/data-filters.tsxpackages/studio/src/features/data/data-grid.tsxpackages/studio/src/features/data/grid-features.tsxpackages/studio/src/features/data/hooks/use-back-relations.tspackages/studio/src/features/data/hooks/use-data-browser.tsxpackages/studio/src/features/data/table-editor.tsxpackages/studio/src/features/database/migrations-route.tsxpackages/studio/src/features/database/schema-diff-model.tspackages/studio/src/features/database/schema-history.tsxpackages/studio/src/features/logs/operation-console.tsxpackages/studio/src/features/reports/metrics-panel.tsxpackages/studio/src/features/reports/query-insights-range.tsxpackages/studio/src/features/reports/query-insights.tsxpackages/studio/src/features/schema/schema-diagram.tsxpackages/studio/src/features/sql/hooks/use-sql-assistant.tspackages/studio/src/features/sql/hooks/use-sql-diagnostics.tspackages/studio/src/features/sql/sql-assistant-bar.tsxpackages/studio/src/features/sql/sql-autocomplete.tspackages/studio/src/features/sql/sql-context.tspackages/studio/src/features/sql/sql-diagnostics-ui.tsxpackages/studio/src/features/sql/sql-diagnostics.tspackages/studio/src/features/sql/sql-editor-panel.tsxpackages/studio/src/hooks/use-admin-query.tspackages/studio/src/lib/admin.tspackages/studio/src/lib/data-view-params.tspackages/studio/src/lib/operation-log.tspackages/studio/src/lib/recording-client.tspackages/studio/src/locales/en.tspackages/studio/vitest.config.ts
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>
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>
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>
Closes the Prisma Studio parity gap identified in
plans/README.mdWave 15. Six plans (200–205), grounded against prisma/studio's source and its normativeArchitecture/*.mddocs rather than its README.What ships
__lunora_schema_historyledger, a timeline, and a visual diff on the existing React Flow canvasDesign decisions worth reviewing
The diff engine moved to
shared/. The Studio's schema history andlunora 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 ascopediscriminant 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/doenforces with, so the warning while you type and the rejection on Run cannot disagree.AI output is never privileged. Generated SQL passes the same
classifyStatementgate 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:[]deps never retried. Fixed with a callback ref..tsfile parses as JSX under packem's Babel. Onlybuild:packagescatches this.Testing it
No
--url, no--token, no sign-in: the seeder reads the running dev server'sURL from
.lunora/dev.jsonand the admin bearer from.dev.vars, seeds the FKparents (
users,channels) first, and wires the refs to real ids.--seed 7makes it reproducible.
That gives you 250 rows across 24 columns with
createdAt/updatedAtspreadover the last six months — enough for pinning, column windowing, match
highlighting,
2026-07-style date search, and reverse-relation counts.Gate
do1364 ·codegen922 ·studio922 ·cli864 — all passing.tsc --noEmit, ESLint--max-warnings=0, Prettier,package.jsonorder, andapi:checkclean across all five packages;build:packagesgreen 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/studioreported232 react-doctor diagnostics, 50 of them error-severity. It now reports 0.
188 were fixed rather than silenced. The ones worth knowing about:
useStatewhere 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.
try/catch/finallyblocks flattened. React Compiler cannot lowerfinally, and one unsupported statement bails the whole component out of theauto-memoization this package depends on. Applied only where the catch swallows
and the try has no early return; the other 7 keep their
finally.useCallbackwrappers deleted, 19useMemobodies 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.
shortcut and the insights visibility-refresh — now
useEffectEvent.accessible name; the chart legend filtered a series on click with no keyboard
path at all.
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(20files, an HMR-only gain),
no-giant-component(10 panels), and a fewfalse 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/…vsreact-hooks-js/…), the directive must beadjacent to the offending line, and the comment form depends on position —
{/* … */}in JSX children,//in an attribute list or arrow body. A//inJSX 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.mdrecords what was deliberately left: infinite scroll (decided against), and the AI Query-Insights recommendations follow-on.🤖 Generated with Claude Code
Summary by CodeRabbit