# Issue 1665 PRD: Exclusive Sibling Fields

Status: draft
Date: 2026-06-01
Related: issue #1665, parent issue #1664

## Problem Statement

NexusCRM cannot currently express or enforce the Salesforce-style invariant
where one child row in a parent-scoped set owns a boolean flag and all sibling
rows must have that flag cleared.

The immediate CRM example is Campaign Member Status. Salesforce models
`CampaignMemberStatus` as one or more member status values defined for a
Campaign. Its `CampaignId` field scopes a status row to a campaign, and its
`IsDefault` field marks whether that status is the campaign's default status.
Salesforce documentation states that every campaign must have a default
CampaignMemberStatus beginning with API version 39.0, and Salesforce UI
documentation describes choosing one default status from the campaign's member
status list.

NexusCRM already has the L1 CRM shape for `CampaignMemberStatus`, including
`CampaignId`, `Label`, `IsDefault`, and `HasResponded`, and already uses those
rows as the runtime-scoped source for `CampaignMember.Status` validation and
`CampaignMember.HasResponded` projection. But the records runtime does not
clear the previous default when another status in the same campaign is written
with `IsDefault=true`. Two sibling rows can therefore both remain default at
the same time.

This is not a CRM-only problem. A Jira-like app could need exactly one default
issue type per project, one active board configuration per board, or one
primary sprint setting per project. The missing capability is a generic L0
substrate: metadata declares an exclusive boolean field and the parent
partition field that scopes it, and the records engine enforces the invariant
for any object using that declaration.

Salesforce references used for this PRD:

- CampaignMemberStatus Object Reference, Spring '25 API v63.0:
  https://developer.salesforce.com/docs/atlas.en-us.254.0.object_reference.meta/object_reference/sforce_api_objects_campaignmemberstatus.htm
- CampaignMember Object Reference, Spring '25 API v63.0:
  https://developer.salesforce.com/docs/atlas.en-us.254.0.object_reference.meta/object_reference/sforce_api_objects_campaignmember.htm
- Campaign Member Statuses Salesforce Help:
  https://help.salesforce.com/s/articleView?id=sales.campaigns_customize_member_status_parent.htm
- TiDB pessimistic transaction and no-gap-lock behavior:
  https://docs.pingcap.com/tidb/stable/pessimistic-transaction
- TiDB user-level locking functions:
  https://docs.pingcap.com/tidb/v8.1/locking-functions/

## Status Quo

NexusCRM has the pieces around this invariant, but not the invariant itself.

Object metadata currently carries executable object-level facts such as
default child rows, child clone rules, lock rules, list fields, and read-source
configuration. These facts round-trip through shared metadata models,
object-spec parsing, live `_System_Object` storage, schema scanning, pack apply,
pack verify, and tenant overlay preflight. There is no equivalent metadata
surface for "this boolean field is exclusive among siblings in this parent
partition."

The CRM L1 pack already defines the relevant Campaign Member Status shape:
`CampaignMemberStatus` has `CampaignId`, `Label`, `SortOrder`, `IsDefault`,
and `HasResponded`. The same pack already defines `CampaignMember.Status` as a
runtime-scoped value checked against `CampaignMemberStatus.Label` for the same
`CampaignId`, and defines `CampaignMember.HasResponded` as an active projection
from the selected status row.

The records write paths do not currently inspect object metadata for exclusive
sibling fields:

- single insert finalizes the incoming row, writes it, then runs post-insert
  side effects such as default child rows and child clone rules
- single update finalizes the merged row and writes the target record by id
- bulk insert validates a survivor set, lands rows in bulk, then runs any
  required post-landing side effects
- upsert delegates to insert or update after resolving the external-id match

Default child rows are adjacent but unrelated. They seed constant child rows
after a parent insert; they do not enforce mutual exclusion among already
existing siblings.

The repository API can update one row by id. The feature needs a new
metadata-validated internal reset operation that can update all active sibling
rows matching `(object, partitionField, partitionValue, exclusiveField=true)`,
excluding the current winner row when one exists.

The current BulkInsert shape is the highest-risk status-quo detail. It can land
all rows before per-row post-processing, so a design that locks or decides
winners only after landing is unsafe unless the partition lock was acquired
before that landing step.

## Salesforce Behavioral Model

Salesforce's public contract exposes three relevant facts.

First, `CampaignMemberStatus` is the per-campaign status catalog. Its
documented fields include `CampaignId`, `Label`, `SortOrder`, `IsDefault`, and
`HasResponded`. `CampaignId` scopes the row to a campaign. `IsDefault` marks
the default campaign member status. `HasResponded` marks whether that status
counts as responded.

Second, `CampaignMember.Status` is the text value used on campaign member
records. Salesforce's CampaignMember object reference says that the Status
field controls the CampaignMember HasResponded flag, that marketing users can
define valid Status values, that they can choose one default status, and that
API clients should use the Status text value instead of the
CampaignMemberStatus record id.

