---
apiVersion: data-spec/v1
id: resolve-curated-layout-cutover-ownership-20260427a
title: "Curated page layout cutover — stage 1/3: resolve legacy assignment route source"
description: "Operator-reviewed classification of legacy _System_LayoutAssignment rows before the curated-layout destructive cutover. Schema/non-destructive apply must have already added nullable RouteSource; this stage only classifies or removes unresolved route rows. Ordinary cmd/setup apply is not the destructive cleanup mechanism."
issue: "#2070"
created: 2026-04-27
scope: both
status: pending-staging
tables_modified:
  - _System_LayoutAssignment
tables: [_System_PageLayout, _System_LayoutAssignment]
idempotent: true
risk: medium
reviewers: ["@codeowners"]
depends_on:
  - schema/non-destructive apply that adds nullable _System_LayoutAssignment.RouteSource
blocks:
  - audit-curated-layout-cutover-ownership-20260427b
  - apply-curated-layout-cutover-20260427c
---

# Curated page layout cutover — resolve legacy assignment route source

## Summary

The curated page-layout cutover now has a strict routing/content split:

- deterministic admin-authored routes are preserved automatically by the
  binary (`RouteSource='admin_authored'` on the assignment row)
- layout content provenance stays on `_System_PageLayout.Source` /
  `IsCustomized` and must not be repurposed for route intent
- unresolved legacy rows are **not** destructively cleaned up
- the destructive step is blocked until every residual blocking row is
  explicitly classified
- ordinary `cmd/setup apply` is treated as schema/non-destructive setup for
  this sequence; it must not be the mechanism that performs curated-layout
  deletion or wildcard replacement

This stage is the operator-reviewed bridge for upgraded clusters. It covers the
current curated cutovers that share `applyCuratedPageLayout`: `User` and
`Opportunity`.

The residual blocking population is intentionally narrow:

- live assignment rows that still point at
  `Source='auto_generated' AND IsCustomized=0` layouts while the assignment row
  itself has no `RouteSource`
- wildcard assignment rows (all routing dimensions NULL) whose `RouteSource`
  is still unresolved, regardless of whether the target row is live
- live `Opportunity` / `User` layouts with no wildcard assignment at all

The runtime cannot distinguish historical admin intent from machine-generated
fallback for the first class, and must not let dead wildcard rows shadow live
defaults for the second. For the no-wildcard class, runtime fallback would
otherwise pick an arbitrary persisted layout and stamp `runtime_fallback`; the
operator must choose the route explicitly instead.
Ordinary pack apply must preserve the legacy `_System_PageLayout.IsDefault`
bits for this class so the reviewed candidate ordering below still reflects the
pre-cutover default evidence.

## Precondition

- Deploy/apply the schema-only or otherwise non-destructive setup that adds the
  nullable `_System_LayoutAssignment.RouteSource` column. Do not run a binary
  path that also performs destructive curated-layout cleanup.
- Execute this stage in a maintenance window, or with the affected layout
  writers quiesced and stage 3 following immediately.
- The runtime fallback path can recreate a missing wildcard assignment while
  traffic is live. Deleting a stale assignment row and then waiting before
  stage 3 can therefore reintroduce the same ambiguity.
- Use the preflight query below as the manifest for this stage. Only operate on
  rows that query returns.

## Preflight — residual ambiguous rows

