# Related List Curation Design

## Status: DRAFT
## Date: 2026-04-02
## Related Analysis: `docs/related-list-editor-duplicate-label-analysis.md`
## Related Issues
- `ywang-official/MetaDrivenPlatform4AI#204`
- `ywang-official/MetaDrivenPlatform4AI#232`
- `ywang-official/MetaDrivenPlatform4AI#393`
- `ywang-official/MetaDrivenPlatform4AI#402`

---

## 1. Goal

Make related lists behave like a clean page-layout feature instead of a raw relationship dump.

The design goal is:

- low maintenance
- clear runtime behavior
- clear admin behavior
- no dependence on runtime schema auto-discovery for what users see

This document defines the clean target architecture for:

- Layout Editor `Related Lists`
- Record Detail `Related List Quick Links`
- runtime related-list counts

---

## 2. Decision Summary

### Final Product Decisions

1. **Runtime is layout-controlled**
   - Detail page related lists and Quick Links must read from `PageLayout.RelatedLists`
   - runtime must not directly render every discovered relationship

2. **Editor is candidate-controlled**
   - the admin editor works from a canonical candidate list
   - `selected` and `available` are derived views, not separate mutable states

3. **Stable identity is mandatory**
   - related lists use one canonical key: `normalizeRelatedListId(objectApiName, lookupField)`
   - random UUIDs are not allowed for related-list identity

4. **Same-label items must be distinguishable**
   - selected rows must show a secondary label such as `via Billing Account`

5. **System noise is hidden by default**
   - system/internal relationships are not part of the default user-facing experience

6. **Count failures must be honest**
   - failed counts must not render as `0`

These six decisions are the cleanest way to reduce maintenance cost. They remove the need for repeated object-specific hotfixes in runtime rendering.

---

## 3. What Defines "Business-Relevant" Related Lists?

This must be defined explicitly, otherwise the product will keep drifting back to auto-discovery.

### Final Definition

**Business-relevant related lists = the related lists explicitly selected on the active page layout.**

That is the only definition runtime should use.

### Where that selection comes from

For an admin:

- the editor exposes a candidate list
- the admin chooses which related lists belong on the page
- the saved layout becomes the single runtime source of truth

For a newly generated or previously empty layout:

- the system may seed a default selection
- but that seed is only a starting point
- it is not a runtime discovery path

### What is the reference for default seed quality?

The default seed should be guided by:

1. Salesforce page-layout intent for first-class standard objects when available
2. generic filtering and ranking heuristics for all other objects

The runtime must not depend on those heuristics after the layout is saved.

---

## 4. Current Problems

The current implementation mixes three responsibilities into one path:

1. discovering possible relationships
2. deciding which ones are worth showing
3. rendering them directly to users

That causes four systemic failures:

### 4.1 Over-discovery
Any child relationship referencing the parent object can surface at runtime.

Symptoms:

- too many Quick Links on Account and Opportunity
- system/internal objects visible in primary user navigation

### 4.2 Duplicate-looking and semantic duplicate entries
Distinct relationships appear with the same label, and semantically overlapping entries can appear together.

Symptoms:

- `Quotes`, `Quotes`
- `Tasks`, `Tasks`
- `Opportunity Products` and `Oppty Product`

### 4.3 Editor state drift
`selected` and `available` are modeled as separate state, so add/remove is not a closed loop.

Symptoms:

- removing a related list does not reliably return it to `Available Relationships`

### 4.4 Count-path failure
Quick Links count loading uses an oversized batch and falls back to fake zeroes.

Symptoms:

- every count appears as `(0)` when the batch exceeds backend limits

---

## 5. Target Architecture

```text
relationship discovery
        ↓
canonical candidate model
        ↓
layout selection (admin)
        ↓
stored PageLayout.RelatedLists
        ↓
runtime rendering (Detail + Quick Links)
```

This separation is the core design change.

### Why this is cleaner

- discovery becomes an editor concern
- layout becomes the runtime contract
- runtime stops guessing
- object-specific bugs stop appearing as endless rendering patches

---

## 6. Canonical Candidate Model

Introduce one canonical related-list candidate model for editor use.

Suggested backend DTO:

```ts
type RelatedListCandidate = {
  id: string;
  objectApiName: string;
  lookupField: string;
  label: string;
  secondaryLabel?: string;
  fieldLabel?: string;
  relatedListLabel?: string;
  isSystem: boolean;
  isRecommended: boolean;
};
```

### Semantics

- `id`
  - canonical identity
  - formula: `normalizeRelatedListId(objectApiName, lookupField)`

- `label`
  - primary business-facing label
  - prefer relationship-level related-list label if present
  - otherwise fallback to child plural label

- `secondaryLabel`
  - UI disambiguation string
  - first choice: human-readable field label
  - fallback: lookup field API name

- `isSystem`
  - true if the candidate should be hidden from default user-facing runtime experience

- `isRecommended`
  - true if the candidate should be part of the default seed / primary picker surface

### Design note

This candidate model is an editor contract, not a runtime rendering contract.

Runtime should not need to rediscover or recompute candidate semantics after a layout has been saved.

---

## 7. Persisted Layout Model

`PageLayout.RelatedLists` remains the runtime source of truth.

For MVP, extend the existing model minimally:

```ts
type RelatedListConfig = {
  id: string;
  label: string;
  secondaryLabel?: string;
  objectApiName: string;
  lookupField: string;
  fields: string[];
};
```

### Why `secondaryLabel` should be persisted

Without a persisted secondary label:

- selected rows lose human-readable disambiguation
- the editor must keep reconstructing display context from other sources

Persisting `secondaryLabel` is a small cost with clear UX value.

### What we are not adding in MVP

- no full relationship metadata object on the layout
- no per-layout related-list label override
- no runtime dependence on editor-only ranking metadata

---

## 8. Backend Changes

### 8.1 Add candidate query path

Add a backend path dedicated to candidate generation for a parent object.

Suggested service entry:

- `GetRelatedListCandidates(ctx, objectApiName string) ([]RelatedListCandidate, error)`

Suggested repository entry:

- `GetRelatedListCandidateRows(ctx, parentObjectAPI string) (...)`

This query should join:

- `_System_Field`
- `_System_Object`
- `_System_Relationship`

and return enough data to compute:

- `label`
- `secondaryLabel`
- `isSystem`
- `isRecommended`

### 8.2 Stop runtime auto-augmentation

Remove runtime dependence on auto-augmenting layouts with discovered related lists.

In particular, this current behavior should stop driving user-facing runtime:

- `augmentLayoutWithRelatedLists()` in `metadata_layouts.go`
- `buildRelatedLists()` as the source of user-facing Quick Links

Discovery can remain as an internal helper for:

- candidate generation
- layout seeding

but not for runtime rendering.

### 8.3 Make Quick Links consume stored layout intent

The runtime path for:

- Detail page related lists
- Quick Links

must consume `PageLayout.RelatedLists`.

That means the product should no longer behave as:

- "scan everything that references this parent and render it"

and should instead behave as:

- "render the related lists selected on this layout"

### 8.4 Candidate classification

The candidate service should compute two lightweight classifications:

#### `isSystem`
True when the candidate is clearly not part of the default business-facing UX.

Examples:

- `*Share`
- `*History`
- `*FieldHistory`
- territory association / internal relation objects
- other system-oriented objects

#### `isRecommended`
True when the candidate belongs in the primary seed / primary picker surface.

Generic recommendation rules for MVP:

1. candidate is not system/internal
2. candidate has a stable direct relationship to the parent
3. candidate is queryable / user-facing

### 8.5 Count endpoint handling

The Quick Links count path must stop treating oversized requests as a normal renderable zero-state.

MVP behavior:

- split count requests into batches within backend limit
- merge results client-side
- if a batch fails, render an honest failure state for those counts

---

## 9. Frontend Changes

### 9.1 Related Lists Editor

The editor must use one canonical candidate source.

Current anti-pattern:

- `availableRelatedLists` stored as separate mutable state

Target behavior:

```ts
allCandidates
selectedIds = new Set(layout.relatedLists.map(rl => rl.id))
selectedCandidates = allCandidates.filter(c => selectedIds.has(c.id))
availableCandidates = allCandidates.filter(c => !selectedIds.has(c.id))
```

This makes add/remove naturally reversible.

### 9.2 Selected rows UI

Every selected row should render:

- primary label: `Quotes`
- secondary label: `via Billing Account`

Fallback when human-friendly metadata is missing:

- `via AccountId`

### 9.3 Available picker UI

The picker should show:

- `Recommended`
- `Additional Relationships`
- `System / Advanced`

This is not because runtime needs three classes forever. It is to reduce admin overload in the chooser.

### 9.4 Quick Links UI

Quick Links should render only the related lists chosen in the current layout.

If a layout intentionally contains two similarly named related lists:

- show disambiguation in admin surfaces
- runtime may show either the same disambiguation or the curated primary subset chosen on the layout

### 9.5 Count rendering

Replace:

- `count ?? 0`

with:

- loading state while pending
- real count when available
- `—` or omitted value on failure

---

## 10. Seeding and Migration

The system needs a clean way to transition away from runtime auto-discovery.

### 10.1 New layouts

When generating a new default layout:

- compute candidates
- choose the recommended subset
- persist that subset into `PageLayout.RelatedLists`

Do not create an empty related-list section and rely on runtime augmentation later.

### 10.2 Existing layouts with empty related lists

For existing layouts that currently rely on runtime discovery:

- run a one-time backfill migration
- seed `PageLayout.RelatedLists` from recommended candidates

This migration is the bridge from old behavior to clean runtime behavior.

### 10.3 Standard-object seed quality

For first-class standard objects where Salesforce or PDF references exist:

- Account
- Opportunity
- Campaign

the seed should prefer those references over purely generic discovery heuristics.

This does **not** mean runtime uses hardcoded allowlists.

It means:

- seed quality for key objects can be curated once
- runtime still reads only layout config

### 10.4 Why this still keeps maintenance low

The maintenance-heavy part is runtime guesswork.

This design removes that.

Even if some seed profiles are curated for key objects, that curation is:

- seed-time only
- isolated
- not part of every runtime request

That is much cheaper than keeping object-specific Quick Links logic in runtime forever.

---

## 11. MVP Scope

### In Scope

1. canonical related-list candidate API
2. stable related-list IDs
3. selected/available derived from one source
4. persisted `secondaryLabel`
5. Quick Links reads from layout config
6. count batching and honest failure display
7. system/internal relationships hidden from default recommended set
8. one-time backfill for layouts currently relying on discovery

### Out of Scope

1. per-layout display-label override
2. full Salesforce parity for every related-list property
3. universal semantic-dedup engine for all objects
4. complete redesign of relationship metadata storage

---

## 12. Why This Is The Lowest-Maintenance Design

This design avoids three expensive failure patterns:

### 12.1 No runtime auto-discovery patchwork
We stop fixing every object by adding more filters in runtime.

### 12.2 No duplicated frontend state model
We stop maintaining `selected` and `available` as separate mutable sources.

### 12.3 No fake count fallback
We stop treating transport failure as business data.

In return, the system becomes simpler:

- one candidate source
- one selected source
- one runtime contract

That is the cleanest architecture available without redesigning the entire metadata subsystem.

---

## 13. Acceptance Criteria

This design is successful only if all of the following are true:

1. Quick Links on Account and Opportunity no longer depend on raw relationship discovery at runtime.
2. Account and Opportunity detail pages no longer show large sets of system/internal related lists by default.
3. Layout Editor selected rows can distinguish multiple `Quotes`, `Tasks`, or `Assets` entries without guesswork.
4. Removing a related list immediately makes it available to add again.
5. Quick Links counts never collapse to fake zeroes because of oversized batch requests.
6. The system can support future curation improvements without adding more runtime object-specific hacks.

---

## 14. Implementation Order

### Step 1
Introduce canonical candidate generation on the backend.

### Step 2
Refactor the editor to derive selected/available from candidates.

### Step 3
Extend persisted layout related-list config with `secondaryLabel`.

### Step 4
Switch Quick Links runtime to layout-controlled rendering.

### Step 5
Add count batching and honest failure rendering.

### Step 6
Run one-time backfill for empty-layout related-list configs.

---

## 15. Final Decision

Do not continue investing in runtime auto-discovery as the user-facing related-list behavior.

The clean path is:

- discover candidates once
- let layout choose
- let runtime render only what layout chose

That is the most normal, maintainable implementation and the closest product behavior to Salesforce.