Third, Salesforce documents existence constraints that are not the same as
exclusive selection. Beginning with API version 39.0, every campaign must have
a default CampaignMemberStatus, and at least one CampaignMemberStatus per
campaign must have `HasResponded=true`. The default-status selection is
exclusive; responded-status membership is not.

Salesforce also prevents deletion of a CampaignMemberStatus when that row is
the default status or is currently used in a Campaign. That delete behavior is
important Salesforce context but is separate from issue #1665.

This PRD intentionally models the documented behavior, not Salesforce's private
implementation. The L0 primitive is generic; the CRM pack supplies the
CampaignMemberStatus declaration.

## Solution

Add an L0 metadata primitive named `ExclusiveSiblingFields`. Each declaration
belongs to object metadata and names:

- the boolean field that must be exclusive among siblings
- the parent reference field that defines the sibling partition

When a write produces a final record image where an exclusive field is true,
the records runtime serializes the affected parent partition, then clears the
same field on all other active sibling rows in that partition inside the same
transaction as the write.

For the Salesforce CRM pack, declare `CampaignMemberStatus.IsDefault` as
exclusive within the `CampaignId` partition. `HasResponded` is not exclusive:
Salesforce requires at least one responded status per campaign, and many status
values may count as responded. That is a different invariant and remains out
of scope for this PRD.

The implementation must be metadata-driven. The records engine must not branch
on `CampaignMemberStatus`, `CampaignId`, `IsDefault`, or any other CRM object
name. It should only interpret the object metadata currently in effect.

## Delivery Model

The target behavior is larger than the minimum issue fix because the metadata
contract affects every write path that can mutate an annotated object. To avoid
scope creep, implementation should be split by activation boundary, not by
helper file. A PR may add dormant substrate, or it may make an active metadata
declaration safe for a clearly named set of write paths. It should not both add
new authoring surfaces and chase every runtime edge case unless that PR is the
activation PR.

The detailed runtime sections below describe the end-state contract. They do
not override the delivery slices. For an intermediate runtime PR, the gate
question is whether every write surface outside that PR's named slice either
delegates to a supported path or fails closed before it can bypass the
invariant. Full implementation of a later slice is not required merely because
the end-state section says that surface eventually "must" participate.

### Activation Rule

`ExclusiveSiblingFields` must not be declared by the official Salesforce CRM
pack until every public write entrypoint that can write the annotated object is
either:

- enforcing the invariant inside the same transaction, or
- failing closed with a documented unsupported-entrypoint error.

This rule is the main scope-control guardrail. It lets metadata, runtime core,
bulk safety, and tenant/admin authoring land independently without exposing a
partially enforced product contract.

### Proposed PR Slices

#### PR 1: Dormant Metadata Contract

Goal: teach the platform how to store, parse, validate, apply, scan, and verify
`ExclusiveSiblingFields` without enabling it in a live CRM pack.

In scope:

- `shared/pkg/models` shape and JSON tags
- `_System_Object` columns for the declaration and pack ownership sidecars
- schema scan/persist round trip
- object-spec parser/build support
- pack preflight validation and source-backed verify
- canonical comparison for empty/missing declarations

Out of scope:

- records runtime enforcement
- parent-row locks
- sibling reset SQL
- event/audit/automation side effects
- tenant overlay/admin authoring
- official CRM pack declaration

Acceptance:

- malformed declarations fail pack validation
- valid declarations round-trip through metadata storage and verify
- no runtime behavior changes when no active declaration exists

#### PR 2: Single-Record Runtime Core

Goal: enforce the invariant for single-record insert, update, upsert, and the
prepared-update commit tail, using only test metadata or a deliberately
non-activated pack fixture.

In scope:

- contract resolution from effective object metadata
- parent-row lock acquisition for reference-backed partitions
- single-record insert reset behavior
- single-record update parent-lock-before-child-lock behavior
- upsert delegation to the existing insert/update paths after branch
  resolution
- prepared update commit using the same parent-lock-first reset path after
  before-save preview has produced its final image
- system-owned sibling reset side effects required to keep audit/outbox/cache
  state coherent for single-record writes, without firing record-triggered
  automation for reset siblings
- typed fail-closed guards for shared-transaction or multi-row entrypoints that
  cannot yet satisfy the invariant in this slice

Out of scope:

- bulk insert winner selection
- composite/batch all-or-none union prelocks
- create tree, generated child fanout, delete cleanup, and other shared
  transaction surfaces that need collection-level planning
- tenant/app-pack/admin validators for those later shared surfaces
- tenant overlay/admin authoring
- official CRM pack declaration
- repair/backfill of existing duplicate true rows

Acceptance:

- direct insert true clears the previous true sibling
- direct update true clears the previous true sibling
- partition moves clear only the final partition
- upsert does not create a third enforcement path
- prepared update commit enforces or fails closed before writing when it cannot
  acquire the required parent locks safely
