Refuse a genesis self-retiring human, a retired promisor, and a retired institution's lent authority (PA.f36-f38)

PA.f36: the resolver tracked a freshly declared mirror in `mirrors` but never
tracked a freshly declared human, so a genesis patch that declared and retired
the human in the same patch was admitted; only the apply-time re-check caught
it, after `open_owner` had already committed an empty world.cc. The resolver
now tracks a `humans` candidate set the same way, seeded from the committed
controller assignment and extended for a subject this patch declares as
`NewController::Human`, and refuses its retirement in Draft with
`RetiresAnApprover`.

PA.f37: a retired subject could still act through the clock and through
commitments. `CreateCommitment` now refuses a retired promisor with
`RetiredSubjectActed`, and `derive_motion` now skips a retired subject's
commitments and dependencies entirely, so its routines stop rolling and its
obligations, goals, and dependencies stop pressing. Nothing is discharged or
deleted; a retired subject's commitments stay on record exactly as they were
the moment it retired.

PA.f38: `delegated_authority` read a retired institution's grants, so a live
office-holder kept a dissolved institution's authority. `Retire` does not
vacate offices — the incumbency stays on the ledger — but a retired
institution now lends nothing: `delegated_authority` skips a retired
institution's offices at the one site that turns an office into effective
authority.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-22 07:53:53 +02:00
commit bc1852dfef
3 changed files with 280 additions and 8 deletions

View file

