Skip to content

refactor(loom-ui): isolate Builder row-root change workflow into authoring hook [198] - #209

Draft
matthewpeterkort wants to merge 1 commit into
feature/postgres-backendfrom
ao/repo-simplify/198-builder-root-workflow
Draft

matthewpeterkort wants to merge 1 commit into
feature/postgres-backendfrom
ao/repo-simplify/198-builder-root-workflow

Conversation

@matthewpeterkort

@matthewpeterkort matthewpeterkort commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

What

Isolates the Builder row-root change workflow (assess → resolve ambiguity → preview → apply) from BuilderWorkspace.tsx into one feature-owned hook, authoring/useRowChangeWorkflow.ts, adjacent to the rootChangeSelection types it already depends on. Closes #198.

Why this reduces reasoning burden (not line count)

Before, understanding "what can abort a row-root request and when" required holding:

  • 3 request refs + 1 epoch ref + 2 state cells (base 556-564), interleaved with publishing and population state;
  • the stale-abort effect (base 989-1039), whose correctness argument mixes workspace-owned values (table, state.*) with workflow-owned refs;
  • five callbacks (base 2094-2365) ~1,100 lines later, separated by unrelated population/suggestions code;
  • the refusal/preview-receipt guards in changeTableRoot, far from both the picker state and the panels.

After, all of that is one 442-line file whose entire surface is the row-root protocol. A reader answers "when is a late response ignored?" in one screen: epoch bump → abortRootChangeRequests → isCurrentAttempt → identity compare, with no unrelated workspace state in scope. What remains in BuilderWorkspace.tsx is selection + composition: a 30-line hook call plus render sites, and the outer workspace keeps deriving table identity and the workspaceAttached/latestState/serverDraft lifecycle bindings the actions read. The hook adds no abstraction layer: one consumer, no registry, no generic request manager, no new effects — the single pre-existing stale-abort effect moved with identical body, dependency values, and registration order.

Equivalence mechanics

500 touched lines, so the move was done with a checked extraction script (rerunnable): 13 anchored regions extracted from the base blob, a documented rename table (render-scope reads in the effect and identity builder only), byte-identity verification of every moved body against base. This includes the workspaceAttached.current && rowChangeRequestEpoch.current === epoch && ... guard line, moved verbatim so the source proof needs only a path repoint.

Behavior preserved (tests unchanged, expectations untouched)

  • No apply until the matching candidate-receipt preview is ready and the user chooses Apply; Boolean(pendingRowChangePreview) action-disabling unchanged.
  • Assessment/preview abort + late-response ignore after owner, explorer, output, snapshot, draft, unmount, or epoch change.
  • NO_CHANGE, BLOCKED, stale, unavailable, request-failure, and CLIENT_CANCELLED outcomes, messages, recovery paths (incl. draft-restore refetch), and first-failure precedence.
  • Ambiguity alternatives validated against current server assessment; earlier route-rebase choices retained.
  • Accessible dialog/picker/preview presentation and identity-matching semantics; readyRowChangePreview is the same predicate the old rootChangePreviewReady boolean computed, narrowed instead of re-checked at the render site.

Validation (matched/passed/skipped recorded)

Check Result
Ticket check: npm --prefix ui/packages/loom-ui run test -- ...BuilderWorkspace.reconciliation.unit.test.tsx 1 file matched, 92/92 passed, 0 skipped
coreProtocol + operationHistory + comprehensive formal (pinned vitest) 3/3 files, 42/42 passed, 0 skipped
Full ExplorerBuilder focused suite 79/79 files, 1076/1076 passed, 0 skipped
Full loom-ui package 97/97 files, 1355/1355 passed, 0 skipped
check-boundaries PASS (214 files)
typecheck:tests + declaration tsc PASS
scripts/check_no_code_comments.py --all-tracked 0 violations
git diff --check clean

Follow-ups outside this ticket's file ownership (flagged, not neutered)

  1. scripts/lib/formal-source-proofs.mjs Inv_DetachedRootChangeDoesNotPreview anchors its mutation in BuilderWorkspace.tsx and now hard-errors Nonunique mutation anchor (drift is detected, check not silently weakened). Proven: repointing only the path to authoring/useRowChangeWorkflow.ts (before/after strings byte-identical) passes the harness's full positive/broken/restored proof — the control still detects the original fault.
  2. docs/ui-contract-inventory/ui-supported-affordances.json fails --source-only with 188 mechanical issues, all from the move: BuilderWorkspace.tsx sha256 + the new watched file, and 10 distinct evidence line-ranges (RowRootPicker/RowChangePreviewPanel composition region) shifted by the net −338 lines. Baseline passes pre-change; needs verifier/tooling-owner regeneration/re-anchoring per batch ownership rules.

