Content Lifecycle V2 — Lineage-Based Versioning¶
Design document for the immutable-publish, lineage-grouped content versioning system. Supersedes the V1
ReplacesId/PendingRetirementapproach.
Problem with V1¶
The V1 lifecycle requires designers to manually set ReplacesId (a GUID text field) to link a new version to the old one, and PendingRetirement to flag items for archival. This is error-prone, doesn't enforce immutability on published content, and provides no version history visibility.
Design Principles¶
- Published content is immutable. You cannot edit a Published entity. You can only create a new version of it.
- Versions are grouped by lineage. All versions of the same conceptual entity share a
LineageId. - Lifecycle transitions are explicit and atomic. The only way content reaches Published is through a batch publish operation.
- Version history is always visible. Clicking a Published entity shows all Dev/Staged/Archived versions in the same lineage.
State Machine¶
┌──────────┐ promote ┌──────────┐ publish batch ┌───────────┐
│ Dev │ ────────────► │ Staged │ ────────────────► │ Published │
│ editable │ ◄────────── │ read-only│ │ immutable │
│ many/lin │ demote │ max 1/lin│ │ max 1/lin │
└────┬─────┘ └──────────┘ └─────┬─────┘
│ discard replaced by │
▼ new publish ▼
┌──────────┐ ┌───────────┐
│ Archived │ ◄──────────────────────────────────────── │ Archived │
│ read-only│ │ read-only │
│ many/lin │ │ many/lin │
└──────────┘ └───────────┘
Transitions¶
| From | To | Trigger | Constraint |
|---|---|---|---|
| Dev | Staged | Designer promotes | Max 1 Staged per lineage. Fails if one already Staged. |
| Staged | Dev | Designer demotes | Always allowed. |
| Dev | Archived | Designer discards | Always allowed. |
| Staged | Published | Publish batch | Only via atomic publish. Old Published in same lineage → Archived. |
| Published | Archived | Publish batch | Only when a newer version in same lineage publishes. |
Never allowed: Direct Dev→Published, Published→Dev, Archived→anything.
Editability Rules¶
| Status | Content Fields | Lifecycle Actions |
|---|---|---|
| Dev | Fully editable | Promote to Staged, Discard to Archived |
| Staged | Read-only | Demote back to Dev |
| Published | Read-only | Create New Version (copies to Dev with same LineageId) |
| Archived | Read-only | None |
Cardinality per Lineage¶
| Status | Count |
|---|---|
| Dev | 0..many |
| Staged | 0..1 |
| Published | 0..1 |
| Archived | 0..many |
Data Model Changes¶
ILifecycleEntity V2¶
public interface ILifecycleEntity
{
ContentStatus Status { get; set; }
Guid LineageId { get; set; } // Groups all versions of the same conceptual entity
}
Removed: ReplacesId (lineage handles this), PendingRetirement (publish batch handles this).
Added: LineageId (Guid, non-nullable, indexed).
New Entity Behavior¶
- First creation:
LineageId = Id(the entity is the origin of its lineage). - "Create New Version" from Published: Copies all content fields into a new entity with a new
Id, sameLineageId,Status = Dev,Versionincremented. - Snapshot query:
WHERE Status = Published(unchanged from V1).
Migration¶
- Add
LineageIdcolumn (uuid, non-nullable) to all 9 content tables. - Backfill:
UPDATE {table} SET lineage_id = id(every existing record is the origin of its own lineage). - Drop
ReplacesIdandPendingRetirementcolumns. - Add index on
(LineageId, Status)for efficient version history queries.
Publish Workflow V2¶
Preview (GET admin/publish/preview)¶
Query all 9 content tables for Status = Staged. Group by entity type. For each Staged item, check if a Published item exists in the same lineage (this is an "update" vs "new content"). Return the preview.
Publish Batch (POST admin/publish)¶
In a single transaction:
- For each Staged entity across all 9 tables: a. Find the Published entity in the same lineage (if any) → set to Archived b. Set the Staged entity to Published
- Create new
GameDataSnapshotfrom all Published-only entities - Commit transaction
- Invalidate cache, broadcast to Redis
Publish Preview Model¶
public class PublishPreview
{
public List<PublishPreviewItem> NewItems { get; set; } = []; // Staged with no Published sibling
public List<PublishPreviewItem> UpdatedItems { get; set; } = []; // Staged replacing a Published sibling
public bool HasChanges => NewItems.Count > 0 || UpdatedItems.Count > 0;
}
public class PublishPreviewItem
{
public required string EntityType { get; set; }
public required Guid EntityId { get; set; }
public required string Name { get; set; }
public Guid? CurrentPublishedId { get; set; } // The Published sibling that will be archived
}
Removed: RetiringItems list, ReplacesId field. Retirement happens automatically when a new version publishes.
Admin UI Changes¶
List Views (all 9 editors)¶
- Default filter: show Published items (the "live" view).
- Status filter dropdown: Dev | Staged | Published | Archived | All.
- Status column shows current state with color-coded chip.
Edit Form Behavior¶
- Published entity selected: Form fields are disabled. Two action buttons:
- "Create New Version" → copies to Dev, opens the new Dev version for editing
-
Version History section below showing all versions in this lineage
-
Dev entity selected: Form fields are editable. Action buttons:
- "Save" (saves changes)
- "Promote to Staged" (validates, changes status; fails if another Staged exists in lineage)
-
"Discard" (archives)
-
Staged entity selected: Form fields are disabled. Action buttons:
-
"Demote to Dev" (moves back to Dev for further editing)
-
Archived entity selected: Form fields are disabled. No actions. Historical reference only.
Version History Panel¶
Below the edit form for any selected entity, show a table:
| Version | Status | Updated | Actions |
|---|---|---|---|
| 3 | Published | 2026-03-14 | Create New Version |
| 2 | Archived | 2026-03-10 | (view only) |
| 1 | Archived | 2026-02-28 | (view only) |
| 4 | Dev | 2026-03-15 | Edit |
| 5 | Dev (draft) | 2026-03-15 | Edit |
Grouped: Published/Staged first, then Dev, then Archived (newest first within each group).
Publish Review Page (replaces Snapshots.razor)¶
The Snapshots page becomes the Publish Review page:
- Top section: current Published snapshot info (version, hash, timestamp, counts)
- Middle section: Publish Preview — table of all currently-Staged items across all entity types, showing entity type, name, whether it's new or replacing an existing Published version
- Bottom section: Rollback controls (unchanged)
- "Publish Set" button only enabled when preview has changes
LifecycleFieldsEditor → Removed¶
The LifecycleFieldsEditor expansion panel with its manual Status dropdown, ReplacesId text field, and PendingRetirement checkbox is removed. Lifecycle transitions happen through explicit action buttons on the edit form, not through editing lifecycle fields directly.
API Changes¶
Existing Endpoints (modified)¶
POST admin/{entity}— rejects saves for Staged/Published/Archived entities (only Dev is writable)DELETE admin/{entity}/{id}— rejects deletes for Published entities
New Endpoints¶
POST admin/{entity}/{id}/promote— Dev → Staged (validates max-1-staged constraint)POST admin/{entity}/{id}/demote— Staged → DevPOST admin/{entity}/{id}/discard— Dev → ArchivedPOST admin/{entity}/{id}/new-version— Creates a Dev copy of a Published entity, returns the new entityGET admin/{entity}/lineage/{lineageId}— Returns all versions in a lineage
Unchanged Endpoints¶
GET admin/publish/preview— Updated query logic but same endpointPOST admin/publish— Updated transition logic but same endpointPOST admin/rollback/{version}— Unchanged
Implementation Order¶
- Migration: Add
LineageId, backfill, dropReplacesId+PendingRetirement - ILifecycleEntity V2: Update interface, update all 9 model classes
- Repository: Rewrite
ProcessLifecycleTransitionsAsyncfor lineage-based logic, add promote/demote/discard/new-version methods - AdminController: Add new endpoints, add write-guard for non-Dev entities
- EchoSpireApiClient: Add client methods for promote/demote/discard/new-version/lineage
- Admin UI: Replace LifecycleFieldsEditor with action buttons, add version history panel, add status filters to list views, upgrade Snapshots → Publish Review
- Tests: Update all 111 admin tests for new lifecycle rules