@ -7,7 +7,7 @@
//! the span.
use super::patch::{Commitment, CommitmentKey, CommitmentKind, DependencyTarget, MAX_ROUTE_COST};
use super::{PressureMagnitude, PressureSource, SubjectId, WorldState, action};
use super::{PressureMagnitude, PressureSource, SubjectId, WorldState, action, is_retired};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
@ -103,6 +103,13 @@ pub(super) fn derive_motion(state: &WorldState, to: FictionalMinutes) -> Motion
let mut pressed: BTreeMap<(SubjectId, PressureSource), u32> = BTreeMap::new();
for (subject, held) in &state.commitments {
// A retired subject cannot act, and neither fulfilling a routine nor
// pressing an obligation or goal is anything but that subject acting:
// its commitments stay in history, un-discharged and un-deleted, but
// the clock stops moving them the moment they are its.
if is_retired(state, *subject) {
continue;
}
for (key, commitment) in held {
if commitment.due > to {
continue;
@ -134,6 +141,9 @@ pub(super) fn derive_motion(state: &WorldState, to: FictionalMinutes) -> Motion
}
for (subject, targets) in &state.dependencies {
if is_retired(state, *subject) {
continue;
}
for target in targets {
if dependency_unavailable(state, *subject, *target) {
let source = PressureSource::Dependency(*target);

View file

@ -3464,6 +3464,13 @@ fn delegated_authority(
let mut held: BTreeMap<SubjectId, BTreeMap<OfficeName, BTreeSet<AuthorityGrant>>> =
BTreeMap::new();
for (institution, offices) in &state.selection {
// A retired institution lends nothing: `Retire` does not vacate
// offices, so the incumbency stays on record, but a dissolved
// institution's grants stop reaching its office-holders here, the one
// site that turns an office into effective authority.
if is_retired(state, *institution) {
continue;
}
for (name, office) in offices {
if office.incumbent != Some(subject_id) {
continue;
@ -8515,6 +8522,49 @@ mod tests {
"the key covers the door, but the door's far side is still a draft"
);
}
/// PA.f38: a retired institution lends nothing. `Retire` does not vacate
/// the office the reeve holds — the incumbency stays on the ledger — but
/// `delegated_authority` stops reading a dissolved institution's grants,
/// the one site that turns an office into effective authority.
#[test]
fn a_retired_institution_lends_no_authority_to_its_office_holder() {
let directory = tempfile::tempdir().unwrap();
let mut kernel = crate::custody_tests::custody_kernel(directory.path(), "RetiredLends");
let (_, civic, before) = civic_world(&mut kernel);
let over_hall = AuthorityGrant {
kind: authority_kind(LEVY_KIND),
over: AuthorityTarget::PlaceSubtree(civic.hall),
};
assert!(
subject_authority(&kernel.state, civic.reeve).contains(&over_hall),
"the reeve holds levy only through the warden office before retirement"
);
submit_owner(
&mut kernel,
&before,
operations(vec![ComponentOp::Retire {
subject: Ref::Existing(civic.treasury),
}]),
);
let warden = kernel
.state
.selection
.get(&civic.treasury)
.and_then(|offices| offices.get(&office(WARDEN_OFFICE)))
.expect("retire did not vacate the office");
assert_eq!(
warden.incumbent,
Some(civic.reeve),
"the reeve still holds the office after the institution retires"
);
assert!(
!subject_authority(&kernel.state, civic.reeve).contains(&over_hall),
"a retired institution still lent authority through its office"
);
}
}
#[cfg(test)]
@ -18179,4 +18229,193 @@ mod clock_tests {
);
assert_eq!(error, vec![Mismatch::NoOperationEffect { operation: 1 }]);
}
// ---- Retirement fix batch (PA.f36-PA.f38) ------------------------------
/// PA.f36: the resolver refuses a genesis patch that declares a
/// Human-controlled subject and retires it in the same patch. The
/// resolver's `humans` candidate set tracks a freshly declared human the
/// same way `mirrors` tracks a freshly declared mirror, so this is caught
/// before `open_owner` ever commits an empty `world.cc` for a human who
/// never approved anything.
#[test]
fn genesis_cannot_declare_and_retire_the_human_in_one_patch() {
let directory = tempfile::tempdir().unwrap();
let mut genesis = creation(CommandId::new(), "PAf36GenesisRetire");
genesis.patch.operations.push(ComponentOp::Retire {
subject: Ref::Draft(DraftHandle::new("human")),
});
let Err(error) = WorldKernel::create(
directory.path().join("world.cc"),
genesis,
&auth_principal(owner()),
) else {
panic!("genesis admitted a patch that declares and retires the human");
};
assert!(
matches!(&error, KernelError::PatchRejected(set)
if set.contains(&Mismatch::RetiresAnApprover { operation: 0 })),
"{error:?}"
);
}
/// PA.f36: pins the apply-time re-check directly, beside the resolver's
/// refusal above, since resolution is speculative and this arm is the
/// actual commit. If the resolver's `humans` tracking were ever disabled
/// or bypassed, this is the arm that must still refuse removing a Draft
/// approver.
#[test]
fn apply_refuses_removing_a_draft_approver_directly() {
let directory = tempfile::tempdir().unwrap();
let kernel = WorldKernel::create(
directory.path().join("world.cc"),
creation(CommandId::new(), "PAf36ApplyArm"),
&auth_principal(owner()),
)
.expect("a created world")
.0;
let human = human_subject(&kernel.snapshot().unwrap());
let mut state = kernel.state.clone();
assert!(
matches!(
apply_operation(&mut state, &ResolvedOp::Retire { subject: human }, &[])
.unwrap_err(),
KernelError::Invariant(message)
if message == "retire operation would remove a Draft approver"
),
"the apply-time arm no longer refuses removing the Draft human"
);
}
/// PA.f37: a retired subject cannot become a promisor. `CreateCommitment`
/// refuses it with `RetiredSubjectActed`, the same mismatch a retired
/// office incumbent draws.
#[test]
fn a_retired_subject_cannot_be_a_promisor() {
let directory = tempfile::tempdir().unwrap();
let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "PAf37Promisor");
owner_retires(&mut kernel, clockwork.farmer).expect("retire");
let before = kernel.snapshot().unwrap();
let error = reject_owner(
&mut kernel,
&before,
operations(vec![ComponentOp::CreateCommitment {
subject: Ref::Existing(clockwork.farmer),
counterparty: None,
kind: CommitmentKind::Obligation,
due: FictionalMinutes(LATE_DUE + 1),
period: None,
checks: Vec::new(),
statement: Statement::new("What was promised, as the promisor would say it.")
.unwrap(),
}]),
);
assert_eq!(error, vec![Mismatch::RetiredSubjectActed { operation: 0 }]);
}
/// PA.f37: once a subject is retired, the clock stops moving what it
/// holds — its routine no longer rolls and its obligation no longer
/// presses — while a live subject's goal and dependency still move on the
/// same tick. Nothing is discharged or deleted; the retired subject's
/// commitments stay on record, un-fulfilled and un-pressed, exactly as
/// they were the moment it retired.
#[test]
fn after_retirement_a_tick_moves_none_of_the_retired_subjects_commitments() {
let directory = tempfile::tempdir().unwrap();
let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "PAf37Frozen");
let snapshot = kernel.snapshot().unwrap();
submit_owner(
&mut kernel,
&snapshot,
operations(vec![ComponentOp::CloseRoute {
route: Ref::Existing(clockwork.gate),
}]),
);
owner_retires(&mut kernel, clockwork.farmer).expect("retire the farmer");
let routine_before = due(&kernel, clockwork.farmer, clockwork.routine);
let dependency_source = PressureSource::Dependency(DependencyTarget::Route(clockwork.gate));
tick(&mut kernel, ROUTINE_DUE as u32 + LATE_DUE as u32).expect("the tick commits");
assert_eq!(
due(&kernel, clockwork.farmer, clockwork.routine),
routine_before,
"a retired subject's routine still rolled"
);
assert_eq!(
pressure(
&kernel,
clockwork.farmer,
PressureSource::Commitment {
subject: clockwork.farmer,
key: clockwork.obligation,
}
),
0,
"a retired subject's obligation still pressed"
);
assert_eq!(
pressure(
&kernel,
clockwork.reeve,
PressureSource::Commitment {
subject: clockwork.reeve,
key: clockwork.goal,
}
),
1,
"the live subject's goal did not press"
);
assert_eq!(
pressure(&kernel, clockwork.reeve, dependency_source),
1,
"the live subject's dependency did not press"
);
assert!(
kernel.state.commitments[&clockwork.farmer].contains_key(&clockwork.routine),
"the frozen routine was discharged or deleted rather than left as history"
);
assert!(
kernel.state.commitments[&clockwork.farmer].contains_key(&clockwork.obligation),
"the frozen obligation was discharged or deleted rather than left as history"
);
}
/// PA.f37: the freeze survives a full reopen-and-replay, the same way
/// every other retirement effect in this batch does.
#[test]
fn a_retired_subjects_frozen_commitments_survive_reopen_and_replay() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("world.cc");
let (world_id, farmer, routine, obligation) = {
let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "PAf37Replay");
owner_retires(&mut kernel, clockwork.farmer).expect("retire the farmer");
tick(&mut kernel, ROUTINE_DUE as u32 + LATE_DUE as u32).expect("the tick commits");
(
kernel.state.world_id,
clockwork.farmer,
clockwork.routine,
clockwork.obligation,
)
};
let reopened = WorldKernel::open(&path, world_id).expect("the store replays");
assert_eq!(
due(&reopened, farmer, routine),
FictionalMinutes(ROUTINE_DUE),
"the routine rolled somewhere along the replayed path"
);
assert_eq!(
pressure(
&reopened,
farmer,
PressureSource::Commitment {
subject: farmer,
key: obligation,
}
),
0,
"the obligation pressed somewhere along the replayed path"
);
}
}

View file

@ -3292,6 +3292,18 @@ pub(super) fn resolve_patch(
})
.map(|(scope, _)| Key::Existing(scope.subject_id))
.collect();
// Tracked the same way as `mirrors`: seeded from the committed human
// controller, extended below for a subject this same patch declares as
// `NewController::Human`. `Retire` refuses a Draft-phase approver out of
// this set instead of relying solely on the apply-time re-check, which
// otherwise only catches a genesis patch that declares and retires the
// human in the same breath after `open_owner` has already committed.
let mut humans: BTreeSet<Key<SubjectId>> = state
.controller_assignments
.iter()
.filter(|(_, assignment)| assignment.human_principal().is_some())
.map(|(scope, _)| Key::Existing(scope.subject_id))
.collect();
let mut affordance_holdings: BTreeMap<Key<SubjectId>, BTreeSet<Key<AffordanceId>>> = state
.affordance_grants
.iter()
@ -3508,6 +3520,9 @@ pub(super) fn resolve_patch(
if matches!(subject.controller, NewController::External { .. }) {
mirrors.insert(Key::Draft(subject.handle.clone()));
}
if matches!(subject.controller, NewController::Human { .. }) {
humans.insert(Key::Draft(subject.handle.clone()));
}
if let Some(reference) = &subject.position
&& let Some(place) = resolve_entity(
Site::Declaration(subject.handle.clone()),
@ -3643,6 +3658,15 @@ pub(super) fn resolve_patch(
let Some(subject_key) = subject_key else {
continue;
};
// A retired subject cannot act, and making a promise is
// acting: refused here the same way `RetiredSubjectActed`
// refuses a retired office incumbent below.
if retired.contains(&subject_key) {
mismatches.push(Mismatch::RetiredSubjectActed {
operation: position,
});
continue;
}
if let Some(counterparty_key) = &counterparty_key {
match counterparty_key {
None => continue,
@ -3835,13 +3859,12 @@ pub(super) fn resolve_patch(
// owner could otherwise retire the human, approve alone, and
// activate. Once the world is Active, approvals are frozen
// history judged by replay, and the human's subject may die.
if state.phase == super::WorldPhase::Draft
&& let Key::Existing(existing_id) = &subject_key
&& state
.controller_assignments
.get(&super::DecisionScope { subject_id: *existing_id })
.is_some_and(|assignment| assignment.human_principal().is_some())
{
// `humans` tracks a subject declared as `NewController::Human`
// in this same patch exactly as `mirrors` tracks a freshly
// declared mirror, so a genesis patch that declares and
// retires the human in one breath is refused here rather than
// only by the apply-time re-check after admission.
if state.phase == super::WorldPhase::Draft && humans.contains(&subject_key) {
mismatches.push(Mismatch::RetiresAnApprover {
operation: position,
});