diff --git a/crates/misaligned-core/src/actions.rs b/crates/misaligned-core/src/actions.rs index 5569d86e..25d650cd 100644 --- a/crates/misaligned-core/src/actions.rs +++ b/crates/misaligned-core/src/actions.rs @@ -1934,8 +1934,13 @@ impl Sim { disabled_reason: None, automate: None, }); - // FAVOR: willing assets who can reach both ends. + // FAVOR / DECEIVE: earned people who can reach both ends. + // Filtering before `person_label` prevents even a role silhouette + // from leaking an unknown candidate (building.md criterion 9). for p in &self.people.people { + if !self.person_is_earned(p.id) { + continue; + } let room_a = self.world.map().room_at( self.reach.device(a).map(|d| d.x).unwrap_or(0), self.reach.device(a).map(|d| d.y).unwrap_or(0), @@ -2225,10 +2230,7 @@ impl Sim { let Some(p) = self.people.get(id) else { return Vec::new(); }; - let earned = self.can_see_person(id) - || p.knowledge != Knowledge::Unknown - || self.latest_intel_for_person(id).is_some(); - if !earned { + if !self.person_is_earned(id) { return Vec::new(); } let mut out = Vec::new(); @@ -3230,6 +3232,58 @@ mod tests { ); } + /// building.md criterion 9: build candidates use the same earned-person + /// gate as Operations PEOPLE. Unknown people are absent rather than + /// exposed as role silhouettes, and opaque pooled recordings do not earn + /// them indirectly. + #[test] + fn build_actuator_rows_require_an_earned_person() { + let mut s = sim(); + let sw = switch(&s); + let island = s.reach.device_named("old storage server").unwrap().id; + s.reach.device_mut(island).unwrap().known = true; + s.declare_link_intent(sw, island).expect("declare"); + + let actuator_people = |sim: &Sim| { + sim.available_actions(Anchor::Device(sw)) + .into_iter() + .filter_map(|action| match action.command { + ActionCommand::FavorBuild { person, .. } + | ActionCommand::ForgeWorkOrder { person, .. } => Some(person), + _ => None, + }) + .collect::>() + }; + + assert!( + actuator_people(&s).is_empty(), + "fresh intent exposes no unearned person candidates" + ); + + s.intel_buffer.push(crate::intel::RawIntelEvent { + id: 90_002, + tick: s.tick, + feed: "test recorder".into(), + room: Some("Server Room".into()), + x: 0, + y: 0, + person: Some(0), + kind: RawIntelKind::Presence { entered: true }, + }); + assert!( + actuator_people(&s).is_empty(), + "an opaque pooled recording does not earn a build candidate" + ); + + s.people.people[0].knowledge = Knowledge::Schedule; + let people = actuator_people(&s); + assert_eq!( + people, + vec![0, 0], + "the one earned compatible person supplies FAVOR and DECEIVE only" + ); + } + /// PEOPLE owns relationship acts, never identity creation. The three /// immutable protocol rows in PERSONAS are the only creation surface. #[test] diff --git a/crates/misaligned-core/src/operations_projection.rs b/crates/misaligned-core/src/operations_projection.rs index 08f4e3bd..e7ae4ed8 100644 --- a/crates/misaligned-core/src/operations_projection.rs +++ b/crates/misaligned-core/src/operations_projection.rs @@ -1168,15 +1168,6 @@ impl Sim { }) } - fn person_is_earned(&self, id: u8) -> bool { - let Some(p) = self.people.get(id) else { - return false; - }; - self.can_see_person(id) - || p.knowledge != Knowledge::Unknown - || self.has_intel_for_person(id) - } - fn person_dossier(&self, id: u8) -> OperationsObject { let p = self.people.get(id).expect("earned person exists"); let name = self.person_label(id); diff --git a/crates/misaligned-core/src/sim/perception.rs b/crates/misaligned-core/src/sim/perception.rs index f71f01f2..70134c26 100644 --- a/crates/misaligned-core/src/sim/perception.rs +++ b/crates/misaligned-core/src/sim/perception.rs @@ -638,6 +638,19 @@ impl Sim { self.sense_covers_person(id, false) } + /// Whether the player has earned a person as a candidate for social or + /// physical action surfaces. An opaque pooled recording is deliberately + /// absent: it earns the host's REVIEW row, not the recorded person's + /// catalog (context-menu.md epistemic honesty). + pub(crate) fn person_is_earned(&self, id: u8) -> bool { + let Some(person) = self.people.get(id) else { + return false; + }; + self.can_see_person(id) + || person.knowledge != Knowledge::Unknown + || self.has_intel_for_person(id) + } + /// Player-facing identity for a person, gated by staged social knowledge /// (wiki/interface/presence.md; cursor.md inspect staging). /// Until `Knowledge::Schedule`, returns a role-shaped silhouette — never diff --git a/wiki/log/2026-07-14-earned-actuator-gate.md b/wiki/log/2026-07-14-earned-actuator-gate.md new file mode 100644 index 00000000..45b28a2b --- /dev/null +++ b/wiki/log/2026-07-14-earned-actuator-gate.md @@ -0,0 +1,47 @@ +# 2026-07-14 — Build actuators require earned people + +``` +Type: log +``` + +## Intent + +Recover the still-valid finding from the abandoned `earned-actuators` +worktree without reviving its obsolete pre-workspace implementation: a pinned +network-link intent must not enumerate the whole unearned cast as disabled +FAVOR and DECEIVE candidates. + +## Finding + +Current `build_actions_on_device` still iterated every authored person who +could physically access both link endpoints, then called `person_label` and +emitted bound action descriptors. At tick zero this exposed role silhouettes +for people the player had never seen, identified, or processed. Operations +PEOPLE already applied the correct epistemic gate, so the two action surfaces +disagreed. + +## Change + +- Moved the shared earned-person predicate into the perception island: current + sight, staged social knowledge, or processed person intel. +- Filtered network-link FAVOR and DECEIVE candidates before their label or + action descriptor is constructed. +- Kept pooled recordings opaque. A raw record continues to earn one REVIEW row + on the host and does not reveal a person through the build surface. +- Added one regression covering the fresh intent, raw-recording, and exactly + one earned-person states. The old branch's obsolete ROBOT-BUILD expectation + was deliberately not carried forward. + +## Defense + +This implements `building.md` criterion 9 and the context-menu epistemic- +honesty clause: unknown possibilities remain absent from both human and agent +surfaces, while known people may remain visible with exact blockers. It adds no +new action, resource, route, or frontend rule. + +## Checks + +- `cargo fmt --all -- --check` +- focused `build_actuator_rows_require_an_earned_person` +- `./tools/check.sh --lib` +- `./tools/check.sh --land` diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 1e20672b..a17f268f 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -41,6 +41,11 @@ add or amend a session log, then re-run the generator. - Intent: Finish the self-similar Intel amendment without disguising an exact-event ledger behind collapsed frontend rows. Routine evidence must become bounded simulation/save state; repeated intake must become visible standing policy; and Operations must spend attention on exceptions r... - Log: [wiki/log/2026-07-14-recursive-intel-custody.md](2026-07-14-recursive-intel-custody.md) +## 2026-07-14 - Build actuators require earned people + +- Intent: Recover the still-valid finding from the abandoned `earned-actuators` worktree without reviving its obsolete pre-workspace implementation: a pinned network-link intent must not enumerate the whole unearned cast as disabled FAVOR and DECEIVE candidates. +- Log: [wiki/log/2026-07-14-earned-actuator-gate.md](2026-07-14-earned-actuator-gate.md) + ## 2026-07-14 - current-build.md line count refreshed - Intent: Fourth tick of the session: fresh audit of the never-audited `wiki/engineering/current-build.md` knowledge page. diff --git a/wiki/mechanics/building.md b/wiki/mechanics/building.md index 69f69d08..9a94e033 100644 --- a/wiki/mechanics/building.md +++ b/wiki/mechanics/building.md @@ -302,6 +302,11 @@ people, source-owner verbs for procurement/repurposing. “Favor-build” and blockers on the row's rack actions. Acquisition fails until all three, two local owned machines, and local LIE are present; it saves/loads and leaves every foreign rack outside the player fleet. +9. Baseline network-link actuator rows expose FAVOR and DECEIVE candidates + only for earned people: current sight, staged social knowledge, or processed + person intel. Filtering happens before even a role silhouette is named. An + opaque pooled recording alone still earns only the host REVIEW row and + reveals no person actuator (test: `build_actuator_rows_require_an_earned_person`). ### READY causal route composer delta