- parent locks are acquired before child row locks
- inaccessible target rows and missing/inaccessible lookup parents keep the
  existing access/error envelope

Safety gate:

- the official CRM pack still does not declare the annotation after this PR, or
  unsupported write entrypoints for annotated objects fail closed before they
  can bypass the invariant.
- fail-closed guards must not reject already-pessimistic callers that are still
  executing the supported single-record path. For example, REST or action
  handlers that wrap a single `Insert` or `Update` in a pessimistic transaction
  are still PR 2 surfaces when the records service can use that existing
  pessimistic boundary safely.
- PR 2 is not required to implement bulk winner selection, all-or-none union
  prelocks, create-tree settling, generated fanout settling, or delete cleanup.
  Those are PR 3 responsibilities as long as the PR 2 implementation rejects
  unsupported annotated-object writes with a documented typed error before the
  first physical write.

#### PR 3: Bulk And Collection Write Safety

Goal: replace PR 2's unsupported-surface guards for collection and shared write
paths with enforcement, and leave any still-unsupported annotated-object path
failing closed.

This can be one PR if small, but should split into PR 3a and PR 3b if review
starts surfacing independent concerns.

PR 3a in scope:

- bulk insert landing-aware winner selection
- duplicate/ignored row semantics
- stale create-event payload repair after in-transaction sibling resets
- partial-success row accounting

PR 3b in scope:

- all-or-none composite/batch create/update/upsert parent-lock union planning
- retry on newly discovered before-save partitions
- stable sorted parent lock ordering across multi-row requests
- partial-success collection behavior through the single-row paths

Out of scope:

- tenant overlay/admin authoring
- official CRM pack activation if any write entrypoint remains unsupported

Acceptance:

- bulk writes leave one landed winner per partition
- duplicate-ignored rows do not clear existing siblings
- collection requests touching partitions in different input orders acquire
  parent locks in the same order
- every annotated-object write entrypoint is covered or explicitly rejected

#### PR 4: CRM Pack Activation

Goal: activate the product behavior by declaring the Salesforce CRM pack
metadata.

In scope:

- companion app-pack change declaring
  `CampaignMemberStatus.IsDefault` exclusive within `CampaignId`
- pack-local contract test for the declaration
- setup/apply/verify smoke against the platform support from PRs 1-3

Out of scope:

- new runtime mechanics
- tenant overlay/admin authoring
- delete restrictions and "at least one default" invariants

Acceptance:

- greenfield install applies and verifies the CRM pack
- direct, bulk, and collection writes against CampaignMemberStatus cannot
  create two defaults for one Campaign

#### PR 5: Tenant/Admin Authoring

Goal: allow L2 custom-object declarations and managed overrides after the
runtime contract is already safe for active metadata.

In scope:

- tenant custom-object overlay support
- locked/defaults-only manageability enforcement
- override removal restoring from `ExclusiveSiblingFieldsPackValue`
- missing sidecar fail-closed behavior

Out of scope:

- initial issue fix for the CRM pack
- runtime lock or reset changes unless a tenant-specific gap is found

Acceptance:

- tenant custom objects can declare and enforce the same invariant
- locked pack-owned declarations cannot be overridden
- defaults-only overrides round-trip and restore cleanly

#### Later Work

These are related Salesforce parity or operations tasks, but they should not
ride on the exclusive-selection implementation:

- at-least-one-true/default-existence invariant
- CampaignMemberStatus delete restrictions
- data repair/backfill for existing duplicate true rows
- arbitrary non-reference partition locks

### Implementation Surface Guidance

Keep each slice's code surface narrow:

- metadata code should not import records runtime code except for existing
  validation snapshot interfaces
- records runtime should interpret generic metadata only and should not mention
  CRM object or field names
- reset side effects should flow through one reusable system logical-update
  helper rather than exclusive-sibling-specific copies of audit/outbox/cache
  behavior
- upsert should delegate to insert/update once the external-id branch is known
  instead of owning independent reset logic
- bulk/collection prelock planning should live outside the single-row core so
  single-row review is not forced to audit bulk winner semantics
- tenant overlay should be delayed until active metadata is safe across the
  base write surface

## User Stories

### Activation Stories

1. As a marketing user, I want selecting a new default campaign member status
   to clear the old default automatically, so that a campaign never has two
   default statuses.

2. As a marketing user, I want the default status change to be atomic, so that
   other users never observe a committed state with two defaults for one
   campaign.

3. As an integrator, I want Salesforce-compatible behavior for
   `CampaignMember.Status` and `CampaignMember.HasResponded` to continue
   working while default-status exclusivity is added, so that existing scoped
   picklist behavior does not regress.

### Direct Write Stories

4. As an API client, I want an insert with `IsDefault=true` to make that new
   row the only default for its campaign, so that API behavior matches the
   platform invariant.

