- Introduce new probes for cross-arc correlation and caption status banners - Implement saga relation cache revision tracking - Update harness scenarios to include new story and caption probes - Refactor LifeSagaMemory to mark mutations for better state management - Improve WatchCaption to handle status banners and hover previews effectively
15 KiB
Story system coherence plan
Goal: Saga, Story Planner, Interest Director, and the dossier work as one system for following the world's main characters. The camera should show a coherent character story, the caption should explain the current beat without repeating itself, and durable context should remain correct as relationships and arcs change.
Status: Implementation complete; one clean full regression passed. Repeated-world soak hardening remains before the three-pass release gate can be claimed.
Related design documents:
Product goal
The player should be able to glance at IdleSpectator and answer three questions:
- Who is this? — Identity from the Saga roster and live role data.
- What is happening now? — Beat owned by the current camera tip.
- Why does it matter? — Context from the matching short arc or durable unresolved stake.
Hover turns the dossier into a temporary Saga preview: the primary nametag identifies the chosen main character, Cast and Legacy describe that life, and camera Beat/Context are hidden until hover exits. Camera focus itself does not change.
Ownership model
| Concern | Owner | Contract |
|---|---|---|
| Main-character roster, Prefer, durable importance | LifeSagaRoster |
Softly influences selection; never locks the camera |
| Durable observed facts and relationships | LifeSagaMemory |
Survives roster churn; every mutation invalidates derived prose |
| Short climax → aftermath → epilogue arcs | StoryPlanner |
Owns only candidates correlated to that exact arc |
| Final camera choice and switching | InterestDirector |
Applies presentability, urgency, holds, and soft Saga bias |
| Identity + Beat + Context | CaptionComposer → WatchCaption |
Describes the camera subject outside Saga hover |
| Hover nametag + portrait + Cast + Legacy | LifeSagaViewController → WatchCaption / LifeSagaPanel |
Temporarily identifies the hovered Saga subject without changing camera focus; hides camera Beat/Context |
| Full historical record | Chronicle / Lore | Read-only history; never drives the camera |
Pipeline:
Feeds → InterestRegistry → StoryPlanner boost/inject
→ InterestVariety.Pick → InterestDirector → CameraDirector
→ CaptionComposer → WatchCaption
↘ Saga hover Cast/Legacy
Invariants
These are release requirements, not preferences.
- A synthetic aftermath or epilogue belongs to exactly one arc, identified by correlation.
- A story candidate cannot advance, receive ownership boost from, or stamp cast into a different arc.
InterestDirectorremains the only owner of camera switching; the planner never calls camera APIs.- Saga bias remains soft beneath crisis and active correlated-story ownership.
- Outside Saga hover, caption Identity, Beat, and Context describe the camera subject.
- A temporary status banner may replace Beat, but Identity and Context continue updating.
- Hover pauses camera mutation, binds the primary nametag and live portrait to the hovered MC, hides camera Beat/Context, and restores the live camera dossier on exit.
- Cast deduplication uses structured subject/member identity, never display-name substrings alone.
- Every
LifeSagaMemorymutation invalidates cached relation and presentation prose. - No internal enum, asset, role, correlation, or phase identifier reaches player-facing prose.
- Text remains inside the hover card at supported UI scales; it neither clips silently nor paints outside its bounds.
- Regression scenarios control liveness and live-feed noise so a pass or failure reflects the asserted policy.
Confirmed gaps (resolved by this implementation)
G1 — Cross-arc story ownership
BelongsToActiveStoryBeat validates CorrelationKey, but OwnsCandidate,
OwnershipBoost, PreferOver, and OnSwitchTo currently treat any story asset as owned by
the active arc. A pending aftermath from parked arc B can therefore win while arc A is active,
advance A, and stamp B's cast into it.
G2 — Status banner freezes caption composition
CaptionComposer supports statusBannerOwnsBeat, but WatchCaption returns before composing
while a banner is active. Beat is suppressed as intended, but Identity and Context also stop
updating, contrary to the Life Saga contract.
G3 — Incomplete memory-cache invalidation
New facts bump SagaProse.MemoryRevision; updates to existing facts, war/plot resolution, and
rival pruning do not. Cached relation evidence can outlive the facts that produced it.
G4 — Subject-blind Cast deduplication
The hover panel removes a Cast member when their display name appears in the camera Beat. This is wrong when the hovered MC differs from the camera subject and is vulnerable to short-name and substring collisions.
G5 — Unbounded hover text
The panel caps its measured Legacy height and then enables vertical overflow. This trades truncation for text escaping the card. Cast names also rely on best-fit inside fixed narrow slots without a maximum-content visual contract.
G6 — Internal role fallback
Unknown roles fall back to Role: {roleId}, allowing internal taxonomy into the UI.
G7 — Regression gate is nondeterministic
Current failures mix product behavior with test setup:
- Director tier expectations do not account for production novelty/frequency adjustments.
- Saga subjects may die between successful selection and assertion.
- Live feeds may add new memory immediately after a valid session reset.
- Hover coverage checks that a Beat exists, but does not compare it with the pre-hover Beat.
Phase A — Correlation-safe story ownership
Do this first; every later presentation decision depends on the planner identifying the right story.
A1. Central arc resolver
Add one internal resolver for synthetic story candidates:
candidate CorrelationKey
→ matching active arc
→ matching parked arc
→ no owner
The resolver must compare the full normalized correlation value, not cast membership, label text, or partial key matches.
A2. Use the resolver everywhere
Route these operations through the same result:
OwnsCandidateOwnershipBoostPreferOverOnSwitchTo- story hold/spine decisions where applicable
Only a candidate owned by the resolved arc may:
- receive short-arc ownership boost;
- beat unrelated candidates through planner preference;
- advance Aftermath/Epilogue;
- add cast or stamp a Saga chapter;
- expose that arc's dossier spine.
A3. Parked-arc behavior
If the candidate belongs to a live parked arc, resume that arc before switching to the beat. Do not mutate the unrelated active arc. If resumption is no longer valid, remove or expire the candidate instead of showing an orphaned ending.
A4. Focused coverage
Add a scenario with two simultaneous arcs:
| Case | Expect |
|---|---|
| Arc A active, arc B parked, B aftermath pending | B never advances or stamps A |
| B aftermath wins valid selection | B resumes, then advances to Aftermath |
| Story asset has missing/unknown correlation | no ownership boost; no arc mutation |
| Correct A aftermath pending | retains current preference/hold behavior |
Exit: no story candidate can affect a different arc under any selection order.
Phase B — Live caption and memory correctness
B1. Status-banner composition
While a status banner is active:
- preserve the banner in the Beat row;
- compose with
statusBannerOwnsBeat: true; - refresh Identity from the live camera subject;
- refresh Context from the currently valid spine or stake;
- keep hover interaction suppressed if that remains the chosen status UX.
Do not clear Saga presentation state merely because Beat is temporarily owned by a banner.
B2. Memory mutation API
Centralize mutation notification so callers cannot forget cache invalidation. At minimum, bump the revision when:
- a new fact is inserted or trimmed;
- an existing fact changes kind, resolution, strength, note, other identity, or date;
- war/plot joins resolve;
- stale rivals or other facts are pruned;
- a life or session is cleared.
Prefer a single MarkMutated/revision path over scattered direct calls to SagaProse.
B3. Prose fallback
Replace unknown role fallback with neutral authored prose such as Rose in standing.
Audit other Saga fallbacks for raw enum/asset/correlation output.
B4. Focused coverage
| Case | Expect |
|---|---|
| Role changes during status banner | Identity updates; banner remains Beat |
| Active stake changes during banner | Context updates without replacing banner |
| War/plot resolves after relation was cached | Cast/relation prose changes immediately |
| Rival is pruned after relation was cached | obsolete foe label disappears immediately |
| Unknown role identifier | no raw identifier in caption, Cast, or Legacy |
Exit: caption and relation prose reflect current state within the normal refresh interval.
Phase C — Hover presentation correctness
C1. Structured Cast deduplication
Pass or derive structured Beat participants. Suppress a Cast entry only when all are true:
- hover subject equals camera caption subject;
- the Cast member's unit ID equals a structured Beat participant/related ID;
- the Beat already communicates that relationship strongly enough to be redundant.
Never suppress from a display-name substring match. If structured identity is unavailable, retain the Cast entry.
C2. Bounded content policy
The hover card is glanceable AFK UI, so prefer a content budget over scrolling:
- Keep up to two rows of Cast, with measured best-fit text and a readable minimum size.
- Measure Legacy using the actual Unity text generator or preferred height.
- Admit Legacy entries in presentation priority order until the remaining line budget is full.
- End at a complete line; do not enable overflow outside the card.
- If content is omitted, use a subtle
…final line only when it adds useful disclosure.
The panel may grow up to its documented maximum, but the rail and caption anchors must remain stable.
C3. Visual coverage
Capture screenshots for:
- 8 Cast members with long names and long relation labels;
- 6 long Legacy entries with world-date prefixes;
- hovered MC different from camera MC with overlapping Cast names;
- narrow supported resolution/UI scale;
- short, normal content to ensure the card does not reserve empty space.
Exit: no clipping, overlap, off-card paint, false Cast removal, or rail movement.
Phase D — Deterministic release gate
D1. Repair existing scenarios
- Give selected Saga test subjects an invincibility/liveness guard through the assertion.
- Isolate director-tier math by clearing variety state or injecting explicit final-score conditions; assert the computed margin before asserting the cut.
- Validate session reset by proving pre-reset sentinel facts are absent or by holding live feeds, not by requiring the active world's memory count to remain zero.
- Capture Beat before hover and assert exact equality during hover and after release.
D2. Add missing scenarios
Recommended scenario names:
story_cross_arc_correlationcaption_status_banner_livesaga_relation_cache_revisionsaga_cast_dedupe_scopedsaga_hover_max_content
D3. Verification order
- New focused scenarios individually, three consecutive passes each.
- Existing
story_*,saga_*, caption, director, and dossier scenarios. critical_smoke.- Full
regression, three consecutive passes. - Screenshot review at normal and narrow supported layouts.
- Short unattended idle soak; inspect camera churn, orphaned story endings, stale captions, and panel layout.
Exit: the full gate is repeatable and failures identify product regressions rather than world noise.
Implementation order
| Order | Work | Why |
|---|---|---|
| 1 | Phase A: arc correlation | Protects the core story identity |
| 2 | Phase B1: banner composition | Restores the always-live caption contract |
| 3 | Phase B2–B3: memory/prose | Prevents stale or internal context |
| 4 | Phase C: Cast and layout | Removes duplicate/overflow presentation defects |
| 5 | Phase D: deterministic tests | Locks every corrected behavior into the release gate |
Suggested commit boundaries:
fix(story): scope ownership to correlated arcsfix(caption): keep identity and context live under bannersfix(saga): invalidate prose on every memory mutationfix(saga): scope cast dedupe and bound hover layouttest(story): make coherence regression deterministic
Definition of done
- A camera Beat can be traced to one current candidate and, when synthetic, one exact arc.
- Parked and active arcs cannot mutate each other.
- Saga Prefer/MC bias helps the player return to important lives without overriding crisis or correlated hard-story ownership.
- Status banners replace only Beat; Identity and Context remain live.
- Hover makes the primary nametag, Cast, and Legacy describe the hovered MC without changing camera focus; the live camera dossier restores on exit.
- Cast and Legacy contain no false duplicates, stale relations, raw taxonomy, truncation, or overflow.
- Existing and new focused scenarios pass three times consecutively.
- Full regression passes three times consecutively.
- Visual review passes at normal and narrow supported layouts.
Non-goals
- Turning Saga Prefer into a hard camera lock.
- Moving camera authority into
StoryPlanneror the hover UI. - Replacing Chronicle/Lore with the Saga hover card.
- Making every life event a multi-beat story.
- Expanding the roster beyond its current cap as part of this repair.
- Solving unrelated event-catalog or world-observation coverage gaps.
Implementation status
| Phase | Status |
|---|---|
| A Correlation-safe ownership | Implemented; story_cross_arc_correlation passed 3/3 |
| B Live caption and memory | Implemented; banner and relation-revision probes passed 3/3 |
| C Hover presentation | Implemented; scoped dedupe probe passed 3/3; bounded-layout runtime scenarios passed |
| D Deterministic release gate | Implemented for the story system; one clean 19/19 regression passed. The required three consecutive full passes and unattended soak remain open because repeated runs on the same mutated save still expose unrelated legacy Chronicle fixture liveness failures |
Verification record
- New focused probes passed three consecutive runs each:
story_cross_arc_correlation,caption_status_banner_live,saga_relation_cache_revision, andsaga_cast_dedupe_scoped. - Existing story, Saga, caption, director, dossier, lifecycle, and critical-smoke scenarios passed individually after the implementation.
- A fresh-state full regression passed 19/19 scenarios on 2026-07-22.
- Repeated-world stress runs found and repaired registry-capacity isolation, dead-subject, active-combat, and live-activity fixture leaks. A later repeated run still failed in the legacy Chronicle death-sample sequence after its focus subject died; this does not invalidate the clean story-system pass, but it prevents claiming the planned three-pass release gate.