Builder CMS live writes + multi-source row-union content databases#1485
Draft
3mdistal wants to merge 37 commits into
Draft
Builder CMS live writes + multi-source row-union content databases#14853mdistal wants to merge 37 commits into
3mdistal wants to merge 37 commits into
Conversation
Proves the full execute orchestration (prepare→execute→reconcile) performs a safe Builder autosave end-to-end using the REAL write client — the one seam previously only unit-tested with a mocked client. Runs `executeBuilder SourceExecutionWithDeps` with real `executeBuilderCmsWrite` against the safe model, asserts the execution succeeds + reconciles and the live/published artifact is unchanged (autosaved value never goes live). Gated behind BUILDER_LIVE_E2E=1 + BUILDER_PRIVATE_KEY + BUILDER_PUBLIC_KEY; skips offline so normal CI stays green. Verified: typecheck, prettier, full guard suite, skip-mode, and a real live run all pass; cleans up its throwaway entry. Authored via Codex (GPT-5.5), reviewed + live-verified by Claude. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a guarded autosave-only toggle next to the Builder source status badge so an editor can turn on live writes without the agent/CLI. Reuses the existing `builderSourceLiveWriteControlState` helper and the file's existing toggle styling; calls the already-wired `onSetBuilderLiveWrites` callback. Guard rails: shown only for the safe write model (a muted hint explains why it's absent on other Builder models), disabled on `!canEdit || sourceActionPending`, autosave-only (enforced by the callback + server gates). role="switch" + aria-checked for a11y. UI-only; no server/action changes. Authored via Codex (GPT-5.5), reviewed + verified by Claude (typecheck, prettier, 81-test component suite, full guard suite). Browser visual pass owed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a concise README section: autosave-only semantics (never changes the live/published artifact), credentials via the existing Builder Connect flow (no separate key entry; .env.local for local dev), the safe-model-only gate (agent-native-blog-article-test), how to enable (toggle or set-content-database-source-write-mode), and the execution guards. Scoped honestly to metadata + existing-entry autosave — body diffs are a later slice. Authored via Codex (GPT-5.5), reviewed by Claude. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per review, the source name + status badge + toggle on one row crowded the header (name truncated to "agent…"). For the safe-model Builder source, drop the redundant Read-only/Live-writes-on badge and move the control to a dedicated "Enable live writes (autosave)" row with a bare switch; the name row gets its space back. Non-safe Builder models keep the Read-only badge + hint; non-Builder sources unchanged. a11y (role=switch/aria-checked) preserved. Authored via Codex (GPT-5.5), reviewed + verified by Claude (typecheck, prettier, IconPencil import still used). UI-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the state-blind autosave/draft/publish intents with a typed write effect that makes state-preservation structural: - autosave / update_in_place send only changed data.* and NO `published` field (Builder PATCH merges data.* and preserves omitted publication state — proven against real Builder), so they cannot change publication state by construction. - publish / unpublish are the ONLY effects that set `published`, gated behind an explicit per-item transition intent (publish) / confirmation (unpublish). - create_draft writes new entries as draft. update_in_place/publish/unpublish trigger webhooks (live-affecting); autosave stays quiet. Dry-run staleness now compares `effect`. Legacy non-autosave push modes reinterpret as update_in_place (no more blind publish/draft). Also updates the source-panel layout test for the decluttered live-writes control (drops the removed "Live writes on" badge copy). Authored via Codex (GPT-5.5), reviewed + verified by Claude (typecheck, prettier, guards, full suite 782 pass). Live-read classification + transitions wired next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ilder writes Adds the "re-check at write time" capability that didn't exist (the dry-run only compared a local snapshot). Before any live-affecting write (update_in_place / publish / unpublish), reads the target entry's current Builder state via a new readBuilderCmsEntryLiveState and blocks before claim/write on: - missing entry (deleted in Builder), - stale: live lastUpdated != the row's lastSourceUpdatedAt baseline (someone edited it since the diff was approved), - transition mismatch: publish on a non-draft, or unpublish on a non-published. autosave/create_draft skip the preflight. Threads publicationTransition / confirmUnpublish through the action schema into the plan. Authored via Codex (GPT-5.5), reviewed + verified by Claude (typecheck, prettier, guards, full suite 789 pass). NOTE for live-test phase: must verify the lastUpdated format matches across the sync baseline and the live read (epoch vs ISO) so update_in_place doesn't falsely block. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…works Live verification caught a feature-breaking bug: Builder delivery returns `lastUpdated` as a NUMBER and stringFromRecord only accepted strings, so the synced `lastSourceUpdatedAt` baseline never captured the entry version — the Task-2 preflight then flagged EVERY update_in_place/publish/unpublish as stale. - Adapter: timestampStringFromRecord captures a numeric lastUpdated as its stringified epoch (string/ISO fallbacks kept). - Execute: toEpochMs + liveTimestampsDiffer normalize both sides to epoch-ms (number / numeric-string / ISO) before comparing; unknown values fall back to strict string compare (never silently skip the guard). - Gated live test extended with real readLiveEntry: update_in_place is NOT falsely stale-blocked + takes content live, a wrong baseline DOES block, and publish/unpublish transitions work — all against real Builder (5/5 live pass). Authored via Codex (GPT-5.5), reviewed + live-verified by Claude (typecheck, prettier, guards, offline suite, 5/5 live). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ublish) Replaces the binary autosave toggle with an intentional write tier: - read_only (default; new sources start here), - stage_only → autosave revisions (human publishes in Builder), - publish_updates → state-preserving live writes (update_in_place). Publication transitions (publish a draft / unpublish a published) are an extra per-source allowance (allowPublicationTransitions) that requires publish_updates and stays per-item + confirmed. capabilities.liveWritesEnabled is now derived from writeMode; legacy pushMode/flags still work for older sources. UI: the source panel's control becomes a three-tier selector (safe model only), revealing "Allow publish/unpublish per item" at publish_updates. Effect derivation maps tier→default effect. Settings/action validate tier combinations and safe-model-only. Authored via Codex (GPT-5.5), reviewed + verified by Claude (typecheck, prettier, guards, offline suite 795 pass, 5/5 live cases against real Builder incl. the new transition gate). Browser visual pass of the tier selector owed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ror)
Adds executeBuilderSourceBatchWithDeps + an execute-builder-source-batch action
to push many approved outbound change-sets in one pass over the proven per-item
pipeline:
- bounded concurrency (default 3, cap 8),
- continue-on-error with per-item {changeSetId, status, message} results,
- a batch summary (total/succeeded/blocked/failed),
- resumable: already-succeeded executions are skipped (per-item idempotency),
- transitions applied ONLY when explicitly mapped per change-set (bulk default
stays update-in-place; never auto-publishes/unpublishes).
Extends prepare so an explicit publish/unpublish prepares the matching gate.
UI: a "Push all approved (N)" affordance (safe-model-only) that shows the
returned summary + non-succeeded per-item messages.
Authored via Codex (GPT-5.5), reviewed + verified by Claude (typecheck, prettier,
all guards incl. no-unscoped-queries, offline suite 801 pass). Batch orchestration
is unit-tested over the already-live-verified per-item path; UI visual pass owed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…he diff
The review/diff now makes publication intent explicit per row (the philosophy:
transitions are deliberate, item-level acts):
- per-row default effect label (Stage autosave / Update in place — keeps current
published/draft state),
- per-row Publish / Unpublish controls, shown ONLY when the source allows
transitions (publish_updates + allowPublicationTransitions); mutually
exclusive; Unpublish is destructive-styled and requires explicit confirm,
- a footer intent summary ("N update in place · M publish · K unpublish"),
- selections flow into the batch `transitions` map (unselected rows push with no
transition — update-in-place/autosave per tier; nothing auto-transitions).
Authored via Codex (GPT-5.5), reviewed + verified by Claude (typecheck, prettier,
guards, offline suite 805 pass). UI visual pass owed (spacing/wrap in real
review payloads).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rewrites the section for the full feature: per-source write tiers (read_only / stage_only / publish_updates), state-preserving update-in-place (never sends `published`, so it can't change publication state; Builder merges fields + preserves envelope), explicit per-row publish/unpublish transitions (gated + unpublish-confirm), new-entries-as-draft, bulk "Push all approved" with summary, the write-time stale guard, and webhook behavior. Keeps the body-diff caveat. Authored via Codex (GPT-5.5), reviewed by Claude (prettier clean). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Contributor
|
Here's a visual recap of what changed:
Open the full interactive recap Recap skipped for |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Closes the "new row does nothing" gap (Alice's testing). A local database item not linked to a Builder entry (no source row) with a non-empty title now becomes an outbound change-set; with a null target it resolves to the create_draft effect (POST as draft — already live-proven). Title + property field values become the create body. Skips titleless rows and rows that already have a stored outbound change-set. loadSourceSnapshot loads the database's items + their property values (owner-scoped — guard:no-unscoped-queries clean) and threads them in; the detection extension is additive (a separate loop — existing title-diff path untouched). New-row create is safe (no baseline comparison, so no normalization risk). Hand-implemented by Claude (Codex unavailable — hung repeatedly), verified: typecheck, prettier, all guards, full suite 807 pass. Functional pass owed (create appearing in the review diff → push → new Builder entry). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…der rows Broadens existing-row change detection from title-only to every mapped property field: local value vs the synced sourceValues baseline, compared with a stable, key-order-insensitive serialization (null/undefined/"" treated as empty; absent local value = "not loaded", skipped — never reported as cleared). Title + changed property fields merge into one change-set; the dedup-vs-stored check is generalized to per-field exact match (preserves "surface a new edit after an older staged record"). Multi-field bodies already flow through the adapter (nestedBuilderPatch) and review UI. Severity note: detection is advisory — the write path is independently safe (state-preserving), so a diff miscompare over/under-reports in the review (caught by human review), it cannot lose data or mis-publish. A false-diff guard test (local==source ⇒ no change-set) covers the main risk. Hand-implemented by Claude (Codex unavailable), verified: typecheck, prettier, all guards, full suite 809 pass. Functional pass owed (edit a field → see it in the review diff → push). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two bugs blocked pushing a new-row create change-set under the publish_updates tier: 1. Gate-key divergence. prepare derived the idempotency-key push mode from the write tier (publish_updates -> "publish") while execute used the change-set's own pushMode (local creates hardcode "autosave"). The keys disagreed only for creates, so execute never found the prepared gate and threw "Prepare the Builder execution gate before executing it." Both paths now resolve push mode through one shared helper, resolveBuilderCmsExecutionPushMode (tier wins). 2. Self-defeating create guard. builderSafetyChecks blocked create_draft whenever the target was a synthetic-fixture row — but an unmatched synthetic-fixture row (sourceRowId `builder-<documentId>`, no real entry) is exactly what makes the effect create_draft, so every create was blocked. The unmatched-row blocker now applies only to effects that write to an existing entry (autosave / update_in_place). Verified live: a new workspace row pushed through the publish_updates tier creates a real Builder draft entry and reconciles the local row to the returned entry id. Updated the two guard tests that codified the old block into positive create assertions, plus regression coverage for the shared push-mode resolver. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
End-user audit of the live-writes flow found internal/debug vocabulary,
redundant sections, and alarming risk treatment on benign changes. This
trims the review dialog and source panel to plain language.
Honest effect labels (server-threaded):
- Add `effect` to ContentDatabaseSourceReviewRowSummary, resolved via a new
resolveBuilderCmsWriteEffect helper (does not require an approved gate).
A create now reads "Create draft", not "Update in place".
Review dialog (shown to every user) — full declutter:
- Drop the "Where it will go" (raw model slug, push mode, read mode) and
"Risk check" (internal chips) sections.
- Replace with one plain destination line ("Writes a new draft to Builder —
won't publish") plus inline warnings shown ONLY when something is actually
wrong (a real conflict, or an unpublish).
- Plain-language result/status ("Ready" / "Needs attention" / "Pushed")
folded into the footer; effect-aware primary button ("Create draft").
Risk recalibration:
- "live writes enabled" was bumping EVERY outbound change to high risk.
Removed — live writes is a capability, not per-change risk. A plain
create/edit is now low; red is reserved for conflicts/unpublish.
Source panel:
- Humanize microcopy ("guarded Builder autosave path" → "Review local edits
before they're written to Builder"; tier descriptions).
- Tidy the Code-mode developer card: drop duplicate chips and the repeated
"live writes enabled", collapse the raw request URL + idempotency key
behind a "Technical details" disclosure.
Verified live: the decluttered source panel renders the humanized copy,
green low-risk, trimmed chips, and the Technical-details disclosure.
Full suite green (814 passed); dialog helpers covered by new unit tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Code-mode "Local Builder changes" card kept rendering a finished (applied) change with the full review ceremony — diff, risk chip, "approved by", and the whole Execution-gate block. A done change doesn't need any of that. - An applied change now collapses to a single confirmation line: "✓ <title> · Created draft · pushed 2h ago". The effect label is read from the recorded execution payload, not re-resolved (the live resolver would mislabel a completed create as an update once the row is matched). - Pending changes keep the diff visible but fold the reasons line, review event, and execution-gate metadata behind a single "Details" disclosure; the risk chip only shows when risk is above low. Verified live in Code mode. Typecheck + DocumentDatabase suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion The Sources panel put the pending-changes badge on the generic "Builder" integration row and made the connected collection drill through provider → space → model-picker to reach its own settings. - Move the pending-changes badge onto the connected collection row (where the changes actually live); drop it from the generic Builder row. - Clicking the connected collection now navigates straight to its detail leaf (write mode + local changes) via a synthesized model summary, instead of reopening the Builder space/model picker. Verified live: badge sits on the Primary collection, and one click opens its detail. Typecheck + DocumentDatabase suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Local Builder changes" roster mixed completed changes with pending
ones and re-rendered the full field diff that the "Review diff" button
right above already opens.
- Split the list: pending changes stay under the section; applied changes
move to a muted "Recently pushed" group, so the section's "review local
edits before they're written" copy actually describes what's under it.
- Replace each pending card's inline Current/Proposed diff with a one-line
scope hint ("N field changes · open Review diff to see details"). The
diff is shown in the review dialog, not duplicated here.
Verified live. Typecheck + DocumentDatabase suites green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The source panel had two cards for one idea: a "1 change ready to push" summary + Review button, then a separate "Local Builder changes" list. The split made the heading wrap (title fighting the button for the row) and put the action above the changes it acts on. Merge into a single "Builder changes" card that reads like a commit flow: - title + one-line intent - pending changes (the items) - a "Review changes" action right below them (the panel only opens the review dialog; the dialog owns the push verb) - a muted "Recently pushed" history, capped at 3 The card is now shown to all users (not just Code mode) so the queued items are visible, not just a count; the per-row "Details" disclosure (execution gate, idempotency key) stays gated to Code mode via a new showDetails prop. Verified live. Typecheck + DocumentDatabase suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tions live in "Add a source" First slice of the multi-source work (row-union design brief). Pure UI/IA; no backend behavior change. - Root Sources view shows only connected sources + "Add another source" (a single "Add a source" button when the database has none). The Integrations and Agent-Native apps sections no longer live at the root. - "Add a source" now hosts the integrations: Builder (live, browse + attach), Notion (coming soon), and Analytics under Agent-Native apps (coming soon). Builder stays offered there even when a Builder source is already connected, so additional collections can be added. Adding an additional Builder source still uses today's attach behavior; making each attached collection independently writable is a later slice per the brief. Verified live: root shows connected + add only; "Add a source" lists Builder/Notion/Analytics; Builder navigates into the space browser. Typecheck + DocumentDatabase suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…source backend) Slice 2 of the row-union multi-source build. Purely additive: every write/review/settings action now accepts an optional sourceId and resolves that specific source; when omitted it falls back to the primary, so single-source behavior is byte-for-byte unchanged. - _database-source-utils: add getContentDatabaseSourceSnapshotById + getContentDatabaseSourceSnapshotForWrite, and getExistingSourceById + getExistingSourceForWrite (by-id-or-primary resolvers). - sourceId added to the request schemas/types and threaded through: prepare-builder-source-review, prepare-builder-source-execution, execute-builder-source-execution (+ realExecutionDeps), execute-builder-source-batch (+ realBatchDeps/runOne), validate-builder-source-execution, stage-builder-revision, review-content-database-source-change-set, set-content-database-source-write-mode, refresh-content-database-source. - stage-builder-revision now resolves its source via the for-write snapshot (was getExistingSource primary guard + a separate primary snapshot). No client wiring yet — the UI passes sourceId in the later UI slice. Full action suite green (178), typecheck + prettier clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… + create-fix + UI) Three Codex instances reviewed three slices in granular detail. Fixes: Backend multi-source (review A): - prepare-builder-source-review resolved the source/guard/approval via the PRIMARY (getExistingSource) even when args.sourceId targeted a non-primary source — so gates landed on the wrong source and the Builder guard checked the wrong type. Now resolves the target snapshot for the guard, approval, and review payload (built from the target, not response.source/primary). - getExistingSource now orders by createdAt (oldest = primary), matching the snapshot resolver, so an omitted sourceId is deterministically the primary on multi-source databases (was unordered after the for-write refactor). Idempotency key (review B): - The execution-gate idempotency key now keys on the RAW resolved push mode (may be "none") instead of collapsing "none" → "autosave". A read-only gate no longer shares a key with a stage-only gate for the same change-set. - Execute keys on the resolved mode, not pushModeConfirmation, removing a prepare/execute key-divergence path (the confirmation is still validated in the plan). Review dialog / card (review C): - Publish/Unpublish transition controls are hidden for create_draft rows, and builderReviewEffectiveRowEffect never relabels a create as publish/unpublish (the adapter always writes a draft when there's no entry id) — no more wrong-action label on a live-write control. - The merged "Builder changes" card shows "Review changes" only when there are reviewable (pending/staged/approved outbound) changes, not for stale source-changed conflicts that open an empty dialog. - The applied-change collapse reports the SUCCEEDED execution's effect/time, not whatever the latest (possibly later-blocked) gate row says. Full suite green (814), typecheck + prettier clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e property (slice 5) Row-union slice 5. Adds the "add an additional Builder collection as its own writable source" path; each collection's entries become their own rows. - attach-content-database-source: new `mode: "add"` path — when a source already exists and mode is "add", insert an additional Builder source (insertSecondarySource) and import its entries as their OWN database items/rows via the existing primary-import machinery (importBuilderCmsEntriesAsDatabaseItems + seed fields/rows + read metadata), instead of replacing the primary or read-only-federating. - importBuilderCmsEntriesAsDatabaseItems: new `skipTitleDedup` flag so a second collection's same-titled rows aren't dropped by the cross-database title dedup (per-source re-import idempotency still handled by builderCmsEntryAlreadyRepresented). - ensureDatabaseSourceProperty: auto-creates a "Source" select property (one option per source + "Local") and tags every item with its owning collection; rows with no source binding are "Local". Option ids preserved across re-runs. - UI: onAttachBuilderSource passes mode "add" when a source already exists, "replace" for the first source. The existing model-picker → Attach flow triggers it. Implemented; typecheck + full suite green (814). Live end-to-end verification (attach a 2nd collection, see row-union + Source tags) is pending Chrome reconnection after the machine restart — the entry import needs the running app's Builder keys. Reuses the proven primary-import path; new DB logic is the Source property + the add orchestration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ce (slice 5) Each mode:"add" attach starts a fresh source with no prior rows, so attaching the same collection again would insert a duplicate source and re-import duplicate rows. databaseSourceExistsForTable now blocks a duplicate add with a clear error. Typecheck + suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e, pagination) Codex review D on the attach-writable slice found: - High: the add-branch seeded the new source from ALL database items (sourceSetupPayload returns everything), binding the PRIMARY's rows to the new source and corrupting row identity / the Source tag. Now snapshots existing items before import and seeds the new source from ONLY the items it imported (before/after documentId diff). - Medium: the auto "Source" select stored the source NAME as its value, but the UI matches select values by option ID (board grouping, pills, filters). Now stores the option id. - Medium: response pagination only scoped the primary's rows to the page, treating row-union secondary rows as join-only. Now scopes every document-backed source's rows; federated join rows (empty documentId) stay intact. Noted (not fixed): no (database_id, source_table) unique index — the duplicate-attach guard is check-then-insert (single-user low risk; a unique index is a follow-up migration). Typecheck + suite green (814). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Sources panel's attached-model leaf and the whole review/push/write-mode path were hardwired to the primary source. A row-union database has multiple writable Builder collections, so opening any of them must land on its own writable leaf and scope writes to that collection. - Resolve the leaf's source by the model being viewed (sourceTable is unique per database — duplicate-attach is guarded), not by assuming primary. - Recompute the leaf's change-set groups, sync state, and write-mode controls from that source; route refresh / review / push / write-mode / disconnect through the source's own id via the already-threaded backend sourceId. - Track an active review source id so the review dialog + push target the opened collection; reset on disconnect. - Per-source reviewable badge on the Sources list; mark every attached collection (not just primary) in the space model list. sourceId omitted ⇒ primary ⇒ byte-for-byte single-source behavior. Purely a client wiring change over the backend sourceId support from 1ab87e4. Typecheck + suite green (814). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two bugs surfaced once each source's own change-set card became visible via the writable leaf: 1. Cross-source create leak: change detection loaded ALL database items as create candidates for every Builder source, so each collection claimed the other collections' rows as new-row create_drafts (e.g. a blog row showed as a create under zz-resources). Now a Builder source excludes documents owned by another source, and a truly unsourced "Local" row creates only against the primary — not fanned out to every attached collection. Single-source is unchanged (no other owners; primary allows unsourced creates). 2. Phantom field change on freshly-imported rows: seedMockSourceFields mapped EVERY local property to a writable Builder field, including the auto-created internal "Source" tagging property → its option-id value diffed against an absent baseline, so every row showed a no-op "1 field change" (and a push would have written the internal tag to Builder). The Source property is now excluded from source-field seeding. Live-verified: page/figma-imports badges 0 (was 6), zz-resources card shows only its own 3 rows. Typecheck + suite green (814). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- a Builder source never creates rows owned by another source - only the primary adopts unsourced "Local" rows as creates Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend (_database-source-utils.ts): - The internal "Source" property exclusion was name-only and applied to all source types — a user field named "Source", or any non-Builder/local-table source, lost its mapping. Now scoped to Builder sources and matched on the internal tag's exact shape (a `select` named "Source"), so user fields and legacy/local sources are untouched. - Primary detection ordered by createdAt alone (nondeterministic on ties) and diverged from the write path. All "primary = oldest" orderings now share an (createdAt, id) tie-break, so the create-adoption primary is definitionally the same source getExistingSource resolves. Client (DocumentDatabase.tsx): - Auto-sync refreshed the primary even when viewing a non-primary source's leaf; it now targets the viewed source (primary at the root), and the throttle no longer blocks syncing a freshly-opened source. - The review push sent a stale builderReviewSourceId to the backend while the dialog rendered primary-derived state; push now targets the same resolved source the dialog shows (activeReviewSource), falling back to primary. - The collapsed Sources settings-row badge summed only the primary's reviewable changes; it now sums across every source so a secondary's pending pushes show. Deferred (documented in the brief as the first slice-6b task): a row tagged for a non-primary collection via the Source property isn't yet adopted as a create_draft by that collection — that needs the new-row source-picker work and ownership-by-tag in change detection. Typecheck + suite green (816). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves the deferred Codex finding from slice 6a: a new, still-unlinked row tagged for a non-primary collection via the visible "Source" select property is now adopted as a create_draft by THAT collection — not silently routed to the primary. loadSourceSnapshot maps each row's Source option-id to its owning sourceId (option name === source name) and passes taggedSourceByDocumentId; the create loop creates a tagged row only against its tagged source, and falls back to the primary-only rule for untagged "Local" rows. Foundation for the new-row source picker (next). Typecheck + tests green (28 in the change-detection suite). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
resyncBuilderCmsSourceSnapshot re-seeded source-row identity from EVERY database item, so refreshing any source claimed every row — fine when only the primary ever auto-synced (single owner), but in a row-union it cross-links every row to every refreshed collection and corrupts ownership. Now, with multiple sources, a resync links only the rows that belong to this source: its remote-backed entries when the read is live (which also self-heals any prior over-claim, since rows are deleted then reseeded), or just the rows it already owned when offline. New / "Local" / other-collection rows stay unlinked so the Source-tag create path can adopt them into the right collection. Single-source is unchanged (all items). Without this, a row created via the new-row source picker is claimed by the primary on the next sync and never surfaces as a create_draft for its tagged collection. Live-verified: each row now has exactly one source link, and a row tagged for zz-resources surfaces a create_draft under zz-resources. Suite green (817). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e 6b)
When a content database has 2+ sources, the "New" button becomes a small picker
("Add a row to…") listing each connected collection plus "Local (no collection)".
Picking a collection creates the row already tagged with that collection via the
auto-created "Source" select, so the backend routes its create_draft to the right
source (ownership-by-tag). Single-source databases keep the plain "New" button.
Live-verified: picking "zz Resources" creates a row tagged zz-resources, and that
row surfaces as a "Create draft" in the zz-resources Builder-changes card — not
the primary. Editing the Source pill afterward re-targets the create (the Source
property is a normal editable select). Typecheck + suite green (817).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lets a logical column draw its values from a different field in each source (row-union). New action bind-content-database-source-field binds an existing, type-compatible, unmapped source field to an existing column (or unbinds it): it repoints the source-field mapping's propertyId and backfills that source's per-row values into the column. Read and write already flow through propertyId, so binding is the whole mechanism — a column can be fed by blog.body AND zz.resourceType at once, each row showing its own source's value. Guards: excludes integration-managed/system/derived fields and the internal Source tag column; requires type compatibility (a text column is permissive); at most one field per source per column. UI: the column-settings menu gains a "Sources feeding this column" section listing each bound source field (with unbind) and a "Bind a field from a source" submenu of bindable, type-compatible fields grouped by source. Threaded `sources` into the column header → PropertyManagementPopover. Live-verified: bound zz-resources "Resource Type" into a "Type" column — the three zz rows show ZIP / Spreadsheet / PDF while blog rows stay blank. Typecheck + suite green (817). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6b (review C):
- Source-tag identity is now id-based, not name-based: each Source select
option's id IS the sourceId ("Local" uses a fixed "local" sentinel that can't
collide with a UUID). Resolving a row's tag to its owning source is pure id
matching, so duplicate collection display names — or a collection literally
named "Local" — can no longer misroute rows (High). ensureDatabaseSource
property builds one option per source keyed by id and tags rows with the
source id directly.
- The new-row picker derives each collection's tag from its source id (always a
valid option), so it can no longer silently create an untagged row when an
option fails to resolve (Medium).
6c (review D):
- The bind action now clears the column's values for ALL of a source's rows
before writing, so binding a sparse field no longer leaves stale/previous
values on rows whose new value is empty (High).
- Rebinding a field that's already bound to another column is rejected (was
silently repointed, orphaning the old column's values) (High).
- "One field per source per column" is enforced server-side, not just in the UI
— a direct action call can no longer bind two fields from one source to one
column (High).
- A multi-value (list) source field can no longer bind into a text column
(silent lossy coercion); enforced server + client (Medium).
Offline-resync stale-over-claim (C2) and the overlap-delete (D4) are left as
documented edges: both are prevented for new databases by the row-union
one-source-per-row invariant and self-heal on a live resync.
Typecheck + suite green (817).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…6b/6c) Boots a real in-memory libsql DB + migrations and drives the actual code paths that previously had only live verification: - bind-content-database-source-field.db.test.ts (6 cases): backfills only the bound source's rows; clears a stale column value when the new field is empty; rejects rebinding an already-bound field; rejects a second field from the same source on one column; rejects a multi-value field into a text column; supports two sources feeding one column + unbind. - resync-content-database-source.db.test.ts: simulates the pre-fix over-claim (source A claims every row incl. source B's) and asserts the live resync self-heals — A keeps only its own remote-backed rows, B is untouched. Mocks the Builder read client so it runs offline-deterministic. Suite 824 passing (+7). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ites # Conflicts: # templates/content/actions/execute-builder-source-execution.ts # templates/content/app/components/editor/DocumentDatabase.layout.test.ts # templates/content/app/components/editor/DocumentDatabase.tsx # templates/content/app/components/editor/DocumentProperties.tsx # templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…or + review dialog
The slice-6 row-union UI re-homed onto main shipped with hardcoded English while
the rest of the editor uses the useT/dbText i18n layer. Wire every flagged
visible string through i18n:
- New catalog keys under `database.*` for the source picker ("Add a row to…",
"Local (no collection)"), the writable-leaf write-mode controls (Builder write
mode, Stage only / Publish updates + descriptions, Read-only), the Builder-
changes card, the SourcesListView, the column field-binding editor ("Sources
feeding this column", "Bind a field from a source"), and the review dialog.
- BUILDER_WRITE_MODE_OPTIONS now carries i18n keys resolved via db().
- builderReviewResultStatus returns a labelKey resolved by the caller; the
review dialog gains useT and routes all its strings through database.* keys
(reusing main's existing keys where they already existed).
- Translations added for all 9 non-English locales (zh-CN, zh-TW, es, fr, de,
ja, ko, pt-BR, hi, ar) so catalog coverage is complete.
guard:i18n-catalogs passes (no raw literals, full coverage, no script
contamination); typecheck clean; suite green (865).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
What this is
Builder CMS live writes plus multi-source row-union content databases, end to end. Originally scoped to live writes; it grew (with sign-off) to cover the full row-union feature. Now rebased on
main(which extracted the database UI intodatabase/DatabaseView.tsxand added theuseTi18n layer).Live writes (the original slice)
agent-native-blog-article-test); other collections stay read-only.Multi-source row-union (slices 6a–6c)
A content database can stack rows from several Builder collections in one table, each row owned by one collection and independently writable.
sourceIdthreaded through every write action. Includes two change-detection fixes (cross-source create leak; phantom no-op diffs from the internal "Source" tag being seeded as a Builder field).create_draftfor that collection. Also fixes a resync over-claim (a refresh used to re-link every row to whatever source refreshed).bind-content-database-source-fieldaction (repoint + backfill, with unbind); column-settings UI ("Sources feeding this column" + "Bind a field from a source"). Guards: excludes managed/system/internal-Source fields, type-compatible only, one field per source per column.Verification
mainrebase: the visible blog+resources row union, the new-row picker creating a correctly-taggedcreate_draft, and a column binding backfilling per-source values (ZIP/Spreadsheet/PDF on resource rows, blank on blog rows).Known follow-ups (non-blocking)
DocumentDatabase.tsxand the shared review dialog use hardcoded English where the rest of the editor now usesuseT/dbText— i18n the new row-union strings.DocumentDatabase.tsx); porting it to the inline-blockDatabaseView.tsxis a follow-up.🤖 Generated with Claude Code