5. As an API client, I want an update that changes a non-default sibling to
   `IsDefault=true` to clear the old default sibling, so that I do not need to
   issue a manual cleanup update.

6. As an API client, I want updating a true exclusive field while changing its
   parent partition to enforce exclusivity in the new partition, so that moves
   do not introduce hidden duplicate defaults.

7. As an API client, I want writes with an exclusive field omitted or false to
   leave sibling rows unchanged, so that ordinary edits do not disturb the
   selected default.

8. As an API client, I want writes whose partition field is null to skip the
   sibling reset, so that incomplete or intentionally unpartitioned rows do not
   run a broad update.

### Bulk And Collection Stories

9. As a bulk API client, I want bulk inserts that contain multiple true rows in
   the same partition to produce one deterministic winner, so that bulk loads
   cannot create split-brain defaults.

10. As a bulk API client, I want duplicate-ignored or non-landed rows not to
    clear the existing default, so that a skipped input row has no side
    effects.

11. As a composite API client, I want all-or-none requests to lock parent
    partitions in a stable union order, so that reversed input order does not
    create deadlock-prone behavior.

### Metadata Authoring Stories

12. As an app-pack author, I want to declare the invariant in object metadata,
    so that the same runtime behavior works for CRM and non-CRM packs.

13. As an app-pack author, I want pack verification to catch malformed
    exclusive-field declarations before install mutates runtime metadata, so
    that bad packs fail closed.

14. As a tenant admin, I want custom objects to declare their own exclusive
    sibling fields, so that tenant metadata can express the same invariant
    without platform code.

15. As a tenant admin, I want L2 overlays to respect L1 manageability, so that
    locked pack-owned exclusive-field declarations cannot be silently replaced.

### Platform Engineering Stories

16. As a platform engineer, I want the records engine to use one generic
    enforcement path, so that future app packs do not add object-specific Go
    branches.

17. As a platform engineer, I want the lock strategy to be correct under TiDB,
    so that the design does not rely on gap locks that TiDB does not provide.

18. As a platform engineer, I want generated and scanned object metadata to
    round-trip this declaration, so that runtime reads one authoritative
    metadata fact from the schema service.

19. As a QA engineer, I want tests at the PersistenceService seam, so that
    behavior is verified from the caller's perspective rather than by testing
    private helper shape.

20. As an operator, I want existing invalid data left untouched until a future
    repair/backfill is explicitly run, so that this feature does not perform
    surprise data cleanup during deploy.

## Implementation Decisions

The decisions below describe the final target design. The delivery model above
controls which decisions belong in each PR. In particular, pack activation and
tenant overlay authoring should remain separate from dormant metadata and from
the first runtime core PR.

### Metadata Shape

Add `ExclusiveSiblingFields` to object metadata.

Go model:

```go
type ExclusiveSiblingField struct {
	FieldAPIName           string `json:"fieldApiName"`
	PartitionFieldAPIName string `json:"partitionFieldApiName"`
}
```

`fieldApiName` is the boolean field that is exclusive among siblings.
`partitionFieldApiName` is the reference field whose target row anchors the
parent partition. The issue's shorthand `partitionField` means this same
partition field API name; the canonical pack JSON should use
`partitionFieldApiName` to avoid implying arbitrary non-field partitions.

The first implementation admits only parent-reference partitions. The
`partitionFieldApiName` must exist on the same object, must be a physical
non-polymorphic reference or master-detail field with one referenced parent
object, and must not be a formula or derived field. This is deliberate: TiDB
does not provide gap locks for the range predicate that would be needed to
serialize arbitrary partition values, but a reference partition gives the
runtime a parent row to lock.

The exclusive field must exist on the same object, must be a physical boolean
field, and must be writable by normal object metadata rules. Formula fields,
active projections, rollups, autonumbers, and intrinsically read-only fields
cannot be exclusive fields.

The declaration is an executable object-metadata fact. Store it on the live
object metadata row as JSON and round-trip it through the schema service,
object-spec parsing, pack apply, and pack verify. Tenant overlay support uses
the same storage shape later, but should be delivered after the base runtime
contract is safe for active metadata. Canonical comparison treats missing,
null, empty string, and empty array as the same empty declaration set.

Add the same ownership shape used by executable child-clone metadata:

- `ExclusiveSiblingFields`
- `ExclusiveSiblingFieldsCustomized`
- `ExclusiveSiblingFieldsPackName`
- `ExclusiveSiblingFieldsPackBusinessKey`
- `ExclusiveSiblingFieldsPackManageability`
- `ExclusiveSiblingFieldsPackValue`

