worldbox-observer-mod/docs/event-live-verification-plan.md
2026-07-16 20:46:22 -05:00

219 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 ids 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=<pipeline_id>`. 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 runs 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=<predicate>`).
**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).