Repository navigation
feat(objects): edit a database object's definition and apply the DDL (#789) - #831
Merged
Merged
Conversation
…futes The provider doc (section 3.11) and the `queryReadOnly` docblock both say a multi-statement string runs its first statement only and that the tail is never executed. Measured on DuckDB v1.5.5 through @duckdb/node-api 1.5.5-r.4 while grounding #778 Phase 3: the error raised comes FROM the second statement and the first statement's effect persists, so the tail runs and nothing rolls back. The measurement quoted beside the claim is real and is about the RESULT, which is that runAndReadAll returns the first statement's rows. The sentence built on it is about EXECUTION, which nobody ran. Recording the distinction rather than the correction, because the fix touches four sites and belongs to whoever owns that path, not to this branch. The security consequence is bounded: the guard beside the docblock reads the whole string before the call and access_mode is fixed read-only on that path. What is wrong is the stated reason, which is the load-bearing half of a security docblock. Refs #789
…tend D57 Found while grounding #778 Phase 3 against live engines and a real browser. None is Phase 3's to fix and none grows that branch beyond this entry. Three are cross-user and two of them lose data silently: D71, an unfinished BEGIN from /api/db/multi-query outlives the request on the provider cached per connection.id. On SQLite and DuckDB the next writer's row is accepted with HTTP 200 and then destroyed by a later rollback with no error at any point. On PostgreSQL the poisoned pooled client is never retired: it answered "current transaction is aborted" for 60s of retries, survived 40s of idleness, and reached a different user's /api/db/maintenance eight minutes later. D72, /api/db/transaction shares txActive per connection.id, so a second Studio user is refused "Transaction already active" with a 400 and is then allowed to roll back the first user's uncommitted write with a 200. D73, session state written by one request is read by every later one across users: a SET from a user-role session was read back by an admin session on the same backend pid. X17, the Source pane captions a truncated part "Complete as shown.", because the caption reads form and form stays "complete" when the text was bounded. Shipped in Phase 2 and invisible until this grounding built an object over the bound. X18, the add-connection button has no accessible name and the a11y gate did not catch the shape. D57 gains its end-to-end measurement: the connected MariaDB provider serves the package kind through the source route while provider-meta answers the MySQL six, so the client's declaration is the wrong server's. Refs #789
…ixes left Found while fixing D71 and D72 in their own PRs (#823, #822). D74: src/app/api/db/query/route.ts has D71's shape. It takes a provider from the same process-wide cache, has no finally and nothing that asks whether a transaction is open, so a lone BEGIN needs ONE request rather than a script. Filed as READ, not measured: the route's text was verified and the mechanism is identical, but nobody has sent a lone BEGIN through it. D71's reproduction drove its SECOND request through this route and watched it fail on a client another route had poisoned, which shows it shares the handle, not that it can poison one. D75: #823 implements the is-a-transaction-open surface on postgres, sqlite and duckdb, the three engines D71 was measured on. The method is optional with no default, so the other fourteen type-ids answer nothing and a script's unfinished BEGIN is still left open there. A declared boundary, recorded because a boundary nobody writes down becomes a fallback the next reader trusts. Refs #789
…us expectation (#789) Phase 3 of #789, from discussion #778, lands its contract before any provider declares anything. src/lib/db/types.ts gains nineteen exported types describing what a build answers, what a plan carries and what an apply did, plus two optional methods on DatabaseProvider, one optional field on ObjectKindSpec and one on the text arm of ObjectSourcePart. All nineteen are published from src/exports/types.ts, so an embedded host can name ObjectEditOutcome. src/lib/db/object-kinds.ts gains the two derivations beside kindHasSource and requireSourceKind: kindAcceptsSourceEdits, which is not conjoined with anything so that a half declaration stays visible to the census, and requireEditableKind, the one entry guard every buildObjectEdit opens with, hoisted before the first provider rather than after nine copies of it exist. tests/helpers/object-surface-conformance.ts gains the edit pairing, unconditional and outside every loop so it certifies the fourteen providers that declare no editable kind, a refusal for an editable kind the expectation counts at zero, and design 7.2's second assertion inside the walk, which drives buildObjectEdit for every editable kind and refuses a declaration with a stub behind it. tests/helpers/object-edit-expectation.ts is the census expectation, transcribed from the design and never derived from a build: four editable pairs on three engines and fourteen abstainers, with a guard asserting the two populations partition the seventeen type-ids exactly once.
…en path (#789) Review of the object edit contract found the conformance helper certifying a provider that declares an editable kind, implements both edit methods and reads no source at all. The edit pairing agrees with it, the zero-count refusal agrees with it, and the `read === undefined` early return then skips the walk, so the `undriven` refusal that exists for exactly that sentence never executes. Measured against the previous commit: that double resolved, 1 pass 0 fail. - the early return now refuses an editable kind when nothing can read a definition for it, which is the same reasoning that put the pairing above the return; - two tests for 9b's green path, which no committed test reached: an editable kind the walk skips while another kind is read, which makes the `undriven` block killable for the first time, and a conforming provider whose `buildObjectEdit` is driven and whose submitted request is pinned; - the helper reads `kindAcceptsSourceEdits()` instead of the field. Measured: mutating the derivation kills 54 tests now and killed none before; - the dead `built === null ||` disjunct is gone, and the census fleet drops its `as DatabaseType`, which suppressed the only compile error a renamed id raises.
The pane needs one place that answers whether a part of an object's definition may be submitted back, and, when it may not, the one sentence the reader can act on. A discriminated union rather than a boolean: a disabled control with no sentence is silence with a tooltip on it. The ORDER is the contract. `truncated` first, then `form`, then the provider's `edit` affordance. Both core facts travel on the document, so both arms are reachable on the standalone shell and on the embedded one, where a host supplies the document and no route of ours runs. That second half is what the route's affordance stripping cannot cover, because a host never reaches `boundSourceDocument`. Testing `edit` first would leave the `bounded` arm with no producer anywhere on the standalone path. The two core facts stay independent and are never conjoined. Measured on PostgreSQL 18.4, `form` stays "complete" on a truncated part, so a part that is both truncated and partial answers `bounded`. From discussion #778.
…_edit audit arm (#789) Core's pure edit module for Phase 3 of the object DDL apply (discussion #778). Three things here are load-bearing and each is measured rather than reasoned. EDIT_PLAN_EXECUTABLE_LIMIT is a DIFFERENT bound from the read's. The inbound text bound equals SOURCE_CHARACTER_LIMIT by construction, because a readable part must be a submittable one. The plan bound covers what this product will SEND, which is the caller's text plus whatever the provider wrapped around it, and collapsing the two refuses the route's own plan for any part within the guard block's length of the read bound. The docblock carries the arithmetic and the reason, so a later reader does not collapse them again. The coordinate map exists because a refusal's position is an offset into the statement the PROVIDER built. Measured in a real browser, an uncorrected coordinate handed to setModelMarkers did not throw and did not warn: Monaco CLAMPED it, so nothing in the platform catches the error. userPositionOf answers `outside` rather than a number for anything it cannot place inside the reader's own text. The audit outcome is not a boolean. Seven measured paths across five engines succeed while destroying something the user was not shown, so applied-with-collateral is a SUCCESS carrying a reason. The map is keyed on a type derived from the outcome union, including conflict's second discriminant, so a new arm with no audit reading fails to compile. Also adds the object_edit event type and the eight object_edit_* reasons, and corrects the correlationId docblock: it is no longer only agent_operation that sets one.
…nse claims (#789) The `provider-refused` arm's reason guard was a mutation survivor: replacing `edit?.offered === false && typeof edit.reason === "string" && edit.reason.trim() !== ""` with `edit?.offered === false` left the suite at 8 pass, 0 fail. Its population is live and is not a double: `isPartShape` in `source-reader.ts` validates every other field of the text arm and never examines `edit`, so a host on the embedded seam reaches this predicate with any `edit` it likes, including `offered: false` with no reason or a blank one. Under the mutation that draws a refusal headline over `undefined` or over three spaces. Two tests, one per half of the guard. Written against the mutated module first: 8 pass, 2 fail. Dropping only the trim kills the blank-reason test, dropping only the type check kills the no-reason test, 9 pass 1 fail each way. Restored: 10 pass. Also corrected two claims written in the present tense about code that does not exist yet. Measured at this commit: `boundSourceDocument` strips no `edit`, and `acceptsSourceEdits` has no reader outside `kindAcceptsSourceEdits`. The docblock now says the route is specified to strip and has not been written, and the test comment says what `isPartShape` actually checks. A docblock paragraph records the arm boundary: a withheld affordance with no usable reason is answered `not-offered`, which understates what the provider did and is the lesser of the two wrongs.
…n three folded guards (#789) Fix round 1 of task 2. Review found that nothing tied a consequence CLASS to its SENTENCE: swapping two arrows in CONSEQUENCE_SENTENCE, or replacing the replaces-whole-container sentence with one saying the apply is safe, killed 0 of 32 tests while format, lint, typecheck and the line gate stayed green. The one live day-one producer of that class is Redis 8.10.0 FUNCTION LOAD REPLACE, which drops every other function in the library, so the mutated dialog would have told the reader the apply is safe on the one path where it is not. Adds a total Record<ObjectEditConsequenceClass, string> of one distinguishing substring per class and asserts each sentence carries its own and no other's. Being total, it also pins the hand-written CLASSES list to the union: a ninth class now fails to compile here, measured, and a ninth entry with CLASSES left at eight fails the keys assertion. Adds three tests for guards raw line coverage cannot see, because the if line itself executes: an inverted user segment, a non-integer sent offset, and an offset at the end of a step whose segments over-cover its text. Each kills its mutation and only its own, 35 pass 1 fail. No source change.
An unsaved edit of one object part now lives under its own storage key, libredb_source_drafts_v1, with its own budget, its own try/catch and a visible failure reason, which is decision H2 of #789 Phase 3 (from discussion #778). The workspace blob is not touched, so shipped defect X14 is neither fixed nor made worse here. Pure functions over an injected Storage, no hook and no DOM, so the whole eviction arithmetic is unit-testable without a browser. The budget is 1,048,576 characters counted as DRAFT_KEY.length plus the serialized record, which is 20 percent of the measured 5,242,880-character origin ceiling and is how that ceiling itself is counted. Eviction counts its own keys and never asks the browser: navigator.storage.estimate() reported usage 0 on Chrome 152 while localStorage held 5,242,880 characters. Four failure reasons and not three. The fourth, budget, is a draft that is under the read bound and still larger than the whole budget once serialized, which a text of newlines reaches at about 520,000 characters. Folding it into too-long would print a false sentence about a text that was never too long.
The embedded seam has no route in front of it: a host's build response and a host's outcome are ordinary JavaScript values whose declared return type is not a runtime guarantee, and there is no second line of defence behind these predicates. MEASURED against tsc 6.0.3: a literal carrying the properties of two arms of a union compiles with no cast, because the excess-property check on a union admits any property declared on any member of it. So a command unit that also carries steps, an outcome that both applied and conflicted, and an unavailable revision carrying a token are all representable. Every predicate here therefore asserts EXACTLY the key set of the arm its discriminant names, walks every level rather than trusting Array.isArray, and refuses a consequence whose catalog read came back empty. No server import in this file, so the browser can name the same predicates the route uses without dragging jose and node:crypto into the client bundle.
…for (#789) Mutation: dropping Array.isArray from the draft store's shape walk killed nothing, 15 pass 0 fail. The brief's own case asks an array store for key "a", which answers undefined with or without the guard, because an array has no "a" index, so the assertion could not see the guard it was for. The guard's live consequence is on the WRITE: without it the array's elements are spread into the record as numbered keys and are written back to storage. That case is now asserted, and the mutation kills it, 14 pass 1 fail.
Found by mutation (c). With the second discriminant's VALUE check removed but its key still required by hasExactKeys, every case in this file still passed, because each conflict arm has its own required key set and that alone refuses a conflict carrying neither `current` nor `sentence`. The population the value check actually protects is a conflict whose discriminant is unknown AND which carries a well formed `current`: without the value check that narrows to object-changed and the dialog renders a diff for a conflict class nothing in this product produces. Nothing else in the file built that population. With this case present the narrow mutation kills one test.
… the store on the write path (#789) Review round 1 of task 6. Six repairs, each with the test that would have caught it. isDraftShape's savedAt and base arms were 100 percent line-covered and certified nothing: deleting either left the suite at 15 pass 0 fail, because their false branch was never taken. Each arm now has its own entry in the readDraft case. base is additionally required to be an object, so a scalar cannot reach the restore banner's revision comparison as a revision. readStore now filters every entry through the same shape walk. The read side already declared the malformed-record population real and the write path then trusted the same bytes: an entry of null raised "TypeError: null is not an object (evaluating 'entry.savedAt')" out of oldestDroppable, and an entry with no savedAt was never selectable as a victim, since undefined < Infinity is false, so every later write was refused budget permanently with a droppable record sitting there. Both are gone and both have a test. DRAFT_KEY is pinned by value: changing the string discards every stored draft, and nothing in the suite read the literal. Both boundary comparisons are pinned from the inside. A text of exactly SOURCE_CHARACTER_LIMIT is a maximal READ and is not too long; a store cost of exactly DRAFT_BUDGET_CHARACTERS is within the budget and evicts nothing. Under >= each prints a false sentence about a text that was never over. The test file no longer cites a path that does not exist in a clone.
…erty (#789) Fix round 1 of task 3. Three defects a reviewer measured against the committed module, each with the test that would have caught it. The segment map is now checked against the step's own text, which is ruling 1a at the only boundary the embedded seam has. `text` is what the engine receives and `segments` is what the dialog renders, and the module accepted a step whose map rendered "SELECT 1" while the engine would have received "DROP DATABASE prod". Two invariants, both computable from the step alone: the segments laid end to end span the text exactly, and every provider segment's bytes are the text at that segment's offset. `hasExactKeys` read `Object.keys` for its exclusion arm, which returns only ENUMERABLE own keys. A command unit hiding `steps`, an applied outcome hiding a `conflict` getter and a built response hiding a `refusal` all passed. The live population is the embedded seam, where the host's answer is a live JS object; `JSON.parse` cannot produce a non-enumerable own property, so the route path never had it. The three accepted-value lists move from `[...] satisfies readonly Union[]`, which compiles silently when the union grows a member the list does not carry, to `Object.keys(... satisfies Record<Union, true>)`, which fails the build both ways. Plus the refusals that had no test population at all: a plan with a fourteenth key, each required plan field emptied on its own, a build response carrying a malformed plan, and a collateral outcome whose `lost` names a malformed consequence.
…only signal (#789) Two consequences of the previous commit's own repair, both found by mutating it rather than by reading it. Filtering every store entry through the shape walk made the existing array-store test vacuous: removing readStore's Array.isArray guard left 20 pass 0 fail, because an array of junk is now dropped element by element anyway. The population that can still see the guard is an array whose ELEMENTS are well-formed drafts, which survive as the numbered keys "0" and "1" of a record, each an undeletable draft nothing can address. The test now builds that. Mutation: 20 pass 1 fail. The Array.isArray arm INSIDE isDraftShape went the other way: deleting it leaves 21 pass 0 fail and no population can change that, since the predicate only ever sees JSON.parse output and a JSON array carries no named member, so the text arm has already refused it. A probe over six parsed bodies and every member of each found 0 disagreements between the two predicates. The arm is removed and the reasoning recorded, because a guard over an empty population is what this epic keeps shipping. dropDraft stays void per its brief signature, and the docblock now names the signal that does exist: readDraft still answers the draft after a refused drop. Asserted, so the pane can tell "dropped" from "the browser refused" and give the restore banner a reason. Mutating the catch to clear the store instead: 20 pass 1 fail.
…ved key (#789) The plan the preview shows and the plan the apply sends are bound by a SHA-256 over a length-framed walk of every leaf, carried inside a 15-minute JWS signed with HMAC(JWT_SECRET, "libredb.object-edit.plan.v1") so a session cookie is not a plan token. The walk drives the top level from PLAN_FIELDS, sorts every level below it, and emits two synthetic leaves, an object's key list and an array's length, so a deletion or an explicitly undefined slot is visible. The test enumerates its tamper population FROM the walk. Refs discussion #778.
Six states and no other way in or out, per discussion #778's Phase 3 design. The diff's left side is the BUILD's pre-image and never the tab's text, because a left side taken from the tab hides a change somebody else made while the tab sat open, and the right side is the whole unit: the exact bytes that will execute. The shaded regions come from the plan's SEGMENTS through providerRanges and never from a search of the sent text, which would shade a reader's own line that happened to match the provider's literal. The acknowledgement is held with the plan id it was given for, so a rebuild resets it; the pinned session value is a line the reader sees before confirming (ruling 2e), and an asserted setting draws nothing; a truncated pre-image disables Apply; and `applying` blocks Escape, the overlay and the close button. Group 30 is registered in tests/run-components.sh first, because a component test file nobody registers there never runs at all. TOTAL_GROUPS is what `grep -c '^run_group '` prints, 45.
…ation (#789) Round 1 review of the seal, five findings, each repaired and each mutation re-run. The frame() docblock claimed, in a measurement's voice, that no pair of ObjectEditPlan values collides unframed. Refuted by running: kind and partId are both plain engine-derived strings, so a value absorbs the next leaf's path literal. The pair is now a test with a control over the unframed walks, and the docblock says what was measured. Four of verifyPlanToken's discriminants were individually deletable with the suite green. One test per clause, holding the other constant, forging the claim side with the module's own key. The per-leaf tamper could not discriminate the 14 synthetic leaves, because deleting a key or popping an element also removes ordinary value leaves. It now adds an undefined slot instead, which moves the synthetic leaf alone, and under the filter mutation the loop names all fourteen. The digest was not injective over the strings a plan can carry: TextEncoder maps every unpaired surrogate to U+FFFD, and String(value) makes 0 and "0" one leaf. The digest is taken over UTF-16 code units and a value leaf carries its typeof. Measured cost over a 975,134-character definition: 3.60 ms. The file header and the TTL rationale attached to the wrong declarations, and PLAN_KEY_LABEL was undocumented.
… draw (#789) Six repairs to the preview dialog, from review round 1 of Task 13. The `failed` state's outcome type now EXCLUDES `conflict: "object-changed"`. That arm carries the server's current text and decision H3 asks for it as a diff with a rebuild; the dialog was collapsing it onto the concurrency sentence, throwing `current` away and leaving Close as the only control, with nothing but a code comment separating the two discriminants. The mount that maps outcomes to states is a later task, so the refusal is the compiler's: a mount branching on `outcome.outcome === "conflict"` alone no longer compiles, and the conflict arm in the failure region is narrowed by the type rather than by a check whose other branch nothing builds. `applied-with-collateral` gets its own arm. It was falling into the `applied` else-arm and printing "Nothing here needs fixing." over an outcome whose non-empty `lost` tuple exists precisely to name what was destroyed. It now reads the catalog facts back, one line each. The diff is bounded by `EDIT_PLAN_EXECUTABLE_LIMIT`, the number both apply routes already enforce, and not by a third number invented here. The standalone path is bounded at both routes; an embedded host's `objectEditor.build` passes through no route, and `isObjectEditPlanShape` bounds no string, so a 50 MB step text was a well-formed plan that went straight into a Monaco model. Above the bound the dialog draws a sentence instead of a diff and disables Apply. The remaining host strings the dialog renders unbounded are named in the task report. The multi-step identity line no longer contradicts itself: with two steps it said "the whole of the right side is what will be sent, 1,243 characters" over a right side of 1,234. It now reads "The diff shows statement 1 of 2, and 1,243 characters will be sent in total." Tests: the overlay block while `applying` is measured rather than asserted by comment, with the control that proves a backdrop press DOES close the dialog in `preview`; the pin dedupe and the acknowledgement's `applying` arm each get the population that can fail them; and the conflict screen's empty decoration set is paired with the paint count, which separates "painted nothing" from "never painted".
One code, and one only. The client's response to an invalid or expired object edit plan is
specific rather than a rendered sentence: it rebuilds the preview from a fresh read and asks the
user to confirm again. A bare `{ error }` would force the browser to match on message text to tell
that apart from every other 400 the apply route answers.
Two other candidates are recorded as dropped in the docblock rather than left to be rediscovered.
"Not editable" is the same 400 `{ error }` `requireSourceReader` already answers for an unreadable
kind, and "too large" is carried by the 413 status itself.
Filed under the database group in the test, which is the friction the group biconditional exists
for: it fails by name until a new code is named in both files.
…on the route (#789) Three changes to the shared object-route plumbing, plus the census that moves with them. `readBoundedJson` reads the request body as a STREAM, counting bytes, and answers three separate sentences where the shipped arm answers one wrong one. The measured fact behind it: there is a hard inbound bound at exactly 10,485,760 bytes, `DEFAULT_BODY_CLONE_SIZE_LIMIT` in Next 16.3.4's `getCloneableBody`, which exists because this repository has middleware and the body is therefore cloned for it. It TRUNCATES rather than refusing, so above it `POST /api/db/query` answers HTTP 500 with a JSON parser's message and `POST /api/db/objects/source` answers HTTP 400 "Empty request body". One condition, two wrong sentences, and neither is true. The count is over the stream and never over `Content-Length`, because a check on the header is satisfied by omitting the header and a chunked body carries none. `handleObjectRequest` gains a third `context` argument on its `run` callback, carrying the resolved connection, the guard's session and the route, and a fourth `options` argument whose `readBody` substitutes the body read. The seven existing call sites are unchanged: a callback declared with two parameters satisfies a three-parameter signature. The default read is deliberately NOT repaired, so five shipped routes keep their answer and this pull request does not grow a fifth subject. `ObjectRouteError` is exported and carries an optional `ApiErrorCode`. `boundSourceDocument` takes the CONNECTED provider's capabilities and DELETES the `edit` affordance from any part that is a refusal, that carries `truncated` whether the provider marked it or this route did, or whose kind the connected provider never declared `acceptsSourceEdits`. The stripping runs AFTER the character bound, and that order is the guard: `boundText` marks truncation itself, so stripping first would carry the affordance through on exactly the parts this route cut. The rate-limit census at the `query` bucket asked by name to be re-counted. Sixteen direct call sites, verified by grep, and the object routes move from seven to nine. `handleObjectRequest`'s own numeral moves in the same commit with its basis, which is the route directories and not a digit.
) The docblock stated the framework bound in one voice. Two of its claims are re-checkable here and two are not, so it now says which is which. Re-checkable by grep against the installed dependency: `DEFAULT_BODY_CLONE_SIZE_LIMIT` is `10 * 1024 * 1024` in `node_modules/next/dist/server/body-streams.js`, and over it `cloneBodyStream` sets `limitExceeded`, pushes `null` into both streams and logs a warning, which ends the body early rather than failing the request. `next-server.js:1289` installs that clone on every non-upgrade request, with `experimental.proxyClientMaxBodySize` as the only override, so the clone is not conditional on a middleware file being present. Not re-checkable here: the HTTP 500 and the HTTP 400 the two routes answer above the bound. Those are a live measurement made earlier in this phase against a running server, and the docblock now says so instead of claiming them in this file's own voice.
…is unconditional (#789) The paragraph added in the previous commit stopped one sentence short. `next-server.js:1289` installs the cloneable body on every non-upgrade request, so the 10,485,760-byte bound is not conditional on a middleware file being present in this repository, even though middleware is what the clone exists for. Written down because the opposite reading is the natural one and would send a later reader looking for a middleware condition that is not there.
… procedures (#789) Phase 3 of #789, from discussion #778: the write half of the object Source tab, on the first of the three day-one engines. `function` and `procedure` declare `acceptsSourceEdits`, the routine read grows the five columns the edit needs, and every routine part now carries the edit affordance the CONNECTED provider answered, which is where it has to be resolved rather than on the client's copy of the declaration (D57). `buildObjectEdit` re-reads the object through the identity expression the listing wrote, answers five refusals before anything is sent, and issues a sealed plan whose unit is ONE step. `applyObjectEdit` sends `plan.unit.steps[0].text` in a single parameterless `client.query()` and never the text again. The SET LOCAL and the statement travel in ONE round trip, and that is load bearing rather than tidy. Measured on PostgreSQL 18.4 (Debian 18.4-1.pgdg13+1) through pg: sent as its own round trip, `SET LOCAL search_path` answers WARNING SET LOCAL can only be used in transaction blocks, the value does not take effect, and pg does not surface that warning as a rejection, so a split pin would leave the apply running under whatever the previous borrower of the pooled connection left with nothing downstream able to see it. The control is the identical CREATE without the pin, which answers ERROR relation "t" does not exist. Three private SQLSTATEs and not one, because the three raises mean three different outcomes, a conflict, a refused and an applied-elsewhere, and one shared code would force the classifier back onto reading messages. The classifier maps SQLSTATE as data; the single message it reads is `tuple concurrently updated`, which arrives under XX000 where the code distinguishes nothing. The fixture gains the object the read bound actually bites, measured at 1,215,122 characters, its 945,116-character control, and the two grants that make the ownership refusal non-vacuous. D63 is NOT fixed here and stays filed.
…ed about (#789) Found by running the mutation the task's own brief demands rather than by reading the test. Mutating the `42P13` arm from `definition` to `privilege` did go red, which is what the brief predicted, but the failure read `Expected "definition" / Received "privilege"` and named none of the nine codes: the loop stops at the first bad case, so a reader could not tell which arm had moved. The code is now paired into the class assertion the way it was already paired into the outcome assertion above it, and the same mutation prints "42P13" in the diff.
PARTIAL fix round for Task 10, committed by the orchestrator after a session
limit killed the fix agent mid-round. It is not reviewed and it does not close
every finding: it is committed so the work is not lost and so a later mutation
window can restore with git checkout -- without destroying it.
What is in it: the readDefaultBody docblock's claim about where the truncation is
reached from, which an earlier revision of it got backwards. next-server.js:1289
calls getCloneableBody on every non-upgrade request, but that call only REGISTERS
the wrapper; the count and the truncation live in cloneBodyStream, which has three
call sites, one of them the middleware adapter. So the INSTALL is unconditional
and the TRUNCATION is conditional on middleware running. Same outcome here because
src/proxy.ts is a middleware, but the stated reason was wrong.
Also the divergence between readDefaultBody, which refuses {} with 400, and
readBoundedJson, which returns it, now said out loud and pinned at both ends.
Verified at this tree: typecheck 0 errors, lint 0 errors, tests/unit 9402 pass 0
fail across 311 files, tests/api 558 pass 0 fail across 33 files.
Refs #789
#789) Fix round 1 for Task 10, part 2: the audit of the partial commit 18f5150 plus the one defect that commit introduced. The audit re-ran all four of the previous round's mutations at this tree rather than reading its report: m1 (the readBody arm replaced by a copy of the default) 94 pass 3 fail, m2 (fabricated session and route) 95 pass 2 fail, m3 (a fabricated connection) 95 pass 2 fail, m4 (>= for >) 96 pass 1 fail, against a 97 pass baseline, and every kill is by name. Findings 1, 2 and 4 are closed with live guards. Finding 3's framework claim was re-measured by grep against the installed next 16.3.4: getCloneableBody at next-server.js:1289 registers, and cloneBodyStream at body-streams.js:80-118 counts and truncates, with three call sites one of which is the middleware adapter, so INSTALL unconditional and TRUNCATION conditional, as the repaired docblock says. The defect that commit introduced: it wrote the handler tests in a new file, tests/api/object-route-handler.test.ts, measured that a second mock.module of @/lib/db in one process breaks tests/api/db/profile.test.ts, deleted the file and appended the tests to tests/api/db-objects.test.ts, and left two citations aimed at the deleted path. So the finding-7 divergence claim named its evidence as a file a reader cannot open. Both citations now name db-objects.test.ts and the describe inside it, and the source one records the measured reason the tests live there, 558 pass 0 fail against 510 pass 5 fail with the file split out. RED first: a new test extracts every tests/**/*.test.ts path cited in object-route.ts and asserts each exists. Before the repair 15 pass 1 fail naming the dead path; after, 16 pass 0 fail. Two mutations on the guard itself: aiming one citation back at the deleted path kills it, and emptying the population by rewriting every cited path kills it too, because the count assertion is what stops a check that runs over nothing from certifying something. Population at this commit: 2 citations in that one file. Scope is one file on purpose; 48 distinct test paths are cited across src/ and a repo-wide check is a lint-surface change. Finding 8 stays open and is NOT this task's file: src/components/object-source/source-editable.ts:22-26 claims the route-side stripping is not written yet and that acceptsSourceEdits has no second reader. Both measured false here. Owner is Task 5; reported for the orchestrator to route. Gates: format 1103 files no fixes, lint 0 errors, typecheck clean, tests/unit 9403 pass 0 fail across 311 files, tests/api 558 pass 0 fail across 33 files, and 0 uncovered DA: lines for src/lib/api/object-route.ts in the raw lcov. Refs #789
…r its rendering moved (#789) The PostgreSQL apply's post-condition compared md5(pg_get_functiondef(oid)) against the plan's revision and raised LB003 when it had not moved, on the premise that the build's byte-identical refusal left that comparison no false positives. It had one. pg_get_functiondef is a canonical rendering, so an edit that differs from the server's bytes and re-renders to the same bytes passes the build and then trips the post-condition: measured on PostgreSQL 18.4 through the shipped provider, app.order_total(integer) re-indented with sql upper-cased answered applied-elsewhere and was rolled back, a legitimate edit reported as somebody else's object and taken back. The pre block now captures the addressed row's pg_proc.xmin into a transaction-local setting and the post block compares that, so LB003 means the row that was addressed is not the row that got written. xmin moves on every rewrite including a byte-identical one, which is the property that makes it a bad revision token and the right post-condition. Measured after the repair: the canonicalized edit answers applied, a unit whose text creates a different routine answers LB003 with count(*) = 0 for the forked name, and the setting does not survive the round trip. Also in this commit, all from the same review: - The identity refusal no longer states a fork it cannot know. A changed parameter DEFAULT is rendered inside the header and is an ordinary in-place replace on 18.4 (count(*) = 1, oid 16787, xmin 857 to 858), so the sentence names that case and its repair instead of claiming the engine would fork. - The failed pooled client is released before the conflict arm takes a second one, which deadlocked nothing at the default pool size and would serialise on a pool of one. - ROUTINE_ROW.revision is the md5 the fixture's definition actually has, with a test that keeps the docblock's recomputability claim true. - Tests for three arms whose mutations survived the review: a NULL may_replace, a check_function_bodies this server answered nothing for, and a SQLSTATE in the table winning over the message it carries. - The search_path column nothing reads is recorded as such.
… not (#789) The docblock on the editable predicate said the route's stripping "IS NOT WRITTEN YET" and that acceptsSourceEdits had no reader outside kindAcceptsSourceEdits. Both halves were true when that file landed in this phase and both are false now: boundSourceDocument reads kindAcceptsSourceEdits and passes the answer to stripEdit at object-route.ts:549 and :588. Task 10's reviewer found it and correctly did not edit another task's file. It is corrected here rather than left because a stale claim about a guard reads as a statement that the guard is absent, and the next person to touch this predicate would have built the stripping a second time. The half that matters to this file is unchanged and is now said as such: a host never reaches boundSourceDocument, so on the embedded shell edit arrives exactly as the host wrote it, and the route's stripping covers the standalone path only. Refs #789
…789) The collateral arm's docblock said the order of `setPreview`, `endEdit` and `onApplied` was load-bearing, and gave a reason: a re-read landing first would paint one frame with no dialog. That was an inference written in a measurement's voice. Mutation window, moving `setPreview` last: this pane's suite 95 pass 0 fail and `tests/components/studio/ source-tab.test.tsx` 33 pass 0 fail. React batches all three inside one promise continuation, so no frame can land between them. The comment now says that, with its numbers, and the code keeps the order it reads best in.
…the first bracket (#789) routineIdentityHeader took the FIRST ")" as the end of the routine's header, so everything after it was never compared. Four ordinary shapes carry a ")" before the parameter list closes, all measured on PostgreSQL 18.4 (Debian 18.4-1.pgdg13+1): a DEFAULT holding a call (DEFAULT abs('-1'::integer)), a DEFAULT holding one inside a string literal, a quoted function name (app."we)ird") and a quoted parameter name (app.pn("a)b" integer, c integer)). Measured through the provider against a live 18.4 before the fix: the build ACCEPTED b integer DEFAULT 2 -> b bigint DEFAULT 2 on app.dc and sealed a plan, and the apply then answered applied-elsewhere with undone true, because the post-condition found the addressed row unrewritten and rolled the unit back. The same statement outside the guarded unit is the silent fork: two pg_proc rows, 16385 (integer) and 16386 (bigint). The scan counts nesting and skips single- and double-quoted text, which is the idea the Trino provider's segment scanner already uses; the docblock says why the two cannot be one scanner. An engine rendering is always cut correctly, so a mis-cut can only ever come from the submitted text and can only ever refuse, never accept. Measured after the fix, live: the same argument-type change is refused with both whole headers shown, and an ordinary body edit on all five paren-carrying fixtures plus app.np() builds and applies. The suite went from 268 pass 4 fail to 272 pass 0 fail.
…vacuous (#789) The scan skips a "'" inside a double-quoted identifier and a '"' inside a string literal, and nothing built either population, so both guards could be deleted and the suite stayed green. Both shapes are real and were measured on PostgreSQL 18.4 (Debian 18.4-1.pgdg13+1): app.mix("a')b" integer, c integer) and app.mix2(a text DEFAULT '")'::text, b integer DEFAULT 1) are what pg_get_functiondef rendered back for them. With the fixtures in, dropping "&& !double" fails 1 test and dropping "&& !single" fails 1, where both survived before. The no-closing-parenthesis arm also gained the assertion that the refusal sentence carries the whole submitted text: mutating that arm to an empty string kept every refusal in place and only emptied the sentence, so the arm was uncovered by anything but a line counter.
… cross-guards are for (#789) The docblock listed four measured shapes that carry a parenthesis before the parameter list closes and said nothing about why each quote flag ignores the other. Both mixed shapes are real on PostgreSQL 18.4 (Debian 18.4-1.pgdg13+1) and are now named with the rendering the engine answered for them.
#789) The build refused an edited header by comparing `text.split("\n")[0]` on both sides, which certifies the identity only while the whole parameter list sits on line one. Measured on trinodb/trino:476 in container `trino-t32` on host port 18532 on 2026-09-15, that is not always true: the formatter renders an identifier verbatim, so a ROW field name holding a newline makes the server's own `SHOW CREATE FUNCTION` text carry the rest of the parameter list on line two. Driven end to end through this provider against that container, an argument type edited below line one was ACCEPTED by the build, the apply SUCCEEDED, the outcome was `applied-elsewhere` with `undone: false`, and `SHOW FUNCTIONS FROM memory.app` answered two `nlrow` rows where it had answered one. Trino has no transaction to take that back and this design will not issue a DROP, so the reader is left with a second function this product will not remove. The wrap the old comment named as unmeasured is now measured and it does not happen: 200 parameters with deliberately long names answer 11,845 characters on ONE line. It was the identifier and not the width, so the old comment's "fails safe either way" was true of the population it imagined and false of the one that exists. `trinoCreateIdentity` reads the qualified name and the argument type list off the same parenthesis scan the overload resolution and the post-apply verification already use, so a build and an apply cannot disagree about which object a text names. The identity is the name and the argument types and nothing else, measured on the same container: a changed argument type forks, a changed name forks, and a changed return type and a renamed parameter are replaced in place, one row before and one after. So a parameter rename and any whitespace inside the header now BUILD, where the first-line comparison refused them as false refusals. Seven tests, RED at 190 pass 6 fail against the old check and GREEN at 197 pass 0 fail. Five mutations killed and two survived, all recorded in the report: putting the key back on the first line kills 2, deleting the empty-name guard kills 1, deleting the no-parenthesis guard kills 1, comparing the signature alone without the name kills 1; the "head ran out of tokens" guard changed no answer over 27 enumerated head shapes and is deleted with that measurement in its place, and the key's length framing survived and is kept as a judgement that says so.
…nature (#789) The control that catches a wrong build-time identity rule compared `trinoCreateSignature(step.text)` against the signature the addressed row was found by, and a signature is the ARGUMENT TYPES alone. So a statement declaring `memory.app.plus_two(x bigint)` over a plan addressed to `plus_one(bigint)` compared EQUAL: the addressed row came back unchanged, the apply answered `applied`, and the revision token handed back was the digest of a text the reader's edit never reached. Measured on trinodb/trino:476 in container `trino-t32` on host port 18532 on 2026-09-15, a changed NAME forks exactly as a changed argument type does, `rename_probe2` beside `rename_probe`, two rows where there was one. That is the same class of hole as the first-line rule this control exists to catch, in the control itself. It now reads the SENT bytes and the pre-image the digest check just accepted with the same `trinoCreateIdentity` the build compares with. The keyword prefix is the caller's, so the build reads `CREATE FUNCTION` and the apply reads `CREATE OR REPLACE FUNCTION` off `TRINO_REPLACE_CLAUSE`, which is also what keeps a reader's own `CREATE OR REPLACE` refused at build time. `wrote` now carries the name the statement DECLARED, qualified, rather than the addressed bare name: on a name fork the addressed name is precisely the object that was not written, so printing it named the wrong object in the one sentence whose job is to name the right one. The three assertions that pinned the bare form move with it. Two tests, both RED first: the name fork answered `applied`, and a plan whose statement carries no readable header now answers `applied-elsewhere` with no name. 199 pass 0 fail. Mutations: putting the signature comparison back kills the name-fork test. The parameter-type reading is one writer again, `trinoArgumentTypesIn`, because the identity holds the parenthesis offsets already and a second copy of "split, then drop the names" is a second reading that can drift from the one the whole overload comparison rests on.
…ll clears the document (#789) X24 was closed one level too high. Holding the preview session open is not enough: the same arm calls onApplied, and both shipped shells answer that by running onSourceChange({ document: undefined, failure: undefined, readAtToken: undefined }). The dialog's mount sat inside the pane's part !== undefined branch, which the document chooses, so the arm that created the report told the shell to unmount it. Measured against the real Studio with a throwaway copy of source-tab.test.tsx: with the re-read hung, report GONE and the loading region in its place; with the re-read answering 500, report GONE for good and the loss surviving only in the audit ring, which is X24 word for word. The mount moves to the foot of the pane, outside every document branch. Nothing it renders needed the part, every prop comes off the preview session, and Radix portals it, so the layout is unchanged. The same probe now prints PRESENT on all three paths. EditHarness could not model this at all: document={state.document ?? props.document} fell back to the static prop, so a clear was unrepresentable and the round-1 test was green over a state neither shell reaches. It now keys on presence, passes failure and readAtToken through, and takes the shell's applied patch and a reader the test can hang or fail. Two new tests, RED at 95 pass 2 fail before the move and 97 pass 0 fail after. Reverting the harness alone also kills both, because each asserts the pane behind the dialog. Two docblocks corrected. git show 3a4511a moves one behavioural line, functions.length < 2 to === 0, inside libraryCollateral, which is the BUILD's warning; the outcome's producer, disappeared.length > 0, landed in 1d6ac88 and was untouched, so 18a99ce's claim that D84's fix grew this outcome's population is false. The warning population grew. Refs discussion #778.
…t is about to X24 made the collateral report reachable, and the frame around it still described a dialog that had not run yet. MEASURED by reading `dialog.textContent` in each of the twelve states this component can reach: all twelve opened with `Apply this definition` and `Nothing runs until you press Apply.`, and all eleven that draw a diff labelled the pre-image column `On the server now`. On the state X24 made reachable, an apply that SUCCEEDED and destroyed a sibling function, all three of those are false at once. `applyFrame` answers the three together, because they are one fact: the title, the disposition sentence, and whether the left column may still claim the server. The right column and the identity line move to the past tense on any state that carries an outcome. `interrupted: "unknown"` gets the pre-apply column label rather than a claim about the server, because nobody knows there and a header claiming it would be a guess printed as a fact. Nine tests, one per reachable disposition, asserting the title and the description by value rather than by presence: every one of these strings already rendered, and every one of them was wrong about what had happened. #789 Phase 3, discussion #778.
…in its alert on a success Three of the region's arms report an apply that SUCCEEDED, and `applied-with-collateral` is the whole reason X24 keeps it on screen after one, so `FailureRegion` named an outcome class it no longer only serves. The attention level is unchanged and that is deliberate: `role="alert"` stays, because an apply that destroyed a sibling function deserves the alert an engine refusal gets. The old assertion for that role drives a REFUSAL, which is a population that does not contain the case, so the success arm is now asserted too. MEASURED: dropping `role="alert"` fails 2 tests with the new one and 1 without it. The `data-testid` is deliberately NOT renamed and the docblock says why with the number. MEASURED: renaming it to `object-source-apply-outcome` fails 10 tests across three suites that read it, `ObjectSourceView.test.tsx` 6, `embedded-source.test.tsx` 3, `source-tab.test.tsx` 1, plus one Playwright assertion on the code child, and none of those four files belongs here. The rename is two string substitutions and it has to travel with an edit to those four files in one commit. #789 Phase 3, discussion #778.
…by stripping quotes (#789) The identity key was built from the form the overload RESOLUTION needs, which lower-cases each rendered type and strips every space and every double quote. That is the only form the engine's two renderings of one signature agree on, and it is far more than this engine's identifier rules allow: two DISTINCT types reduce to one key whenever they differ only inside a quoted identifier. Measured on trinodb/trino:476 in container trino-t32r1 on host port 18633 on 2026-09-15, three pairs that reduced to one key and coexist as two rows in SHOW FUNCTIONS FROM memory.app: row("a b" bigint) beside row("ab" bigint), row("Ab" bigint) beside row("ab" bigint), and dq(row("a""b" bigint)) beside dq(row(ab bigint)). Driven end to end through this provider against that coordinator with one rowf overload present, deleting the one space inside the quoted ROW field name BUILT, the apply answered applied with a revision token, and the catalog then held two rowf rows. The reader was told the edit landed and never saw the applied-elsewhere sentence. The key now folds identifiers the way Trino does: an undelimited identifier folds to lower case, a delimited one is the text between its quotes with "" standing for one quote, and a delimited one that spells a legal bare lower-case identifier is emitted bare because it names the same thing. Measured on the same container, each applied over the other with the row count unchanged: row(ab bigint) over row("ab" bigint), row(Ab bigint) over the same, and memory.app.PLUS_ONE over memory.app.plus_one. A quoted FUNCTION name folds its case as well, CREATE FUNCTION memory.app.casefn over an existing memory.app."CaseFn" answering ALREADY_EXISTS errorCode 12, while a quoted ROW FIELD name does not, so the two halves of the identity fold by different rules. The overload resolution keeps the stripped form, which is what it needs.
… tense as well The easy half of this item is a conditional title; the trap is a body that still reads as a plan under it. INVENTORY, read top to bottom on a completed apply: between the outcome region and the diff sit the session-pin line and the revision note, and both described what was about to happen. A reader who has just been told their sibling function is gone was also told that LibreDB "runs" this apply with search_path pinned and "re-reads the definition first and refuses if it differs". All three now key off the same `sent` flag the headers and the identity line use. The `unavailable` arm gets its own past tense rather than sharing one, because it is the arm that admits the apply could not tell. MEASURED, three mutations back to the present tense: the pin line fails 1 test, the `compared` note 2, the `unavailable` note 1. #789 Phase 3, discussion #778.
…r cannot answer (#789) The control compared the sent statement's identity against the addressed one with the same reader the build refuses with, so it was guaranteed to be blind to exactly the population the build's rule was blind to. Measured on trinodb/trino:476 in container trino-t32r1 on host port 18633 on 2026-09-15: a live fork through the shipped provider answered applied rather than applied-elsewhere, because the second, independent chance to tell the reader the truth was spent on the same reading. It now also asks the engine. SHOW CREATE FUNCTION answers one row per overload and both reads the apply already makes carry that count, so comparing the count before and after costs no round trip and reads no identity at all: if the write ADDED an overload of the addressed name, the statement created an object instead of replacing one, whatever any reading of its text says. The question is asked only where the two readings of the sent text disagree, that is where the name and the argument types as WRITTEN differ from the pre-image's byte for byte while the folded identity calls them equal. A header the reader never touched cannot reach it, which is what keeps another session's sibling overload reported as applied, the outcome its own test pins. Also, from the same review: the reader is renamed trinoCreateFunctionIdentity and its docblock records that it is FUNCTION shaped, taking the first top-level parenthesis pair as a parameter list, which a view has never had; the view deferral comment names it as the second thing to fix before that kind is un-deferred; and the build's docblock no longer claims that the current side of the comparison is the path segment or that the comparison is the quote-free form, neither of which was true of the code beside it.
…both halves of its control (#789) Three mutations survived the fix round's first pass, and each one is a step of the reading that nothing was measuring. Dropping the whitespace outside a quoted identifier changed no answer: measured on trinodb/trino:476 in container trino-t32r1 on 2026-09-15, CREATE OR REPLACE FUNCTION memory.app.dec(x decimal(10,2)) over decimal(10, 2) replaced it in place, one row before and one after, re-rendered back with the space, so that step is correct and now has the test that says so. Comparing only the argument types in the post-apply control's byte-exact trigger changed no answer either, because no apply test moved the NAME in a way the folded reading calls harmless: the fork arm and its control now drive both halves, a re-cased type and a re-cased qualified name, each one measured applied in place on that container. Every mutation re-run against this tree, baseline 207 pass 0 fail: the key back to the stripped comparison form 201/6; the doubled-quote arm deleted 206/1; the name half no longer folding quoted case 206/1; the type half folding it 206/1; a delimited identifier never emitted bare 206/1; the whitespace step dropped 206/1; the engine-count control removed 206/1; the count asked with no byte-exact trigger 206/1; the count fired on "did not shrink" 206/1; the overload count a constant 206/1; the trigger on the name alone 206/1; on the types alone 206/1; comparing type-list lengths only 206/1; and the post-apply rule back to trinoCreateSignature 206/1.
…ady ran (#789) Fix round 1 of task 33, three defects a reviewer measured on the finished screen. THE CONSEQUENCE ROW. `describeConsequence` opens all eight of its sentences with `Applying this ...`, and the dialog drew that list on the `failed` state as well. On the collateral screen, which is the screen this item exists for, the reader was told "The engine applied this change and it destroyed something else" and "FUNCTION LIST LIBRARYNAME orders answers: order_count is gone." and then, one row lower and in the same warning styling, "Applying this replaces the whole container ... the library registers order_total and order_count." The prediction is now drawn only while there is still an apply for it to predict, `preview` and `applying`. The population is not a corner case: `libraryCollateral` attaches `replaces-whole-container` to every Redis function plan whose library registers a function, and that provider is the only day-one producer of `applied-with-collateral`. THE REFUSAL TITLE. `The engine refused this change` was printed over a `guard` refusal, where this product's own code says no engine spoke: a pooled client somebody else left in a transaction is refused before any bytes leave (D78, day-one PostgreSQL). That arm now reads `LibreDB refused this change`. Every other class at apply time is a verdict an engine reached. THE EXPIRED DISPOSITION. `Nothing was applied and the definition on the server is unchanged.` is a claim about a server this client has not spoken to for up to fifteen minutes, which is the window the `conflict` state exists for. It now reads `Nothing was applied by this attempt.` Tests: the whole screen is read on a plan that carries a consequence, a pin and a `compared` revision, with a preview control, so no future-tense sentence can survive on a completed apply; the `applying` body is asserted by value, which is where both surviving mutations lived. The frozen testid's handover count is corrected from 10 tests over five files to 24 over six.
…ly produces it (#789) The whole-screen future-tense read was taken over a statement plan only. Redis is the only day-one producer of `applied-with-collateral` and every Redis plan is `medium: "command"`, so the screen a reader meets first draws one row that read never saw: `This apply sends a command, not a statement.` MEASURED as a survivor: rewriting that row to `This apply will send a command` left the suite at 65 pass, 0 fail. With the command plan in the read it is 66 pass, 1 fail. The new guard is a SHAPE as well as a list. Four patterns (`will`, `Applying this`, `Nothing runs`, `re-reads`) are matched over `dialog.textContent`, so a future-tense row nobody inventoried fails them too, and each negative is paired with a control asserting all four DO match on the matching preview, in both media. The `-payload` assertion is the non-vacuity check: suppressing that block on a completed apply gives 66 pass, 1 fail rather than a silent pass. The statement list also gained `I understand what this will replace.`, which the first inventory missed: it is a future-tense sentence, and the ack row is gated on `preview`/`applying` so it is correctly absent from a completed screen.
…pin the edit the fix refuses (#789) PAREN_HEADER_FIXTURES holds seven shapes and its guard asserts seven, while four places in the same block still said five and the listed population left out app.mix and app.mix2, the two mixed-quote shapes the scan's two cross-guards exist for. The next reader would have taken those two for inventions. Re-measured on 2026-09-15 on a postgres:18 container this task created and removed, answering PostgreSQL 18.4 (Debian 18.4-1.pgdg13+1): all seven CREATE FUNCTION statements plus app.np() were applied to a fresh database and md5(pg_get_functiondef(oid)) answered the eight constants in this file byte for byte, so the population is re-runnable rather than transcribed. Adds the narrowing c2437ec introduced, which was written down nowhere: changing a LATER parameter's DEFAULT is an ordinary in-place replace and is now refused as identity. Measured on the same container, app.dc DEFAULT 2 to DEFAULT 3 left one pg_proc row with oid 16385 unmoved and xmin moving 754 to 762. The test carries the pre-c2437ec1 reading as its control, so it asserts the refusal is about where the header ends and not about something else.
… commit made stale (#789) RE-MEASURED at this commit, rename applied to this file alone and nothing else touched: `ApplyPreviewDialog.test.tsx` 51 pass / 16 fail, `ObjectSourceView.test.tsx` 91/6, `embedded-source.test.tsx` 41/3, `source-tab.test.tsx` 32/1. That is 26 tests over six files, not 24, and `grep -o '"-failure[a-z-]*"'` on the owning suite gives 26 call sites, not 24. The old number was taken before the two tests that landed in the same commit as it, each of which added a `-failure` call site, so it was shipped as a measurement of a tree it did not describe. The docblock now says that a count over the suite has to be re-taken whenever the suite changes.
…lsified (#789) The doc still told a reader the identity comparison cuts at the first `)`, still carried a FALSE-ACCEPT paragraph that has no case left on the server's side, and still described the acceptance run's `applied-elsewhere` row as a path a reader can reach. What replaces them is the measured population and not the story everyone told. The headline example, a `numeric(10,2)` first parameter, CANNOT reach this comparison from the server's side: `pg_get_functiondef` renders parameter types through `format_type(t, NULL)` and drops the modifier. The shapes that do carry a parenthesis before the parameter list closes are seven, and they are listed: a DEFAULT holding a call, a DEFAULT holding either parenthesis inside a string literal, a quoted function name, a quoted parameter name, and the two mixed-quote shapes. Re-measured on 2026-09-15 on a postgres:18 container this task created and removed, answering PostgreSQL 18.4 (Debian 18.4-1.pgdg13+1): all seven renderings and their md5 came back byte for byte, and so did the typmod line. Adds the narrowing the fix accepted, which was written down nowhere: a change to a LATER parameter's DEFAULT is now refused although PostgreSQL replaces it in place, measured on the same container as one pg_proc row with oid 16385 unmoved and xmin moving 754 to 762. The two halves of "it used to apply" are given different evidence classes, because only one of them was run.
…s wrong (#789) The handover paragraph carried the count and not the recipe. Both facts below were MEASURED in one mutate-and-restore window at this tree, with the rename applied to this file alone: - A whole-tree grep for `object-source-apply-failure` finds five files and 21 occurrences and misses this region's OWN suite, which composes the prefix and writes `testId("-failure")` at 26 call sites. A grep-driven rename would leave the 16 failing tests there behind. - The substitution runs over this paragraph too and turns it into a false one, "still reads object-source-apply-outcome ... KNOWN STALE NAME". The 26-over-six-files count re-measured unchanged after the command-medium tests landed: 16 + 6 + 3 + 1.
… count to the array (#789) The provider doc said a parameter RENAME and a reader-typed type modifier were "both measured on 18.4 as in-place replaces". The rename half was transcribed, never run, and is false. MEASURED on 2026-09-15 on a postgres:18 container answering PostgreSQL 18.4 (Debian 18.4-1.pgdg13+1): CREATE OR REPLACE FUNCTION app.tmn(zzz numeric(10,2), b integer) over app.tmn(a numeric, b integer) answers ERROR 42P13 cannot change name of input parameter "a", HINT Use DROP FUNCTION app.tmn(numeric,integer) first, and leaves the row untouched. PostgreSQL refuses a rename through CREATE OR REPLACE, so the build's identity refusal costs the reader nothing there and the SQL editor the refusal points at cannot perform it either. The doc now says that, and says DROP FUNCTION drops the routine's grants and dependents. The typmod half reproduced and stays, re-measured. The two oid/xmin pairs in that paragraph both read 16385/754 and came from two different probe databases, which one cluster cannot produce: an oid counter is global to the cluster. Both the narrowing edit and the typmod edit were re-run in ONE database in ONE session, with the seven fixtures created in the array's order plus app.np(), and the paragraph now quotes that session and says the numerals are provenance and not the claim. The count of the header population was stated in nine prose places and asserted in one. The doc now states it once and a new test in the owner suite reads that sentence out of docs/providers/postgres.md and asserts the word against PAREN_HEADER_FIXTURES.length, so an eighth shape is a RED and not a silent drift. The narrowing test selects its fixture by object identity instead of by array index. Mutations: doc numeral SEVEN to EIGHT, 277 pass 1 fail; the sentence reworded so the regex finds nothing, 277 pass 1 fail; the array reordered, 278 pass with identity selection and 277 pass 1 fail with the index selection restored. All restored, suite back to 278 pass 0 fail. Refs #789, discussion #778.
…x readers (#789) The region reports three arms that SUCCEEDED and was named object-source-apply-failure. Task 33 renamed the component to OutcomeRegion and correctly refused the testid, because its ownership was two files and the readers span six. Done here as one commit: four literals, six files, 47 call sites. Worth recording how it went, because the file had already written down exactly how it would go wrong and I did it anyway, in order. Trap one: a whole-tree grep for the literal finds five files and 21 occurrences and MISSES this region's own suite, which composes the id (const testId = (suffix) => `object-source-apply${suffix}`). The literal-only substitution took ApplyPreviewDialog.test.tsx to 16 fail, and the composed forms were 23 "-failure" plus 3 "-failure-code", which is the 26 the docblock had counted. So it is four literals over six files, not two over five. Trap two: the substitution rewrites the paragraph describing it into a false one, because that paragraph quotes the old name. It came out saying the new name was a KNOWN STALE NAME. Both are now in the docblock as the record of a rename that happened, rather than as a forecast of one that has not. Verified by exit code: format, lint, typecheck, run-components (46 groups) and tests/unit all 0; the owning suite 67 pass 0 fail; zero occurrences of the old literal in src, tests or e2e. Refs #789
…789) Fix round 1 of task 34 bound one sentence in docs/providers/postgres.md to PAREN_HEADER_FIXTURES.length and then, in the same commit, wrote two more sentences stating that count in prose with nothing reading them: the doc's in-place-replace paragraph for app.dc and the docblock of the DEFAULT-change test. An eighth fixture would have gone red once, at the bound sentence, and left both prose counts wrong with the suite green, which is the drift the round 1 guard existed to remove. Neither sentence needs the count, so both now say "the shapes above" and "the fixtures above", and a second test refuses any spelled numeral or digit written directly in front of `shapes` or `fixtures` in either file. Measured before the prose was corrected: the doc leg and the test leg each failed on their own, 278 pass 1 fail, Ran 279, and the same two sentences are what grep -nEi finds over the two files. Mutations after the fix: reinserting "seven shapes" in the doc, "seven fixtures" in the test file, and the digit form "8 fixtures" each give 278 pass 1 fail with the new test dying by name, and the round 1 mutation SEVEN to EIGHT still kills its own test. Two prose quotations of the numeral went with them, `toBe(7)` and the doc's own word, because a quotation of a bound literal goes stale the same way. The docblock's two line-number citations are replaced by names for the same reason. Also repairs a duplicated "the" and an over-width line in the re-measurement paragraph, both found by the round 2 review. 279 pass 0 fail, Ran 279 tests across 1 file. format, lint, typecheck exit 0; bun test tests/unit 9507 pass 0 fail.
|
cevheri
added a commit
to nycjay/libredb-studio
that referenced
this pull request
Sep 15, 2026
Brings in the object model (libredb#811, libredb#820, libredb#831). Conflicts resolved in database-compose.yml (both services kept), src/lib/db/factory.ts (main's comment, db2 kept in the type list), bun.lock (regenerated by bun install) and the operator bundle CSV (taken from main, regenerated in a later commit). This commit does not typecheck on its own: Db2Provider still implements the removed getSchema() instead of the object methods. The next commit converts it.
This was referenced Sep 15, 2026
cevheri
added a commit
that referenced
this pull request
Sep 15, 2026
D84, X24 and X25 describe defects that waves 13 to 18 of #789 closed. The gate task that would have deleted them ran before those waves, so they shipped with #831 describing code that no longer exists. Verified on main 249b076 before deleting: - D84: redis.ts:637 is `functions.length === 0`, not `< 2` - X24: ObjectSourceView.tsx:403 APPLIED_OUTCOMES is `new Set(["applied"])` - X25: StudioWorkspace.tsx handleApplied clears `document: undefined` The two section index counts move with them: drivers D1-D83 at 41, studio X2-X23 at 16. The docblocks and test comments that cite the three ids are left alone; they say why the code has its shape, and that history is true.
cevheri
added a commit
that referenced
this pull request
Sep 15, 2026
…emicolon rider (#789) (#839) * docs(backlog): delete three entries whose defects are fixed on main D84, X24 and X25 describe defects that waves 13 to 18 of #789 closed. The gate task that would have deleted them ran before those waves, so they shipped with #831 describing code that no longer exists. Verified on main 249b076d before deleting: - D84: redis.ts:637 is `functions.length === 0`, not `< 2` - X24: ObjectSourceView.tsx:403 APPLIED_OUTCOMES is `new Set(["applied"])` - X25: StudioWorkspace.tsx handleApplied clears `document: undefined` The two section index counts move with them: drivers D1-D83 at 41, studio X2-X23 at 16. The docblocks and test comments that cite the three ids are left alone; they say why the code has its shape, and that history is true. * docs(objects): name what enforces the write-path bound and kind The stripEdit docblock still said nothing on the write path enforced either fact and closed with an until-it-does sentence. Both halves landed: the edit-plan route re-resolves the kind on the connected provider, and the submitted text plus all three day-one buildObjectEdit reads are held to EDIT_CHARACTER_LIMIT. The paragraph now names the enforcer of each half and where it sits. Closes DOC5. * fix(postgres): re-resolve the editable kind in the object edit apply Trino and Redis both open applyObjectEdit with requireEditableKind and PostgreSQL did not, so a plan carrying a kind this provider does not declare editable reached the round trip and was refused only afterwards by the re-read's address derivation, with the wrong sentence and the DDL already sent. The return is discarded, as Redis discards it: the apply sends the plan's bytes verbatim and needs nothing off the spec. No shipped path reaches it. The edit-apply route asks the same question on the connected provider and the statement was minted by a buildObjectEdit that asked it too; the population is a @libredb/studio consumer calling applyObjectEdit directly. Closes D83. * fix(objects): stop describing an unreadable apply answer as a timeout The apply preview dialog printed "The engine stopped this statement before it finished" for every interrupted outcome. On committed: "unknown" nothing observed the engine at all, and that population includes the outcome the Source pane synthesises for an apply answer isObjectEditOutcomeShape refuses, where the engine may have finished perfectly and the answer is what could not be read. The first line and the title now split on committed. rolled-back keeps the measured clause, because only a provider that opened and closed the transaction itself may claim it. unknown says the one thing true of the whole population: this product never read an answer for the apply. Closes X20. * fix(postgres): refuse an editable kind with no routine statement on the read readObjectSource built the edit affordance whenever kindAcceptsSourceEdits was true, and that helper reads may_replace and owner, which only the routine statement selects. A non-routine kind declared editable would therefore have drawn offered: false with "owned by another role", which reports a declaration drift in the words of an ownership problem. The read now raises the build's own sentence through the build's own address helper, before the round trip, so the two refuse the drift with one spelling. Closes D81. * fix(objects): bound every host-supplied string the object edit wire accepts The four shape predicates in object-edit-wire.ts checked shape and bounded no string, so a refusal sentence, a revision reason, a truncation reason and each consequence fact reached the dialog at whatever length their producer wrote them. These are not this application's prose: a refusal sentence is the engine's own message and libraryFact in the Redis provider builds observed from FUNCTION LIST. The embedded seam was already covered by use-connection-adapter; the standalone path and the shape layer itself were not. Every prose and identifier string now takes SOURCE_CHARACTER_LIMIT, which is the number Phase 2's isSourceDocumentShape already bounds its host-supplied rendered strings with, and an overrun is a refused shape rather than a silent truncation for the same reason it is there. The unit's executable text is measured with planExecutableLength and bounded by EDIT_BODY_BYTE_LIMIT, deliberately looser than the routes' own EDIT_PLAN_EXECUTABLE_LIMIT. Both routes run this predicate first and the executable bound second, and the second check is the one that can name the number: measured with the tighter bound here, a unit one character over it made both routes answer a shape complaint about a size problem. Closes backlog D80. * fix(objects): release the apply diff models before disposing them MEASURED in Chromium on 2026-09-14, on every successful apply driven through the UI: "TextModel got disposed before DiffEditorWidget model got reset", one console error per apply. @monaco-editor/react disposes the original and the modified model and only then the widget, so both go while the widget still holds them and Monaco's own disposal path writes that line. The diff is now wrapped in a component whose cleanup calls setModel(null) and then disposes both models itself. A component and not an effect on the dialog, because React destroys a deleted subtree's effects from the top down: a cleanup on the parent of DiffEditor runs before the wrapper's in both cases, the dialog closing and the diff alone going when a preview expires under it. The keep flags stay false, so the wrapper is still the backstop against the retained text the browser probe measured. The suite's Monaco double now carries the wrapper's own unmount, transcribed from its shipped dist, and raises Monaco's sentence when a model is disposed while still attached to a live widget. Without the fix both new tests report it. Closes X21. * fix(objects): pin every source-line citation the object-route docblock writes The DOC5 rewrite named where each half of the write-path obligation is enforced, and one of the six pointers it wrote was invalidated twelve lines later by a commit on the same branch: the read-definition bound moved from postgres.ts:3090 to 3102 and 3090 became a comment inside a helper. That is the defect DOC5 was filed for, re-created at a smaller size, because nothing in CI reads a code-comment file:line pointer. The guard holds every number in one place, the cited file itself. The table in the test names the ANCHOR rather than the line, greps it, derives the number the docblock must be writing and asserts it writes it, so a correct renumbering costs nothing and a stale one goes red with the number to write. Three assertions, each measured non-vacuous: the stale one listed all five wrong citations, a probe citation added to the docblock was reported unguarded, and a duplicated anchor was reported ambiguous. Five citations were wrong, not one. The four beyond the DOC5 pointer were already wrong before this branch and sit in the same docblock under the same "measured by grep" banner, so leaving them would have made the new guard certify a paragraph it knows is stale: the three producers of `edit` (postgres 2799 to 2947, trino 1249 to 1257, redis 1657 to 1674) and the Redis decided-position range (1649-1656 to 1667-1673). Also pins a documented fact the D81 test left unmeasured: postgres.md says the declaration-drift refusal is raised before the round trip, so it costs no query. Measured by moving the guard below the read, where the thrown sentence stays identical and only the new `expect(sent)` assertion goes red. * test(objects): pin the command arm's executable bound and the wire's import closure Three review findings on the D80 work, all in the two files that work owns. The command arm of isObjectEditUnitShape survived mutation: replacing its isWithinTheExecutableBound call with `return true` kept all 48 tests green at 100 percent line coverage, because every command case in the suite tops out at SOURCE_CHARACTER_LIMIT + 1 and is refused by the name bound three orders before the executable bound decides anything. The new case sizes a payload against EDIT_BODY_BYTE_LIMIT minus the name and the argument token, so the at-the-limit control also proves those two are counted, and the same payload as a statement step is accepted to show the refusal is the sum. Measured: red on the mutant, green on the module. The isWithinTheExecutableBound docblock claimed the bound exists for "the seam with no route in front of it, where a host's answer is a live JS object and nothing between it and the dialog counts anything". That is false here. The embedded shell counts the whole answer at 4,400,000 characters as it snapshots it, edit-apply reads its body through readBoundedJson at 8,388,608 BYTES before the parse, edit-plan measures the same unit against EDIT_PLAN_EXECUTABLE_LIMIT on the next line, and ObjectSourceView narrows what those routes answered. So the predicate cannot answer false today and the docblock now says so: it is a ceiling above every bound in front of it, and the SOURCE_CHARACTER_LIMIT bounds beside it are the ones that bite. The isStep docblock named EDIT_PLAN_EXECUTABLE_LIMIT while pointing at the helper that exists to reject that constant. It now names the one the code uses. The module's "NO SERVER IMPORT" rule stopped holding by construction when this module gained its first value imports, so a test walks the value-import closure and fails on the first bare specifier anywhere in it. Measured non-vacuous: a temporary `import { SignJWT } from "jose"` turns it red, and the same walk from object-route reaches jose two hops out. * fix(objects): say the disposition, not the transport, on an unknown apply X20's first replacement clause, "The apply was sent and LibreDB never read an answer for it.", is false for the largest part of the population it prints for. `ObjectSourceView.landApplyError` turns EVERY rejection of `applier.apply` except `EDIT_PLAN_INVALID` into `interrupted`/`unknown`, and `source-applier.postJson` rejects on every non-ok status carrying the route's own sentence, so a proxy's 502 arrives here WITH the answer it produced. Measured through the suite's own shapes, the shipped render was: The apply was sent and LibreDB never read an answer for it. Whether it was applied is unknown.The apply failed: HTTP 502. Trino is the second member: TRINO_APPLY_VERDICT maps timeout and cancelled to interrupted from the coordinator's own EXCEEDED_TIME_LIMIT and ADMINISTRATIVELY_KILLED fault names, i.e. from an answer read in full. Pointing the other way, "the apply was sent" is equally unmeasured: an appFetch rejection on a dead network reaches the same arm, and `applyFrame` already tells the same reader "Whether it reached the server is unknown.", which the body line contradicted. The clause now states the DISPOSITION, which is the one thing every member shares: "Whether it was applied is unknown: LibreDB has no answer that says whether it landed." The split on `committed`, the titles and the frame are unchanged. * docs(backlog): file the half of X21 that was deleted without landing X21's "Done when" had two conjuncts: the widget releases the models before they are disposed, AND the E2E spec can assert an empty console after an apply. Only the first landed in ef37e436; the entry was deleted with the second undone and nothing in the repo recorded that it is owed. `e2e/object-edit.spec.ts` does install a console listener, but `watchForCspViolations` keeps only messages containing "Content Security Policy", so it cannot see the Monaco disposal error or any other class. The symptom X21 was filed for was therefore never re-read after the fix, which is pinned only by a component test against a hand-written Monaco double. X26 states that, and says why widening the CSP filter is not the fix on its own. * test(objects): drive the release's null arm, and stop repeating a count nobody measured Two minor findings on ef37e436, both inside the file they were raised against. `ReleasedDiffEditor`'s `if (mounted === null) return;` was never taken by any test: the double fires `onMount` synchronously, so the ref was always written. The guard shares a physical line with its return, so the line gate read 100 percent with the branch dead, and replacing it with a throw kept all 71 tests green. The double now models Monaco's loader not resolving, which is how the branch is reached in the product: `@monaco-editor/react` calls `onMount` only once its loader has, so a dialog closed or a preview expiring mid-load unmounts a diff whose ref was never written. Verified by removing the guard: the new test is the only one that fails. X21's "one console error per apply" was repeated as measured and disagrees with the mechanism the same docblock cites: `diffEditorWidget.js:233-240` subscribes `onWillDispose` to BOTH models, so the sentence is raised once per model disposal. MEASURED with the double by deleting `setModel(null)`: four, two from this component's disposal and two from the wrapper's, which without the release still reads both models out of the live widget. The count is dropped rather than restated, since the Chromium reading was of the sentence's presence. * docs(objects): correct five sentences that D80's fix made false c18cc43a bounded every string the four object-edit-wire predicates accept. Five places in the tree still argued from the opposite, and one of them handed the reader a grep that now answers against the sentence it supports. Each is corrected to what is true rather than edited to drop the false clause, because the surrounding paragraphs reason FROM the old claim: - use-connection-adapter.ts closed on "the standalone half is filed rather than claimed as covered". It now says the per-string half is closed on both shells, and why the whole-answer bound is still not redundant: it measures the sum, and it measures it before any predicate walks the object. - source-applier.ts reported the gap as open. It is closed; the paragraph's own reasoning about who owns the bound was right and is kept. - ApplyPreviewDialog.tsx argued from a 50 MB step text being a well-formed plan. It no longer is. The band that still reaches this dialog is between EDIT_BODY_BYTE_LIMIT and EDIT_PLAN_EXECUTABLE_LIMIT, so the example moves to eight million characters and the bound keeps its reason. - The two test comments follow the same two corrections. * fix(query): end the transaction one statement left open on a shared handle POST /api/db/query took a provider from the process-wide cache and executed, with no finally and nothing that asked whether a transaction was open. That is the leak #823 closed on /api/db/multi-query, reached by one word instead of by a script. Driven live on PostgreSQL 18.4 through this handler, real provider and real cache, only the session mocked. A lone BEGIN answered 200 and released its pooled client in status T. The next request on the same cached provider, which is any other signed-in user of that stored connection, ran CREATE TABLE inside the stranger's transaction: 200, same client object, and an independent reader saw no such table. BEGIN followed by a statement naming a missing relation left the client in E and the next request answered HTTP 500 "current transaction is aborted". After the fix the same two runs leave the client in I, the next user's CREATE is durable, and the caller is told openTransaction: "rolled-back". The finally and the type guard are the ones /api/db/multi-query already spells, so there is one spelling of this. What the route ends is the transaction its own statement left open, before the handle is reachable by anyone else, so the only work it can discard is the caller's own; the interactive session POST /api/db/transaction drives holds a client of its own and is never touched. Closes D74. * fix(db): let an SSH-tunnelled connection build and apply an object edit plan getOrCreateProvider rewrites host and port to the tunnel's local endpoint before the provider is constructed, and every sealing provider digests this.config. The edit routes recompute that digest from the record the request resolved, which still names the far end, so the two could never be equal: 100 percent of tunnelled connections were refused EDIT_PLAN_INVALID with no sentence saying why (X23). The factory now carries the pre-rewrite endpoint alongside the rewritten host and port, under a symbol key, and connectionFingerprint frames that when it is present. The symbol is the access control: JSON.parse can produce no symbol key, so no stored or posted connection can decide the digest, which is the footing ProviderExecutionContext already has. It is not a field on DatabaseConnection, whose keys are an exhaustive write list for three readers that classify what a user may fill in. All three rewrite sites carry it: getOrCreateProvider, acquireExecutionProfileProvider and withOneShotTunnel. The bastion stays framed separately by tunnelRoute, so two databases behind one bastion and one database behind two bastions still answer different digests. A tunnelled provider built without the marker still answers the local endpoint and is still refused: the compare was not widened. Driven end to end against a real bastion forwarding to PostgreSQL 18.4: build, apply and restore of app.order_total(integer) all land, the two digests agree, and the pre-fix digest does not. That drive is kept as tests/live/ssh-tunnel-edit-plan.ts, opt-in and not collected by the core runner. The CI half drives both sides through the real factory, and the non-tunnelled digest is pinned byte for byte. * fix(objects): refuse a PostgreSQL edit whose text carries more than one statement A PostgreSQL object edit spliced the reader's text into one parameterless simple query, and PostgreSQL runs every statement in one of those. Measured live on 18.4: the definition of app.order_total(integer) followed by a semicolon and DROP FUNCTION app.r19f1_victim(); built with consequences: [], applied at HTTP 200 with a plain "applied", and the victim routine's count went 1 to 0, with the dropped routine named in neither object_edit audit event. The count is not ours to take. A dollar-quoted body may carry any number of semicolons and a BEGIN ATOMIC body carries them by construction, so buildObjectEdit asks the engine: it parses the submitted text alone as a named prepared statement inside a transaction block it has poisoned with SELECT 1/0, then rolls that block back. In an aborted block the server performs no parse analysis, no planning and no execution, and its multi-command check still runs because it precedes the aborted-block check in exec_parse_message. 25P02 is one statement and the build proceeds; 42601 refuses, with the engine's own sentence and position for a grammar error; anything else refuses unsupported, because the count was never established. A pooled client somebody else left in a transaction is refused rather than probed, since the rollback would destroy their work. applyObjectEdit now counts the results the round trip answered. A simple query answers one result per statement, so the emitted unit answers four, and anything else is interrupted with committed: "unknown" rather than a plain applied. That is what makes the audit event's claim as wide as the round trip. Measured through the provider on 18.4 on 2026-09-15: the rider is refused at build and the victim's count reads 1 before and after, while the legitimate edit, whose body carries a semicolon inside $function$, and a BEGIN ATOMIC body of two SELECTs both still build and apply. docs/providers/postgres.md and docs/SECURITY.md control 3.6 now describe the guard and say what it does not cover. The two postgres.ts line numbers object-route.ts cites moved with this diff and are updated with it. * fix(query): stop ending a transaction this route cannot name a5212482 gave POST /api/db/query a finally calling provider.endOpenQueryTransaction(). It closes the D74 leak serially and destroys other people's committed work under concurrent traffic, so it is removed and D74 stays open. endOpenQueryTransaction() acts on PostgresProvider.lastQueryClient, one field that query() overwrites on every call for every concurrent caller of the provider cached per connection id. The client it rolls back is whichever client anybody recorded last, not the client this request's statement ran on. Measured 2026-09-15 on PostgreSQL 18.4 through the shipped handlers, real provider and real cache, each arm twice with a pre-change control and nothing thrown. A plain SELECT pg_sleep(3) sent here rolled back a concurrent /api/db/multi-query script mid-flight: the script answered 200, hasError false, all four statements success including its COMMIT, and its CREATE TABLE was gone where the control keeps it. The same plain SELECT discarded an interactive POST /api/db/transaction session's committed CREATE TABLE while the status route still read inTransaction and commit answered "Transaction committed". The leak itself also survived the finally: the opener's client was left idle in transaction because another request had overwritten the pointer before its finally ran. The route carries the measurement as a comment and ends nothing. The tests now pin that a request here does not discard a transaction another caller of the shared provider opened, with the double's own ender called by hand as the control, and that the failure path invents no transaction verdict either. D74 needs query() to name the client its call borrowed, which is postgres.ts and not this route. * test(query): reset the shared provider double in beforeEach The cross-request test installs a persistent getOrCreateProvider double. Putting the default back at the end of that test meant a failing assertion would skip the reset and leak the double into every test after it. It is restored in beforeEach instead. * docs(db): correct four sentences the X23 fix made false, and guard two of them The X23 fix landed with three invented identifiers, four stale line citations and one claim a thirty-line probe falsifies. The mechanism is unchanged; only what the tree says about it is. WithTunnelFarEnd argued that the far end must not be a field on DatabaseConnection because three maps are exhaustive over keyof DatabaseConnection, and named none of the three correctly: CONNECTION_FIELD_RELEVANCE and CONNECTION_FIELD_CLASSES exist nowhere, and connectionFields is a per-engine UI field list in db-ui-config.ts. The real maps are FIELD_OWNERSHIP, CONNECTION_RELEVANCE and CONNECTION_FIELDS. The argument was right and now the pointers are too, held in a table the tests check from both sides: the docblock must name exactly the table, and every row must be a real Record<keyof DatabaseConnection, ...> declaration in the file it names. The same commit inserted 34 lines above the tunnel branch in factory.ts and left four factory.ts:485 pointers behind, two here and two in the test. They are renumbered and guarded the way object-route-edit.test.ts guards object-route.ts: the table names the anchor, the test derives the line. That guard caught its own first renumbering being stale. "The compare was not widened" is false in one case. createSSHTunnel pools on the connection id alone and TunnelInfo carries no far end, so the marker is the address the factory ASKED to forward to, never one read back. MEASURED against the live bastion: a provider built on a live tunnel's id with the record changed to db-elsewhere.invalid:6543 dialled the existing forward, answered libredb_dev at 172.23.0.2, and sealed db-elsewhere.invalid:6543, which is what the routes recompute, so that plan verifies for a machine the statement never reaches. Before X23 it was refused by an unequal digest rather than by a check. The three sentences that stated the far end as fact now state what the code enforces and name the limit; the fix needs TunnelInfo to carry what it forwards to, which is src/lib/ssh/tunnel.ts, and is filed. docs/SECURITY.md still described the pre-X23 frame in the paragraph that says a change to the frame is a change to this control, so a reader of the security posture page held exactly the belief X23 was filed about. security:check cannot see it: it resolves links and pins row ids and says in its own header that it does not read whether prose matches behaviour. * fix(postgres): narrow the non-execution claim, and pin the refusal order it rests on The review of 54801e35 measured two exemptions to "nothing can run here". On 18.4, inside a poisoned block, a NAMED Parse of COMMIT, ROLLBACK, END or ABORT answers no error at all, runs, and ENDS the block the check opened; and an empty, whitespace-only or comment-only text answers 25P02 from Bind rather than Parse, so the named statement is created and survives the ROLLBACK on a client that goes back to the pool. Neither class reaches the check today. The identity refusal answers two refusals earlier, because routineIdentityHeader renders the whole text when it finds no closing parenthesis and neither text renders this routine's header. So the ORDER of the six refusals is load-bearing and not only the saving the comment claimed, and moving the count check above the identity check would put both classes on the wire. The claim is narrowed to what was measured, the real reason for the order is written down, and two tests assert that neither class reaches the server, each with the control that the probe transaction was never opened. * docs(backlog): close D76 and X23, reopen D74 and D78 on measurement, file three Wave 2 of the Phase 3 follow-up. The orchestrator applies every backlog change so no two agents race the same file. Closed: D76, the PostgreSQL semicolon rider, and X23, the SSH-tunnelled connection that could never build a plan. Both were driven end to end against a live engine before being deleted. Reopened, and this is the honest half of the wave. D74's route-level fix was implemented, measured to be WORSE than the leak, and removed: copying the finally from /api/db/multi-query closes the serial case and destroys other callers' committed work under concurrent traffic, because endOpenQueryTransaction acts on a single lastQueryClient field that every concurrent request on a shared cached provider overwrites. D74 now carries that measurement and says no route can fix it. D78 gains the producer inventory the entry asked for, measured across three surfaces. Filed: - D86, a pooled SSH tunnel serves a far end the record no longer names, so after X23 both sides of the seal agree on a server the statement never reaches. Found by the review of the X23 fix, measured against the live bastion, and reachable from the ordinary UI. - D87, endOpenQueryTransaction names one shared pointer. This predates every entry here: /api/db/multi-query has carried that finally since #823. Three arms measured, each against a control, and two docblocks that say it cannot happen are refuted. - D88, a multi-statement text sent to the single-statement query route answers HTTP 500 after both statements have already run. Also corrects two factory.ts line citations the X23 commit's own insertion moved, derived from the anchors rather than copied. * docs(mysql,mssql): declare why endOpenQueryTransaction() is absent (D75) Neither provider can name the session its own query() ran on. mysql2 3.24.4 keeps no transaction flag on Connection or PoolConnection and surfaces SERVER_STATUS_IN_TRANS only on an OK packet, which a failed statement never sends; mssql 12.7.2 acquires and releases the tedious Connection inside Request.query(), so the Connection.inTransaction tedious does maintain never reaches the provider. Both docs now say which absence it is, and a test per provider pins the surface as absent and the doc as having declared it. * feat(redis): end a MULTI a statement left open on the cached connection The provider holds ONE ioredis connection and `getOrCreateProvider` caches the provider per connection id for the whole process, so a bare MULTI sent through query() belonged to whoever borrowed the handle next. Measured 2026-09-15 on redis 7.4.11 through ioredis 5.11.1: after a bare MULTI every later command on that connection answers the string "QUEUED" and does nothing, SET, GET and even CLIENT INFO alike, while a second connection is untouched. So the next user's command does not fail, it silently does not happen, and the schema explorer's SCAN is queued with it. The ask and the act are one call because they cannot be separated here: ioredis publishes no state for a MULTI sent through call(), and the server cannot be asked on the same connection because a CLIENT INFO inside the MULTI is itself queued. The server's own refusal of a DISCARD, "ERR DISCARD without MULTI", is the reading, and it costs the connection nothing. Any other DISCARD failure is raised rather than read as an answer. Part of D75. * fix(studio): refuse the new-tab shortcut while an object apply is in flight D82. The new-tab shortcut is registered on `document` by `StudioTabBar` so it works while Monaco owns focus (#745), and Radix's modal aria-hides the tab strip rather than removing it, so a keydown from inside the apply dialog reaches that listener. `addTab` ends with `setActiveTabId`, and the standalone shell renders the Source pane only for an active Source tab, so the new Query tab unmounted `ObjectSourceView` and the apply dialog with it after the statement had been sent. A `conflict`, a `refused` or a `failed` answer then landed on nothing: no dialog, no banner, no throw. Only `applied` survived, through `onApplied`. `ObjectSourceView` now publishes the in-flight window through the new optional `onApplyInFlightChange`, and the shell refuses the shortcut for as long as it is open, with a toast saying what the reader is waiting for. Refusing rather than keeping the pane mounted is the smaller change and it matches what the dialog already does in this window: while applying it withholds its close button and prevents Escape, a press outside and every other interaction outside. The reasoning, and the cost of the other shape, is written in the handler. The false reachability paragraph in `Studio.tsx` is corrected rather than deleted: it now states what was measured, and the stale-closure argument it carries is kept, because that is still why the clear reaches the right tab. Three tests in the standalone shell's source suite, all driving the shortcut from the dialog's own element: the refusal with the conflict landing afterwards, the control that the shortcut works again once the answer is on screen, and the release of the flag when the pane goes away mid apply. * docs(oracle): declare why endOpenQueryTransaction() is absent (D75) query() executes every statement with autoCommit: true and closes the pooled connection in its finally, so no transaction survives a statement for the next borrower to inherit. oracledb 6.10.0 publishes connection.transactionInProgress and its Thin close() rolls back an in-progress transaction before the session returns to the pool, but neither is reachable once the connection is closed. * docs(clickhouse,libsql,cassandra,druid): declare the endOpenQueryTransaction() absence (D75) None of the four holds a session a transaction could survive on, and each doc now says so in the entry's own words. ClickHouse pins no session_id and sends one HTTP request per statement; the libSQL transport closes its Hrana stream in the same pipeline and drops the baton; CQL has no statement that opens a transaction and cassandra-driver 4.9.0 declares no begin, commit or rollback; Druid SQL cannot write through this endpoint and the transport carries no session identifier. A test per provider pins the surface as absent and the doc as having declared which absence it is. * docs(trino): record the endOpenQueryTransaction() absence as unmeasured (D75) Trino carries a transaction on X-Trino-Transaction-Id, and the transport writes and reads neither that header nor the started-transaction one, so the provider holds nothing between statements to end. What a live coordinator does with a transaction whose id was dropped has not been run here, so the doc records the absence as unmeasured rather than arguing it into one of the other two. * style: wrap the three D75 absence assertions the formatter reflowed * docs(search): name the endOpenQueryTransaction absence on elasticsearch and opensearch One provider implementation serves both type-ids and neither grammar has a transaction verb, so there is nothing for the route to end. Measured 2026-09-15 through the provider: on Elasticsearch 9.1.4 BEGIN, START TRANSACTION, COMMIT and ROLLBACK each answer "mismatched input '<verb>' expecting {'(', 'DEBUG', 'DESC', 'DESCRIBE', 'EXPLAIN', 'SELECT', 'SHOW', 'SYS', 'WITH'}"; on OpenSearch 3.8.0 each answers "Query must start with SELECT, DELETE, SHOW or DESCRIBE: <verb>". A SELECT on the same provider straight afterwards still answers. The transport is one stateless POST per statement, so there is no session for a transaction to live on either. Each doc now names WHICH absence it is, and a test pins both halves: the method is undefined on the provider, and the doc carries exactly one of D75's three absences. Part of D75. * docs(mongodb): name the endOpenQueryTransaction absence, read from the driver MongoDB does have multi-document transactions on a replica set, so this is not an argument from the engine's name. A transaction lives on a ClientSession (startSession, session.startTransaction, session.inTransaction, mongodb 7.6.0) and an operation joins it only when that session is passed in its options. The provider holds a MongoClient and a Db and never creates a session, and query() dispatches over a closed eleven-operation set. Measured 2026-09-15 against MongoDB 8 in a replica set: startTransaction, commitTransaction, abortTransaction, runCommand and bulkWrite are each refused with "Unsupported operation: <name>", and while a separate client held an uncommitted insert inside an open session transaction, this provider's count answered 0. Part of D75. * docs(couchbase): name the endOpenQueryTransaction absence, measured on 8.0.2 CE SQL++ transactions do work over REST, so this was measured rather than assumed. On Couchbase Community Edition 8.0.2, 2026-09-15, against POST /query/service: BEGIN WORK answers a txid with transactionRemainingTime 15s, so the transaction is named by that id and not by a connection; a SELECT 1 straight afterwards without the txid answered success with no transactionRemainingTime at all, so it was not inside it. The transport never sends a txid, so nothing this provider sends can join one, and there is nothing for it to end: ROLLBACK WORK with no txid is refused with code 17002, and reusing the txid after the window answered code 17010 "Transaction timeout", so the cluster ends it itself. Part of D75. * docs(libredb): name the endOpenQueryTransaction absence The command grammar is a closed set of five verbs and the dispatcher refuses everything else by name. Measured 2026-09-15 through the provider against @libredb/libredb: begin, commit, rollback, transact and start transaction are each refused with "Unknown command <verb>. Supported: get, put, delete, prefix, range", and a put straight afterwards still writes. The kernel's transact() is a single atomic call rather than a session that stays open, and no verb reaches it. Part of D75. * fix(objects): build the PostgreSQL fork fixture, and correct six sentences D76 left behind The fixture half is D77. docker/postgres-init/03-object-fixture.sql held no routine whose parameter list closes after a closing parenthesis inside a string literal, so the identity header's parenthesis scan, and the applied-elsewhere outcome it exists to keep a reader away from, had no live producer here at all: the seven shapes the suite measures were constants, and the only producer of applied-elsewhere on this engine was a driver double. The fixture now builds app.dl(a text DEFAULT ')', b integer DEFAULT 1), and two tests bind the SQL to those constants so the object cannot be deleted quietly. Measured against the rebuilt container on 18.4, driving the real provider: md5(pg_get_functiondef(oid)) answered 0c7937f7060d7bc0c2a583af49227203, which is the constant the suite carries; the same edit changing the later parameter to bigint was refused identity with both whole headers; and a plan built elsewhere carrying that fork applied, raised LB003 from the emitted unit's own post-condition, answered applied-elsewhere with undone: true, and left one pg_proc row with oid 16782 and xmin 837 unmoved, against the control of the same plan without the fork, which answered applied. The rest is the wave 2 review of D76. src/lib/audit.ts and tests/security/object-edit-audit.test.ts both gave "this repository has measured itself unable to count the statements in a routine body" as the reason the object_edit claim is narrow, and cited a posture-page limit the posture page no longer states. Both paragraphs are rewritten to reason from what is true: the count is not taken here, it is asked of the engine on both sides of the plan, and what survives is that a count says how many statements ran and never which. A new test reads docs/SECURITY.md and asserts every claim those two docblocks cite it for, so the citation cannot dangle again. docs/SECURITY.md and docs/providers/postgres.md carried the unqualified claim that the server performs no parse analysis, no planning and no execution in an aborted block. Both are narrowed to the population it was measured over and both now name the exemption and the refusal order that keeps it away, which is what 31bfa6b2 did for the provider. The poison constant's own docblock is narrowed with them. The malformed refusal's no-position arm had no test: every 42601 the suite made carried a position, bun folded the ternary, and the line gate could not see it. It is driven now, and the arm was mutated to prove the test discriminates. Two docblocks in postgres.ts disagreed about whether an apply-time copy of the single-statement check would guard an empty population, and docs/providers/postgres.md repeated the empty reading. Resolved the other way, which is the one the apply's own entry guard already names: the population is a @libredb/studio consumer building its own plan, applyObjectEdit sends plan.unit.steps[0].text verbatim, and the result count reports such a round trip rather than preventing it. The interrupted arm carries a count mismatch outside what src/lib/db/types.ts documents it as. The deviation, why no other arm fits, and why adding a seventh on the provider side alone would be worse are written into the provider and the doc. Neither types.ts nor ApplyPreviewDialog.tsx is this slot's file, so the sentence that is still wrong there is reported rather than edited. The D76 test set is thirteen tests of which nine fail pre-fix, and the fourth that passes is labelled where it sits. * fix(studio): refuse the palette's table rows in the apply window too, and enumerate the door Review of D82 found the docblock it added replaced one false reachability claim with another. "This window already refuses every way out; the shortcut was the one way out the dialog could not reach" is false: `CommandPalette` registers a SECOND document-level listener, for Cmd/Ctrl+K, so it fires from inside the apply dialog, focus moves into its input, and selecting a table calls this shell's `onTableClick` -> `handleTableClick`, which ends with `setActiveTabId(newId)` and unmounts the pane and the dialog. Reproduced with the real palette over a held apply: the dialog went away and the conflict never rendered, which is the D82 consequence exactly. The door is closed rather than only recorded. Both gestures that open and activate a tab now share one refusal, `refuseWhileApplying`, so they cannot drift apart in wording or in rule. All ten palette entries were then driven from inside the held window: every one leaves the dialog standing and the conflict reaches the reader. The docblock now enumerates what was measured instead of asserting that nothing else can reach the window, including the three entries that were not measurable here because they leave the page through a router the harness doubles. The toast also stops telling the reader to try again "once the answer is on screen", which is the moment the refusal ends: acting on that wording unmounts the pane and throws the answer away. `ObjectSourceView` is mounted by two shells and its docblock told both that D82 was answered. `StudioWorkspace.tsx` passes no `onApplyInFlightChange`, hands the tab bar the bare `tabMgr.addTab` and mounts the pane only for an active Source tab, and a probe on that shell with the host's `apply` held loses the conflict. The pane now names which shell refuses and which does not, and the optional prop is labelled as a shipped gap rather than a convenience. The dangling D82 citation in the pane's own build-window test is corrected on the same reading. Two tests, both driving the palette's table row from inside the apply window: the refusal with the conflict landing afterwards, and the control that the row opens its tab once the answer is on screen. * fix(redis): ask before discarding, and stop claiming the editor path is closed Two defects the D75 review found in a50d10d4. A blind DISCARD is refused on an ordinary read-only ACL. Measured on redis 7.4.11 with `ACL SETUSER ro on >pw ~* +@read +ping +info`: PING answers PONG and DISCARD answers "NOPERM User ro has no permissions to run the 'discard' command". `POST /api/db/multi-query` awaits this call in a finally, so the raise replaced a response that already carried the statement results it had earned. The ask and the act are separable after all, which the old comment denied. The server answers the status QUEUED instead of a command's own reply while a MULTI is open, so PING reads the state without changing it: PONG means none and no DISCARD is sent, QUEUED means one is open and a DISCARD follows. Measured live through this provider: the read-only user now answers none and keeps its connection, while an ACL that can MULTI but not DISCARD (+@read +ping +multi +set) still raises, and must, because that transaction is open and unclosable. The server's own "DISCARD without MULTI" is read as rolled-back only AFTER the ask saw QUEUED, where it means another caller on this shared connection ended it in between. The doc claimed a MULTI typed into the editor is ended here. It is not: the surface has one caller, POST /api/db/multi-query, and use-query-execution.ts gates that route on dialectIsSql, which this provider's queryLanguage "json" makes false. An editor run goes to POST /api/db/query, which ends nothing on purpose (D74). 5.2a and the supportsTransactions row now say which route ends it and which does not, and a test pins both the capability and the sentences. * docs(providers): name redis as an implementer, and file couchbase under the right absence Five D75 absence sections written in this wave said the surface is implemented on postgres, sqlite and duckdb, after the redis commit earlier in the same wave had made that false. They now name redis too, and each type-id's test pins the enumeration over collapsed whitespace so re-wrapping the paragraph cannot turn it red. Couchbase was filed under "the engine has no transaction to leave open" while its own measurements record the opposite: BEGIN WORK answers a txid and holds a cluster-side transaction for transactionRemainingTime 15s, so a statement run through query() does leave one open. The real absence is "the driver cannot be asked": the HTTP transport sends statement, timeout, metrics, scan_consistency and at most query_context, args, readonly and profile, never a txid, so nothing it sends can name or end that transaction. The section now also records that this is unbuilt work rather than a missing engine feature, since toQueryResult already hands back the results array the txid arrived in. * docs(providers): measure the server-side transaction ask on mysql and mssql (D75) The D75 sections argued the absence from the handle: "a pool cannot be asked what a session it already handed back is doing". That is false, and the mysql provider refutes it itself - cancelQuery() addresses a released session by name with KILL QUERY. So the sections shut a door nobody had tried. Measured instead, against live servers. MySQL 8.0.46 with mysql2 3.24.4, asked after release() keyed on the session's threadId. performance_schema.events_transactions_current joined to performance_schema.threads on processlist_id reports ACTIVE for BEGIN alone, BEGIN plus a failing statement, and BEGIN plus INSERT plus a failing statement, and the terminal state for the control. information_schema.innodb_trx answers nothing for the first two, because InnoDB registers a transaction only once it does InnoDB work, so it misses exactly the failed script this surface exists for. SQL Server 2022 (16.0.4265.3) with mssql 12.7.2, asked from a second pool keyed on the session's SPID. sys.dm_exec_sessions.open_transaction_count is 1 and sys.dm_tran_session_transactions has a row with and without a failing statement, 0 and no row for the control. A resolved Request carries parent and no connection, so the provider never learns the session id to key that on. Both labels stay "the driver cannot be asked", which is what was measured: mysql2 publishes no transaction flag on a connection, and mssql.Request never hands one over. What the sections no longer claim is that nothing else can be asked. mysql also records why the measured ask is not enough on its own: the type-id serves MariaDB, where performance_schema is OFF by default and answers NULL rather than failing. Also drops the closed implementer list "postgres, sqlite and duckdb" from all eight sections. redis implements the surface too, so the list was stale the day it was written; the set is read from the type now. mssql's "the only supported way to pin one" is corrected as well: ConnectionPool.acquire is public and documented. * fix(objects): make the applied-elsewhere double discriminate, and drop a backlog citation that points at nothing Two findings from the wave 3 review of f54afea8. The in-suite `applied-elsewhere` case raised LB003 whatever SQL it was handed, so the fork spliced into the unit changed nothing and the outcome came from APPLY_VERDICT_BY_SQLSTATE alone. Measured: replacing `text: forked` with `text: step.text` left the test passing, 2 pass 0 fail. The double now answers the question the emitted post-condition DO block asks, whether the routine the plan addresses is the routine the text rewrote, and the test drives the unforked plan as a control. Re-measured after the change: the same unfork takes it red with "expected a fork, got applied". Both the provider docblock and docs/providers/postgres.md said the interrupted-arm residual was "in the backlog". docs/BACKLOG.md carries no entry for it, so a reader following either sentence finds nothing, which is the class the same commit was sent to repair. Both sentences now say where the residual really is recorded, and a new guard asserts the general rule: every backlog mention in those two files must name an entry docs/BACKLOG.md holds, the way the cancel-mapper sentence already names B4. It listed exactly those two sentences before the correction. No executable source line changes; the provider diff is two comment lines and the cited anchors at 3017 and 3174 do not move. * fix(objects): bind the whole app.dl statement, and correct three coordinates the review caught The three minor findings from the wave 3 review of f54afea8, all in this slot's own files. The fixture-to-suite binding matched two substrings that both stopped at the parameter list's closing parenthesis, so `RETURNS bigint` or a body of `SELECT 2` in the SQL file left every assertion green while `definition` and `revision` stopped describing what the engine renders. Each piece of the statement is now asserted on both sides, in the two spellings the two sides legitimately have. Measured by mutating the fixture to `RETURNS bigint` and `SELECT 2`: the test goes red naming the return type, and was restored. Three coordinates: `app.order_total(integer)` and `app.touch_order` are built by 02-sample-data.sql and not by the fixture file that listed them, `app.touch_order` is a procedure taking `order_id integer`, and the "130 lines below" in the multi-statement docblock was a hand-copied number. It now says where rather than how far, which is the form that cannot go stale. docs/SECURITY.md's build-side paragraph had swallowed the pre-existing apply-side sentence. It is its own paragraph again, with its own lead. The provider diff is comment-only and the two anchors object-route.ts cites stay at 3017 and 3174. * fix(db): add redis to the implementer list, and derive it so it cannot go stale Three repairs the wave left across slot boundaries. The required tests/unit/lib/api/object-route-edit.test.ts was red: four citations in object-route.ts named lines that later commits had moved. Each is re-derived from the anchor the test itself uses, in its own paragraph, because two cited sites share a path prefix and a blanket substitution rewrites both to a single value. D75 gave redis the endOpenQueryTransaction surface, and two prose sites still named only postgres, sqlite and duckdb. Eight provider docs were corrected around them; these two had no guard, which is why they survived a whole wave. The new test derives the set from the provider sources and asserts each site's enumeration names exactly it. That test took three narrowings, each forced by a mutation that should have failed and did not. Reading the whole file was vacuous, because the prose names redis elsewhere. Taking the first sentence carrying the verb picked queryReadOnly's docblock, which says the same words about another surface. Taking the whole sentence was vacuous again, because the types.ts sentence runs on past a colon and names redis a second time. The claim is the enumeration before the verb, and dropping a name from either site now fails. And two docblocks said the interactive session can never be lastQueryClient. D87 measured the opposite: beginTransaction takes a client from pg's LIFO idle list and is handed the object a previous query() recorded. Both now say what was measured and point at D87. * docs(backlog): close D75, D77 and the standalone half of D82, file three Wave 3 of the Phase 3 follow-up, applied by the orchestrator so no two agents race the file. D75 is closed. All fourteen type-ids that did not implement endOpenQueryTransaction now answer: redis implements it, and thirteen declare which of the three absences it is, each in its own provider doc with what was read or measured to reach it. D77 is closed: the fixture builds app.dl, whose parameter DEFAULT holds a closing parenthesis inside a string literal, so applied-elsewhere has a producer this repository creates rather than only a driver double. D82 keeps the half that is still open. The standalone shell refuses both document-level gestures that move the active tab during an apply, the shortcut and the command palette's table rows; the embedded shell still loses the answer, measured on that shell rather than inferred. Filed: - D89, a count mismatch is reported on the interrupted arm, whose documented meaning says the engine never answered. The provider weighed a seventh arm and left it, because ApplyPreviewDialog has no exhaustiveness check and an arm added on the provider side alone would render as a clean success, which is worse. Both ways out are costed. - D90, three type-ids declare an absence that is not final. mysql and mssql were measured live at the SERVER, which can be asked (performance_schema ACTIVE against a ROLLED BACK control on MySQL 8.0.46; open_transaction_count 1 against 0 on SQL Server 2022), and neither was implemented on that ask for a reason each doc records. trino's code side is settled and its live side is unmeasured. - X27, the published shortcut description names one of its two exceptions. Four sentences in provider docs and one docblock promised "this is filed as its own change" and named nothing. They now name D89 and D90. * docs(shortcuts): name both windows that refuse the new-tab shortcut The published description named only the rename input. D82 added a second, equally transient refusal: the shortcut is declined while an object apply is in flight, so the shipped shortcut table under-described the shortcut. docs/FEATURES.md is regenerated by scripts/sync-shortcuts.mjs, not hand-edited. The unit suite now asserts the exact description string, so the two exceptions cannot silently diverge from the guard again. * fix(workspace): refuse the new-tab shortcut while a host apply is in flight (D82) The embedded shell rendered StudioTabBar with the bare tabMgr.addTab and passed onApply with no onApplyInFlightChange, so the one document-level listener that moves the active tab fired from inside the apply dialog. Measured: with the host's apply held between Confirm and the answer, the strip gained a Query tab, the dialog was gone, and the held conflict rendered nothing at all. The shell now mirrors the pane's in-flight window and wraps onAddTab the way Studio.tsx does. The refusal is rendered by this shell as a live region rather than toasted: sonner needs a mounted Toaster, this package re-exports none and this shell mounts none, so a toast here would be a line no adopter can see. The palette half of D82 does not apply: this shell renders no CommandPalette, and the only other global listener, in ui/sidebar.tsx, is on window and toggles a sidebar rather than opening a tab. * feat(workspace): let a host tell the embedded workspace the catalog moved (D79) Every statement in the embedded shell leaves through onQueryExecute, which answers a result set and never says what the statement changed, so a CREATE OR REPLACE run in the query editor left every open Source tab showing the pre-apply text with no stale banner. The catalog-change counter saw only an object apply the workspace itself issued. StudioWorkspace now publishes StudioWorkspaceHandle through the ref prop, with one member, catalogChanged(). A handle rather than a field on the onQueryExecute answer: a field can only describe a statement the workspace issued, and the party this is about is the host, which also runs migrations and schema editors this package never sees. A host that wants the per-statement behaviour calls the handle from inside its own onQueryExecute, so the field is reachable from the handle and not the other way round. The announcement marks open Source tabs stale and re-reads nothing: it carries no address, so clearing a tab would throw away a definition that may still be current. An apply the workspace issued still clears its own tab, because that one is addressed. The mount's docblock sentence about what an absent banner means moves with the change. * fix(postgres): bind endOpenQueryTransaction to the caller's own call scope (D87, D74) `PostgresProvider.endOpenQueryTransaction()` acted on `lastQueryClient`, one field that `query()` overwrote on every call. `getOrCreateProvider` caches one provider per connection id for the whole process, so that field was shared by every concurrent request on a stored connection and the ender rolled back whichever client anybody recorded last. Measured 2026-09-15 on PostgreSQL 18.4 through `pg` 8.23 and the shipped route handlers, with only the session mocked, each arm against a control: - A plain read's ender rolled back a concurrent `/api/db/multi-query` script mid-script. The script answered 200, hasError false, all four statements "success" including its COMMIT, and its table was gone. Control without the ender: the table exists. - `beginTransaction()` was handed the very client a previous `query()` recorded, pg's idle list being LIFO, so the ender rolled an interactive session's committed CREATE TABLE away while commit still answered "Transaction committed". - The leak it exists for survived anyway: request A `BEGIN; SELECT pg_sleep(2)` with request B 700 ms in left A's own client idle in transaction, and the next CREATE TABLE answered 200 and was invisible to an independent reader. The caller now names its call scope. `query()` takes a fourth argument, the provider records the client it borrowed under that scope and only when the server's ReadyForQuery byte says "T" or "E" at the moment the call releases it, and `endOpenQueryTransaction(scope)` ends exactly those clients and spends the scope. A statement that left nothing open puts nothing within any ender's reach, which is what takes the interactive session out of range. With that in place `POST /api/db/query` gets the finally D74 asks for: it mints a scope, runs its statement under it and ends it, so a lone BEGIN no longer releases a pooled client in status T for the next signed-in user of that connection to run inside. Re-driven live, all three arms above are clean and the serial leak closes: the BEGIN is told "rolled-back", no backend is left idle in transaction, and the next request's write is visible to an independent reader. The implementer shape check moves from the multi-query route to `types.ts`, now that two routes ask it, and the two false docblock sentences about the interactive session go with the fix. `sqlite`, `duckdb` and `redis` hold one session for the whole provider, take no argument and are unchanged. Two coordinate guards follow the moved lines. * fix(objects): give the source pane the retry the tree has, and read the whole console (X22, X26) Recovered from a wave-4 agent the session limit killed mid-task. Green on format, lint, typecheck and the pane's own 100-test suite; the E2E run that would close X22 and X26 has not been driven yet, so this commit is the work and not the verification. X22's cause is named rather than waited out. The restored-tab test failed its first attempt on both CI runs it has had, because every test in the file signs in as one shared account and that account hits the 120-requests-per-60-seconds query bucket, whose window is longer than the assertion's 30 seconds. The old helper reloaded the page to get back, which re-hydrates the whole application against the very budget it is waiting for. Behind that sat a real product gap. The object tree has always offered tree-retry when its read is refused; the source pane drew the engine's sentence and no control at all, and a tab holding a failure never re-reads, because needsRead is document === undefined && failure === undefined. So a reader whose read was refused had no way back except reopening the tab. The pane now offers Try again, bound to the arm that a second ask can actually answer. X26 reads the whole console across a real apply, not one known string: a guard over one message is a guard over a population of one, which is how X21's Monaco line went unnoticed. The allowlist is keyed on origin as well as text, because Chromium's resource-load failure names no URL in the message. It carries one measured entry, the object tree's first read answering 400 on /api/db/objects/counts before provider-meta lands, which is filed rather than excused silently. The blocked-resource-load test doubles as the watcher's control, so the empty array the apply test asserts is a clean page and not a dead listener. * fix(workspace): pin the refusal's retirement and put it where it can be announced (D82) Wave 4 review of 7fe83dcc, findings F1, F2, F3, F5, F6 and F7. The retirement line in the apply-in-flight mirror was shipped behaviour that no test pinned: deleting `if (!inFlight) setRefusedWhileApplying(false)` left the file at 51 pass, 0 fail, because the two tests whose comments spoke about it never raised the refusal at all. One new test raises it and asserts it is gone once the answer lands, and the unmount test now raises it too. With the line deleted both fail; with it back the file is 52 pass, 0 fail. The refusal is now portaled to `document.body`. Radix aria-hides everything outside the modal, so the `output` rendered inside the workspace box was on screen and absent from the accessibility tree: measured, `queryAllByRole("status")` answered the pane's applying region alone. `aria-hidden` marks once and installs no observer, so a body child created after the dialog opened is not marked; portaled, the region is returned by role, which the test now asserts. Being a body child is also what makes the z-order argument hold: both layers sit in the root stacking context, so a transform, filter or contain on the host's wrapper can no longer become this element's containing block. The remaining cost, a fixed box the host cannot place, is written down where it is paid. The listener enumeration was false. There are four keydown listeners in `src`, not two, and `DataProfiler` is mounted by this shell; the conclusion holds and each one now says why. The counter docblock opened with its pre-D79 claim in the present tense and corrected itself fifteen lines later. It opens with what it counts now. * docs(object-source): correct two paragraphs that still assert the defect D82 closed Wave 4 review of 7fe83dcc, finding F4. Both paragraphs said the embedded shell passes no `onApplyInFlightChange` and that an embedded host therefore keeps losing a conflict, a refused or a failed answer. It passes it as of 7fe83dcc, and the surrounding reasoning rested on the false clause, so the paragraphs are rewritten rather than trimmed: the `previewHere` binding is defence in depth against a shell that does not pass the callback, which is no longer either shipped shell, and the optional prop's undefined arm now has no shipped caller at all. Making the prop required is named as the option it is, with what it would cost. * docs(workspace): replace three unmeasured comment claims with what a probe answers Wave 5 review of the D82 repair found the prose wrong in the class the commit was written to repair. Three fixes, each backed by a run rather than an argument. The portal docblock claimed "everything else that floats in this shell reaches the body the same way, through a Radix portal". QuerySafetyDialog and DataProfiler, both mounted by this shell, are in-place fixed inset-0 z-50 divs, and DataProfiler records staying in the subtree as a deliberate choice. The paragraph now says that, and says the portal here is what the live region needs rather than house style. The same docblock claimed "nothing inside the workspace box can paint above it". Measured in Chromium with the two layers reproduced: with the shell root as the tree has it, elementFromPoint at an in-box fixed z-[60] child answers the child, not the body-level z-50 overlay. The control arm, the same probe with a transform on that root, answers the overlay. So the in-box placement 7fe83dcc shipped was readable and what it cost was the live region; the containing-block risk the portal removes is the host's wrapper, which is what the paragraph now states. While rewriting the cost paragraph, the one cost it omitted is now in it: STUDIO_SCOPED_CSS scopes the font stack and letter-spacing to [data-studio-workspace], so the body-level node renders in the host page's face at letter-spacing normal, measured both ways. The colour tokens are on :root and survive. The control test's comment printed "of the D82 tests only the refusal test above fails", a count this describe's fourth test invalidated. Re-measured by replaying the current file against 7fe83dcc^ source: 3 fail, 1 pass, the pass being the control. Comments only. No executable line added or altered, so coverage cannot move. * fix(query): render the rolled-back notice a lone statement now earns 0448fe0b gave /api/db/query the openTransaction field when the ender learned to name the caller's own call scope, and the route's comment claimed the client renders the notice "from the field's presence alone". Measured false by the review of that commit: the notice sat inside use-query-execution's multiStatement branch, and a single statement never sets that flag. So the field was answered and never rendered for its whole first commit. A reader who typed a lone BEGIN had it rolled back in silence and their next statement autocommitted instead of joining the transaction they had asked for, which is the exact harm the notice beside it says it exists to prevent. The notice is now computed once and raised on both paths, with its own toast on the lone-statement path because that path has no summary toast to append to. Two tests: the notice on a lone statement, and the control that the same endpoint and the same lone statement say nothing without the field. The first fails when the new branch is removed. Also corrects the standalone shell's listener enumeration. Studio.tsx said there are TWO document keydown listeners; there are four registrations and three on document, because DataProfiler registers one and this shell mounts it. The conclusion is unchanged, since that handler is bound only while the profiler is open and only closes the profiler, …
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Phase 3 of #789, from discussion #778. Phases 1 and 2 gave the object tree and the read; this makes the definition editable.
A reader can now open an object's source, edit it and apply it, on four kinds across three engines: PostgreSQL functions and procedures, Trino functions, Redis functions. Every apply goes through a mandatory preview, and the bytes drawn there are the bytes the engine receives: the build mints a sealed plan and the apply takes only that plan.
The other fourteen type-ids declare no editable kind, and each provider doc says which absence it is: no replace in place, a catalog we cannot address, or a strategy that would destroy something a success never showed. Per engine, never a blanket "not supported".
New surface: two routes under
/api/db/objects, anobject_editaudit arm, security control 3.6, nineteen additive exported types and an optional host editor. No version bump, since the change is additive and 0.16.0 is unreleased.Residual, filed as D76: on PostgreSQL the reader's text travels as a multi-statement round trip, so a semicolon can carry a rider the plan never named. Refused on Trino and Redis, sealed and shown on PostgreSQL.
Do not merge yet.