Pack apply owns the live declaration while `ExclusiveSiblingFieldsCustomized`
is false. Defaults-only L2 overrides own the live declaration while the bit is
true once tenant/admin authoring is enabled. Unrelated object edits must not
transfer ownership of this fact. Persist canonical `[]` into
`ExclusiveSiblingFieldsPackValue` when a source-backed object spec owns the
surface but omits the property; `NULL` means unresolved ownership, not "the
pack owns empty."

### Salesforce CRM Pack Declaration

The Salesforce CRM L1 pack declares exactly one exclusive sibling field for
Campaign Member Status:

- object API name: `CampaignMemberStatus`
- exclusive field API name: `IsDefault`
- partition field API name: `CampaignId`

The pack must not declare this invariant for Responded. Salesforce treats
Responded as a value class where multiple statuses can count as responded, not
as a single selected status.

Example object-spec JSON:

```json
{
  "apiName": "CampaignMemberStatus",
  "exclusiveSiblingFields": [
    {
      "fieldApiName": "IsDefault",
      "partitionFieldApiName": "CampaignId"
    }
  ]
}
```

### Transaction Boundary Selection

This section is the end-state transaction-boundary contract. During PR 2,
single-record and prepared-update paths must satisfy it. Bulk, collection,
tree, generated child, and delete-cleanup paths may instead fail closed for
annotated objects until the PR 3 collection/shared-surface work lands.

The transaction boundary must be selected before the final write image is
known. Before-save automation can mutate the exclusive field or the partition
field, so the runtime cannot wait until after before-save flow execution to
decide whether a pessimistic transaction is required.

In the end state, every direct write to an object with non-empty
`ExclusiveSiblingFields` enters an outer pessimistic transaction for insert,
update, upsert, bulk insert, and composite/batch mutation entrypoints. This is
true even when the inbound payload omits the exclusive field.

The boundary predicate must also recurse through metadata-authored child write
side effects. A root insert must enter the outer pessimistic transaction if its
default child rows or child clone rules can insert an object whose metadata
has `ExclusiveSiblingFields`. This mirrors the existing rollup transaction
boundary pattern: the outermost write chooses the safe transaction mode before
any nested insert can run. A nested exclusive child insert must never discover
inside a plain transaction that it needs to widen to pessimistic mode, because
the transaction manager correctly fails closed for that shape.

Bulk operations over multiple objects must choose the pessimistic path when any
object in the operation is annotated, or when any reachable metadata-authored
side effect can create annotated child rows.

### Write-Time Enforcement

The enforcement point is the final write image, after before-save automation,
normalization, system-default materialization, field filtering, lookup
validation, validation rules, and uniqueness checks have determined that the
write is otherwise allowed, but before the first physical write that can create
or preserve an exclusive true row commits.

The reset trigger differs by write shape:

- insert enforces when the final image has the exclusive field set to true and
  the parent partition value is non-empty
- update enforces when the final update sets or transitions the exclusive
  field to true
- update also enforces when the row remains true while the partition field
  changes, even if the patch did not explicitly include the exclusive field

An ordinary update to another field on a row that is already true in the same
partition does not perform a sibling reset. It may still need the parent lock
for lock ordering, but it is not a touched-partition repair operation.

The sibling reset is a system-owned physical update, not a recursive normal
record update. It must not run before-save flows, validation rules, or the same
exclusive-field hook for every sibling row it clears. The reset is constrained
by metadata-validated column names and by the partition value:

- same object as the incoming write
- same partition field value
- same exclusive field currently true
- active rows only, where active means `IsDeleted = 0` or `IsDeleted IS NULL`
- exclude the incoming row when the incoming row already exists or has landed

The reset repository seam must be explicit and narrow. It accepts the already
validated object metadata, exclusive field API name, partition field API name,
partition value, and optional winner id. It builds SQL only from
metadata-validated identifiers and runs on the active transaction executor. It
returns the ids of rows it changed so audit/outbox/cache hooks can observe the
system side effect without re-entering normal record automation.

Sibling resets stamp normal modification system fields and emit record-update
audit/outbox entries with a system side-effect origin. They do not evaluate
FLS/CRUD, before-save flows, validation rules, duplicate rules, or additional
exclusive-sibling hooks for the sibling rows they clear.

### TiDB Serialization

Every write that can enforce an exclusive sibling field must execute in a
pessimistic transaction. The runtime must acquire the parent reference row lock
before any insert, update, or bulk landing for the affected partition.

This chooses parent-row locking over range locking. TiDB's documented no-gap
lock behavior means `SELECT ... FOR UPDATE` over sibling rows with a
non-unique parent field does not block a concurrent insert into the same
partition. Locking the parent row gives every cooperating writer one concrete
row key to acquire before changing the child partition.

When one write touches more than one partition, acquire parent locks in a
stable sorted order by parent object and parent id. This avoids deadlocks
between bulk writes that touch the same partitions in different input orders.

If the runtime needs to lock a non-empty participating parent partition and the
parent row cannot be locked because it is missing or inaccessible to the system
write path, the affected record fails closed before any physical write or
sibling reset. This applies even when ordinary lookup validation is skipped,
such as bulk insert with `SkipValidation=true`.

