# Layout Default And Intent SSoT Reset

## Status

Draft

## Context

PR `#2101` has now failed repeated review rounds in one architectural class:

- effective default vs UI-visible default split across different storage
- routing authorship vs content ownership encoded through overlapping signals
- migration/data-spec logic preserving history through a different model than runtime

This document is the reset point. It does **not** try to salvage the current
point-fix chain. It defines one durable model for the shared-state facts that
kept splitting across runtime, UI, control-plane, and migration code.

## 1. Salesforce Baseline

Salesforce exposes two admin-facing concepts here:

1. **Page layout content**
   Admin edits a layout's fields, sections, and related lists.
2. **Layout assignment / default routing**
   Admin chooses which audience sees which layout, and which layout is the
   default fallback for audiences without a more specific assignment.

Our own design docs already encode that split:

- `Source + GenerationVersion` on `_System_PageLayout` exist to answer
  "should this layout regenerate from schema?" and were explicitly chosen to
  replace the older `IsCustomized` conflation.
  Evidence: [docs/design/layout-system-research.md:851](../design/layout-system-research.md#L851)
- "Default" in the assignment matrix means "fall back to the wildcard
  assignment", not "copy the current default layout ID into every cell".
  Evidence: [docs/design/phase2-multi-layout-assignment.md:183](../design/phase2-multi-layout-assignment.md#L183)

The important product-boundary point is not Salesforce's hidden storage. It is
the external contract:

- assigning a layout to an audience is **routing**
- editing a layout is **content ownership**
- these are different admin actions and must not silently mutate each other

## 2. Current Repo Evidence

### 2.1 Effective default already lives on wildcard assignment

- `SetDefaultLayout` updates the specificity-0 wildcard assignment so runtime
  fallback follows the new default:
  [backend/internal/infrastructure/persistence/repo_layout.go:168](../../backend/internal/infrastructure/persistence/repo_layout.go#L168)
- `ResolveLayout` creates/repairs a wildcard assignment when no assignment is
  found:
  [backend/internal/features/metadata/metadata_layouts.go:179](../../backend/internal/features/metadata/metadata_layouts.go#L179)
- curated apply also clears and re-inserts the wildcard assignment:
  [backend/internal/controlplane/packs/curated_page_layout_reconcile.go:291](../../backend/internal/controlplane/packs/curated_page_layout_reconcile.go#L291)

This means the system's actual fallback route is already `_System_LayoutAssignment`
rather than `_System_PageLayout.IsDefault`.

### 2.2 UI still reads `_System_PageLayout.IsDefault`

- list API returns `layout.IsDefault` directly:
  [backend/internal/interfaces/rest/ui_api_handler.go:265](../../backend/internal/interfaces/rest/ui_api_handler.go#L265)
- delete guard blocks only when `layout.IsDefault` is true:
  [backend/internal/interfaces/rest/ui_api_handler.go:671](../../backend/internal/interfaces/rest/ui_api_handler.go#L671)

So one shared-state fact, "is this the effective default layout?", already has
two read paths.

### 2.3 Runtime now treats routing authorship and content ownership differently

- `SetDefaultLayout` now preserves assignment authorship but intentionally does
  not mutate the layout row:
  [backend/internal/infrastructure/persistence/repo_layout.go:214](../../backend/internal/infrastructure/persistence/repo_layout.go#L214)
- `CreateAssignment` does the same for targeted routes:
  [backend/internal/infrastructure/persistence/repo_layout.go:479](../../backend/internal/infrastructure/persistence/repo_layout.go#L479)
- regeneration still keys only on page-layout provenance:
  [backend/internal/features/metadata/metadata_layouts.go:132](../../backend/internal/features/metadata/metadata_layouts.go#L132)

This split is correct in principle: routing does not mean content ownership.

### 2.4 The stage-1 migration spec still preserves admin route by mutating layout provenance

- Action A in the current spec flips `Source='admin_created'` and
  `IsCustomized=1`:
  [data-specs/2026-04-27a-resolve-curated-layout-cutover-ownership.md:107](../../data-specs/2026-04-27a-resolve-curated-layout-cutover-ownership.md#L107)

That directly conflicts with the runtime contract above, because the same row
then stops participating in schema-driven regeneration.

## 3. Problem Statement

The current implementation has four distinct facts, but only three overlapping
signal groups:

| Fact | Signals currently used |
|---|---|
| effective default | wildcard `_System_LayoutAssignment`, `_System_PageLayout.IsDefault` |
| UI-visible default | `_System_PageLayout.IsDefault` |
| routing authorship | `_System_LayoutAssignment.CreatedById`, sometimes `_System_PageLayout.Source` via migration |
| content ownership / regeneration eligibility | `_System_PageLayout.Source`, `_System_PageLayout.IsCustomized` |

This is why each review round fixed one split and exposed another. The model
still has no unique owner for each fact.

## 4. Candidate Paths

### Path A — Keep current schema, overload existing fields harder

Possible shape:

- keep using wildcard assignment as the effective default
- derive UI `isDefault` from wildcard assignment
- keep using `_System_LayoutAssignment.CreatedById` as the routing-authorship
  signal
- rewrite stage-1 remediation to backfill `CreatedById` instead of mutating
  `PageLayout.Source`

Why this is **not** the recommended path:

1. `CreatedById` is an audit field, not a routing-intent field.
2. Historical rows with unknown creator force the migration either to guess or
   to falsify the creator.
3. The current failure class exists precisely because one field was repeatedly
   asked to carry more than one meaning.

Path A is smaller, but it keeps the audit-vs-intent conflation alive.

### Path B — Introduce one explicit routing-provenance field on assignment

Recommended shape:

- add `_System_LayoutAssignment.RouteSource`
- legal values:
  - `runtime_fallback`
  - `managed_seed`
  - `admin_authored`
- keep `_System_PageLayout.Source` exclusively for layout-content provenance
  and regeneration eligibility
- treat `_System_PageLayout.IsDefault` as a compatibility projection only, not
  the source of truth

Why Path B is recommended:

1. It gives routing provenance a dedicated home.
2. It lets migration classify history without falsifying an audit field.
3. It preserves the already-correct runtime insight: routing does not mutate
   layout content ownership.
4. It creates one clean cut between runtime and data-spec remediation.

## 5. Chosen Model

### 5.1 Fact 1 — Content ownership / regeneration eligibility

**SSoT:** `_System_PageLayout.Source` + `GenerationVersion`

**Meaning:**

- `auto_generated` → eligible for schema-driven regeneration
- `admin_created` / `cloned` / `curated` → not eligible for auto-regeneration

**Writers that may change this fact:**

- layout content creation/save/clone flows
- curated pack apply when installing the curated row itself
- explicit content-oriented remediation only

**Writers that must never change this fact:**

- `SetDefaultLayout`
- `CreateAssignment`
- runtime fallback assignment repair
- assignment-focused migration specs

### 5.2 Fact 2 — Routing authorship / provenance

**SSoT:** `_System_LayoutAssignment.RouteSource` (new field)

**Meaning:**

- `runtime_fallback` → platform created the row to repair missing fallback
- `managed_seed` → control plane seeded a managed default route
- `admin_authored` → admin explicitly authored or re-authored the route

**Why not `CreatedById`:**

- `CreatedById` remains audit data
- `RouteSource` carries behavior semantics
- migration can mark a reviewed historical route as `admin_authored` without
  lying about who originally created the row

### 5.3 Fact 3 — Effective default

**SSoT:** the specificity-0 wildcard row in `_System_LayoutAssignment`

**Read path:** one helper, shared everywhere

Proposed helper boundary:

- `LoadEffectiveDefaultLayoutID(objectAPIName)` returns the wildcard target
- `IsEffectiveDefault(layoutID, objectAPIName)` or equivalent projection

**Consumers that must use the same helper:**

- `ResolveLayout`
- setup layout list API
- delete guard
- curated apply / verify
- any future "show me the default layout" UI or API

### 5.4 Fact 4 — UI-visible default

**SSoT:** none of its own; it is a projection of Fact 3

The frontend still needs `layout.isDefault` in API responses. That remains a
response field, but it must be computed from the wildcard-assignment helper,
not read directly from persisted `_System_PageLayout.IsDefault`.

**Transitional rule:**

- `_System_PageLayout.IsDefault` may remain as a compatibility column for one
  release
- no runtime decision may read it directly
- if retained temporarily, it must be mirrored only via the same helper-driven
  write path

## 6. Runtime Shape

### 6.1 Assignment writers

All assignment writers must stamp `RouteSource` explicitly:

| Writer | RouteSource |
|---|---|
| runtime fallback in `ResolveLayout` | `runtime_fallback` |
| `SetDefaultLayout` | `admin_authored` |
| targeted `CreateAssignment` via UI/API | `admin_authored` |
| curated pack wildcard insert | `managed_seed` |

Evidence for the current writer set:

- runtime fallback:
  [backend/internal/features/metadata/metadata_layouts.go:179](../../backend/internal/features/metadata/metadata_layouts.go#L179)
- admin wildcard writer:
  [backend/internal/infrastructure/persistence/repo_layout.go:168](../../backend/internal/infrastructure/persistence/repo_layout.go#L168)
- admin targeted writer:
  [backend/internal/infrastructure/persistence/repo_layout.go:403](../../backend/internal/infrastructure/persistence/repo_layout.go#L403)
- curated seed writer:
  [backend/internal/controlplane/packs/curated_page_layout_reconcile.go:299](../../backend/internal/controlplane/packs/curated_page_layout_reconcile.go#L299)

### 6.2 Default readers

Every reader of "default layout" must stop reading raw `_System_PageLayout.IsDefault`
and instead call the shared helper.

That includes:

- list API
- delete guard
- control-plane verify
- any summary view that decorates a layout as default

### 6.3 Curated cutover

Curated cutover preserve / delete rules become:

- preserve when a live route points to the row with `RouteSource='admin_authored'`
- ignore `PageLayout.Source` for routing preservation decisions
- soft-delete only platform-owned `auto_generated` rows that have no live
  admin-authored route

This keeps routing preservation and regeneration eligibility on separate axes.

## 7. Migration / Data-Spec Shape

### 7.1 Schema stage

Add `_System_LayoutAssignment.RouteSource` as nullable first.

New writes set it immediately. Legacy rows remain `NULL` until classified.
This stage is schema/non-destructive only. Ordinary `cmd/setup apply` may be
used only if it is limited to schema and non-destructive metadata setup; it must
not also run the curated-layout destructive cleanup path.

### 7.2 Stage 1 — resolve historical routing provenance

Stage 1 must classify unresolved legacy assignment rows by updating
`RouteSource`, not `PageLayout.Source`.

Allowed actions:

- reviewed historical admin route:
  `RouteSource = 'admin_authored'`
- reviewed managed/platform default:
  `RouteSource = 'managed_seed'`
- reviewed stale runtime fallback / orphan wildcard:
  delete the assignment row
- reviewed live-layout object with no wildcard route:
  insert one wildcard assignment with reviewed `RouteSource`

Forbidden action:

- mutating `_System_PageLayout.Source` as a surrogate for routing intent
- ordinary pack apply stamping unresolved historical `RouteSource`
- ordinary pack apply clearing legacy `_System_PageLayout.IsDefault` evidence
  when no live wildcard route exists

### 7.3 Stage 2 — audit

The audit invariant becomes:

- zero relevant rows with `RouteSource IS NULL`
- zero target objects with live layouts but no wildcard assignment
- zero orphan or duplicate wildcard rows
- an explicit platform-owned layout deletion candidate manifest
- an explicit wildcard replacement candidate manifest

The audit query and the destructive path must operate on the same row set. If a
row is not present in the stage-2 manifest, stage 3 must not delete,
soft-delete, or replace it.

### 7.4 Stage 3 — manual destructive cutover

Destructive cleanup may proceed only after stage 2 has zero blocking rows and
the destructive candidate manifests have been captured.

Deletion predicate:

- `PageLayout.Source='auto_generated'`
- no live assignment with `RouteSource='admin_authored'`
- no unresolved legacy assignment rows

The destructive mechanism is an explicit operator runbook or SQL transaction
over the stage-2 manifest, with row-level backup and rollback instructions.
Normal `cmd/setup apply` is not the destructive cutover gate.

## 8. Declared Delta From Salesforce

This design adds one internal field, `RouteSource`, that Salesforce does not
surface at the product boundary.

This is an acceptable internal delta because:

1. Salesforce's user-visible contract is still preserved:
   layout content editing stays separate from layout assignment.
2. The delta is not tenant-specific and does not leak into admin-facing
   authoring semantics.
3. The existing storage shape lacks any non-overlapping representation for
   routing provenance; reusing `CreatedById` would keep audit and behavior
   conflated.

Next-tenant impact:

- none at the product boundary
- all tenants, greenfield and upgraded, share the same runtime model
- only upgraded tenants need the stage-1/2 remediation path

## 9. Verification Plan

### 9.1 Unit / integration

1. `SetDefaultLayout` stamps `RouteSource='admin_authored'` and does not mutate
   `PageLayout.Source`
2. `CreateAssignment` stamps `RouteSource='admin_authored'`
3. runtime fallback stamps `RouteSource='runtime_fallback'`
4. greenfield curated apply creates managed wildcard routes with
   `RouteSource='managed_seed'`
5. list API `isDefault` projection follows wildcard assignment, not stored
   page-layout bit
6. delete guard blocks deletion of the effective default even when the stored
   `_System_PageLayout.IsDefault` bit is stale
7. migration audit blocks any `RouteSource IS NULL` legacy route, any
   no-wildcard target object, and any orphan wildcard row

### 9.2 Operator verification

1. run stage-2 audit query and confirm zero unresolved rows
2. capture the stage-2 destructive candidate manifests
3. run the stage-3 manual destructive transaction against only those manifests
4. re-run the audit and confirm no blocker or unapproved candidate remains
5. list layouts for the object and confirm exactly one effective default is
   projected in API output

## 10. Minimal Execution Order

1. Land this model decision doc.
2. Add `RouteSource` to `_System_LayoutAssignment`.
3. Introduce the shared effective-default helper and switch all readers to it.
4. Update all assignment writers to stamp `RouteSource`.
5. Rewrite the three `2026-04-27a/b/c` specs around `RouteSource`, not
   `PageLayout.Source`.
6. Keep ordinary apply schema/non-destructive; move destructive curated-layout
   cleanup into the explicit stage-3 manual runbook.
7. Only then resume review on the original cleanup goal.
