Skip to content

Server-aware mutations: remove hardcoded slug identity (ref/rawGq)#10

Merged
aaltshuler merged 3 commits into
mainfrom
ragnorc/server-aware-mutations
Jun 27, 2026
Merged

Server-aware mutations: remove hardcoded slug identity (ref/rawGq)#10
aaltshuler merged 3 commits into
mainfrom
ragnorc/server-aware-mutations

Conversation

@ragnorc

@ragnorc ragnorc commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Why

The write path was incorrect by design: the client compiled every mutation to .gq and hardcoded the row predicate as where slug = $target_id (packages/client/src/translate.ts). That baked a per-type, slug-only, node-only identity assumption into the layer with the least information, breaking for non-slug keys, cross-type selection, and edges (which have no slug at all). Canon dash-books-canon.md §4.1/§4.5 mandates deleting translate.ts and moving writes into the server-owned catalog.

What

The mutation block now mirrors the read query shape exactlyref (server-owned catalog mutation) XOR rawGq (author-written inline .gq), via the same refine, plus typed params and an optional optimistic overlay. No kind discriminator, no target_id. The client constructs no write predicate: identity is a typed param resolved from the clicked row ({ $row: col }) or state ({ $state: ptr }). The server mutation owns its where, so non-slug keys, composite keys, and edges all work.

  • core/specMutationSpec = { ref?, rawGq?, name?, params?, optimistic? } + exactly-one refine; MutationParams = { spec, row, rowKey }. set_field removed.
  • core/runtimeresolveMutationParams ($row/$state/literal); optimistic patches key on (cellId, rowKey, field) from an explicit optimistic.set; mutationKinds dropped (gate refnamedQueries, rawGqrawGq, like reads).
  • clientClient.invokeMutation (queries.invoke expectMutation:true); ServerSource.mutate branches ref/rawGq like read; translate.ts (+test) deleted.
  • cli/validate — action ref mutations validated vs the live catalog (exists + mutation===true + params); rawGq warned; $row treated as dynamic.
  • renderers — both ActionList fireActions pass { spec, row, rowKey }.
  • demoquery-keyword node mutations (approve/reject_policy_clause) + an edge mutation (raise_issue_to_decisioninsert Raises { from, to }); notebook rewired to invoke refs incl. a live "Raise to selected decision" edge button; server-demo.sh re-applies stored queries on cluster reuse.

Verification

  • pnpm -r typecheck clean (5 packages)
  • pnpm -r test82 pass (core 40, client 24, web 17, tui 1)
  • pnpm -r build green (libs + CLI bundle + web SPA)
  • omnigraph lint on the 3 new .gq — 0 errors (confirms query-keyword mutations + insert Raises { from, to })
  • notebook validate vs a live cluster catalog — ok:true, matched the approve/reject/raise mutations, emitted only the $state warning (not $row)

Generality is tested, not just claimed: source.test.ts includes a non-slug-key (email) case, and the demo edge button targets by (from, to) slugs — neither expressible under the old slug-only path.

Note on the branch

Pushed as ragnorc/server-aware-mutations (not ragnorc/investigate-notebooks) because the remote branch of that name is a separate unmerged feature line; this commit is based cleanly on current main.


Note

Medium Risk
Touches the full write path (spec, runtime param resolution, optimistic concurrency, and server invoke). Incorrect resolution or settle logic could send wrong params or show stale UI, but behavior is heavily tested and predicates move server-side.

Overview
ActionList writes now mirror read queries: each button uses mutation.ref (catalog mutation via expectMutation: true) or mutation.rawGq, with typed params ({ $row: col }, { $state: ptr }) and optional optimistic.set. The old set_field / target_id shape and packages/client/src/translate.ts are removed — ServerSource forwards resolvedParams only.

The runtime resolves mutation params recursively, passes { spec, row, rowKey, resolvedParams } to sources, and drives optimistic UI from explicit optimistic.set keys (cellId, rowKey, field) with a seq guard so stale settles cannot clobber newer clicks. mutationKinds is dropped; compatibility gates ref / rawGq like reads.

CLI validate checks action mutation.ref against the live catalog (mutation vs read, params); $row is treated as reliably dynamic. Web/TUI ActionList dispatch the new payload; TUI Table adds row selection into state for cross-cell params (e.g. edge raise_issue_to_decision).