A participating parent partition is one where the write might create, preserve,
move, or clear an exclusive true row:

- an insert whose final exclusive field is true
- an update whose old or final exclusive field is true
- an update that changes the exclusive field or partition field in a way that
  could produce a true row after before-save recomputation
- a bulk candidate row that meets the same criteria

False-row edits that neither set the exclusive field true nor move an
exclusive true row do not require a parent lock and do not fail merely because
their partition value points at a missing parent.

An ordinary edit to an already-true row in the same partition participates for
lock ordering only. It acquires the parent lock before the child lock, but it
does not perform sibling repair unless the update reset trigger is satisfied.

Do not use a post-insert lock. A post-insert reset can be safe only if the
partition lock was already acquired before the insert. The critical invariant
is "lock first, write second"; the exact reset statement may occur before or
after the physical insert as long as no uncommitted or committed interleaving
can bypass the lock.

Advisory locks remain a fallback option only if parent-row locking proves
impossible for a future non-reference partition type. They are not the preferred
v1 design because TiDB user-level locks require careful global lock ordering,
are not tied to transaction rollback, and would introduce a second lock
discipline beside existing row-level pessimistic transactions.

### Insert Behavior

Single-record insert enters a pessimistic transaction for annotated objects,
runs before-save processing to compute the final image, locks the final parent
partition if the final exclusive field is true and the partition value is
non-empty, clears any existing active siblings in the same partition, then
inserts the new record in the same transaction. If the insert fails, the
transaction rolls back the sibling reset.

Default child rows inherit this behavior because the existing default-child-row
mechanism inserts through the same record insert path, and because the root
transaction-boundary predicate has already widened parent inserts that can
create annotated child rows.

### Update Behavior

Single-record update must not lock the child row before the parent partition.
Current update code locks the target row as part of loading the old record, so
the implementation must reshape the annotated-object update path around a
parent-lock-first discipline.

The target algorithm is:

1. Build a non-locking planning image for the existing row and requested
   update. This read is used only to discover candidate parent partitions.
2. Start or reuse the outer pessimistic transaction selected by the transaction
   boundary predicate.
3. Acquire parent locks in stable order for every non-empty participating
   partition that the planning image says may be touched: the old partition
   when the old row is true, the requested partition when the request can make
   the row true, and any final true-row partition discovered by a preliminary
   before-save pass.
4. Only after those parent locks are held, lock and reload the target child row.
5. Recompute the update lifecycle from the locked row and original request.
6. If recomputation discovers a required parent partition that was not locked,
   abort the transaction with a retryable internal restart signal, release the
   child lock by rolling back, and restart from step 2 with the expanded parent
   lock set. Do not acquire a new parent lock after holding the child lock.
7. Once the locked-row recomputation is stable, clear active siblings in the
   final partition excluding the target row only if the update reset trigger is
   satisfied, then update the target row in the same transaction.

This lock order prevents deadlocks with insert and bulk paths, which acquire
the parent row first and may then update sibling child rows during the reset.

If an update changes the partition and the row remains true, only the final
partition is reset. The old partition may be left with no true row; maintaining
"at least one true" is a separate invariant.

### Upsert Behavior

Upsert must inherit the same semantics by delegating to the single-record
insert or update path after the external-id lookup resolves whether the row is
new or existing. The external-id lookup itself does not satisfy the exclusive
partition lock. Once the branch is known, the operation must enter the same
parent-lock-first insert or update path and must not add a third write path for
this invariant.

### Composite, Batch, And Prepared Mutation Behavior

This section intentionally mixes two delivery horizons. `CommitPreparedUpdate`
is part of PR 2 because it is the single-record update commit tail used by
before-save automation. Composite, batch, tree, and other multi-row/shared
transaction behavior is PR 3; until then, those surfaces may fail closed for
annotated objects rather than implementing the full union-planning behavior
below.

Composite collection create/update/upsert and `CreateRecords` /
`UpdateRecords` / `UpsertRecords` are write entrypoints, not a separate
semantics surface.

For all-or-none composite/batch mutations that run in one transaction, the
implementation must pre-plan the union of participating parent partitions
across all annotated rows in the request, acquire those parent locks in stable
order before any child row lock or write, then run each row's locked
insert/update/upsert work. Do not process rows in input order while acquiring
parent locks lazily; two requests touching partitions A and B in opposite input
orders must still acquire locks in the same sorted order.

For all-or-none create, the pre-plan uses the candidate create images and then
recomputes after before-save processing. If recomputation discovers an unheld
required parent partition, abort and restart the whole all-or-none transaction
with the expanded lock set.

For all-or-none upsert, external-id branch resolution may happen before
exclusive parent locking, but the subsequent insert/update work must
participate in the same stable parent-lock plan. If a later locked recompute
discovers an unheld required parent partition, abort and restart the whole
all-or-none transaction with the expanded lock set.

