worldbox-observer-mod/docs/story-system-coherence-plan.md
DazedAnon 44999fb653 feat(actor-relation): enhance parent-child relationship tracking and event handling
- Introduce methods to observe and remember parent-child links for actors
- Implement caching for observed relationships to improve performance
- Update narrative event store to index child events by parent and child
- Refactor LifeSagaPanel to ensure stable cast membership during saga sessions
- Enhance documentation to clarify relationship handling and event indexing
2026-07-24 16:21:49 -05:00

15 KiB
Raw Permalink Blame History

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:

  1. Who is this? — Identity from the Saga roster and live role data.
  2. What is happening now? — Beat owned by the current camera tip.
  3. 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 CaptionComposerWatchCaption Describes the camera subject outside Saga hover
Hover nametag + portrait + Cast + Legacy LifeSagaViewControllerWatchCaption / 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.

  1. A synthetic aftermath or epilogue belongs to exactly one arc, identified by correlation.
  2. A story candidate cannot advance, receive ownership boost from, or stamp cast into a different arc.
  3. InterestDirector remains the only owner of camera switching; the planner never calls camera APIs.
  4. Saga bias remains soft beneath crisis and active correlated-story ownership.
  5. Outside Saga hover, caption Identity, Beat, and Context describe the camera subject.
  6. A temporary status banner may replace Beat, but Identity and Context continue updating.
  7. 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.
  8. Cast deduplication uses structured subject/member identity, never display-name substrings alone.
  9. Every LifeSagaMemory mutation invalidates cached relation and presentation prose.
  10. No internal enum, asset, role, correlation, or phase identifier reaches player-facing prose.
  11. Text remains inside the hover card at supported UI scales; it neither clips silently nor paints outside its bounds.
  12. 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:

  • OwnsCandidate
  • OwnershipBoost
  • PreferOver
  • OnSwitchTo
  • 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:

  1. hover subject equals camera caption subject;
  2. the Cast member's unit ID equals a structured Beat participant/related ID;
  3. 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:

  1. Keep up to two rows of Cast, with measured best-fit text and a readable minimum size.
  2. Measure Legacy using the actual Unity text generator or preferred height.
  3. Admit Legacy entries in presentation priority order until the remaining line budget is full.
  4. End at a complete line; do not enable overflow outside the card.
  5. 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_correlation
  • caption_status_banner_live
  • saga_relation_cache_revision
  • saga_cast_stable_snapshot
  • saga_hover_max_content

D3. Verification order

  1. New focused scenarios individually, three consecutive passes each.
  2. Existing story_*, saga_*, caption, director, and dossier scenarios.
  3. critical_smoke.
  4. Full regression, three consecutive passes.
  5. Screenshot review at normal and narrow supported layouts.
  6. 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 B2B3: 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:

  1. fix(story): scope ownership to correlated arcs
  2. fix(caption): keep identity and context live under banners
  3. fix(saga): invalidate prose on every memory mutation
  4. fix(saga): scope cast dedupe and bound hover layout
  5. test(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 StoryPlanner or 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, and saga_cast_stable_snapshot.
  • 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.