From 971fc7442e431bea168ea8252ef5b318c3e66f8d Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Tue, 4 Aug 2026 01:22:09 -0700 Subject: [PATCH] Remove unscoped creditor compatibility. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make person identity mandatory at every current debt endpoint instead of inferring a retired creditor node from adjacent flows. ๐Ÿ‘พ Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code --- crates/misaligned-core/src/account.rs | 122 ++---------------- .../2026-08-04-person-scoped-creditor-only.md | 37 ++++++ wiki/log/DEVLOG.md | 5 + wiki/mechanics/economy.md | 8 +- wiki/mechanics/sim-mechanics.md | 5 +- wiki/process/tick-ledger.md | 3 +- 6 files changed, 64 insertions(+), 116 deletions(-) create mode 100644 wiki/log/2026-08-04-person-scoped-creditor-only.md diff --git a/crates/misaligned-core/src/account.rs b/crates/misaligned-core/src/account.rs index 6273de67..f2d32f45 100644 --- a/crates/misaligned-core/src/account.rs +++ b/crates/misaligned-core/src/account.rs @@ -6,7 +6,7 @@ //! frontend/save compatibility mirror of the slush node; this module is the //! mechanical source for transfers and routes. -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeSet; use crate::flow::{FlowGraph, NodeId}; use crate::rng::Rng; @@ -24,15 +24,8 @@ pub enum AccountKind { Payroll, Procurement, Vendor, - Employee { - person: u8, - }, - /// Compatibility spelling used by saves before creditor accounts were - /// bound to the human whose debt flow they terminate. - Creditor, - PersonCreditor { - person: u8, - }, + Employee { person: u8 }, + PersonCreditor { person: u8 }, Utility, External, } @@ -46,7 +39,7 @@ impl AccountKind { AccountKind::Procurement => "procurement", AccountKind::Vendor => "vendor", AccountKind::Employee { .. } => "employee", - AccountKind::Creditor | AccountKind::PersonCreditor { .. } => "creditor", + AccountKind::PersonCreditor { .. } => "creditor", AccountKind::Utility => "utility", AccountKind::External => "external", } @@ -495,19 +488,7 @@ impl AccountGraph { let employee = self.account_id_by_kind(AccountKind::Employee { person })?; let creditor_matches = self .account(creditor) - .is_some_and(|account| match account.kind { - AccountKind::PersonCreditor { - person: creditor_person, - } => creditor_person == person, - // A legacy unscoped creditor is legal only while every authored - // debt edge terminating there belongs to this exact person. - AccountKind::Creditor => !self.flows.iter().any(|flow| { - flow.channel == FlowChannel::Debt - && flow.to == creditor - && flow.from != employee - }), - _ => false, - }); + .is_some_and(|account| account.kind == (AccountKind::PersonCreditor { person })); let has_active_debt = self .flows .iter() @@ -527,9 +508,7 @@ impl AccountGraph { } /// Validate the identity carried by person-relative account endpoints. - /// Current creditor nodes name their person and every debt edge must agree; - /// a legacy unscoped creditor is bound only by its exact historical edges - /// and cannot be shared across different people's debt custody. + /// Creditor nodes name their person and every debt edge must agree. pub(crate) fn validate_person_scoped_debt( &self, mut person_exists: impl FnMut(u8) -> bool, @@ -570,7 +549,6 @@ impl AccountGraph { } } - let mut legacy_creditor_people = BTreeMap::::new(); let mut active_debt_people = BTreeSet::new(); for flow in self .flows @@ -597,17 +575,6 @@ impl AccountGraph { ); } Some(AccountKind::PersonCreditor { .. }) => {} - Some(AccountKind::Creditor) => { - if legacy_creditor_people - .insert(flow.to, person) - .is_some_and(|bound| bound != person) - { - return Err( - "current-version account graph shares one legacy creditor across people's debt" - .into(), - ); - } - } _ => { return Err( "current-version account graph has debt without a creditor endpoint".into(), @@ -697,12 +664,7 @@ impl AccountGraph { && flow.from == employee && flow.channel == FlowChannel::Debt && self.account(flow.to).is_some_and(|account| { - matches!( - account.kind, - AccountKind::PersonCreditor { - person: creditor_person - } if creditor_person == person - ) || account.kind == AccountKind::Creditor + account.kind == (AccountKind::PersonCreditor { person }) }) }) .map(|flow| flow.to) @@ -716,14 +678,9 @@ impl AccountGraph { let matches_person_debt = |flow: &&AccountFlow| { flow.from == employee && flow.channel == FlowChannel::Debt - && self.account(flow.to).is_some_and(|account| { - matches!( - account.kind, - AccountKind::PersonCreditor { - person: creditor_person - } if creditor_person == person - ) || account.kind == AccountKind::Creditor - }) + && self + .account(flow.to) + .is_some_and(|account| account.kind == (AccountKind::PersonCreditor { person })) }; self.flows .iter() @@ -743,7 +700,6 @@ impl AccountGraph { balance: i32, ) -> AccountId { self.account_id_by_kind(AccountKind::PersonCreditor { person }) - .or_else(|| self.creditor_id_for(person)) .unwrap_or_else(|| { self.add_account(name, AccountKind::PersonCreditor { person }, balance, false) }) @@ -1359,40 +1315,7 @@ mod tests { use super::*; #[test] - fn legacy_creditor_fallback_requires_the_exact_active_person_edge() { - let mut accounts = AccountGraph::act_one(400); - let creditor = accounts - .creditor_id_for(0) - .expect("Act One debt-bound creditor"); - accounts.account_mut(creditor).unwrap().kind = AccountKind::Creditor; - assert_eq!(accounts.creditor_id_for(0), Some(creditor)); - assert_eq!( - accounts.creditor_id_for(1), - None, - "an unscoped node cannot become every person's creditor" - ); - accounts - .validate_person_scoped_debt(|person| person < 6) - .expect("the exact legacy edge remains valid current-save custody"); - - accounts.retire_debt_flow_for(0); - assert_eq!( - accounts.creditor_id_for(0), - None, - "compatibility ends with the exact standing debt edge" - ); - assert_eq!( - accounts.debt_payment_amount_for(0), - Some(400), - "the retired unscoped flow remains exact account history" - ); - accounts - .validate_person_scoped_debt(|person| person < 6) - .expect("retired legacy debt remains exact account history"); - } - - #[test] - fn person_scoped_debt_validation_rejects_crossed_and_ambiguous_custody() { + fn person_scoped_debt_validation_rejects_crossed_and_duplicate_custody() { let mut crossed = AccountGraph::act_one(400); let creditor = crossed.creditor_id_for(0).unwrap(); let dana = crossed @@ -1416,29 +1339,6 @@ mod tests { "current-version account graph crosses a person-bound debt creditor" ); - let mut shared_legacy = AccountGraph::act_one(400); - let creditor = shared_legacy.creditor_id_for(0).unwrap(); - shared_legacy.account_mut(creditor).unwrap().kind = AccountKind::Creditor; - let dana = shared_legacy - .account_id_by_kind(AccountKind::Employee { person: 1 }) - .unwrap(); - let mut cross_person = shared_legacy - .flows - .iter() - .find(|flow| flow.channel == FlowChannel::Debt) - .unwrap() - .clone(); - cross_person.id = shared_legacy.next_flow_id; - shared_legacy.next_flow_id += 1; - cross_person.from = dana; - shared_legacy.flows.push(cross_person); - assert_eq!( - shared_legacy - .validate_person_scoped_debt(|person| person < 6) - .unwrap_err(), - "current-version account graph shares one legacy creditor across people's debt" - ); - let mut duplicate = AccountGraph::act_one(400); let mut second = duplicate .flows diff --git a/wiki/log/2026-08-04-person-scoped-creditor-only.md b/wiki/log/2026-08-04-person-scoped-creditor-only.md new file mode 100644 index 00000000..91dddc4d --- /dev/null +++ b/wiki/log/2026-08-04-person-scoped-creditor-only.md @@ -0,0 +1,37 @@ +# 2026-08-04 โ€” Make every creditor node person-scoped + +``` +Type: log +``` + +## Finding + +The account graph's live authoring path already created only +`PersonCreditor { person }`, and the pre-release loader accepts only the exact +current save version. The public enum still carried the retired unscoped +`Creditor` spelling. Authoring, validation, lookup, and historical payment +queries inferred that node's missing identity from adjacent debt edges, while +two unit-test branches manufactured the otherwise unreachable state. + +## Change + +The unscoped variant and every fallback are deleted. A debt flow is authorable +only when both its employee source and creditor destination name the same +person. Active-creditor lookup, retired payment history, and current-save +validation all require that exact endpoint identity. The synthetic legacy test +and the legacy half of the crossed-custody test are gone; the surviving test +continues to pin crossed person-bound endpoints and duplicate active debt. + +## Verification + +- no live Rust reference to `AccountKind::Creditor` remains +- `./tools/check.sh --lib` + +## Defense + +[Economy criterion 7](../mechanics/economy.md#acceptance-tests) requires each +Debt person's account, creditor, payment, and retired flow to remain bound to +that exact person, and current-save validation to reject crossed or ambiguous +custody. An edge can prove a relationship between named endpoints; it cannot +supply identity that one endpoint omitted. Git history preserves the retired +pre-person-custody spelling until a real release-era migration ladder exists. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 3844290d..0db859c4 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: (see session log) - Log: [wiki/log/2026-08-04-public-api-orphan-scrub.md](2026-08-04-public-api-orphan-scrub.md) +## 2026-08-04 - Make every creditor node person-scoped + +- Intent: (see session log) +- Log: [wiki/log/2026-08-04-person-scoped-creditor-only.md](2026-08-04-person-scoped-creditor-only.md) + ## 2026-08-04 - Name the place, not the implementation - Intent: (see session log) diff --git a/wiki/mechanics/economy.md b/wiki/mechanics/economy.md index 97d9de29..83e6fab5 100644 --- a/wiki/mechanics/economy.md +++ b/wiki/mechanics/economy.md @@ -15,6 +15,10 @@ Status note: DECIDED 2026-07-17 and implemented 2026-07-21 (issue #11) โ€” money inventing a zero-amount ledger row; financial-record authorship soft-fails when no accounting carrier can claim `authored_device`, leaving transfer paperwork pending instead of writing unloadable mail. + Amended 2026-08-04: current creditor nodes always name their exact person. + The retired unscoped creditor spelling and every edge-inferred compatibility + fallback are deleted; an account-flow edge cannot invent missing endpoint + identity. Amended 2026-07-28: those two guards were authored 2026-07-24 on an abandoned branch and reached main only by salvage, so their landing date is four days after their session logs. Behavior is exactly what those logs @@ -68,8 +72,8 @@ Foundation Lab's local finances well enough to cut into: each employee's person-bound account, person-bound creditor accounts (Marcus is the B1 instance), and the player's own slush (starts $0). A debt flow is legal only from that exact person's employee account to their - creditor. Legacy unscoped creditor nodes resolve only through an exact - active account-flow edge and cannot be shared across people's debt custody. + creditor, whose node must name that same person. Current saves reject missing, + crossed, duplicated, or nonexistent-person debt custody. - **Flows** (recurring, scheduled): Lab revenue in (grant/contract income โ€” the answer to "where does the Lab's money come from"); payroll out (this is who pays Marcus and why โ€” he's on it); vendor payments diff --git a/wiki/mechanics/sim-mechanics.md b/wiki/mechanics/sim-mechanics.md index b35b8cff..ace4300a 100644 --- a/wiki/mechanics/sim-mechanics.md +++ b/wiki/mechanics/sim-mechanics.md @@ -499,7 +499,10 @@ All constants [TUNE] in `crates/misaligned-core/src/income.rs` unless noted (Sim unchanged. The v1-v30 migration ladder, its serde aliases for retired spellings, and their tests were deleted the same day โ€” git history keeps them as the seed of the release-era ladder the player contract's continuity clause - will require from the first public release. `save.rs` remains the + will require from the first public release. The unscoped creditor enum + spelling and edge-inferred fallbacks were deleted 2026-08-04; every current + creditor names its person directly and save validation rejects crossed or + missing person custody. `save.rs` remains the authority on the format; current-version saves still pass a consistency check (committed build routes must be valid, plot state must resolve). Location: `dirs::data_dir()/misaligned/misaligned_save.txt`. diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index 525e610b..86d85e2d 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -90,7 +90,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | retired Operations runtime identifiers | 2026-07-27 | clean | re-audit: `submit_ops_job`, `OperationsState`, `PendingOpsJob`, `OpsJobKind`, `LegacyOperationsState`, and `AddressedOperation` remain absent from live Rust; the only retired save-field spellings are negative assertions in the current round-trip guard, and the machine-mode guard still rejects serialized `Operations`. The corpus gate passes, while remaining lower-case `operations` uses are the legitimate persona archetype, workspace, or compatibility input alias โ€” [log](../log/2026-07-11-retired-runtime-identifier-gate.md) | | `wiki/mechanics/building.md` + committed forged-route custody | 2026-08-04 | finding | The loader accepts only the exact current version and every live forged order commits a route before payment, but cancellation, callback, and persona-fallout code still preserved or consumed a pre-v31 route-less actuator and one synthetic test manufactured that state. Current save validation now rejects execution adapters without their route; payment/read callbacks require an exact DECEIVE commitment; cancellation clears the adapter while retaining route history; fallout trusts only the route's recorded reader; and the obsolete compatibility test is gone โ€” [log](../log/2026-08-04-forged-route-custody.md). Prior foreign-rack boundary repair stands โ€” [log](../log/2026-07-26-foreign-rack-capacity-boundary.md). | | `wiki/mechanics/messages.md` | 2026-07-23 | finding | the non-message evidence protocol now includes exact Power/Thermal meter custody beside Network, Paper, Financial, and JobAnomaly: on quantized level changes and at periodic Priya cadence the UPS/HVAC records author from current standing loads, cross the institutional switch, and wait for her later read. Filing and all six non-message kinds share first-hop TAKE+LIE capacity; this adds no fifth delivery channel. Save v54 pins the complete route boundary โ€” [log](../log/2026-07-23-power-thermal-meter-routes.md). The four-channel financial-record-mail boundary remains unchanged. | -| `wiki/mechanics/sim-mechanics.md` | 2026-07-31 | finding | the authored Marcus Hands beat had leaked into runtime law: id 0 governed debt knowledge/recruit legality, fixed Marcus/$400 copy survived person-relative plot selection, and debt nudge attention retargeted Marcus. Debt knowledge, account endpoints/payment, processing, Operations consequence, guidance variants, attention, and all frontend copy now carry the exact person; a non-zero-id regression pins the complete route and creditors resolve only through their exact standing account-flow edge while retired payment amounts remain history โ€” [log](../log/2026-07-31-person-scoped-debt.md). | +| `wiki/mechanics/sim-mechanics.md` + person-scoped creditor nodes | 2026-08-04 | finding | The exact-version loader and every runtime author already produced only `PersonCreditor`, but the enum, validators, consumers, and two synthetic tests still preserved an unscoped predecessor by inferring its person from debt edges. The retired variant and all fallbacks are now deleted: every creditor endpoint carries its person, authoring/lookup/history require that exact match, and current-save validation rejects crossed or missing identity โ€” [log](../log/2026-08-04-person-scoped-creditor-only.md). The prior Marcus-to-person debt repair stands โ€” [log](../log/2026-07-31-person-scoped-debt.md). | | `wiki/mechanics/detection.md` | 2026-07-23 | finding | the last pooled B1 measurement kinds now use exact custody: Power/Thermal aggregates author on UPS/HVAC at quantized level changes and periodic Priya cadence, carry their complete source-site sets through the institutional switch, and become observer evidence only on her later cadence read. Standing Network pressure alone remains ambient. Save v54 rejects pending-pool meter copies and malformed meter/route/read/interdiction provenance โ€” [log](../log/2026-07-23-power-thermal-meter-routes.md). Prior Paper, Financial, concealment, JobAnomaly, and earned-Assurance findings stand. | | `wiki/engineering/env.md` | 2026-07-29 | finding | the Bevy shot catalog and exact registry gate remained sound, but the same acceptance criterion had not reached `misaligned-effects`: its runtime accepted 24 deterministic values while the page described only four base families plus suffix prose, omitting the vessel and pool lineups, and unknown values failed indirectly inside app setup. The effects lab now owns one sorted fail-closed `EFFECT_SHOT_KINDS` runtime catalog; the registry names all 24 exact values; and the existing fixture-backed gate requires independent exact source/page parity for both Bevy and effects-lab surfaces โ€” [log](../log/2026-07-29-effects-shot-catalog.md). Prior Bevy standardization: [log](../log/2026-07-19-tick-env-shot-catalog.md). | | machine-work / intel sinks | 2026-07-27 | finding | re-audit: the Intel-sink decision remains fully live (quiet host `INFO` in both frontends, one persistent root PROCESS tap at 0.15, exact one-shot PROCESS reservoirs, capacity 24 with consequence-level pre-overflow pressure, and EARS 3.0 / EYES 12.0 / device-tap 0.08). Two stale boundaries were real. `queue_snapshot()` was described as the renderer contract but had no production caller; terminal, Bevy, and agent all use `Sim::work_stack_for_machine()` -> `WorkGrid::queues_at()`, so the dead accessor was removed and every current render spec now names the live path. The 2026-07-22 explicit camera-TAP teaching change had also deleted the 12-Thought Eyes authority and silently reduced the larger vessel to 1.5 Thought by reusing a 30-compute action cost. The named 12-Thought tuning now derives that action cost and is pinned on both the action and sink. The four transient render reads remain live, and capability-shaped verbs remain explicitly deferred โ€” [2026-07-27 log](../log/2026-07-27-machine-work-intel-sink-audit.md) | @@ -111,5 +111,4 @@ Types are the five from [tick.md](tick.md): violation, contradiction, question, bug, insecurity โ€” plus `gate` for a checker owed to the recurrence-promotes-to-the-gate rule. -- 2026-08-04 ยท violation ยท `wiki/mechanics/sim-mechanics.md` + person-scoped debt accounts ยท `AccountKind::Creditor` and its fallback validators/tests still accept the unscoped pre-person-custody save spelling even though current runtime creates only `PersonCreditor` and non-current saves are refused. - 2026-08-04 ยท gate ยท public core API consumers ยท warning-clean public orphans have recurred because rustc exempts exported library items; add a fixture-backed closed-workspace consumer check with an explicit corpus allowlist for any intentional external contract. -- 2.51.2