Not run in this environment: authenticated Builder browser acceptance (.codex/skills/verify-loom-ui) — no live authenticated stack here; CI browser lane / paired-driver gate remains the authority.

Non-goals respected

No behavior/feature changes, no generic async framework, no mass renames, no comment additions, no useEffect/useLayoutEffect additions, no test expectation changes.

CI observation (run 43495764126 / 43495764254 @ 8dfcc02)

All 8 failing lanes trace to exactly these coordination items; every behavioral lane passes (Go/Postgres/Arango/ClickHouse/contracts/shell all green):

  1. verification/builder-interactions/reviewed-sources.json pins the reviewed sha256 of BuilderWorkspace.tsx; Formal models and generated replays asserts it and fails with Review required: ...BuilderWorkspace.tsx (3e1691e7... expected vs d4fe5076... actual). This is the same re-pin mechanism as 80e88771 (125-row disposition): it needs a reviewed-sources re-pin wave recording the new hash + hook file, which is verification-lane ownership per .codex/skills/builder-interaction-formal-verification/SKILL.md (no invented sync commits for unreviewed patches).
  2. Tests / Python tests fails on test_compact_manifest_preserves_pins_and_rejects_fragment_drift with the same 188 inventory issues — one gate, one fix.

Proven locally: normalize_ui_contract_inventory.py regenerates hashes/watched-files but does NOT repair the stale anchors — the ~188 stale ranges live in the fragment evidence strings (BuilderWorkspace.tsx:NNNN). Mechanical re-anchor rule that resolves them: map pre-hunk ranges unchanged, ranges inside the 351 extracted lines to useRowChangeWorkflow.ts at the documented region offsets, post-hunk ranges shifted by the per-region net delta (total −338); then rerun the normalizer.

…[198]

BuilderWorkspace owned the assess -> resolve ambiguity -> preview -> apply
row-root change lifecycle inline: three request refs, an epoch ref, two
state cells, the stale-abort effect, and five action callbacks declared
beside unrelated workspace and publishing state (base lines 556-564,
989-1039, 2094-2365). The workflow's only UI is the RowRootPicker /
RowChangePreviewPanel composition, and authoring/rootChangeSelection.ts
already owns its identity comparison and discriminated UI-state types, so
the lifecycle now lives in one feature-owned hook beside those types.

- authoring/useRowChangeWorkflow.ts: request lifecycle, epoch/refusal
  protocol, stale-abort effect, and select/resolve/refresh/cancel/apply
  actions. Inputs are the typed request identity fields, assess/preview
  ports, preview limit, and narrow apply/reload/message callbacks; output
  is the existing discriminated state plus the actions and a render-derived
  readyRowChangePreview narrowing (same identity predicate as the removed
  rootChangePreviewReady boolean).
- BuilderWorkspace keeps table selection, panel rendering, the workspace
  refs the actions read, and the assess mutation whose isLoading still
  gates publishing. No new useEffect: the single stale-abort effect moved
  with its body, dependency values, and registration order.
- Bodies moved verbatim via a checked extraction (documented renames only
  in the effect/identity render-scope reads); every moved body verified
  byte-identical against the base blob, including the
  workspaceAttached.current guard line.

Behavior preserved unchanged: preview-before-apply, abort/ignore-after-
identity-change, NO_CHANGE/BLOCKED/stale/unavailable/request/
CLIENT_CANCELLED outcomes and messages, ambiguity validation against the
current server assessment with route-rebase retention, request dispatch
timing, and first-failure precedence.

Validation: BuilderWorkspace.reconciliation.unit.test.tsx 92/92; core
protocol + operation history + comprehensive formal 42/42; full ExplorerBuilder
79 files 1076/1076; full loom-ui package 97 files 1355/1355, 0 skipped;
typecheck:tests and declaration emit clean; check-boundaries passes;
no-code-comments gate 0 violations; git diff --check clean.

Coordination (outside this ticket's file ownership, flagged not neutered):
scripts/lib/formal-source-proofs.mjs Inv_DetachedRootChangeDoesNotPreview
still anchors its workspaceAttached mutation at BuilderWorkspace.tsx and
now hard-errors "Nonunique mutation anchor"; repointing only the path to
authoring/useRowChangeWorkflow.ts (before/after strings unchanged) passes
the full positive/broken/restored proof, so the control still detects the
fault. docs/ui-contract-inventory needs its BuilderWorkspace.tsx evidence
ranges re-anchored (10 distinct ranges shifted by the net -338 lines),
plus regenerated watchedFiles hash/file-set.

Closes #198
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