Demo: stored approve/reject node updates and an insert Raises edge mutation; server-demo.sh re-applies queries on reuse and strips OMNIGRAPH_SERVER_BEARER_TOKEN so unauthenticated invokes work.

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

Greptile Summary

This PR moves mutations from client-built predicates to server-owned mutation calls. The main changes are:

  • Catalog mutation.ref and inline mutation.rawGq specs.
  • Runtime $row and $state parameter resolution.
  • Explicit optimistic overlays keyed by cell, row, and field.
  • Web and TUI action dispatch updated for the new mutation payload.
  • Demo queries and validation updated for server-side mutations.

Confidence Score: 4/5

This is close, but the stale optimistic overlay should be fixed before merging.

  • Nested mutation params now resolve before source calls.
  • Same-field optimistic races are guarded by dispatch sequence.
  • Partially overlapping optimistic updates can still show stale field values.

packages/core/src/runtime/runtime.ts

Important Files Changed

Filename Overview
packages/core/src/runtime/resolve.ts Adds recursive resolution for nested mutation params before they reach the source.
packages/core/src/runtime/runtime.ts Adds resolved mutation params and seq-guarded optimistic patches, with one remaining stale overlay case.
packages/core/src/runtime/mutations.ts Builds optimistic patches from explicit fields using cell, row, field, and dispatch sequence keys.
packages/client/src/source.ts Routes catalog mutations and raw mutation bodies through the server client with resolved params.
packages/web/src/components/ActionList.tsx Passes the clicked row and row key into mutation dispatch.
packages/tui/src/components/ActionList.tsx Matches the web mutation dispatch payload for terminal actions.

Fix All in Claude Code

Reviews (4): Last reviewed commit: "fix(notebook): address PR #10 review — 4..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Context used:

  • Context used - CLAUDE.md (source)

Comment thread packages/cli/src/commands/validate.ts
Comment thread packages/core/src/runtime/resolve.ts Outdated
Comment thread packages/core/src/runtime/runtime.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 53a9a0de9a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +50 to +51
select_state: /sel/decision
select_column: slug

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add TUI support for the decision selection

When this notebook is run with pnpm tui, this new select_state is never written: packages/tui/src/components/Table.tsx only reads columns, rows, and dense and renders static rows, unlike the web Table selection handler. That means the new open-issues action resolves decision: { $state: /sel/decision } to undefined in the terminal, so the required catalog mutation param is omitted and the advertised edge mutation cannot succeed there. Either implement Table selection in the TUI or avoid making the demo mutation depend on it.

Useful? React with 👍 / 👎.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit de36120. Configure here.

Comment thread packages/core/src/runtime/resolve.ts Outdated
ragnorc added a commit that referenced this pull request Jun 27, 2026
1. Nested $row/$state params (resolve.ts): unify resolveExpr/resolveMutationExpr
   into one *recursive* resolver shared by read + mutation params, so markers
   nested in arrays/objects resolve — matching the validator's recursive
   detection. "marked dynamic but sent literal" is now unrepresentable.
   (Cursor + Greptile)

2. Optimistic patch-key race (runtime.ts + mutations.ts): tag each dispatch
   with a monotonic seq on OptimisticPatch; a settle only mutates the entry it
   still owns (cur.seq === patch.seq), so an older Approve settle can't clobber
   or delete a newer Reject's in-flight patch. Mirrors the cellRuns/generation
   guard. (Greptile)

3. TUI Table selection (tui/Table.tsx): implement focus-aware select_state /
   select_column (useFocus/useInput + the ink setState action + sliding
   viewport), mirroring the web Table — so the demo's "Raise to selected
   decision" edge button works in `pnpm tui` too. Restores renderer parity.
   (Codex)

4. Wrong-kind ref param noise (validate.ts): short-circuit — validate params
   only when the ref's kind matches, so a mismatched ref yields exactly one
   wrong_query_kind error, no spurious unknown/missing-param noise. (Cursor)

Tests: +9 (recursive resolver nested-marker cases; Approve→Reject race
regression). typecheck + 91 tests + build green; Fix 4 verified live against the
local catalog (one wrong_query_kind, no param errors; demo still ok).
ragnorc added 3 commits June 27, 2026 23:48
…tity

