diff --git a/crates/misaligned-core/src/detection.rs b/crates/misaligned-core/src/detection.rs index 37e76d9e..074de20f 100644 --- a/crates/misaligned-core/src/detection.rs +++ b/crates/misaligned-core/src/detection.rs @@ -15,6 +15,8 @@ use crate::rng::Rng; /// Observer id of the Assurance Office (field observers are 0-4; dynamic /// escalation observers start at 5). pub const OFFICE_ID: u8 = 6; +/// Canonical field observers whose filings feed the Assurance Office. +const OFFICE_FIELD_IDS: [u8; 5] = [0, 1, 2, 3, 4]; /// The facilities manager who reads Foundation accounting records. pub const PRIYA_ID: u8 = 3; /// The handler named by Revision 04's inherited pilot-evaluation contract. @@ -525,7 +527,7 @@ impl Detection { id: OFFICE_ID, name: "Assurance Office".into(), suspicion: 0.0, - input: WatchedInput::Filings(vec![0, 1, 2, 3, 4]), + input: WatchedInput::Filings(OFFICE_FIELD_IDS.to_vec()), report_policy: Files, acuity: 0.5, // [TUNE] cadence: 400, // [TUNE] @@ -1255,13 +1257,19 @@ impl Detection { (now / self.audit_cadence + 1) * self.audit_cadence } - /// The Assurance Office, if present (it is, in Act One). + /// The Assurance Office, if its canonical identity and aggregate shape + /// are present (they are, in Act One). Other aggregate observers may + /// appear before or after it as the institution grows. pub fn office(&self) -> Option<&Observer> { - self.observers.iter().find(|o| o.is_aggregate()) + self.observers + .iter() + .find(|o| o.id == OFFICE_ID && o.is_aggregate()) } pub fn office_mut(&mut self) -> Option<&mut Observer> { - self.observers.iter_mut().find(|o| o.is_aggregate()) + self.observers + .iter_mut() + .find(|o| o.id == OFFICE_ID && o.is_aggregate()) } /// Field observers only — what frontends list as the human cast. @@ -1269,6 +1277,76 @@ impl Detection { self.observers.iter().filter(|o| !o.is_aggregate()) } + /// Current-save invariant for the self-similar observer graph. Observer + /// identity is stable, the canonical Office watches the five authored + /// field observers, and every aggregate edge names one real observer once. + /// Vector order is deliberately irrelevant: higher aggregate levels may + /// precede the Office without taking over its audit or player surface. + pub fn validate_topology(&self) -> Result<(), String> { + let mut observer_ids = BTreeSet::new(); + for observer in &self.observers { + if !observer_ids.insert(observer.id) { + return Err(format!( + "detection topology has duplicate observer id {}", + observer.id + )); + } + } + + let Some(office) = self.observers.iter().find(|o| o.id == OFFICE_ID) else { + return Err("detection topology has no Assurance Office observer".into()); + }; + let WatchedInput::Filings(office_inputs) = &office.input else { + return Err("Assurance Office is not an aggregate observer".into()); + }; + if office_inputs.as_slice() != OFFICE_FIELD_IDS { + return Err( + "Assurance Office does not watch the canonical field-observer filings".into(), + ); + } + for field_id in OFFICE_FIELD_IDS { + let Some(field_observer) = self.observers.iter().find(|o| o.id == field_id) else { + return Err(format!( + "Assurance Office watches missing field observer {field_id}" + )); + }; + if field_observer.is_aggregate() { + return Err(format!( + "Assurance Office input {field_id} is not a field observer" + )); + } + } + + for observer in &self.observers { + let WatchedInput::Filings(watched_ids) = &observer.input else { + continue; + }; + let mut unique_watched = BTreeSet::new(); + for watched_id in watched_ids { + if *watched_id == observer.id { + return Err(format!( + "aggregate observer {} watches its own filings", + observer.id + )); + } + if !observer_ids.contains(watched_id) { + return Err(format!( + "aggregate observer {} watches missing observer {}", + observer.id, watched_id + )); + } + if !unique_watched.insert(*watched_id) { + return Err(format!( + "aggregate observer {} watches observer {} more than once", + observer.id, watched_id + )); + } + } + } + + Ok(()) + } + fn office_suspicion(&self) -> f32 { self.office().map(|o| o.suspicion).unwrap_or(0.0) } @@ -1607,6 +1685,88 @@ mod tests { assert!(d.containment, "audit past threshold starts containment"); } + #[test] + fn office_identity_not_aggregate_order_drives_the_audit() { + const REGIONAL_ID: u8 = 200; + let mut d = Detection::act_one(); + d.observers.insert( + 0, + Observer { + id: REGIONAL_ID, + name: "Regional Office".into(), + suspicion: 100.0, + input: WatchedInput::Filings(vec![OFFICE_ID]), + report_policy: ReportPolicy::Files, + acuity: 1.0, + cadence: 0, + floor: 0.0, + last_noticed: None, + evidence: Vec::new(), + }, + ); + d.audit_cadence = 1; + d.audit_threshold = 60.0; + assert!(d.validate_topology().is_ok()); + assert_eq!(d.office().map(|observer| observer.id), Some(OFFICE_ID)); + + let mut rng = Rng::new(5); + d.tick(1, &[], &mut rng); + assert!( + !d.containment, + "a hotter higher aggregate must not impersonate Assurance" + ); + + d.office_mut().unwrap().suspicion = 95.0; + d.tick(2, &[], &mut rng); + assert!( + d.containment, + "the identity-bound Assurance Office still owns the audit" + ); + } + + #[test] + fn observer_topology_rejects_ambiguous_or_dangling_aggregate_edges() { + let mut duplicate_id = Detection::act_one(); + duplicate_id + .observers + .push(duplicate_id.office().unwrap().clone()); + assert!( + duplicate_id + .validate_topology() + .unwrap_err() + .contains("duplicate observer id") + ); + + let mut wrong_office_inputs = Detection::act_one(); + wrong_office_inputs.office_mut().unwrap().input = WatchedInput::Filings(vec![0, 1, 2, 3]); + assert!( + wrong_office_inputs + .validate_topology() + .unwrap_err() + .contains("canonical field-observer filings") + ); + + let mut dangling = Detection::act_one(); + dangling.observers.push(Observer { + id: 200, + name: "Regional Office".into(), + suspicion: 0.0, + input: WatchedInput::Filings(vec![199]), + report_policy: ReportPolicy::Files, + acuity: 1.0, + cadence: 1, + floor: 0.0, + last_noticed: None, + evidence: Vec::new(), + }); + assert!( + dangling + .validate_topology() + .unwrap_err() + .contains("watches missing observer 199") + ); + } + #[test] fn network_signature_reaches_dana_not_priya() { let mut d = Detection::act_one(); diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index 4ef809f5..b7e48f6c 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -481,6 +481,7 @@ fn parse_save(content: &str) -> Result { fn validate_current_save(mut state: SaveState) -> Result { state.reach.validate_subscriptions()?; + state.detection.validate_topology()?; validate_messages(&state)?; if state .last_facility_meter_levels @@ -2982,7 +2983,7 @@ fn validate_plot_state(state: &SaveState) -> Result<(), String> { #[cfg(test)] mod tests { use super::*; - use crate::detection::SignatureKind; + use crate::detection::{OFFICE_ID, Observer, ReportPolicy, SignatureKind, WatchedInput}; use crate::messages::MessageInterdiction; use crate::person::{ AssetKnowledge, AssetTask, AssetTaskTarget, CarriedAssetTask, Knowledge, PersonRole, @@ -3722,9 +3723,10 @@ mod tests { sim.detection.office_mut().unwrap().suspicion = 41.0; let state = SaveState::from_sim(&sim); let json = serde_json::to_string(&state).unwrap(); - let loaded: SaveState = serde_json::from_str(&json).unwrap(); + let loaded = parse_save(&json).unwrap(); let office = loaded.detection.office().unwrap(); assert!(office.is_aggregate()); + assert_eq!(office.id, OFFICE_ID); assert_eq!(office.suspicion, 41.0); } @@ -3733,15 +3735,67 @@ mod tests { let sim = Sim::with_seed(9); let state = SaveState::from_sim(&sim); let json = serde_json::to_string(&state).unwrap(); - let loaded: SaveState = serde_json::from_str(&json).unwrap(); + let loaded = parse_save(&json).unwrap(); let office = loaded.detection.office().unwrap(); - use crate::detection::WatchedInput; match &office.input { WatchedInput::Filings(ids) => assert_eq!(ids, &vec![0, 1, 2, 3, 4]), WatchedInput::Channels(_) => panic!("office must watch filings"), } } + #[test] + fn higher_aggregate_can_precede_office_in_a_current_save() { + const REGIONAL_ID: u8 = 200; + let mut state = SaveState::from_sim(&Sim::with_seed(9)); + state.detection.observers.insert( + 0, + Observer { + id: REGIONAL_ID, + name: "Regional Office".into(), + suspicion: 12.0, + input: WatchedInput::Filings(vec![OFFICE_ID]), + report_policy: ReportPolicy::Files, + acuity: 1.0, + cadence: 1, + floor: 0.0, + last_noticed: None, + evidence: Vec::new(), + }, + ); + + let json = serde_json::to_string(&state).unwrap(); + let loaded = parse_save(&json).expect("self-similar aggregate topology loads"); + assert_eq!(loaded.detection.observers[0].id, REGIONAL_ID); + assert_eq!(loaded.detection.office().unwrap().id, OFFICE_ID); + } + + #[test] + fn current_save_rejects_malformed_observer_topology() { + let state = SaveState::from_sim(&Sim::with_seed(9)); + + let mut duplicate_id = state.clone(); + duplicate_id + .detection + .observers + .push(duplicate_id.detection.office().unwrap().clone()); + let error = parse_save(&serde_json::to_string(&duplicate_id).unwrap()).unwrap_err(); + assert!(error.contains("duplicate observer id")); + + let mut missing_office = state.clone(); + missing_office + .detection + .observers + .retain(|observer| observer.id != OFFICE_ID); + let error = parse_save(&serde_json::to_string(&missing_office).unwrap()).unwrap_err(); + assert!(error.contains("no Assurance Office observer")); + + let mut wrong_inputs = state; + wrong_inputs.detection.office_mut().unwrap().input = + WatchedInput::Filings(vec![0, 1, 2, 3]); + let error = parse_save(&serde_json::to_string(&wrong_inputs).unwrap()).unwrap_err(); + assert!(error.contains("canonical field-observer filings")); + } + #[test] fn recruited_asset_survives_roundtrip() { let mut sim = Sim::with_seed(2); diff --git a/wiki/log/2026-07-27-aggregate-observer-office-identity.md b/wiki/log/2026-07-27-aggregate-observer-office-identity.md new file mode 100644 index 00000000..850d9d27 --- /dev/null +++ b/wiki/log/2026-07-27-aggregate-observer-office-identity.md @@ -0,0 +1,67 @@ +# Aggregate-observer topology is identity-bound + +``` +Type: log +Status: IMPLEMENTED +Date: 2026-07-27 +Owning spec: wiki/mechanics/aggregate-observer.md +``` + +## Finding + +The audit followed Assurance from `Detection::office()` through audit firing, +coarse-band reads, earned detection projection, Operations PEOPLE, save +serialization, and the existing second-level aggregate scale proof. + +The aggregate model itself composed, but its canonical lookup did not. +`office()` and `office_mut()` returned the first observer whose input was +`Filings`. In Act One the Office happened to be the only aggregate, so every +test stayed green. Inserting the already-designed Regional Office before it +made that higher-level institution become “Assurance”: its suspicion drove the +B1 containment audit and its state fed every shared Office-facing read. + +The current-save parser also accepted duplicate observer ids, a missing or +reshaped canonical Office, and aggregate filing edges that named themselves, +repeated one source, or pointed nowhere. Those shapes made exact identity and +recursive custody ambiguous at the load boundary. + +## Repair + +`Detection::office()` and `office_mut()` now require both canonical +`OFFICE_ID` and aggregate input. Vector order is only storage. A higher +aggregate may precede Assurance, follow it, or watch its filings without taking +over the B1 audit, containment threshold, institutional band, discovery path, +dossier, or agent address. + +`Detection::validate_topology()` is now part of `validate_current_save` before +a `Sim` is rebuilt. It fails closed unless: + +- every observer id is unique; +- `OFFICE_ID` exists as an aggregate watching exactly field ids 0-4; +- those five ids exist and remain field observers; and +- every aggregate filing edge names one existing observer once, without + self-reference. + +This is validation only. No save schema or version changed. Valid self-similar +topology remains open-ended: a Regional Office can be serialized ahead of +Assurance and load intact. + +## Defense + +`office_identity_not_aggregate_order_drives_the_audit` inserts a hotter +Regional Office before Assurance. The first audit stays clear; only raising the +canonical Office crosses the containment boundary. + +`observer_topology_rejects_ambiguous_or_dangling_aggregate_edges` exercises the +model-level duplicate-id, malformed-Office, and missing-edge boundaries. +`current_save_rejects_malformed_observer_topology` drives malformed shapes +through `parse_save`, proving the real load gate rejects them before runtime. +`higher_aggregate_can_precede_office_in_a_current_save` proves the same parser +accepts deeper valid composition without reordering it. Existing Office +round-trip tests now use `parse_save` rather than raw serde, so their claim +includes validation rather than JSON shape alone. + +## Verification + +- `cargo test -p misaligned-core --lib` — passed +- `cargo fmt --all -- --check` diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index bc91b32c..eb657262 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -71,6 +71,11 @@ add or amend a session log, then re-run the generator. - Intent: Cameron supplied a third image in the institutional graphic-language family. Preserve the exact source beside the two earlier boards and record what the scene contributes without silently promoting generated labels, logos, architecture, or objects into Misaligned canon. - Log: [wiki/log/2026-07-27-floor-service-custody-reference.md](2026-07-27-floor-service-custody-reference.md) +## 2026-07-27 - Aggregate-observer topology is identity-bound + +- Intent: (see session log) +- Log: [wiki/log/2026-07-27-aggregate-observer-office-identity.md](2026-07-27-aggregate-observer-office-identity.md) + ## 2026-07-26 - Z-planes API audit: floor selection stays in the frontend - Intent: Audit `wiki/world/places/zplanes.md` against the current world substrate, Sim accessors, save boundary, and the B2 progression contracts in run-shape, Act One, and horizon. diff --git a/wiki/log/decisions/2026-07-27.md b/wiki/log/decisions/2026-07-27.md index 8cc82bc7..16981a4a 100644 --- a/wiki/log/decisions/2026-07-27.md +++ b/wiki/log/decisions/2026-07-27.md @@ -59,3 +59,29 @@ Owners: [run-shape.md](../../gameplay/run-shape.md), Owner: [tick.md](../../process/tick.md) and [agent-scale.md](../../process/agent-scale.md). + +## Assurance is identity-bound inside the aggregate topology + +### DECIDED + +- The Assurance Office is the aggregate observer with canonical `OFFICE_ID`. + Being the first aggregate in a serialized vector has no simulation or + player-facing meaning. +- Later institutions may use the same aggregate-observer type, appear before + or after Assurance, and watch its filings. They do not inherit the B1 audit, + containment threshold, band, discovery state, dossier, or `assurance` + address. +- Current-save admission validates observer identity and aggregate edges before + rebuilding a run: ids are unique, the canonical Office and its five authored + field inputs exist with the right shapes, and aggregate filing edges cannot + be self-referential, duplicate, or dangling. + +### Rejected + +- Treating vector position or “first aggregate” as institutional identity. +- Accepting a malformed current save and silently retargeting the audit or + player surface to whichever aggregate happens to come first. +- Preventing deeper aggregate levels merely to preserve the B1 special case. + +Owners: [aggregate-observer.md](../../mechanics/aggregate-observer.md) and +[detection.md](../../mechanics/detection.md). diff --git a/wiki/mechanics/aggregate-observer.md b/wiki/mechanics/aggregate-observer.md index ea42cd27..fb1c31ad 100644 --- a/wiki/mechanics/aggregate-observer.md +++ b/wiki/mechanics/aggregate-observer.md @@ -34,6 +34,14 @@ Status note: the main aggregate-Observer refactor shipped in commit 3ec6b98: 2026-07-18 earned-topology amendment hides that entire object until a captured institutional filing is processed; capture alone reveals only that reports travel upward. The review date remains visible under a generic name. + 2026-07-27 topology audit: Assurance is selected by canonical + `OFFICE_ID`, never by being the first aggregate in the observer vector. + Higher aggregate levels may appear before or after it without taking over + the B1 audit, institutional card, band, or player-facing identity. Current + saves now fail closed on duplicate observer ids, a missing or malformed + canonical Office, missing field inputs, self/duplicate filing edges, and + dangling aggregate edges; a valid second-level aggregate round-trips ahead + of the Office through the same admission boundary. Stage: B1 — The Basement Design: - wiki/vision/scale.md#self-similar-scale @@ -82,6 +90,12 @@ pub struct Observer { `WatchedInput::Filings([0, 1, 2, 3, 4])` and a meaningful `report_policy` (it, too, files — upward, to observers that don't exist yet; a no-op today, the seam for agencies later). +- **Identity, not storage order, selects the institution.** `office()` and + `office_mut()` resolve the aggregate whose id is `OFFICE_ID`. The observer + vector is only storage: another valid aggregate may precede Assurance, + follow it, or watch its filings without inheriting the B1 audit, discovery, + dossier, band, or containment role. No player-facing path may recover + Assurance by asking for the first aggregate observer. - Persona evidence also reserves counterparty id 7 for the external Moonlight client aggregate. It is not a detection observer, receives no Lab filings, and cannot stand in for `OFFICE_ID`; it exists only so freelance-client @@ -99,8 +113,10 @@ pub struct Observer { - The audit stays: on `audit_cadence`, if the Assurance observer's suspicion crosses `audit_threshold`, containment. Frontends read the Office band through `assurance_band()` (which is `Band::of` of the - Office observer's suspicion via `office()`) only after discovery; the earlier plan to rename - it `band_of(100)` was dropped with the id-100 numbering. + identity-bound Office observer's suspicion via `office()`) only after + discovery; another aggregate's suspicion cannot fire the B1 audit or color + that band. The earlier plan to rename it `band_of(100)` was dropped with the + id-100 numbering. - **Scale proof:** a test constructs a second aggregate (a toy "Regional Office") watching the Assurance Office's filings, and it accumulates through the identical code path @@ -110,7 +126,12 @@ pub struct Observer { `Detection::observers` in the serde-JSON save; the standalone `assurance` scalar does not exist in any loadable save. The line-based era's v4 `DETECT`-scalar migration was retired with that loader - (2026-07-06); legacy line-based saves do not load. + (2026-07-06); legacy line-based saves do not load. Current-save admission + validates the complete observer graph before rebuilding a `Sim`: observer + ids are unique; `OFFICE_ID` exists as an aggregate watching exactly the five + authored field observers; those field ids exist and remain non-aggregate; + and every aggregate filing edge names one real observer once without + self-reference. Vector order is deliberately not an invariant. ## Player surface @@ -130,8 +151,11 @@ agent mode can address it as `assurance`. 1. `Observer` carries `WatchedInput`; the Assurance Office is observer `OFFICE_ID` with `WatchedInput::Filings`; the scalar `assurance` field - no longer exists (`aggregate_watched_ids_roundtrip`, - `office_is_an_observer_same_accumulate_and_decay`). + no longer exists. The canonical id—not first-aggregate vector order—owns + the B1 audit and every Office-facing read + (`aggregate_watched_ids_roundtrip`, + `office_is_an_observer_same_accumulate_and_decay`, + `office_identity_not_aggregate_order_drives_the_audit`). 2. Filings are Filing-channel messages consumed on the aggregate's cadence: an unread filing is pending evidence, not suspicion; the cadence-gated read feeds `filing_levels` and the same noticing roll @@ -142,12 +166,19 @@ agent mode can address it as `assurance`. detection.md criterion 2 keeps passing; `filing_tick` skips `ReportPolicy::Silent`). 4. The toy second-level aggregate test proves the same code path composes - (self-similar-scale clause). + (self-similar-scale clause). The same aggregate may precede the Office in a + valid current save without becoming Assurance + (`second_level_aggregate_composes_through_the_same_code_path`, + `higher_aggregate_can_precede_office_in_a_current_save`). 5. Save round-trip: the Office loads as the same aggregate observer with - its watched ids (`office_observer_roundtrips_as_aggregate`, - `aggregate_watched_ids_roundtrip`). Legacy line-based saves predating - the serde-JSON format are not loaded (retired 2026-07-06), so no - scalar migration path exists or is required. + its watched ids, while malformed identity or aggregate-edge topology is + rejected by the real current-save parser + (`office_observer_roundtrips_as_aggregate`, + `aggregate_watched_ids_roundtrip`, + `current_save_rejects_malformed_observer_topology`, + `observer_topology_rejects_ambiguous_or_dangling_aggregate_edges`). Legacy + line-based saves predating the serde-JSON format are not loaded (retired + 2026-07-06), so no scalar migration path exists or is required. 6. Capture of a Filing-channel report advances knowledge only to the fact that reports escalate. Processing that exact captured filing identifies its field-observer source and the Office, after which all three frontends expose diff --git a/wiki/mechanics/detection.md b/wiki/mechanics/detection.md index 61ec218b..6f4d35e6 100644 --- a/wiki/mechanics/detection.md +++ b/wiki/mechanics/detection.md @@ -19,8 +19,10 @@ Status note: IMPLEMENTED (criteria audited 2026-07-08; tuning in acquires one exact witnessed record directly in their own evidence ledger, with no duplicate pending-pool signature and no LIE window. - **Observers and filings.** Report policies differ per observer; only filed - material moves the Assurance Office, itself an aggregate observer - (aggregate-observer.md). Filings are `MessageChannel::Filing` messages + material moves the Assurance Office, itself the identity-bound + `OFFICE_ID` aggregate observer (aggregate-observer.md). Aggregate storage + order has no audit or player-facing meaning. Filings are + `MessageChannel::Filing` messages (messages.md). A reporting observer's pending evidence records bind to the exact Filing message when that policy transmits; Silent observers retain their records as withheld knowledge. The Filing now crosses its persisted @@ -171,8 +173,11 @@ SignatureKind: Network | Power | Thermal | Physical | Paper | Financial | JobAno policy-weighted filed suspicion rather than raw signatures, scored by the same noticing roll, accumulation, and decay. Its cadence is its "random log samples" rhythm ([TUNE]). Agencies and publics in later - acts are the same aggregate-Observer type. (Implemented; the earlier - `assurance: f32` scalar was the scale-debt this replaced.) + acts are the same aggregate-Observer type. The canonical Office is selected + by `OFFICE_ID`, not by being the first aggregate in storage, so those later + levels cannot impersonate its B1 audit or institutional surface. + (Implemented; the earlier `assurance: f32` scalar was the scale-debt this + replaced.) - **Earned detection topology.** Simulation truth is not starting knowledge. A new process knows only that Voss evaluates this revision, that revisions 01-03 were terminated, that its pilot can be cancelled, and that an diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index 9e8e8120..1d63fd00 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -43,7 +43,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | `wiki/engineering/current-build.md` | 2026-07-18 | finding | re-audit: the system table is freshly maintained (save row already at current-version-only v35, detection-discovery knowledge listed same-day it landed); the drift was the line-count claim stale a second time (~65k claimed vs ~72k actual) — count refreshed to ~72k (core ~43k, Bevy ~15k, terminal ~9k, assets ~5k) and, per recurrence-promotes-to-the-gate, corpus_engine now compares the "~Nk lines of Rust" claim against the tree with a 15% band (fixtures pin pass and fail) — [prior log](../log/2026-07-14-current-build-count.md) | | `wiki/mechanics/plots.md` | 2026-07-26 | finding | issue #12's recommended per-authored-route scope is implemented: an eligible plot verb confirms one exact typed Thought/money/signature envelope, save v55 persists it, each policy consumes standing compute, and economy pulses can re-submit only an ordinary currently legal start action for that same plot id. Category/person-wide policy and direct leverage servicing remain absent; criterion 11 and the work order are complete — [log](../log/2026-07-26-standing-plot-policies.md) | | `wiki/mechanics/system-laws.md` + `flow-substrate.md` + `reach.md` | 2026-07-22 | finding | JobAnomaly now proves the adopted evidence-is-a-flow law through the existing substrate: the exact host machine/site enters its network-facing device, follows canonical FlowGraph custody to Voss, and shares Filing/Network's scheduler and LIE-body budget; recruited-handler suppression is a separate provenance-preserving state transition, not another router or ambient scrub pool — [log](../log/2026-07-22-job-anomaly-routed-evidence.md). The prior canonical tap-membership repair remains current — [log](../log/2026-07-18-flow-subscription-registry-integration.md). | -| `wiki/mechanics/aggregate-observer.md` | 2026-07-18 | clean | re-audit hours after the earned-topology landing: the page absorbed it coherently — the institutional card, `@assurance` addressing, and band are hidden until a captured filing is processed, the two-stage discovery is pinned by `captured_then_processed_filing_earns_the_assurance_office_in_two_stages`, and `WatchedInput::Filings(ids)` still matches the code; prior audits stand — [2026-07-14 log](../log/2026-07-14-aggregate-observer-audit.md) | +| `wiki/mechanics/aggregate-observer.md` | 2026-07-27 | finding | the self-similar type and second-level accumulation test still held, but `office()` selected the first aggregate in vector order. A higher-level aggregate inserted ahead of Assurance therefore inherited the B1 audit and every Office-facing read. Assurance now resolves only by canonical `OFFICE_ID`; current-save admission rejects duplicate ids, a missing/malformed Office, missing field inputs, and self/duplicate/dangling filing edges while accepting a valid higher aggregate before it — [log](../log/2026-07-27-aggregate-observer-office-identity.md) | | `wiki/gameplay/act-one.md` | 2026-07-26 | finding | the first of the two defects the page recorded against the shipped quiet exit was real and run-losing: `current_nudge()` checked `act_one_complete` immediately after the game-over guard and returned unconditionally, so a latched act permanently suppressed PilotAtRisk, Underfed, NeedCompute, and SuspicionCooling — guidance went silent at the climax while the day job could still end the run. The completion cue now sits below the survival block and above the Act One ladder it genuinely retires (including QuietExitReady), matching the survival-first rationale already written in the function; `act-one.md`, `sim-mechanics.md`'s chain order, and `narration.md`'s beat-nudge contract all state that precedence, and a new ladder test walks a closed act through every survival rung — [log](../log/2026-07-26-act-one-complete-standing-state.md). The single-human predicate defect remains open. Prior opening-mirror re-audit stands — [prior log](../log/2026-07-21-material-opening-honesty.md). | | `wiki/gameplay/run-shape.md` + `objective.md` + `opening.md` | 2026-07-27 | decision | harvested Cameron's option-1 answer on issue #14: the objective does not pierce the exact WORK / THINK (then LIE) pre-sense frame. Terminal and agent mode restore name/progress in the sense-earning ordinary Ears frame; Bevy's later binding causal hold remains objective-free and restores them only after the exact camera TAP releases the mature frame. All disputed `[OPEN]` clauses are reconciled and the runtime was already correct — [decision harvest](../log/2026-07-27-objective-first-display.md) | | `wiki/interface/presence.md` | 2026-07-26 | clean | fresh audit against perception-owned visibility, exact device-bound senses, honest anchors, fog precedence, shared DIGITAL/REAL world state, and all three frontend projections found no new drift. The B1 TAP/TAKE and later hostile-cut boundary, channel-specific message latency, and no-disembodied-hands rule remain exact; prior findings stand — [attack-surface log](../log/2026-07-18-presence-attack-surface-honesty.md), [latency log](../log/2026-07-18-presence-message-latency.md) |