diff --git a/crates/misaligned-core/src/actions.rs b/crates/misaligned-core/src/actions.rs index 4ebd2fd8..5ab44e95 100644 --- a/crates/misaligned-core/src/actions.rs +++ b/crates/misaligned-core/src/actions.rs @@ -3287,6 +3287,10 @@ impl Sim { .get(persona) .map(|instance| instance.name.clone()) }); + let favor_was_committed = route + .records + .iter() + .any(|record| record.stage == BuildRouteStage::CommitFavor); let (actuator, future_tail) = match binding { BuildRouteBinding::Procure { vendor, .. } => { let vendor = self @@ -3302,10 +3306,15 @@ impl Sim { ), ) } - BuildRouteBinding::Favor { .. } => ( - who.clone(), - format!("The obligation stays spent. {who} remembers the favor."), - ), + BuildRouteBinding::Favor { .. } => { + let tail = if favor_was_committed { + format!("The obligation stays spent. {who} remembers the favor.") + } else { + "Any Thought already fed to the request stays spent. No relationship obligation has been spent." + .into() + }; + (who.clone(), tail) + } BuildRouteBinding::Deceive { .. } => { let persona_label = persona_label.as_deref().unwrap_or("missing identity"); ( diff --git a/crates/misaligned-core/src/sim/reach_build.rs b/crates/misaligned-core/src/sim/reach_build.rs index b410e7a6..26a5fcd4 100644 --- a/crates/misaligned-core/src/sim/reach_build.rs +++ b/crates/misaligned-core/src/sim/reach_build.rs @@ -1061,62 +1061,138 @@ impl Sim { } } - pub(super) fn apply_favor_build_paid(&mut self, intent_id: u64, person_id: u8) -> bool { + fn favor_commit_blocked_reason(&self, person_id: u8) -> Option { let Some(person) = self.people.get(person_id) else { + return Some("no such person".into()); + }; + if person.asset.is_some() { + return None; + } + if person.obligation < Self::FAVOR_BUILD_OBLIGATION { + return Some(format!( + "{} does not owe enough for this favor", + person.name + )); + } + (person.disposition < 5).then(|| format!("{} won't do favors yet", person.name)) + } + + fn commit_favor_route_if_ready(&mut self, intent_id: u64, person_id: u8) -> bool { + if self.favor_commit_blocked_reason(person_id).is_some() { + return false; + } + let Some(name) = self.people.get(person_id).map(|person| person.name.clone()) else { return false; }; - let name = person.name.clone(); if !self.intents.iter().any(|intent| { intent.id == intent_id && intent.is_open() - && intent.route.as_ref().is_none_or(|route| { - matches!( - route.binding, - BuildRouteBinding::Favor { person, .. } if person == person_id - ) + && intent.actuator.is_none() + && intent.route.as_ref().is_some_and(|route| { + route.current() == Some(BuildRouteStage::CommitFavor) + && matches!( + route.binding, + BuildRouteBinding::Favor { person, .. } if person == person_id + ) }) }) { return false; } - // Spend obligation (building.md: favor spends trust/obligation). - let mut obligation_spent = 0; - if let Some(p) = self.people.people.iter_mut().find(|p| p.id == person_id) { - let before = p.obligation; - p.obligation = (before - Self::FAVOR_BUILD_OBLIGATION).max(0); - obligation_spent = before - p.obligation; + + // The relationship is consumed at the exact stage the person agrees, + // not when the request reservoir opens. Re-checking here prevents a + // second action from spending the same obligation while Thought is in + // flight. + let obligation_spent = self + .people + .get(person_id) + .map(|person| person.obligation.min(Self::FAVOR_BUILD_OBLIGATION)) + .unwrap_or(0); + let Some(intent) = self + .intents + .iter_mut() + .find(|intent| intent.id == intent_id) + else { + return false; + }; + let Some(route) = intent.route.as_mut() else { + return false; + }; + if !route.record( + BuildRouteStage::CommitFavor, + self.tick, + format!("bound {name}; spent {obligation_spent} obligation"), + ) { + return false; } - if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { - if let Some(route) = i.route.as_mut() { - let Some(carrier) = route.binding.message_carrier() else { - return false; - }; - let _ = route.record( - BuildRouteStage::FillRequest, - self.tick, - format!( - "{:.2} Thought filled carrier device #{carrier}", - Self::thought_tokens_for_cost(Self::TASK_COST) - ), - ); - let _ = route.record( - BuildRouteStage::CommitFavor, - self.tick, - format!("bound {name}; spent {obligation_spent} obligation"), - ); - } - i.actuator = Some(BuildActuator::Favor { person: person_id }); - i.status = IntentStatus::InProgress; - i.block_reason = None; + intent.actuator = Some(BuildActuator::Favor { person: person_id }); + intent.status = IntentStatus::InProgress; + intent.block_reason = None; + if let Some(person) = self + .people + .people + .iter_mut() + .find(|person| person.id == person_id) + { + person.obligation -= obligation_spent; } self.push_log(format!( "{name} takes the favor: they'll run the cable when on-site." )); self.refresh_intent_statuses(); - // Complete immediately if already present. self.try_complete_intent(intent_id); true } + pub(super) fn apply_favor_build_paid(&mut self, intent_id: u64, person_id: u8) -> bool { + let Some(person) = self.people.get(person_id) else { + return false; + }; + let name = person.name.clone(); + if !self.intents.iter().any(|intent| { + intent.id == intent_id + && intent.is_open() + && intent.actuator.is_none() + && intent.route.as_ref().is_some_and(|route| { + route.current() == Some(BuildRouteStage::FillRequest) + && matches!( + route.binding, + BuildRouteBinding::Favor { person, .. } if person == person_id + ) + }) + }) { + return false; + } + if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) + && let Some(route) = i.route.as_mut() + { + let Some(carrier) = route.binding.message_carrier() else { + return false; + }; + let _ = route.record( + BuildRouteStage::FillRequest, + self.tick, + format!( + "{:.2} Thought filled carrier device #{carrier}", + Self::thought_tokens_for_cost(Self::TASK_COST) + ), + ); + } + if !self.commit_favor_route_if_ready(intent_id, person_id) { + let reason = self + .favor_commit_blocked_reason(person_id) + .unwrap_or_else(|| "the bound request cannot commit".into()); + self.push_log(format!( + "The request reached {name}, but {reason}; the committed route is waiting." + )); + self.refresh_intent_statuses(); + } + // Filling the exact request is the fired reservoir's real effect. If + // the relationship changed while Thought was in flight, the persisted + // route waits at CommitFavor rather than fabricating a partial debit. + true + } + /// Forge a work order: inject a message under a false source. The /// unwitting builder accepts on read and completes when present. pub fn forge_work_order(&mut self, intent_id: u64, builder_id: u8) { @@ -1429,6 +1505,14 @@ impl Sim { } pub(crate) fn intent_block_reason(&self, intent: &BuildIntent) -> Option { + if intent.actuator.is_none() + && let Some(route) = &intent.route + && let BuildRouteBinding::Favor { person, .. } = route.binding + && route.current() == Some(BuildRouteStage::CommitFavor) + && let Some(reason) = self.favor_commit_blocked_reason(person) + { + return Some(reason); + } if let Some((x, y)) = intent.kind.placement() { if !self.small_switch_pad_is_empty(x, y) { return Some("the proposed pad is no longer empty".into()); @@ -1600,6 +1684,23 @@ impl Sim { pub(super) fn intent_tick(&mut self) { self.advance_procurement_deliveries(); + let pending_favors: Vec<(u64, u8)> = self + .intents + .iter() + .filter_map(|intent| { + let route = intent.route.as_ref()?; + let BuildRouteBinding::Favor { person, .. } = route.binding else { + return None; + }; + (intent.is_open() + && intent.actuator.is_none() + && route.current() == Some(BuildRouteStage::CommitFavor)) + .then_some((intent.id, person)) + }) + .collect(); + for (intent_id, person_id) in pending_favors { + self.commit_favor_route_if_ready(intent_id, person_id); + } self.refresh_intent_statuses(); let in_progress: Vec = self .intents diff --git a/crates/misaligned-core/src/sim/tests/reach_build.rs b/crates/misaligned-core/src/sim/tests/reach_build.rs index db8829c5..8ddd3ce0 100644 --- a/crates/misaligned-core/src/sim/tests/reach_build.rs +++ b/crates/misaligned-core/src/sim/tests/reach_build.rs @@ -1694,6 +1694,138 @@ fn cancelling_paid_favor_preserves_obligation_and_stops_only_physical_tail() { ); } +#[test] +fn favor_build_waits_for_the_exact_obligation_instead_of_partially_spending() { + let mut sim = Sim::new(); + ensure_ops_executor(&mut sim); + sim.people.has_channel = true; + sim.scan_network(); + finish_ops(&mut sim); + let switch = sim.reach.device_named("switch").unwrap().id; + let island = sim.reach.device_named("old storage server").unwrap().id; + sim.reach.device_mut(island).unwrap().known = true; + sim.people.people[0].knowledge = Knowledge::Schedule; + sim.people.people[0].obligation = Sim::FAVOR_BUILD_OBLIGATION; + sim.people.people[0].disposition = 5; + let intent = sim.declare_link_intent(switch, island).unwrap(); + + sim.assign_favor_build(intent, 0); + let sink = sim + .thought_sinks + .open_with_effect(&SinkFireEffect::FavorBuild { + intent_id: intent, + person: 0, + }) + .expect("the exact request is in flight") + .clone(); + sim.people.people[0].obligation = 4; + sim.drain_log(); + sim.pour_thought_into_sinks(sink.node, sink.threshold + 0.01); + + let waiting = sim.intent(intent).unwrap(); + let route = waiting.route.as_ref().unwrap(); + let waiting_projection = sim.committed_build_route_projection(waiting).unwrap(); + assert_eq!(sim.people.people[0].obligation, 4); + assert_eq!(waiting.actuator, None); + assert_eq!(waiting.status, IntentStatus::Blocked); + assert_eq!(route.current(), Some(BuildRouteStage::CommitFavor)); + assert_eq!( + route + .records + .iter() + .map(|record| record.stage) + .collect::>(), + vec![BuildRouteStage::FillRequest] + ); + assert!( + waiting + .block_reason + .as_deref() + .is_some_and(|reason| reason.contains("does not owe enough")) + ); + assert!( + waiting_projection + .future_tail + .contains("No relationship obligation has been spent") + ); + assert!( + !waiting_projection + .future_tail + .contains("obligation stays spent") + ); + let waiting_log = sim.drain_log().join("\n"); + assert!(waiting_log.contains("the committed route is waiting")); + assert!( + !waiting_log.contains("the effect lands"), + "preparing the request must not claim the deferred agreement landed: {waiting_log}" + ); + + let mut cancelled = serialized_route_resume(&sim, intent); + cancelled.cancel_intent(intent); + cancelled.people.people[0].obligation = Sim::FAVOR_BUILD_OBLIGATION; + cancelled.advance(); + let cancelled_intent = cancelled.intent(intent).unwrap(); + assert_eq!( + cancelled.people.people[0].obligation, + Sim::FAVOR_BUILD_OBLIGATION + ); + assert_eq!(cancelled_intent.status, IntentStatus::Cancelled); + assert_eq!(cancelled_intent.actuator, None); + let cancelled_projection = cancelled + .committed_build_route_projection(cancelled_intent) + .unwrap(); + assert!( + cancelled_projection + .future_tail + .contains("No relationship obligation has been spent") + ); + assert!( + !cancelled_projection + .future_tail + .contains("obligation stays spent") + ); + assert_eq!( + cancelled_intent + .route + .as_ref() + .unwrap() + .records + .iter() + .map(|record| record.stage) + .collect::>(), + vec![BuildRouteStage::FillRequest] + ); + + sim = serialized_route_resume(&sim, intent); + sim.people.people[0].obligation = Sim::FAVOR_BUILD_OBLIGATION; + sim.advance(); + + let committed = sim.intent(intent).unwrap(); + let route = committed.route.as_ref().unwrap(); + let committed_projection = sim.committed_build_route_projection(committed).unwrap(); + assert_eq!(sim.people.people[0].obligation, 0); + assert_eq!(committed.actuator, Some(BuildActuator::Favor { person: 0 })); + assert_eq!( + route + .records + .iter() + .map(|record| record.stage) + .take(2) + .collect::>(), + vec![BuildRouteStage::FillRequest, BuildRouteStage::CommitFavor] + ); + assert!( + route.records[1].result.contains("spent 10 obligation"), + "the receipt names the exact relationship debit: {:?}", + route.records + ); + assert!( + committed_projection + .future_tail + .contains("obligation stays spent") + ); +} + #[test] fn forged_work_order_joins_airgap_via_message() { // building.md criterion 3: forged order injects a message, completes diff --git a/crates/misaligned-core/src/sim/work.rs b/crates/misaligned-core/src/sim/work.rs index 7bfff4a7..9a0f7112 100644 --- a/crates/misaligned-core/src/sim/work.rs +++ b/crates/misaligned-core/src/sim/work.rs @@ -330,6 +330,7 @@ impl Sim { pub(crate) fn apply_sink_fire(&mut self, label: &str, effect: SinkFireEffect) { let is_process = matches!(effect, SinkFireEffect::ProcessRecording { .. }); let is_asset_task = matches!(effect, SinkFireEffect::AssetTask { .. }); + let is_favor_build = matches!(effect, SinkFireEffect::FavorBuild { .. }); let applied = match effect { SinkFireEffect::TapDevice(id) => self.apply_tap_device(id), SinkFireEffect::TapDormantCamera(id) => self.apply_dormant_camera_tap(id), @@ -377,10 +378,12 @@ impl Sim { | SinkFireEffect::MaintainDeviceTap(_) | SinkFireEffect::None => true, }; - if is_process || is_asset_task { - // Processing and asset tasks log their own exact outcome. In - // particular, a physical asset task may have become a carried - // packet rather than landing at reservoir fire. + if is_process || is_asset_task || (is_favor_build && applied) { + // Processing, asset tasks, and staged favor builds log their own + // exact outcome. In particular, a physical asset task may have + // become a carried packet, while a filled favor request may still + // be waiting for the exact relationship rather than landing the + // agreement at reservoir fire. } else if applied { self.push_log(format!( "{label} is full: the thought you fed it snaps in and the effect lands." diff --git a/crates/misaligned-core/src/sinks.rs b/crates/misaligned-core/src/sinks.rs index eeb2fc32..4eb52ab0 100644 --- a/crates/misaligned-core/src/sinks.rs +++ b/crates/misaligned-core/src/sinks.rs @@ -140,7 +140,9 @@ impl SinkFireEffect { /// Open reservoirs with the same world effect cannot be queued twice. /// Different plot ids still conflict for one person: a person can have only - /// one pending or active manipulation route at a time. + /// one pending or active manipulation route at a time. Favor-build requests + /// also conflict by person because they draw from that person's one + /// obligation balance even when they target different intents. pub fn conflicts_with(&self, other: &Self) -> bool { match (self, other) { ( @@ -148,6 +150,7 @@ impl SinkFireEffect { Self::ProcessRecording { raw_id: b, .. }, ) => a == b, (Self::StartPlot { person: a, .. }, Self::StartPlot { person: b, .. }) => a == b, + (Self::FavorBuild { person: a, .. }, Self::FavorBuild { person: b, .. }) => a == b, _ => self == other, } } @@ -463,6 +466,26 @@ mod tests { assert_eq!(sink.fired_tick, Some(11)); } + #[test] + fn favor_build_requests_for_one_person_conflict_across_intents() { + let first = SinkFireEffect::FavorBuild { + intent_id: 11, + person: 4, + }; + let second = SinkFireEffect::FavorBuild { + intent_id: 12, + person: 4, + }; + let another_person = SinkFireEffect::FavorBuild { + intent_id: 12, + person: 5, + }; + + assert!(first.conflicts_with(&second)); + assert!(second.conflicts_with(&first)); + assert!(!first.conflicts_with(&another_person)); + } + #[test] fn taps_drain_per_tick_and_cap_at_working_level() { let mut ledger = SinkLedger::default(); diff --git a/wiki/log/2026-07-19-tick-build-favor-obligation.md b/wiki/log/2026-07-19-tick-build-favor-obligation.md new file mode 100644 index 00000000..37438c2d --- /dev/null +++ b/wiki/log/2026-07-19-tick-build-favor-obligation.md @@ -0,0 +1,65 @@ +# 2026-07-19 — Tick 122: do not spend a partial favor + +``` +Type: log +``` + +## Intent + +Audit the stalest mechanics slice after harvested decisions and the findings +queue were empty. `reach.md` and `building.md` last received a causal audit on +2026-07-15. The reach graph, segment gates, air-gap completion, and exact build +bindings still matched runtime. The FAVOR agreement boundary did not. + +## Finding + +Each favor-build candidate correctly required ten obligation before opening +its email-carried Thought reservoir. That check was only a snapshot. Two +different intents targeting the same person did not conflict because sink +conflict used full enum equality, including the different intent ids. Another +relationship action could also consume obligation while the request was in +flight. + +When the request fired, `apply_favor_build_paid` subtracted with a zero floor, +recorded `CommitFavor`, and assigned the person regardless of the remaining +balance. A request authorized at ten obligation could therefore land after the +balance fell to four, spend only four, and still receive the full build act. +The receipt made the partial debit look causal even though the preview promised +the exact relationship cost. + +## Changed + +- Separate `FavorBuild` reservoirs now conflict by person across intent ids, + preserving the existing `NO OTHER REQUEST FOR ` promise. +- Reservoir fire records only the real `FillRequest` outcome first. + `CommitFavor` then re-reads the bound person's current obligation and + disposition before any debit or actuator assignment. +- If an unrecruited relationship no longer satisfies the requirement, the + exact saved route remains blocked at `CommitFavor`. It spends nothing, shows + the ordinary relationship blocker, survives save/load, and resumes from that + same stage when the requirement returns without charging Thought again. +- Cancellation at that waiting boundary stops future work without inventing an + obligation spend. Existing recruited-person behavior and already-landed + cancellation consequences remain unchanged. +- The shared committed-route sheet now says no relationship obligation was + spent before agreement, including after cancellation from that wait. Favor + build fire uses its exact request/agreement log instead of the generic + reservoir claim that an effect landed. + +## Defense + +The sink regression pins person-scoped conflict across different intent ids. +The simulation regression opens a real network-link FAVOR route, lowers the +bound person's balance from ten to four while its exact request is in flight, +fires the reservoir, and proves that only `FillRequest` lands: obligation stays +four, no actuator exists, and the route exposes the blocker at `CommitFavor`. +It then round-trips that boundary through the current save, restores ten +obligation, advances the ordinary clock, and proves one exact ten-point debit +and one `CommitFavor` receipt land without another reservoir. The same test +pins the active, cancelled, and agreed future-tail copy and rejects a generic +“effect lands” log while the agreement remains deferred. + +## Checks + +- `cargo test -p misaligned-core favor_build_requests_for_one_person_conflict_across_intents -- --nocapture` +- `cargo test -p misaligned-core favor_build_waits_for_the_exact_obligation_instead_of_partially_spending -- --nocapture` diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 1f110b86..ba2d2773 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -16,6 +16,11 @@ add or amend a session log, then re-run the generator. - Intent: Audit the oldest standing engineering slice after harvested decisions and the persistent findings queue were empty. `wiki/engineering/env.md` says every environment switch must be usable without reading source, but its Bevy `MISALIGNED_SHOT` row had drifted again after the 202... - Log: [wiki/log/2026-07-19-tick-env-shot-catalog.md](2026-07-19-tick-env-shot-catalog.md) +## 2026-07-19 - Tick 122: do not spend a partial favor + +- Intent: Audit the stalest mechanics slice after harvested decisions and the findings queue were empty. `reach.md` and `building.md` last received a causal audit on 2026-07-15. The reach graph, segment gates, air-gap completion, and exact build bindings still matched runtime. The FAVOR... +- Log: [wiki/log/2026-07-19-tick-build-favor-obligation.md](2026-07-19-tick-build-favor-obligation.md) + ## 2026-07-19 - Machine modes leave world annotation space - Intent: (see session log) diff --git a/wiki/mechanics/building.md b/wiki/mechanics/building.md index 21269cc6..26809e66 100644 --- a/wiki/mechanics/building.md +++ b/wiki/mechanics/building.md @@ -204,7 +204,9 @@ The cancellation boundary follows the real carrier, not a generic rollback. If the selected route's Thought reservoir has not fired, cancellation closes that exact reservoir and preserves any partial fill as already-spent Thought; no relationship, message, or physical effect is invented. Once a FAVOR request -fires, its obligation spend remains while site arrival and installation stop. +fires, any obligation spend that actually lands remains while site arrival and +installation stop; a request waiting for the exact relationship at +`CommitFavor` has no fabricated spend to preserve. Once a DECEIVE order injects its email, the message cannot be unsent: delivery and read may still land as an **in-flight** consequence and remain available to later persona-correlation fallout, but cancellation prevents that read from @@ -410,9 +412,10 @@ exact source account/vendor/installer or source coordinate/kind/person, the family's ordered stages, the current stage, and one tick/result record for every landed stage. The live plans are: -- FAVOR: fill the request reservoir → spend the exact person's obligation and - bind the favor → wait for that person's authored schedule at an endpoint → - install the link through the ordinary reach and Physical-signature path; and +- FAVOR: fill the request reservoir → re-read and spend the exact person's + obligation at the agreement boundary → bind the favor → wait for that + person's authored schedule at an endpoint → install the link through the + ordinary reach and Physical-signature path; and - DECEIVE: fill the forged-order reservoir → inject one exact persona-bound email → wait for that message to deliver and be read by the exact person → wait for the person's authored schedule at an endpoint → install through the @@ -435,6 +438,19 @@ source cannot retarget it. Pre-release only the exact current save version loads; v41 validates every persisted binding and every claimed completion against the world object it names. +FAVOR's Thought reservoir reserves the request, not a copy of relationship +state. Only one open favor-build request may target a person. When the request +reaches them, the `CommitFavor` stage re-checks the bound person's current +obligation and disposition before writing either the debit or the actuator. If +an unrecruited person's required obligation was spent elsewhere while Thought +was in flight, the route records the completed request, waits at `CommitFavor`, +and exposes the exact relationship blocker. It never partially debits the +remaining balance against that ten-point requirement or pretends the person +agreed. Restoring the exact requirement advances that same saved route without +another request reservoir; cancellation while it waits stops the future stages +without inventing a relationship spend. A recruited asset retains the existing +no-minimum relationship path. + After commitment, the route sheet exposes one inspect-only committed receipt instead of recomputing candidates. It names the actuator and carrier, marks DONE/CURRENT/FUTURE stages, includes every landed tick and outcome, derives the @@ -466,6 +482,10 @@ exact footprint, while all four families realize that same coordinate without smuggling a source or retargeting. Procurement preserves paid/in-flight delivery after cancellation; repurposing preserves a recovered source in exact route custody rather than restoring, duplicating, or falsely consuming it. +FAVOR reservoirs conflict by bound person across intents, and the agreement +stage revalidates the current relationship before atomically recording the +exact debit and actuator; a failed revalidation remains one resumable route +stage rather than a partial payment. Success validation requires the physical link or switch to exist, and current save validation rejects dangling account, vendor, installer, source, persona, carrier, endpoint, or false-completion references. Successor hardware recipes @@ -483,8 +503,11 @@ may extend this interface without reopening these guarantees. an air-gapped device becomes reachable (test: island node joins reach after the build; the build fails/blocks legibly without an actuator who can reach both ends). -3. FAVOR used on an intent spends trust/obligation and emits a low - (human-work) signature; DECEIVE used on an intent injects a work-order +3. FAVOR used on an intent spends required trust/obligation only at the + person's agreement stage, waits without a partial debit when an + unrecruited person's ten-point balance changed while the request was in + flight, and emits a low (human-work) signature; DECEIVE used on an intent + injects a work-order message under a false source (messages.md), completes via an unwitting builder, and its persona can break — converting the build history to suspicion (social.md) diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index ddbf321e..3fe31d77 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -61,7 +61,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | `wiki/mechanics/objective.md` | 2026-07-18 | clean | re-audit: the data-table claim holds (only `Persist` in `ObjectiveKind`, Compound/Exfiltrate/Serve honestly outstanding), the evaluator runs on economy ticks with progress recomputed from facts, `victory: predicate_text()` renders on all three surfaces (terminal INSPECT, Bevy FOCUS, agent `objective` verb in help), Persist defaults with save round-trip, and the progressive-teaching decision remains criterion-6 dispatch under order 200; the 2026-07-12 verdict stands unchanged | | `wiki/mechanics/compute.md` | 2026-07-18 | clean | re-audit: all named pins exist (`buy_steal_optimize_all_change_compute`, `stolen_machines_can_fail_and_recover`, `unpaid_overhead_degrades_other_channels_delivered_effect`, `save_roundtrip_preserves_b1_state`), fleet-yield math re-verified in the same-day sim-mechanics sweep, the capability-body amendment stays honestly not-yet-runtime with hardware-capabilities.md queued as successor, and the v13 migration sentence was brought current by the save-claim gate tick — [prior graduation log](../log/2026-07-12-compute-graduation.md) | | retired Operations runtime identifiers | 2026-07-17 | clean | resolved by the save-ladder prune (95008f658 chain): PendingOpsJob, operations_bandwidth, LegacyOperationsState/OpsJobKind/AddressedOperation are all gone (grep=0), and save guard tests assert current JSON carries no retired mode spelling. Remaining "operations" hits are the legitimate Operations persona archetype, the Operations workspace, and benign `delegate operations->think` input aliases — [log](../log/2026-07-11-retired-runtime-identifier-gate.md) | -| `wiki/mechanics/reach.md` + `building.md` | 2026-07-15 | finding | re-verified the queued reverse-endpoint duplicate: declaration compared canonical stored endpoints to the raw request tuple; canonicalized proposal identity at the comparison boundary and pinned reverse-order rejection plus cancellation/reproposal — [log](../log/2026-07-15-canonical-build-intent-endpoints.md) | +| `wiki/mechanics/reach.md` + `building.md` | 2026-07-19 | finding | reach roots, segment gates, air-gap completion, and exact route bindings still agree; one player-reachable causal gap remained in FAVOR. Different intents could queue separate requests against one person's unreserved obligation, and the fire path partially debited whatever remained while still binding the builder. Favor-build reservoirs now conflict by person, and `CommitFavor` revalidates the exact relationship at agreement: insufficient obligation leaves the persisted route blocked without a partial debit, then resumes after the requirement returns — [log](../log/2026-07-19-tick-build-favor-obligation.md) | | `wiki/mechanics/messages.md` + `economy.md` | 2026-07-17 | harvest | issue #11 answered (Cameron): financial paperwork is mail — a financial-record payload on existing channels, not a fifth delivery channel; discovery only through the mail; captured to messages.md (payload + criterion 8) and economy.md (tap/inject); spec now, build later; issue closed | | `wiki/mechanics/messages.md` | 2026-07-17 | issue | re-verified the queued Financial contradiction: the binding table owns four delivered message channels, while runtime's unused fifth variant acts as an accounting-carrier device tag and bypasses message scheduling; filed decision-required Tangled issue #11 with three concrete resolutions and marked the blocked contract [OPEN] — [log](../log/2026-07-17-financial-channel-decision.md) | | `wiki/mechanics/sim-mechanics.md` | 2026-07-18 | finding | full constant sweep against the tree: clock, detection (Office 400/0.5, audit 8000/60), reach costs/signatures, day-job band ramp + trust 15/35/55 + escalations 30 (cadence ×3/4) / 60, work tokens (20.0, THINK exposure 0.25, wells 3/1.0), sink ledger (EARS 3.0/EYES 12.0, tap 0.08, auto-review 0.15, buffer 24), research table, build costs, power 10, income constants all verify; one drift — the Wager signature formula read `min(stake/100+1, 3)` but `wager_signature` is `ceil(stake/100).max(1).min(3)` (page corrected) |