Skip to content

fix(lokee): revert only against the current database, snapshot first, and say it in the reader's words - #273

Merged
huyplb merged 6 commits into
mainfrom
feat/lokee-revert-direction-and-snapshot
Aug 18, 2026
Merged

fix(lokee): revert only against the current database, snapshot first, and say it in the reader's words#273
huyplb merged 6 commits into
mainfrom
feat/lokee-revert-direction-and-snapshot

Conversation

@huyplb

@huyplb huyplb commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Four commits taking schema history from "works" to "shippable". Two are correctness bugs; the rest is the launch-readiness pass.

1. What you review is not what runs

The diff came from compareLokeeVersions(original, target) while the plan and the execute call went to planLokeeRevert(original) — which reverses from the live head and ignores Target entirely. With an older version on Target you reviewed one script and Execute applied a different, usually larger one.

Reproduced in the UI: a dialog headed "Original Version 10 → Target Version 13" above a button that would have applied v15 → v10.

A revert restores the live database, so it is only coherent when Target is the newest version. Anything else is refused, with a one-click Use current database to fix it. An open dialog now follows the picker instead of holding the pair it opened with — otherwise that button changed the bar behind the modal and appeared to do nothing.

2. Snapshot before touching anything

The revert route now captures the live schema first. The restore point matters, but the correctness win matters more: planRevert reverses from the newest captured version, not from what is in the database, so a schema edited by hand since the last capture was being reversed against a picture that no longer existed.

When the snapshot finds drift the request is refused (schema_drifted), not applied — the caller reviewed a plan built on the old head. Verified against the live API:

result
No drift → revert v2 → v1 ok=true, column dropped
Hand-add a column, retry schema_drifted, nothing applied, drift kept as its own version

3. The loudest warning was on the safest operation

ReversalRisk defines lossy as "succeeds but destroys or truncates data". Re-creating a dropped column does neither — it adds one back, empty, because the rows went when it was dropped. Classifying it lossy meant a plan of nothing but ADD COLUMN came back risk: lossy, displayed "This revert destroys data", and demanded a data-loss acknowledgement — on the single most common thing anyone does with version history: bringing a database that has fallen behind back up to date.

Verified on a database three versions behind: risk: lossy, lossyCount: 2risk: safe, lossyCount: 0, executes without confirmation, both columns restored. The note explaining the column comes back empty stays; the gate does not.

This is also what makes migrate any version → latest usable — the mechanism was already there, buried under a false alarm.

4. Launch-readiness pass

  • A deleted column was typed table. objectType: row.object_type ?? 'table', and the delta row carries no type — so a deleted column drew with a table's icon and slipped through the table filter as a phantom table. The key already says what it is.
  • Compare keys off the cards. ORDERS.NOTE is an uppercased match key that exists nowhere in the user's database (CLAUDE.md is explicit about this). Cards read NOTE, with deleted from orders beneath.
  • Filters that can change what you see. The type list was the union of all dialects, so SQLite users saw MQT — a Db2-only term — beside Procedure and Function boxes that could never match. Now offers only types the history contains: 8 checkboxes down to 2 on the SQLite demo.
  • Words, not internals. "reused" → "unchanged" throughout; "Content-addressed schema history (Lokee)" → what the feature does; header reads "Schema history" everywhere.
  • A first run you can act on. The empty state was a paragraph pointing at a button on another bar. It now holds the database picker and Take first snapshot.
  • The run button names its destination — "Update to v2" when nothing is destroyed, "Revert to v2" when something is, instead of "Execute migration (3)".
  • The author line earns its place — shown only when versions actually have different authors, rather than the same address down the whole column.
  • [sqlite] /tmp/app.db.mainsqlite · /tmp/app.db · main; the dotted suffix read as a file extension.

5. Licensing

The React Flow attribution badge is hidden. @xyflow/react is plain MIT (node_modules/@xyflow/react/LICENSE) and its only condition is that the notice accompany the software — the badge is not a licence term. xyflow asks that you subscribe to Pro when hiding it; the project has chosen not to.

That surfaced the condition that was unmet: the client bundles 38 third-party packages under MIT/BSD/ISC, all requiring their notice to travel with the code, and NOTICE carried only Fox Schema's own Apache-2.0 terms. Added THIRD-PARTY-NOTICES.md (generated by scripts/generate-third-party-notices.mjs), un-ignored it — .gitignore blanket-ignores *.md and would have kept it out of the repo — and the Dockerfile now copies LICENSE, NOTICE and it into the image.

Only gap: @google-cloud/secret-manager ships no licence file of its own; its entry names the licence and links upstream.

