Skip to content

Add typed Page relationships to Content - #4574

Draft
3mdistal wants to merge 7 commits into
mainfrom
codex/content-typed-relationships
Draft

Add typed Page relationships to Content#4574
3mdistal wants to merge 7 commits into
mainfrom
codex/content-typed-relationships

Conversation

@3mdistal

@3mdistal 3mdistal commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Content coordinators need to assign deliverables to people records and inspect the same assignment from either Page. Existing relation values are property-shaped payloads: they do not provide one durable relationship with consistent access checks across both Pages and the route used, atomic bulk changes, and recoverable history.

This adds the first usable slice of typed Page relationships. An authorized collection admin can configure Contributors or a single Assignee, choose the collection of eligible Pages, and optionally add a reverse column. Linked names navigate to their Pages; a separate edit control changes assignments. Page Info shows the applicable fields and independent relationship History with Undo. There is no separate Connections list or fallback list when a column is absent. The UI and agents call the same Actions.

Behavior and implementation

  • A relationship has stable identity independent of the columns that display it. Removing a column or database membership preserves surviving Page relationships. Trash suspends visible use. Restoring both endpoints resumes surviving activations; explicit removals made during trash stay removed. Permanent Page deletion prevents resurrection.
  • Configuration supports local directional relationships within one Content space, a database of eligible targets, forward one/many values, and an optional inverse-many column. Inverse editing is explicitly enabled. A person record is a Page: assigning it does not invite anyone, grant access, send a notification, or reserve capacity.
  • Bulk editing uses one list: checked means linked from every selected row, a dash means some, and unchecked means none. Clicking a row selects or clears that person for the selected rows; Apply commits the deliberate changes and Cancel discards them. Untouched assignments remain intact.
  • Add, observed remove, single-value replacement, and bounded bulk changes commit atomically. Removal previews retain the original selection, so later concurrent additions survive. Operation receipts distinguish a retry from a new intended change. Valid concurrent max-one replacements intentionally serialize: the later commit wins and records the displaced choice, while displacement still requires current authority. A stale clear remains observation-scoped; it is never upgraded into replacement.
  • Relationship history records the change, origin, and authorizing person in the same transaction. Undo compensates the recorded change under current permissions and constraints rather than restoring an entire Page snapshot over newer work.
  • Access checks cover candidates, traversal, row values, history, exports, and mutations. Viewing an incoming link alone does not permit severing it. Mixed-permission bulk requests fail without changing the authorized subset.
  • Existing setters, imports, row migration, source binding, and duplication routes cannot overwrite canonical relationships through legacy payloads. Unsupported writes fail explicitly. Existing legacy relation data remains readable; this does not automatically convert or synchronize it.

Compatibility and operations

The additive Content migrations retain main's migrations and add the relationship tables, event ordering, and revision-document index after them. No existing tables or columns are dropped and no legacy data is backfilled into guessed relationship types. The fresh PostgreSQL release and repeated runtime startup passed after compilation completed. The first development requests during compilation returned 503 before subsequent HTTP 200 responses. Existing history is indexed by pnpm migrate:production from templates/content in bounded batches of 100 revisions, transactionally per revision. Server startup does not perform the backfill. History fails explicitly until its relevant index is ready; a failed release can be retried. The migration statements run sequentially and record completion only at the end: an interrupted run can leave a partial additive schema, which restart completes through idempotent DDL. This verifies restart behavior, not transactional DDL rollback.

The broader Typed Relationships capability remains in_progress. Query-backed target selection, governed or symmetric types, inline semantic references, Graph/Canvas, and provider-owned relationship writes are outside this approved slice. This slice ships without a feature flag; Relation becomes directly available. Rolling application code back requires retaining the additive tables. Older code cannot read the canonical edge-backed values and can accept independent legacy JSON values, producing blank or divergent cells. Those legacy values do not overwrite canonical relationships and are ignored by the new code, so rollback is read-incompatible.

Permanent Page deletion retains inaccessible relationship lineage, events, revisions, and endpoint tombstones, including Page IDs and attribution, to prevent resurrection. This deliberately retains audit data after the Page is deleted.