```sql
WITH target_objects AS (
  SELECT 'Opportunity' AS ObjectApiName
  UNION ALL
  SELECT 'User' AS ObjectApiName
),
live_layout_candidates AS (
  SELECT
    t.ObjectApiName,
    pl.`Id` AS LayoutId,
    pl.`LayoutName`,
    ROW_NUMBER() OVER (
      PARTITION BY LOWER(t.ObjectApiName)
      ORDER BY
        CASE WHEN COALESCE(pl.`IsDefault`, 0) = 1 THEN 0 ELSE 1 END,
        pl.`CreatedDate` ASC,
        pl.`Id` ASC
    ) AS CandidateRank
  FROM target_objects t
  JOIN `_System_PageLayout` pl
    ON LOWER(pl.`ObjectApiName`) = LOWER(t.ObjectApiName)
   AND (pl.`IsDeleted` = 0 OR pl.`IsDeleted` IS NULL)
),
missing_wildcard AS (
  SELECT c.*
  FROM live_layout_candidates c
  WHERE c.CandidateRank = 1
    AND NOT EXISTS (
      SELECT 1
      FROM `_System_LayoutAssignment` la
      WHERE LOWER(la.`ObjectApiName`) = LOWER(c.ObjectApiName)
        AND la.`RoleId` IS NULL
        AND la.`ProfileId` IS NULL
        AND la.`RecordTypeId` IS NULL
    )
)
SELECT
  'unresolved_assignment' AS BlockingClass,
  la.`Id`           AS AssignmentId,
  la.`ObjectApiName`,
  la.`LayoutId`,
  la.`RoleId`,
  la.`ProfileId`,
  la.`RecordTypeId`,
  la.`Specificity`,
  CASE WHEN pl.`Id` IS NULL THEN 0 ELSE 1 END AS TargetPresent,
  pl.`LayoutName`
FROM `_System_LayoutAssignment` la
LEFT JOIN `_System_PageLayout` pl
  ON pl.`Id` = la.`LayoutId`
 AND LOWER(pl.`ObjectApiName`) = LOWER(la.`ObjectApiName`)
 AND (pl.`IsDeleted` = 0 OR pl.`IsDeleted` IS NULL)
WHERE LOWER(la.`ObjectApiName`) IN ('opportunity', 'user')
  AND (
    (
      pl.`Id` IS NOT NULL
      AND pl.`Source` = 'auto_generated'
      AND COALESCE(pl.`IsCustomized`, 0) = 0
      AND (la.`RouteSource` IS NULL OR TRIM(la.`RouteSource`) = '')
    ) OR (
      la.`RoleId` IS NULL
      AND la.`ProfileId` IS NULL
      AND la.`RecordTypeId` IS NULL
      AND (la.`RouteSource` IS NULL OR TRIM(la.`RouteSource`) = '')
    )
  )
UNION ALL
SELECT
  'missing_wildcard' AS BlockingClass,
  NULL AS AssignmentId,
  mw.ObjectApiName,
  mw.LayoutId,
  NULL AS RoleId,
  NULL AS ProfileId,
  NULL AS RecordTypeId,
  0 AS Specificity,
  1 AS TargetPresent,
  mw.LayoutName
FROM missing_wildcard mw
ORDER BY ObjectApiName, BlockingClass, Specificity DESC, AssignmentId;
```

**Interpretation**

- **0 rows**: no legacy ambiguity remains. This stage is a no-op for the
  cluster; proceed to stage 2 to record the zero-row audit.
- **>= 1 row**: each row must be classified before stage 3 may run.
  - `BlockingClass='unresolved_assignment'` rows that should remain as real
    admin-authored routing intent: use Action A
  - `BlockingClass='unresolved_assignment'` rows that are confirmed
    platform-managed curated routes: use Action B
  - `BlockingClass='unresolved_assignment'` rows that are machine-generated
    fallback / stale compatibility state: use Action C
  - `BlockingClass='missing_wildcard'` rows require an explicitly reviewed
    wildcard route: use Action D

## Changes

### Action A — classify a confirmed historical admin route

Use this when the operator has confirmed that the assignment row expresses a
real admin-authored routing choice that must survive the cutover.

```sql
UPDATE `_System_LayoutAssignment`
SET `RouteSource` = 'admin_authored',
    `LastModifiedDate` = CURRENT_TIMESTAMP
WHERE `Id` = '${AssignmentId}';
```

**Idempotency**: the `SET` values are the desired final state. Re-running after
the row is already classified produces the same state.

### Action B — classify a confirmed managed/platform route