Reviewer notes

  • The e2e type-filter assertion was updated deliberately: it asserted Procedure and Function filters are visible on a SQLite seed that has neither. It now asserts the types the seed actually has, and that MQT/Procedure are absent.
  • Reclassifying re-create as safe removes a confirmation gate. That is the intended effect — the operation destroys nothing — but it is the change most worth a second opinion.

Gates

npx tsc --noEmit clean · npx vitest run 1955 pass / 2 expected-fail · eslint 0 errors.

🤖 Generated with Claude Code


Note

Medium Risk
Revert and snapshot logic on the live schema path can block or apply DDL incorrectly if drift or version targeting is wrong; reclassifying re-create as safe removes a confirmation gate on catch-up plans. Licensing and Docker copy changes are low operational risk but affect compliance artifacts.

Overview
Revert correctness ties the compare modal to what actually runs: execute only when Target is the newest captured version (otherwise block with Use current database), keep an open compare dialog synced with the bar’s pair, and label runs as Update to vN vs Revert to vN based on whether the plan destroys data. The revert API now snapshots the live schema first and returns schema_drifted when drift is found so reviewed plans are not applied against a stale head.

Reversal risk reclassifies re-creating dropped tables/columns as safe (not lossy) so catch-up ADD COLUMN plans skip false data-loss confirmation; explanatory notes about empty re-created objects remain.

History graph and labels infer object type from delta keys (fixes deleted columns showing as tables), split compare keys on cards via objectDisplayName, offer only object-type filters present in the history (offeredObjectTypes), and refresh copy (unchanged vs reused, schema history subtitle formatting, empty-state Take first snapshot, compare-only version labels).

Distribution adds THIRD-PARTY-NOTICES.md and scripts/generate-third-party-notices.mjs, tracks the file in .gitignore, extends NOTICE, copies LICENSE/NOTICE/notices into the Docker image, and hides React Flow’s on-canvas attribution (hideAttribution: true) with MIT notice in third-party file.

Tests update e2e SQLite credential helpers and schema-history type-filter expectations; vitest covers revert targeting, run-button labels, and graph display helpers.

Reviewed by Cursor Bugbot for commit 8299992. Bugbot is set up for automated code reviews on this repo. Configure here.

huyplb and others added 4 commits August 18, 2026 16:04
…shots first

Three changes to the revert path, one of them a correctness bug.

**What you review is now what runs.** The diff came from
`compareLokeeVersions(original, target)` while the plan and the execute call
went to `planLokeeRevert(original)` — which reverses from the *live head*,
ignoring Target entirely. With an older version on Target the reader reviewed
one script and Execute applied a different, usually larger one. Reverting
restores the live database, so it is only coherent when Target is the newest
version; anything else is refused, with a one-click "Use current database" to
fix it. An open dialog now follows the picker instead of holding the pair it
opened with, so that button visibly does something.

**Snapshot before touching anything.** The revert route captures the live schema
first. That leaves a version to come back to, but the reason that matters is
correctness: `planRevert` reverses from the newest *captured* version, not from
what is in the database, so a schema edited by hand since the last capture was
being reversed against a picture that no longer existed. When the snapshot finds
drift the request is refused (`schema_drifted`) rather than applied — the caller
reviewed a plan built on the old head, and running a different one silently is
the surprise this exists to stop. Verified live: a hand-added column is captured
as its own version and the revert leaves the database untouched.

**The version menus say which choices cannot be reverted.** The newest version
on Original reads "current, nothing to restore"; an older version on Target
reads "compare only". Both stay selectable — they are legitimate comparisons —
but they no longer look identical to a choice that can run and then dead-end at
a greyed-out button with the reason hidden in a tooltip.

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

A launch-readiness pass over the schema history module, reading it as someone
seeing it for the first time.

**One real bug, found by reading a card.** A deleted column came back from the
graph DTO typed as `table` — `objectType: row.object_type ?? 'table'`, and the
delta row carries no type. So a deleted column was drawn with a table's icon and
colour, and slipped through the default table filter as a phantom table. The key
already says what it is (`column:ORDERS.NOTE`), so the fallback now reads the
kind from the key.

**Names the database actually uses.** Children were labelled with their compare
key — `ORDERS.NOTE` — which CLAUDE.md is explicit is an uppercased match key and
never an identifier. Cards now read `NOTE` with `deleted from orders` beneath.

**Filters that can change what you see.** The object-type list was the union of
every dialect, so a SQLite user got MQT — a term only Db2 uses — beside
Procedure and Function boxes that could never match. It now offers the types the
history contains; a type the user ticked themselves stays. On the SQLite demo
that is 8 checkboxes down to 2.