Verification

Main’s new document-history work is integrated. The earlier implementation passed the bounded first-slice acceptance and CI at 4ddb016872; the revised creation copy, clickable fields, independent History and bulk toggles now pass fresh implementer-run browser checks on the refinement commit. No merge or production deployment is claimed.

  • Retained integration checks at 4ddb016872 passed: 60 relationship regression tests; 161 UI/history tests; 51 parity tests; 52 migration specs; affected database history/lifecycle/row-migration suites; typecheck; production build; and all 71 guards. The earlier 69-test relationship run includes separate-connection PostgreSQL concurrency proof. Formatting and lint pass.
  • Main’s document-history migration stays unchanged at version 90. Relationship migrations retain their names and SQL at versions 91–93, in order. Fresh PostgreSQL release and exact repeat passed with schema read-back; the temporary cluster was removed. Relationship rendering/state/query files are unchanged by the main merge, and focused tests cover the shared editor/history composition.
  • Implementer-run browser stories used disposable synthetic team/deliverable Pages: configuration, forward/inverse editing, authority denial and hidden endpoints, atomic bulk changes, preserved concurrent additions, Property removal, History/Undo, lifecycle transitions and CSV download.
  • The real internal agent read roles, capacity and effort through supported Actions, made one atomic two-row assignment, replayed the exact request, and queried assignments. Independent database read-back found one receipt, one revision and two Events with agent/run attribution. The already-open table updated without navigation or reload; final durable Undo restored the original values and reported accurate removal/restoration history.
  • Live QA found and fixed two boundary defects: relationship mutations were missing from action-driven UI invalidation, and Undo counted preserved activations to permanently deleted targets as occupying max-one slots. Page property queries are included because they contain canonical relationship values; the final dependency repair passed focused tests and independent review after the v2 live run. Mutation and all three Undo paths now share endpoint eligibility. Real stale conflicts and current access checks remain enforced. Undoing a replacement that began empty now records a removal rather than a false restoration.
  • Exact-request UI Retry passed after both a pre-commit failure and a lost HTTP-200 response. Independent database reads verified one durable application per operation.
  • Full mobile and desktop workflows, persistence, Escape/focus return and the 639/641px bulk breakpoint passed. The earlier 24-frame comparison records the previous UI and is retained as historical evidence. Fresh refinement checks cover desktop 1280, compact 900 and mobile 390 widths, keyboard toggles, linked navigation, no-column History, column Undo and reload, max-one search replacement, cross-column draft/retry isolation, and an interrupted bulk request followed by Retry. Motion remains unverified by still images.
  • Upgrade proof: the release command indexed 49 existing revisions without changing original revision/event/receipt fingerprints. Retry history recovered old records; fresh release and repeated runtime startup passed after compilation.
  • The temporary local Builder connection, browser tabs, local servers, isolated database cluster, and declared temporary credential files were removed after acceptance. Evidence is preserved separately.

Fresh refinement verification also passed 121 focused editor/configuration/bulk tests, Content typecheck and production build. All 71 guards passed across the full run and the corrected tracked-import rerun. A concurrent MCP add arrived in the browser before Apply; removal preserved its newer activation. Removing both source columns and undoing both removals preserved exact edge identities and activations in Action read-back. Independent refinement review found no remaining findings after fixes to frozen observations, hidden max-one choices, mixed-state rendering and cross-column drafts.

Independent review closed the candidate/history scaling and mutation findings and found no remaining blockers in the final Undo repair. Candidate access/search predicates precede SQL pagination; history uses an access-complete normalized index and keyset pagination; exact receipt replay precedes volatile observations; duplicate removals and malformed tenant/endpoint event data fail before persistence. The approved main integration is complete. Incremental review verified the refresh repair, and the final main integration passed independent technical review. The older stale-slot comment remains open because it conflicts with the approved later-valid-commit-wins replacement contract described above. Required GitHub approval remains pending.