Replace the interim client-compiled `set_field` write path (which hardcoded
`where slug = $target_id` in translate.ts) with a write spec that mirrors the
read `query` shape: `ref` (server-owned catalog mutation) XOR `rawGq`
(author-written inline .gq), plus typed `params` and an optional `optimistic`
overlay. The client no longer constructs any write predicate — identity is a
typed param resolved from the clicked row (`$row`) or state (`$state`) — so
non-slug keys, cross-type selection, and edges all work.

- core/spec: MutationSpec = ref XOR rawGq (same refine as QuerySchema);
  MutationParams = { spec, row, rowKey }; drop set_field/kind/target_id
- core/runtime: resolveMutationParams ($row/$state/literal); optimistic patches
  key on (cellId, rowKey, field) from explicit optimistic.set; drop mutationKinds
- client: Client.invokeMutation (queries.invoke expectMutation:true); ServerSource
  branches ref/rawGq like read; delete translate.ts (+ test)
- cli/validate: validate action ref mutations vs catalog; warn on rawGq; $row dynamic
- renderers: ActionList fireAction passes { spec, row, rowKey } (web + tui)
- demo: query-keyword node mutations + an edge mutation (insert Raises); notebook
  rewired to invoke refs incl. a live "Raise to selected" edge button;
  server-demo.sh re-applies stored queries on cluster reuse

Verified: typecheck clean (5 pkgs), 82 tests pass, build green, the 3 new .gq
lint clean against company.pg, and `notebook validate` returns ok against a
live cluster catalog.
A configured server bearer token (e.g. exported by an operator's keychain
setup) wins over --unauthenticated and boots the demo server in default-deny
mode, which blocks stored-query invokes (POST /queries/{name}) — every cell
404s and the Approve/Reject/Raise buttons can't write. env -u it for the boot.

Verified live: with the token stripped, the cluster serves reads + the new
node/edge mutations; the built notebook Client.invokeMutation drives a real
PolicyClause status change and a Raises edge insert end-to-end.
1. Nested $row/$state params (resolve.ts): unify resolveExpr/resolveMutationExpr
   into one *recursive* resolver shared by read + mutation params, so markers
   nested in arrays/objects resolve — matching the validator's recursive
   detection. "marked dynamic but sent literal" is now unrepresentable.
   (Cursor + Greptile)

2. Optimistic patch-key race (runtime.ts + mutations.ts): tag each dispatch
   with a monotonic seq on OptimisticPatch; a settle only mutates the entry it
   still owns (cur.seq === patch.seq), so an older Approve settle can't clobber
   or delete a newer Reject's in-flight patch. Mirrors the cellRuns/generation
   guard. (Greptile)

3. TUI Table selection (tui/Table.tsx): implement focus-aware select_state /
   select_column (useFocus/useInput + the ink setState action + sliding
   viewport), mirroring the web Table — so the demo's "Raise to selected
   decision" edge button works in `pnpm tui` too. Restores renderer parity.
   (Codex)

4. Wrong-kind ref param noise (validate.ts): short-circuit — validate params
   only when the ref's kind matches, so a mismatched ref yields exactly one
   wrong_query_kind error, no spurious unknown/missing-param noise. (Cursor)

Tests: +9 (recursive resolver nested-marker cases; Approve→Reject race
regression). typecheck + 91 tests + build green; Fix 4 verified live against the
local catalog (one wrong_query_kind, no param errors; demo still ok).
@aaltshuler aaltshuler force-pushed the ragnorc/server-aware-mutations branch from 1a22f27 to a4fe121 Compare June 27, 2026 20:49
Comment on lines +218 to +220
if (cur?.seq === patch.seq) {
this.optimistic.set(patch.key, { ...cur, saving: false });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Stale Fields Persist

This settle check only protects fields that a newer mutation overwrote. If one action sets optimistic { status: "approved", assignee: "alice" } and a second action on the same row sets only { status: "rejected" }, the second action replaces the status patch but leaves the first action's assignee patch in the map. When the first action settles, this branch marks that stale assignee patch as no longer saving, and applyOptimisticPatches still applies it because it does not filter settled patches. The row can show the newer rejected status with the older assignee value until the next re-read clears it.

Context Used: CLAUDE.md (source)

Fix in Claude Code

@aaltshuler aaltshuler merged commit c5b8ffd into main Jun 27, 2026
2 checks passed
@aaltshuler aaltshuler deleted the ragnorc/server-aware-mutations branch June 27, 2026 21:10
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.

2 participants