# Event live verification plan Goal: every catalog event is **accurate and not bugged** in production, with honest coverage tracking. Inject stays; full sim grows by **hook family**, then loops ids where one apply API covers many. Do **not** over-claim. Weaker proof gets a weaker `live_level`. ## What “works” means | Failure mode | Required proof | |--------------|----------------| | False positive (fires when claim is false) | Full sim + `EventOutcome` / drop log (negative case required) | | False negative (never fires) | Full sim through **vanilla** API (not inject / not `domain_feed`) | | Bad prose / wrong subject | Inject + reason; sim when label needs live fields | | Catalog drift | Inventory audits + inject | | Registry wiring | Inject (`event_inject_coverage`) | | Expected policy drop | Apply ran + authored drop reason matched (`id_drop`) | ## Coverage levels Record per id in `.harness/event-live-coverage.tsv` (regenerated each run; **do not commit**). | Level | Meaning | Counts as “verified”? | |-------|---------|------------------------| | `none` | Inject only; no live fire this run | No | | `feed` | Called production feed entry with synthetic payload (skips Harmony / some Busy/world wiring) | Partial - glue inside feed only | | `pipeline` | **Vanilla** fire proved this id’s shared hook family; **this id was not necessarily fired** | Partial - siblings may share predicate/API | | `id_drop` | Real apply/fire ran; result was an **authored** drop (reason matched) | Yes for “accurate drop” | | `id` | This id went through real fire and asserted note/interest as expected | Yes | | `unsupported` | No deterministic fire API; explicit reason in `notes` | Honest gap | | `fail` | Fire attempted; unexpected miss/drop/wrong key | Broken | ### Claiming rules (strict) 1. **`id` / `id_drop`:** only the id that was actually fired this run. 2. **`pipeline`:** only when (a) vanilla entry was used, and (b) siblings share the **same confirm predicate and same fire API**. Note must include `shared=`. Never mark an id `pipeline` merely because another id in the domain passed. 3. **`feed`:** never promote to `pipeline` or `id` without a vanilla path. 4. **`domain_feed` / inject:** never write any live level except leaving `none`. 5. Ledger is **computed from the last run’s results** + catalog seed. No hand-maintained allowlist of “known smokes.” Ambient / `CreatesInterest=false`: not camera-live goals. Optional B-log loop later; default leave at `none` unless explicitly asserted as `id_drop` / ActivityLog-only. ## Current baseline (done) - Inventory audits in `event_coverage` - `event_inject_coverage` - all camera-worthy ids register - `event_live_outcome_smoke` - family lovers/parent, one status, one happiness (proof exists; ledger will record from run, not a static list) - Observe→confirm for relationship Beh paths - Docs: `event-e2e.md`, `event-observation.md` ## Phase 0 - Coverage ledger + Busy policy **Deliverable:** harness writes `.harness/event-live-coverage.tsv` every live/coverage run. Columns: `domain`, `id`, `camera`, `inject`, `live_level`, `pipeline`, `notes` - Seed rows from the same `AuthoredIds` loops as inject (`camera` / `inject` from that run or mirror rules) - `live_level` defaults to `none`; overwrite only from live runners’ results - Suite prints: `id=N id_drop=N pipeline=N feed=N none=N unsupported=N fail=N` - Fail the assert on `fail > 0` once live runners exist; do **not** fail on `none` until Phase 3 exit tightening ### Busy bypass (required before Phase 2/3 interest asserts) Most feeds early-out on `AgentHarness.Busy`. Define per domain: | Domain | During live fire | Primary assert | |--------|------------------|----------------| | Happiness | `HappinessSourceHook.Harness` / existing apply path | Router notes + log (interest optional) | | Status | ActivityLog primary; optional force-interest flag later | Status counters + log | | Relationship | `ForceRelationshipFeeds` (already used for parent) | Interest key + drop log | | WorldLog / war / plot / book / trait / decision / … | Explicit `Force*Feeds` or harness “allow live feeds” mode | Interest key and/or domain log | Document the flag in `notes` when used (`busy_bypass=ForceRelationshipFeeds`). **Maintainability:** new catalog ids appear as `none` automatically until a runner claims them. ## Phase 1 - Shared apply loops (highest confidence / hour) Loop camera-worthy ids through existing **vanilla-ish** apply APIs (`happiness_apply`, `status_apply`). ### 1a Happiness For each **Signal** id in `EventCatalog.Happiness.AuthoredIds` (skip Ambient unless doing an optional B-log pass): 1. Reset notes / clear subject counters 2. `happiness_apply` with that id 3. Classify: - expected note / `LastEffectId` → `id` - authored drop reason matched → `id_drop` - else → `fail` 4. `pipeline` field: `happiness_apply` Do **not** require camera interest while Busy unless a force path is on. ### 1b Status For each `CreatesInterest` status (gains): 1. Clear status + activity status counters 2. `status_apply` 3. Assert gain + log prose → `id`; authored skip/drop → `id_drop`; else `fail` 4. `status_remove` / egg hatch as a **separate** pipeline (`status_loss` / `hatch`) with its own ids **Exit:** every camera-worthy happiness Signal and status gain id is `id`, `id_drop`, or `fail` (no silent `none` for those sets). Suite fails on `fail`. ## Phase 2 - Confirm-gated relationship families One full sim **per `EventOutcome` predicate** (and separate setter smokes where there is no confirm helper). ### Confirm smokes (predicate + negative case) | Pipeline | Sim | Assert | |----------|-----|--------| | `GainedLover` / bond | `happiness_bond_lovers` | notes + log; exercised id → `id` | | parent / `add_child` | `relationship_set_parent` | `rel:add_child`; id → `id` | | `LostLover` / `clear_lover` | bond then clear via game API | emit or authored drop | | `GainedFamily` / `family_group_join` | join when family exists **and** when not | key vs `outcome_unconfirmed` | | `LostFamily` / leave-remove | leave/remove when applicable | key vs unconfirmed | | `StillSeekingLover` / `find_lover` | seeking Beh path | confirm rules | Negative tests are mandatory for confirm predicates (especially join-with-no-family). ### Setter smokes (no shared confirm sibling claim) `new_family`, `family_group_new`, `become_alpha`, `baby_created`, etc.: each needs its own vanilla fire for `id`. Do **not** mark them `pipeline` off `add_child` or join. Siblings may get `pipeline` only under Claiming rule 2 (`shared=`). **Exit:** every relationship camera id is `id`, `pipeline` (with valid `shared=`), `id_drop`, `unsupported`, or `fail` - never unexplained `none`. ## Phase 3 - Remaining hook families Prefer **vanilla** mutation. If only feed-invoke is feasible, record `feed` and keep improving to vanilla. | Pipeline | Minimum vanilla sim | Id loop? | Also cover | |----------|---------------------|----------|------------| | WorldLog | Post/construct real world history message the patch sees | Yes if asset id is the parameter | | | Disasters | Through paired WorldLog / disaster trigger | Via paired worldLog | | | War types | `WarManager.newWar` (or equivalent) | Per type if API takes type | | | Eras | Force age change | Per era if possible | | | Plots | `setPlot` / finish; phases as separate pipelines | Per plot id if API takes id | | | Books | Create/burn vanilla path | Per book id if API takes id | | | Traits | `addTrait(id)` / remove | Yes | | | Decisions | Decision cooldown / real decision entry for interesting ids | Yes for camera decisions | | | Combat | Existing combat interest path / attack-target gate | Pipeline smoke + known gates | | | Boat | Unload/board path the patch uses | Pipeline smoke | | | Meta (city/kingdom) | Real civic create if possible; else `unsupported`/`feed` | | | Building | Destroy/damage path | Pipeline smoke | | | Libraries | Only with real gain/cast API; else `unsupported` | Loop when API exists | | **Rules:** 1. Do not call `domain_feed` / inject and call it live. 2. No fire API → `unsupported` + reason, not FAIL and not fake `pipeline`. 3. After each fire: teardown (expire interest keys, clear statuses/wars/plots as needed). 4. Assert: expected note/key **or** expected drop; flag unexpected `outcome_unconfirmed`. 5. Busy bypass per Phase 0 table; record in `notes`. **Exit:** every camera domain has ids in `id` / `id_drop` / `pipeline` / `feed` / `unsupported` - zero unexplained `none` for camera rows. Then optionally tighten CI to fail if camera `none` + `feed` exceed a budget. ## Phase 4 - Wire into suite (weight-aware) | Scenario | Role | When | |----------|------|------| | `event_inject_coverage` | Wiring gate | Always / `event_suite` | | `event_live_apply_loop` | Phase 1 happiness+status | `event_suite` + regression | | `event_live_relationship` | Phase 2 confirms + negatives | `event_suite` (stable) | | `event_live_pipelines` | Phase 3 | **Regression only** (flake budget proven 3/3) | | `event_live_coverage` | Write ledger + summary assert (`fail==0`) | End of whichever live scenarios ran | | `event_suite` | audits + inject + apply loop + relationship + outcome smoke | Day-to-day event gate | | `regression` | `event_suite` + `event_live_pipelines` (+ other product smokes) | Full gate | Keep `event_live_outcome_smoke` as a short smoke nested in `event_suite`. ## Phase 5 - Docs sync - Update `docs/event-e2e.md` with levels, Busy policy, suite split - Update `event-observation.md` “adding an event”: name confirm predicate, fire API, and expected live level - **Source of truth:** harness-generated `event-live-coverage.tsv` for live proof; `event-trigger-audit.tsv` stays design/call-site audit - do not duplicate `live_verified` into both unless generating one from the other ## Implementation status | Phase | Status | |-------|--------| | 0 Ledger + Busy notes | Done - `EventLiveCoverageLedger`, assert `event_live_coverage` | | 1a Happiness Signal apply loop | Done - `EventLiveApplyLoopHarness` | | 1b Status gain apply loop | Done - same harness; spawn human for emotions | | 2 Relationship confirms | Done - `EventLiveRelationshipHarness` (leave via Beh/`setFamily(null)`, remove with Prefix emit + anchor hint, baby via `createBabyActorFromData`; join often `pipeline` via setFamily) | | 3 Other pipelines | Done - `EventLivePipelinesHarness` (traits/decisions/WorldLog/…; building destroy; library id loops for worldLaw/item/subspecies_trait/power/gene/phenotype/meta traits; spell/biome + boat/combat best-effort) | | 4 Suite wiring | Done - `event_suite` nests apply+relationship; `event_live_pipelines` in regression; flake budget 3/3 | | 5 Docs sync | Done - `event-e2e.md`, `event-observation.md` | Scenarios: `event_live_apply_loop` + `event_live_relationship` nest in `event_suite`. `event_live_pipelines` runs in regression (flake budget proven). ## Non-goals - One hand-written harness step per catalog id - Calling inject / `domain_feed` “live” - Promoting `feed` to “fully verified” - Full meteorology sim for every disaster when a shared WorldLog pipeline covers the feed (still record `pipeline`/`feed` honestly) - Failing CI on camera `none` before Phase 3 exit - Committing generated coverage TSV - Marking sibling ids `pipeline` without shared predicate **and** shared fire API ## Success criteria - Camera happiness Signal + status gains: each `id`, `id_drop`, or fixed `fail` - Relationship: every confirm predicate has positive **and** negative sim; setters not piggybacked - Every other camera domain: `id` / `id_drop` / `pipeline` / `feed` / `unsupported` - no unexplained `none` - `fail == 0` on live runs - New catalog ids show up as `none` until a runner claims them - Coverage vocabulary never equates inject or `feed` with production-accurate trigger proof ## After this plan Live *fire* proof is largely done. Idle *watch quality* (selection = presentation, ambient fall class demotion, focus hold, tip placeholder) is a separate track: [camera-presentability-plan.md](camera-presentability-plan.md).