For partial-success composite/batch mutations, each row may use the single-row
parent-lock-first insert/update/upsert path independently, preserving existing
per-row result semantics.

`PreviewBeforeUpdate` remains a read-only preview surface. `CommitPreparedUpdate`
is a write surface and must use the same annotated-object pessimistic boundary
and parent-lock-first enforcement as `Update`. It derives candidate partitions
from the prepared record image, prepared effective/internal updates, and any
extra updates supplied at commit time. If this path cannot safely acquire
parent locks before the target child row lock, it must fail closed for
annotated objects until migrated.

### Bulk Insert Behavior

Bulk insert for an annotated object must take a landing-aware path. The runtime
must know which candidate rows actually landed before it can safely decide the
winner for each partition.

The bulk algorithm is:

1. Run the existing validation and normalization passes to produce the survivor
   set.
2. Identify survivor rows whose final exclusive field value is true and whose
   partition value is non-empty.
3. Lock all affected parent rows in sorted order before any physical insert.
   If any candidate true row has a non-empty partition whose parent row cannot
   be locked, fail that input row before landing and remove it from the winner
   set.
4. Use a landing-aware bulk insert for annotated objects, even if no other
   feature currently needs landing evidence.
5. For each exclusive field and partition, choose the last landed true row in
   survivor input order as the winner.
6. If a partition has a landed winner, clear the exclusive field on every other
   active sibling in that partition, including earlier landed rows in the same
   batch.
7. If a partition has no landed winner because all candidate true rows failed
   or were ignored as duplicates, do not clear the existing true sibling.

This preserves bulk efficiency where possible, respects partial success, and
avoids the parent issue's unsafe shape where all rows are inserted before any
partition serialization exists.

Bulk result accounting is based on input-row landing and validation status.
Sibling reset side effects never create extra bulk result rows and never turn a
non-landed duplicate into the partition winner.

### Pack Support And Tenant Overlay Support

Pack apply validates all declarations before mutating metadata. The validator
checks field existence, field type, parent-reference shape, duplicate
declarations, and self-consistency against same-install specs plus live
metadata fallback.

Pack apply persists the pack ownership sidecars for every source-backed
object-spec row, including canonical `ExclusiveSiblingFieldsPackValue`. Pack
verify compares the canonical stored JSON to the canonical pack payload unless
the column-scoped customization bit and sidecar state indicate a legitimate L2
override. Missing pack ownership sidecars on a source-backed object are
unresolved state and must fail closed with a repair/reapply instruction rather
than guessing from `IsCustom` or row-level `IsCustomized`.

Tenant overlay can declare exclusive sibling fields on tenant custom objects.
For standard-object L2 overrides, tenant apply reads only persisted ownership:

- no pack owner on a true tenant custom object: allow the L2 declaration
- no pack owner on a source-backed object: fail unresolved
- locked pack owner: reject the override
- defaults-only pack owner: write the live declaration and set
  `ExclusiveSiblingFieldsCustomized=1`
- override removal: restore live declaration from
  `ExclusiveSiblingFieldsPackValue` and set
  `ExclusiveSiblingFieldsCustomized=0`

Tenant apply must not resolve source-backed pack transports to decide ownership
during ordinary overlay application.

Delivery note: pack parsing/apply/verify belongs in the dormant metadata PR.
The official CRM pack declaration belongs in the activation PR. Tenant overlay
support belongs in its own later PR unless the product explicitly requires L2
authoring before CRM activation.

### Current Data

This feature does not retroactively repair existing duplicate true rows.
Runtime writes that trigger enforcement converge the participating partition
they touch. A future repair command can scan and normalize existing data if the
product needs migration support.

## Testing Decisions

Good tests should exercise observable behavior at the highest available seam.
Private helper tests are acceptable for validation and lock-order edge cases,
but the main proof must be through PersistenceService and pack apply/verify
entrypoints.

Tests should be grouped by delivery slice. A metadata PR should not need to
run or review record-locking tests. A single-record runtime PR should not need
bulk winner-selection tests. The activation PR should mostly prove that the
already-landed substrate works for the CRM pack declaration.

Test the metadata parser and validation surface with object specs that cover:

- valid boolean/reference declarations
- missing exclusive field
- non-boolean exclusive field
- formula or derived exclusive field
- missing partition field
- non-reference partition field
- polymorphic or multi-target partition field
- duplicate declarations on the same object
- same-install references and live-fallback references

Test single insert behavior:

- annotated object inserts choose a pessimistic transaction even when the
  inbound payload omits the exclusive field, because before-save automation can
  set it later
- two sequential inserts in the same partition with the exclusive field true
  leave only the second row true
