Add typed Page relationships to Content - #4574
Conversation
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
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)
| await validateObservation(tx, { | ||
| token: plan.change.observedSlotToken, | ||
| kind: "slot", | ||
| typeId: plan.bundle.type.id, | ||
| sourcePageId: plan.sourcePageId, | ||
| context, | ||
| }); |
There was a problem hiding this comment.
🔴 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.
|
Here's a visual recap of what changed: Open the full interactive recap
|
There was a problem hiding this comment.
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)

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
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:productionfromtemplates/contentin 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.4ddb016872passed: 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.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.