content_product_impact:
  lane: contract_fulfillment
  features: []
  capabilities:
    - content.relationship.edge
  record_change: included
  proof:
    - pnpm test:content-product-impact
    - pnpm guards
    - Focused Content relationship, lifecycle, compatibility, and MCP tests
    - Disposable real-interface relationship workflow verification
  rationale: Implements the approved local directional relationship slice while retaining the broader capability's unverified boundaries.

@netlify

This comment has been minimized.

@builder-io-integration builder-io-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Builder reviewed your changes and found 5 potential issues 🔴

Review Details

Code Review Summary

PR #4574 adds a substantial typed Page-relationship subsystem for Content: durable relationship identities, scoped candidate/traversal reads, atomic mutations, lifecycle handling, revision history/causal undo, UI configuration and editing, and compatibility guards around legacy property writes. The overall decomposition into authority, core, lifecycle, read, and compatibility modules is sensible, and the accompanying database/concurrency/undo tests cover many important invariants. I classified this as high risk because it introduces access-controlled data mutation, durable audit lineage, concurrency semantics, and new action contracts.

Key Findings

  • 🔴 HIGH: max-one replacement does not compare the picker’s observed activation IDs with current slot state, allowing a stale concurrent replacement to overwrite a newer choice.
  • 🟡 MEDIUM: operation receipt replay happens after state-dependent planning/authorization, so delayed retries can fail instead of returning the committed result after later lifecycle changes.
  • 🟡 MEDIUM: duplicate remove entries can commit a revision whose undo validation cannot succeed.
  • 🟡 MEDIUM: candidate and history listing perform unbounded reads and per-item authorization before pagination, creating scaling risks.

The review found no additional confirmed authorization bypass or legacy write-path bypass.

🧪 Browser testing: Will run after this review (PR touches UI code)

Comment on lines +668 to +674
await validateObservation(tx, {
token: plan.change.observedSlotToken,
kind: "slot",
typeId: plan.bundle.type.id,
sourcePageId: plan.sourcePageId,
context,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Replace accepts stale slot observations and overwrites newer choices

The slot token records the activation IDs observed by the picker, but the replace path validates only token identity/TTL and then reads the current slot. Two users can submit replacements from the same observation: after the first commits, the second still has a valid token and silently retires the first user's newer choice instead of returning STALE_SELECTION. Compare the token's observed activation IDs with the current slot activations under the cardinality lock.

Additional Info
Found by 1 of 4 review agents; confirmed by inspecting the replace path and concurrency semantics.

Fix in Builder

Comment thread templates/content/actions/mutate-content-relationships.ts
Comment thread templates/content/actions/mutate-content-relationships.ts Outdated
Comment thread templates/content/actions/list-content-relation-candidates.ts Outdated
Comment thread templates/content/actions/list-content-relationship-history.ts Outdated
builder-io-integration[bot]

This comment was marked as outdated.

builder-io-integration[bot]

This comment was marked as outdated.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Here's a visual recap of what changed:

Visual recap

Open the full interactive recap

Large diff — this recap is a summarized view (top files + schema/API deltas).

builder-io-integration[bot]

This comment was marked as outdated.

builder-io-integration[bot]

This comment was marked as outdated.

@builder-io-integration builder-io-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Builder reviewed your changes — no new findings

Review Details

Incremental Code Review Summary

Reviewed the current head 4ddb01687 against the previously reviewed relationship-refresh head b16e542d6. This update merges the Content document-history/database/plugin work and reconciles migration ordering. I reviewed the integration points affecting typed Page relationships: schema and migration sequencing, document update/import/restore/trash/delete paths, relationship endpoint lifecycle handling, access-scoped reads, and action refresh/invalidation.

The relationship-focused review found no confirmed new actionable issues. The existing stale max-one replacement comment remains unresolved and was intentionally not reposted. The large PR diff exceeds the GitHub diff endpoint limit, so agents reviewed the current commit range and surrounding source directly.

Risk remains high because the PR combines access-controlled relationship mutation with document lifecycle/history infrastructure. Browser verification will follow because this incremental update includes Content UI and document-history UI changes.

🧪 Browser testing: Will run after this review (PR touches UI code)

@3mdistal
3mdistal marked this pull request as draft September 9, 2026 14:25
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