12 KiB
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)
id/id_drop: only the id that was actually fired this run.pipeline: only when (a) vanilla entry was used, and (b) siblings share the same confirm predicate and same fire API. Note must includeshared=<pipeline_id>. Never mark an idpipelinemerely because another id in the domain passed.feed: never promote topipelineoridwithout a vanilla path.domain_feed/ inject: never write any live level except leavingnone.- 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 registerevent_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
AuthoredIdsloops as inject (camera/injectfrom that run or mirror rules) live_leveldefaults tonone; 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 > 0once live runners exist; do not fail onnoneuntil 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):
- Reset notes / clear subject counters
happiness_applywith that id- Classify:
- expected note /
LastEffectId→id - authored drop reason matched →
id_drop - else →
fail
- expected note /
pipelinefield:happiness_apply
Do not require camera interest while Busy unless a force path is on.
1b Status
For each CreatesInterest status (gains):
- Clear status + activity status counters
status_apply- Assert gain + log prose →
id; authored skip/drop →id_drop; elsefail 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:
- Do not call
domain_feed/ inject and call it live. - No fire API →
unsupported+ reason, not FAIL and not fakepipeline. - After each fire: teardown (expire interest keys, clear statuses/wars/plots as needed).
- Assert: expected note/key or expected drop; flag unexpected
outcome_unconfirmed. - 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.mdwith 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.tsvfor live proof;event-trigger-audit.tsvstays design/call-site audit - do not duplicatelive_verifiedinto 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; boat/combat best-effort; libraries unsupported) |
| 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
feedto “fully verified” - Full meteorology sim for every disaster when a shared WorldLog pipeline covers the feed (still record
pipeline/feedhonestly) - Failing CI on camera
nonebefore Phase 3 exit - Committing generated coverage TSV
- Marking sibling ids
pipelinewithout shared predicate and shared fire API
Success criteria
- Camera happiness Signal + status gains: each
id,id_drop, or fixedfail - Relationship: every confirm predicate has positive and negative sim; setters not piggybacked
- Every other camera domain:
id/id_drop/pipeline/feed/unsupported- no unexplainednone fail == 0on live runs- New catalog ids show up as
noneuntil a runner claims them - Coverage vocabulary never equates inject or
feedwith production-accurate trigger proof