Use this when the operator has confirmed that an unresolved assignment row is
the platform-managed default route, for example a legacy curated wildcard row
whose target is the reviewed curated layout.

```sql
UPDATE `_System_LayoutAssignment`
SET `RouteSource` = 'managed_seed',
    `LastModifiedDate` = CURRENT_TIMESTAMP
WHERE `Id` = '${AssignmentId}';
```

**Idempotency**: the `SET` values are the desired final state. Re-running after
the row is already classified produces the same state.

### Action C — remove a confirmed runtime-fallback / dead assignment row

Use this when the operator has confirmed that the assignment row does not
represent admin intent and may be discarded so the curated cutover can seed the
correct platform-owned wildcard/default routing. This is the required action
for stale wildcard rows and machine-generated fallback routes.

```sql
DELETE FROM `_System_LayoutAssignment`
WHERE `Id` = '${AssignmentId}';
```

**Idempotency**: deleting the same row twice is safe. The second run affects
zero rows.

### Action D — create a reviewed missing wildcard route

Use this for `BlockingClass='missing_wildcard'`. The operator must choose the
reviewed live layout and route source explicitly:

- `RouteSource='admin_authored'` only when the route is confirmed historical
  admin intent
- `RouteSource='managed_seed'` when the route is the reviewed platform-managed
  curated/default route

```sql
INSERT INTO `_System_LayoutAssignment` (
  `Id`, `ObjectApiName`, `LayoutId`, `RoleId`, `ProfileId`, `RecordTypeId`,
  `Specificity`, `CreatedDate`, `LastModifiedDate`, `RouteSource`
)
SELECT
  '${NewAssignmentId}',
  pl.`ObjectApiName`,
  pl.`Id`,
  NULL,
  NULL,
  NULL,
  0,
  CURRENT_TIMESTAMP,
  CURRENT_TIMESTAMP,
  '${ReviewedRouteSource}'
FROM `_System_PageLayout` pl
WHERE pl.`Id` = '${ReviewedLayoutId}'
  AND LOWER(pl.`ObjectApiName`) = LOWER('${ObjectApiName}')
  AND (pl.`IsDeleted` = 0 OR pl.`IsDeleted` IS NULL)
  AND '${ReviewedRouteSource}' IN ('admin_authored', 'managed_seed')
  AND NOT EXISTS (
    SELECT 1
    FROM `_System_LayoutAssignment` la
    WHERE LOWER(la.`ObjectApiName`) = LOWER(pl.`ObjectApiName`)
      AND la.`RoleId` IS NULL
      AND la.`ProfileId` IS NULL
      AND la.`RecordTypeId` IS NULL
  );
```

**Idempotency**: after the reviewed wildcard route exists, the `NOT EXISTS`
guard makes re-runs a no-op. First apply must affect exactly one row; zero rows
on first apply means the reviewed layout/source/object inputs are wrong or a
wildcard route appeared concurrently.

## Operator protocol

1. Run the preflight query and capture the manifest.
2. For each returned row, classify it:
   - If an unresolved assignment route is real admin intent, run **Action A**
     once for that row's `AssignmentId`.
   - If an unresolved assignment route is platform-managed curated/default
     intent, run **Action B** once for that row's `AssignmentId`.
   - If an unresolved assignment row is machine-generated fallback / stale
     compatibility state, run **Action C** once for that row's `AssignmentId`.
   - If a row is `BlockingClass='missing_wildcard'`, run **Action D** once
     with the reviewed object, layout, new assignment ID, and route source.
3. Re-run the preflight query.
4. If any rows remain, repeat step 2.
5. Once the preflight returns zero rows, proceed to stage 2.

## Rollback

Use the row-level backup captured before apply:

- restore deleted `_System_LayoutAssignment` rows from the backup
- restore `_System_LayoutAssignment.RouteSource` for rows changed by
  **Action A** or **Action B**
- delete reviewed wildcard routes inserted by **Action D**

No irreversible schema or data drop happens in this stage.