- insert true in partition A does not affect partition B
- insert false or omitted does not affect an existing true sibling
- insert true with null partition skips reset
- insert true with a non-empty partition whose parent row cannot be locked
  fails before the row lands and does not reset siblings

Test update behavior:

- annotated object updates acquire parent locks before locking the target child
  row
- update row B to true while row A is true clears row A
- update a non-exclusive field does not clear siblings
- update a non-exclusive field on an already-true row in the same partition
  acquires the parent lock for ordering but does not perform sibling repair
- update a true row into a new partition clears the new partition's existing
  true sibling
- update false does not select a replacement sibling
- before-save flow changes to the exclusive field or partition are enforced
  from the final recomputed image
- if recomputation discovers a parent partition that was not locked, the write
  restarts rather than acquiring a parent lock after the child lock
- false-row edits that do not set the exclusive field true and do not move a
  true row do not fail because of a missing partition parent

Test bulk insert behavior:

- multiple true rows in one partition produce the last landed true row as the
  winner
- partitions are independent inside the same batch
- duplicate-ignored winners that do not land do not clear existing siblings
- `IgnoreDuplicates=true` chooses the last landed true row, not the last
  attempted true row
- `SkipValidation=true` plus a missing parent partition fails the affected row
  and does not clear existing siblings
- partial success reports remain aligned with the existing bulk result shape
- annotated bulk inserts choose a pessimistic transaction before pass-2 can
  land any rows

Test concurrency under the TiDB-backed integration seam:

- two concurrent inserts setting true for the same partition serialize and
  commit with exactly one true row
- two concurrent updates setting different rows true serialize and commit with
  exactly one true row
- two bulk inserts touching overlapping partitions do not deadlock and preserve
  one true per partition
- insert-vs-update and bulk-vs-update contention do not deadlock because both
  paths acquire parent locks before child locks
- all-or-none batch update pre-plans and locks the union of parent partitions
  in sorted order before any child row lock
- all-or-none batch update restarts the whole transaction if row recomputation
  discovers an unheld required parent partition
- all-or-none composite create pre-plans parent partitions across annotated
  rows and locks them in sorted order before any row lands
- all-or-none composite upsert resolves branches, then locks participating
  parent partitions in sorted order before any insert/update work
- all-or-none composite create/upsert requests touching partitions in reversed
  input order do not deadlock under the TiDB-backed integration seam
- default child rows and child clone rules that create annotated child rows run
  under a pessimistic outer transaction before any nested insert

Test pack and overlay support:

- a pack with `exclusiveSiblingFields` applies and verifies cleanly
- malformed declarations fail preflight before any metadata mutation
- the CRM pack declaration for Campaign Member Status round-trips
- tenant custom object declarations apply and enforce at runtime
- pack ownership sidecars persist canonical `[]` for omitted/empty pack values
- locked standard-object override attempts fail closed
- defaults-only overrides set and honor the column-scoped customization bit and
  restore from `ExclusiveSiblingFieldsPackValue` when removed
- missing sidecars on a source-backed object fail tenant apply and pack verify
  with a repair/reapply instruction
- prepared update commit on an annotated object uses the same parent-lock-first
  path and safely resets siblings when it sets or moves an exclusive true row

Regression coverage:

- default-child-row tests continue to pass
- child-clone-rule ownership tests continue to pass
- runtime-scoped picklist tests for Campaign Member Status continue to pass
- active projection tests for CampaignMember.HasResponded continue to pass
- audit/outbox tests prove sibling reset emits system-origin update events
  without firing record-triggered automation for reset siblings

## Out of Scope

This PRD does not implement the "at least one true row per partition" invariant.
Salesforce requires every campaign to have a default CampaignMemberStatus and
at least one responded status, but those are separate existence invariants from
exclusive selection.

This PRD does not implement Salesforce's delete restrictions for default or
in-use CampaignMemberStatus rows.

This PRD does not change CampaignMember.Status text-value behavior or the
CampaignMember.HasResponded projection.

This PRD does not add a general arbitrary-value partition lock. V1 supports
reference-backed parent partitions only.

This PRD does not backfill existing duplicate true rows.

This PRD does not attempt to exactly reproduce any Salesforce internal
implementation detail that is not exposed through documented object behavior.

## Further Notes

The decisive design constraint is TiDB's locking model. A sibling-row range
lock is tempting but unsafe because TiDB does not support gap locking for the
concurrent insert case. The safe v1 shape is to make the metadata contract
reference-backed and serialize writes on the parent row before touching child
rows.

The design intentionally treats this as L0 substrate plus L1/L2 metadata. L0
owns the generic interpreter and lock discipline. The CRM pack owns the
Campaign Member Status declaration. Tenant overlays own tenant-specific
declarations and overrides.

The issue body names a simple pre-write reset helper. That is necessary but not
sufficient. The implementation is only correct when the helper is paired with a
transaction boundary that acquires the partition lock before insert, update, or
bulk landing.