**Words, not internals.** "reused" was the store's vocabulary for one object
pointed at by many versions; a reader is asking what moved. Summary reads
"12 unchanged", the legend "Unchanged", the edge key "Unchanged from previous
version". "Content-addressed schema history (Lokee)" — an internal codename and
an implementation detail — is now what the feature does. The header says Schema
history everywhere.

**A first run you can act on.** The empty state was a paragraph pointing at a
button on another bar: the one screen where a newcomer has nothing to act on was
the one screen with no action on it. It now holds the database picker and a
"Take first snapshot" button.

Also `[sqlite] /tmp/app.db.main` → `sqlite · /tmp/app.db · main`; the dotted
schema suffix read as a file extension.

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

The last launch blockers in schema history, all found by walking the flow the
user described — a database that has fallen behind, brought back up to date.

**The loudest warning was on the safest operation.** `ReversalRisk` defines
`lossy` as "succeeds but destroys or truncates data". Re-creating a dropped
column does neither: it adds one back, empty, because the rows went when it was
dropped. Classifying it lossy meant a plan of nothing but ADD COLUMN came back
`risk: lossy`, showed "This revert destroys data", and demanded a data-loss
acknowledgement — on the single most common thing anyone would do with version
history. It is now `safe`, and keeps the note explaining the column comes back
empty, because that is worth knowing while the gate is not.

Verified against a live database three versions behind: the plan went from
`risk: lossy, lossyCount: 2` to `risk: safe, lossyCount: 0`, and executes
without a confirmation, restoring both columns.

**The button says where it goes.** "Execute migration (3)" gave a count and no
destination, and read as an undo even when the plan only adds. Direction cannot
come from version numbers — the plan always runs from the head, so the target is
always the lower number — so it comes from what the plan does: a plan that
destroys nothing is catching a database up ("Update to v2"), one that destroys
something is rolling it back ("Revert to v2").

**The author line earns its place.** It answered "who did this?" only when the
versions disagreed; on a single-user install it was the same address repeated
down the whole column, crowding out the date and change count that do differ.
Shown only when the versions actually have different authors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t are actually required

No Pro subscription, so the on-canvas "React Flow" badge is gone. That is
permitted: @xyflow/react is plain MIT (node_modules/@xyflow/react/LICENSE), and
its only condition is that the copyright and permission notice accompany the
software — displaying a badge in the UI is not a licence term. xyflow asks that
you subscribe when you hide it; that is a request, and this project has chosen
not to.

Which leaves the condition that *is* real, and was not being met. The client
bundles 38 third-party packages — React Flow among them — under MIT, BSD and
ISC, all of which require their notice to travel with the code. `NOTICE` only
carried Fox Schema's own Apache-2.0 terms, so shipping a build distributed their
code without their notices.

- `THIRD-PARTY-NOTICES.md` reproduces each package's own licence text,
  generated from the installed tree by `scripts/generate-third-party-notices.mjs`
  and regenerable when dependencies change.
- `.gitignore` blanket-ignores `*.md`, which would have kept the file out of the
  repo entirely; added to the tracked exceptions beside README and SECURITY.
- The Dockerfile copies LICENSE, NOTICE and the new file into the image, so the
  notices reach whoever runs the container rather than stopping at the repo.

Only gap: @google-cloud/secret-manager (Apache-2.0) ships no licence file of its
own; its entry names the licence and links upstream.

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

cursor Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_d64947dc-7207-491d-b503-230b343264ca)

Comment thread apps/web/src/frontend/components/lokee-weave/LokeeWeaveView.tsx Fixed
…o longer has

Fallout from #271, already on main. Hiding host / port / username / password for
file dialects was right for the product and broke every e2e that saves a SQLite
credential: `addSqliteCredential` fed `conn-host-input` a value, and Playwright
sat waiting 30s for a field that is not rendered. `schema-revert.test.ts` failed
in `beforeAll` and skipped all four cases.

The helper now passes only what a file dialect shows — a path — and
`addCredential` fills host, port, username and password only when the caller
supplies them, so live-server dialects are unaffected.

This gets the suite past setup and into the tests. It is not green yet: the
target picker does not end up with the new credential selected, so
`snapshotTarget` finds its button still disabled. Root cause not yet found —
recorded here rather than left as a silent red.

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

cursor Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_d76ad6ce-12b7-4a98-95c5-22fa2a7997d0)

Flagged by the code-quality bot on #273. Pre-existing rather than new — the
symbol had no reference before this branch either; the file being in the diff is
what surfaced it.

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

cursor Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_debeb405-a541-4a76-adbb-3ce454689fcc)

@huyplb
huyplb merged commit 70bad2a into main Aug 18, 2026
11 checks passed
@huyplb
huyplb deleted the feat/lokee-revert-direction-and-snapshot branch August 18, 2026 23:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant