Add the play authority, Ruled facts and Mint (kernel), schema consumer.v6 (play agent Cut 3, PA-Q5/PA-Q15)
SystemCapability::Play is a new system capability, minted only by the new PlayPort (mailbox.rs): unconfined, declares in Active with no answer, may advance time, and is the sole author of FactStanding::Ruled and ComponentOp::Mint (quantity with no evidence receipt). One new admission rule, require_ruler, refuses a Ruled fact or a Mint from any other caller in both reduce and apply_effect; the evidence gate on Canonical facts and Admit is untouched for every author, Play included. controller_work rows bump to v17 and the consumer schema bumps to consumer.v6 in the same commit, since the shape change and the version are one bump. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
86a3a2f6b4
commit
4c4973b7e8
8 changed files with 993 additions and 19 deletions
|
|
@ -2234,7 +2234,7 @@ mod tests {
|
|||
assert!(super::super::journal::verify_state_shape(&previous).is_err());
|
||||
assert_eq!(
|
||||
super::super::STATE_SCHEMA,
|
||||
"ghostlight.world_state.consumer.v5"
|
||||
"ghostlight.world_state.consumer.v6"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -2295,4 +2295,224 @@ mod tests {
|
|||
assert!(!rendered.contains(SECRET), "{rendered}");
|
||||
assert!(rendered.contains("<redacted>"), "{rendered}");
|
||||
}
|
||||
|
||||
// ---- Cut 3: nobody but the play authority may rule or mint ----------
|
||||
|
||||
fn ruled_patch(handle: &str) -> WorldPatch {
|
||||
WorldPatch {
|
||||
declarations: vec![Declaration::Fact(FactDeclaration {
|
||||
handle: DraftHandle::new(handle),
|
||||
label: "The Ruling".into(),
|
||||
statement: Statement::new("Stated by the table.").unwrap(),
|
||||
standing: FactStandingRef::Ruled,
|
||||
})],
|
||||
operations: Vec::new(),
|
||||
evidence: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn mint_patch(mirror: &Mirror) -> WorldPatch {
|
||||
WorldPatch {
|
||||
declarations: Vec::new(),
|
||||
operations: vec![ComponentOp::Mint {
|
||||
holder: Ref::Existing(mirror.first),
|
||||
resource: Ref::Existing(mirror.grain),
|
||||
qty: Quantity(3),
|
||||
}],
|
||||
evidence: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn elaborator_over(mirror: &Mirror) -> CallerId {
|
||||
CallerId::System(SystemCapability::Elaborator {
|
||||
jurisdiction: JurisdictionKey::PlaceSubtree(mirror.commons),
|
||||
})
|
||||
}
|
||||
|
||||
/// P3.2: the owner, seed, elaborators and consumers are refused a `Ruled`
|
||||
/// fact both at `reduce` and (transitively, since the mailbox always
|
||||
/// applies through `apply_effect`) at commit. Table-driven over the four
|
||||
/// non-`Play` authors the map names.
|
||||
#[test]
|
||||
fn no_one_else_may_rule_a_fact() {
|
||||
let refused_at_declaration = |error: KernelError, handle: &str| {
|
||||
assert!(
|
||||
matches!(&error, KernelError::PatchRejected(set)
|
||||
if set.contains(&Mismatch::RuledWithoutAuthority {
|
||||
site: patch::Site::Declaration(DraftHandle::new(handle)),
|
||||
})),
|
||||
"{error:?}"
|
||||
);
|
||||
};
|
||||
|
||||
// The owner in Draft: the seed's caller.
|
||||
{
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (mut kernel, _mirror) = draft_kernel(directory.path(), "OwnerDraftRule");
|
||||
let snapshot = kernel.snapshot().unwrap();
|
||||
let error = kernel
|
||||
.submit(
|
||||
command(
|
||||
&snapshot,
|
||||
CommandId::new(),
|
||||
CallerId::Principal(owner()),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: None,
|
||||
patch: ruled_patch("rule"),
|
||||
},
|
||||
),
|
||||
&auth_principal(owner()),
|
||||
)
|
||||
.unwrap_err();
|
||||
refused_at_declaration(error, "rule");
|
||||
}
|
||||
|
||||
// The owner in Active, with a real answer.
|
||||
{
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (mut kernel, mirror) = mirror_kernel(directory.path(), "OwnerActiveRule");
|
||||
let answered = missing_structure(&kernel, mirror.first);
|
||||
let snapshot = kernel.snapshot().unwrap();
|
||||
let error = kernel
|
||||
.submit(
|
||||
command(
|
||||
&snapshot,
|
||||
CommandId::new(),
|
||||
CallerId::Principal(owner()),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: Some(PatchAnswer::Boundary(answered)),
|
||||
patch: ruled_patch("rule"),
|
||||
},
|
||||
),
|
||||
&auth_principal(owner()),
|
||||
)
|
||||
.unwrap_err();
|
||||
refused_at_declaration(error, "rule");
|
||||
}
|
||||
|
||||
// An elaborator with a valid answer.
|
||||
{
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (mut kernel, mirror) = mirror_kernel(directory.path(), "ElaboratorRule");
|
||||
let answered = missing_structure(&kernel, mirror.first);
|
||||
let error = submit_as(
|
||||
&mut kernel,
|
||||
elaborator_over(&mirror),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: Some(PatchAnswer::Boundary(answered)),
|
||||
patch: ruled_patch("rule"),
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
refused_at_declaration(error, "rule");
|
||||
}
|
||||
|
||||
// A consumer on its own mirror.
|
||||
{
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (mut kernel, mirror) = mirror_kernel(directory.path(), "ConsumerRule");
|
||||
let answered = missing_structure(&kernel, mirror.first);
|
||||
let error = as_consumer(
|
||||
&mut kernel,
|
||||
CommandBody::AdmitPatch {
|
||||
answers: Some(PatchAnswer::Boundary(answered)),
|
||||
patch: ruled_patch("rule"),
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
refused_at_declaration(error, "rule");
|
||||
}
|
||||
}
|
||||
|
||||
/// P3.2's other half: the same four callers may not mint.
|
||||
#[test]
|
||||
fn no_one_else_may_mint() {
|
||||
let refused_at_op_zero = |error: KernelError| {
|
||||
assert!(
|
||||
matches!(&error, KernelError::PatchRejected(set)
|
||||
if set.contains(&Mismatch::RuledWithoutAuthority {
|
||||
site: patch::Site::Operation(0),
|
||||
})),
|
||||
"{error:?}"
|
||||
);
|
||||
};
|
||||
|
||||
// The owner in Draft: the seed's caller.
|
||||
{
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (mut kernel, mirror) = draft_kernel(directory.path(), "OwnerDraftMint");
|
||||
let snapshot = kernel.snapshot().unwrap();
|
||||
let error = kernel
|
||||
.submit(
|
||||
command(
|
||||
&snapshot,
|
||||
CommandId::new(),
|
||||
CallerId::Principal(owner()),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: None,
|
||||
patch: mint_patch(&mirror),
|
||||
},
|
||||
),
|
||||
&auth_principal(owner()),
|
||||
)
|
||||
.unwrap_err();
|
||||
refused_at_op_zero(error);
|
||||
}
|
||||
|
||||
// The owner in Active.
|
||||
{
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (mut kernel, mirror) = mirror_kernel(directory.path(), "OwnerActiveMint");
|
||||
let snapshot = kernel.snapshot().unwrap();
|
||||
let error = kernel
|
||||
.submit(
|
||||
command(
|
||||
&snapshot,
|
||||
CommandId::new(),
|
||||
CallerId::Principal(owner()),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: None,
|
||||
patch: mint_patch(&mirror),
|
||||
},
|
||||
),
|
||||
&auth_principal(owner()),
|
||||
)
|
||||
.unwrap_err();
|
||||
refused_at_op_zero(error);
|
||||
}
|
||||
|
||||
// An elaborator with a valid answer: `require_patch_author` demands
|
||||
// one for an elaborator whatever the patch declares.
|
||||
{
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (mut kernel, mirror) = mirror_kernel(directory.path(), "ElaboratorMint");
|
||||
let answered = missing_structure(&kernel, mirror.first);
|
||||
let error = submit_as(
|
||||
&mut kernel,
|
||||
elaborator_over(&mirror),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: Some(PatchAnswer::Boundary(answered)),
|
||||
patch: mint_patch(&mirror),
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
refused_at_op_zero(error);
|
||||
}
|
||||
|
||||
// A consumer on its own mirror: an operations-only patch declares
|
||||
// nothing, so no answer is needed to reach `require_ruler`.
|
||||
{
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (mut kernel, mirror) = mirror_kernel(directory.path(), "ConsumerMint");
|
||||
let error = as_consumer(
|
||||
&mut kernel,
|
||||
CommandBody::AdmitPatch {
|
||||
answers: None,
|
||||
patch: mint_patch(&mirror),
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
refused_at_op_zero(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,8 +76,8 @@ pub(super) const CELL_TOOL_STEP_BUDGET: usize = 2;
|
|||
/// carried by tool identity, never by a model-written argument.
|
||||
const HANDLE_SEPARATOR: &str = "__";
|
||||
const PERSONA_WORD_BUDGET: usize = 180;
|
||||
const CONTROLLER_WORK_ROW: &str = "controller_work.v16";
|
||||
const CONTROLLER_WORK_SCHEMA: &str = "ghostlight.controller_work.v16";
|
||||
const CONTROLLER_WORK_ROW: &str = "controller_work.v17";
|
||||
const CONTROLLER_WORK_SCHEMA: &str = "ghostlight.controller_work.v17";
|
||||
|
||||
/// The Interpreter's byte-span capture tool. It is not the generated `speak`
|
||||
/// affordance tool: one captures an utterance out of preserved prose, the other
|
||||
|
|
|
|||
|
|
@ -584,6 +584,7 @@ pub(super) fn verify_state_shape(state: &WorldState) -> Result<(), JournalError>
|
|||
super::patch::is_canonical_text(evidence.text())
|
||||
}
|
||||
super::FactStanding::Claimed { by } => state.subjects.contains_key(by),
|
||||
super::FactStanding::Ruled => true,
|
||||
};
|
||||
if !state
|
||||
.entities
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ pub use elaboration::{
|
|||
pub use lens::{Lens, LensWeights};
|
||||
pub use local_inference::{DEFAULT_LOCAL_MODEL_PREFIX, LocalBinding};
|
||||
pub(crate) use mailbox::ElaborationPort;
|
||||
pub use mailbox::{ConsumerPort, ControllerPort, MailboxError, SeedPort, WorldMailbox};
|
||||
pub use mailbox::{ConsumerPort, ControllerPort, MailboxError, PlayPort, SeedPort, WorldMailbox};
|
||||
pub use patch::{
|
||||
CommitmentKey, DraftHandle, JurisdictionKey, Mismatch, PatchAnswer, Ref, RefKind, RefName, Role,
|
||||
Site, Statement, WorldPatch,
|
||||
|
|
@ -173,8 +173,8 @@ impl VerifiedPrincipalEvidence {
|
|||
}
|
||||
}
|
||||
|
||||
pub const STATE_SCHEMA: &str = "ghostlight.world_state.consumer.v5";
|
||||
pub(crate) const COMMIT_SCHEMA: &str = "ghostlight.world_commit.consumer.v5";
|
||||
pub const STATE_SCHEMA: &str = "ghostlight.world_state.consumer.v6";
|
||||
pub(crate) const COMMIT_SCHEMA: &str = "ghostlight.world_commit.consumer.v6";
|
||||
|
||||
/// Compatibility tag derived from [`STATE_SCHEMA`]: the trailing
|
||||
/// `<family>-<version>` pair (e.g. `foundation-v1`). Callers that publish a
|
||||
|
|
@ -480,6 +480,10 @@ enum SystemCapability {
|
|||
Consumer {
|
||||
consumer: ConsumerId,
|
||||
},
|
||||
/// The play table. Admitted for unanswered, unconfined Active patches, the
|
||||
/// only author of `Ruled` facts and `Mint`, and for the clock. Minted only
|
||||
/// by `PlayPort`.
|
||||
Play,
|
||||
}
|
||||
|
||||
/// What a confined patch author may write. `None` from `require_patch_author`
|
||||
|
|
@ -1664,12 +1668,14 @@ fn reduce(state: &WorldState, command: &CommandEnvelope) -> Result<WorldEffect,
|
|||
// a jurisdiction complaint about a reference that does not resolve.
|
||||
require_answer(
|
||||
state,
|
||||
&command.caller,
|
||||
answers.as_ref(),
|
||||
!(patch.declarations.is_empty() && patch.evidence.is_empty()),
|
||||
)?;
|
||||
let confinement = require_patch_author(state, &command.caller, answers.as_ref())?;
|
||||
let resolved = patch::resolve_patch(state, command.id, patch, None, None)
|
||||
.map_err(KernelError::PatchRejected)?;
|
||||
require_ruler(&command.caller, &resolved).map_err(KernelError::PatchRejected)?;
|
||||
if let Some(ground) = confinement {
|
||||
confine_to_ground(state, &resolved, ground).map_err(KernelError::PatchRejected)?;
|
||||
}
|
||||
|
|
@ -1709,8 +1715,11 @@ fn require_system_capability(caller: &CallerId, body: &CommandBody) -> Result<()
|
|||
let admitted = matches!(
|
||||
(capability, body),
|
||||
(SystemCapability::Clock, CommandBody::AdvanceTime { .. })
|
||||
| (SystemCapability::Play, CommandBody::AdvanceTime { .. })
|
||||
| (
|
||||
SystemCapability::Elaborator { .. } | SystemCapability::Consumer { .. },
|
||||
SystemCapability::Elaborator { .. }
|
||||
| SystemCapability::Consumer { .. }
|
||||
| SystemCapability::Play,
|
||||
CommandBody::AdmitPatch { .. }
|
||||
)
|
||||
);
|
||||
|
|
@ -1725,7 +1734,10 @@ fn require_system_capability(caller: &CallerId, body: &CommandBody) -> Result<()
|
|||
/// session. Nothing
|
||||
/// else may tick.
|
||||
fn require_clock_caller(state: &WorldState, caller: &CallerId) -> Result<(), KernelError> {
|
||||
if caller == &CallerId::System(SystemCapability::Clock) {
|
||||
if matches!(
|
||||
caller,
|
||||
CallerId::System(SystemCapability::Clock) | CallerId::System(SystemCapability::Play)
|
||||
) {
|
||||
Ok(())
|
||||
} else {
|
||||
require_owner(state, caller)
|
||||
|
|
@ -1741,13 +1753,26 @@ fn require_clock_caller(state: &WorldState, caller: &CallerId) -> Result<(), Ker
|
|||
/// jurisdiction whose deficit is nonzero.
|
||||
fn require_answer(
|
||||
state: &WorldState,
|
||||
caller: &CallerId,
|
||||
answers: Option<&PatchAnswer>,
|
||||
declares: bool,
|
||||
) -> Result<(), KernelError> {
|
||||
if caller == &CallerId::System(SystemCapability::Play) && state.phase == WorldPhase::Draft {
|
||||
return Err(KernelError::WrongPhase {
|
||||
expected: WorldPhase::Active,
|
||||
actual: state.phase,
|
||||
});
|
||||
}
|
||||
match (state.phase, answers) {
|
||||
(WorldPhase::Draft, None) => Ok(()),
|
||||
(WorldPhase::Draft, Some(_)) => Err(KernelError::AnswerNotDerived),
|
||||
(WorldPhase::Active, None) if declares => Err(KernelError::AnswerRequired),
|
||||
(WorldPhase::Active, None) if declares => {
|
||||
if caller == &CallerId::System(SystemCapability::Play) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(KernelError::AnswerRequired)
|
||||
}
|
||||
}
|
||||
(WorldPhase::Active, None) => Ok(()),
|
||||
(WorldPhase::Active, Some(PatchAnswer::Boundary(claimed))) => {
|
||||
exact_boundary(state, claimed).map(|_| ())
|
||||
|
|
@ -2050,6 +2075,7 @@ fn admit_resolved(state: &mut WorldState, resolved: &ResolvedPatch) -> Result<()
|
|||
!(patch::is_canonical_text(evidence.text()) && resolved.evidence.contains(evidence))
|
||||
}
|
||||
FactStanding::Claimed { by } => !state.subjects.contains_key(by),
|
||||
FactStanding::Ruled => false,
|
||||
};
|
||||
if !patch::is_canonical_text(&declared.entity.label)
|
||||
|| !patch::is_canonical_text(declared.fact.statement.as_str())
|
||||
|
|
@ -2143,6 +2169,9 @@ fn apply_operations(
|
|||
ResolvedOp::Admit { resource, qty, .. } => {
|
||||
deltas.entry(*resource).or_default().admitted += u128::from(qty.0);
|
||||
}
|
||||
ResolvedOp::Mint { resource, qty, .. } => {
|
||||
deltas.entry(*resource).or_default().admitted += u128::from(qty.0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -2177,7 +2206,8 @@ fn operation_resources(operation: &ResolvedOp) -> Vec<EntityId> {
|
|||
match operation {
|
||||
ResolvedOp::Transfer { resource, .. }
|
||||
| ResolvedOp::Consume { resource, .. }
|
||||
| ResolvedOp::Admit { resource, .. } => vec![*resource],
|
||||
| ResolvedOp::Admit { resource, .. }
|
||||
| ResolvedOp::Mint { resource, .. } => vec![*resource],
|
||||
ResolvedOp::Transform {
|
||||
from_resource,
|
||||
into_resource,
|
||||
|
|
@ -2394,6 +2424,22 @@ fn apply_operation(
|
|||
.ok_or_else(overflow)?;
|
||||
set_held(state, *holder, *resource, admitted);
|
||||
}
|
||||
ResolvedOp::Mint {
|
||||
holder,
|
||||
resource,
|
||||
qty,
|
||||
} => {
|
||||
if !custody_referents_exist(state, *holder, *resource) {
|
||||
return Err(unknown());
|
||||
}
|
||||
if qty.0 == 0 {
|
||||
return Err(zero());
|
||||
}
|
||||
let minted = held(state, *holder, *resource)
|
||||
.checked_add(qty.0)
|
||||
.ok_or_else(overflow)?;
|
||||
set_held(state, *holder, *resource, minted);
|
||||
}
|
||||
ResolvedOp::Bind { subject, target } | ResolvedOp::Release { subject, target } => {
|
||||
let bind = matches!(operation, ResolvedOp::Bind { .. });
|
||||
if !state.subjects.contains_key(subject) || !dependency_target_exists(state, *target) {
|
||||
|
|
@ -3662,6 +3708,7 @@ fn snapshot(state: &WorldState) -> Result<WorldSnapshot, KernelError> {
|
|||
standing: match &record.standing {
|
||||
FactStanding::Canonical { .. } => FactStandingView::Canonical,
|
||||
FactStanding::Claimed { by } => FactStandingView::Claimed { by: *by },
|
||||
FactStanding::Ruled => FactStandingView::Canonical,
|
||||
},
|
||||
confidence: held.confidence,
|
||||
source: held.source,
|
||||
|
|
@ -4290,6 +4337,7 @@ fn require_patch_author(
|
|||
) -> Result<Option<PatchGround>, KernelError> {
|
||||
match caller {
|
||||
CallerId::Principal(principal) if principal == &state.owner => Ok(None),
|
||||
CallerId::System(SystemCapability::Play) => Ok(None),
|
||||
CallerId::System(SystemCapability::Elaborator { jurisdiction }) => {
|
||||
let answer = answers.ok_or(KernelError::Unauthorized)?;
|
||||
let ground = PatchGround::Jurisdiction(*jurisdiction);
|
||||
|
|
@ -4514,6 +4562,11 @@ fn confine_to_ground(
|
|||
mismatches.push(Mismatch::OutsideJurisdiction { site });
|
||||
}
|
||||
}
|
||||
// Redundant with `require_ruler`, which refuses a confined caller's
|
||||
// `Ruled` fact outright; kept so this match stays total.
|
||||
FactStanding::Ruled => {
|
||||
mismatches.push(Mismatch::OutsideJurisdiction { site });
|
||||
}
|
||||
}
|
||||
}
|
||||
for declared in &resolved.channels {
|
||||
|
|
@ -4558,6 +4611,38 @@ fn confine_to_ground(
|
|||
}
|
||||
}
|
||||
|
||||
/// The one author of a `Ruled` fact and a `Mint`. `Ok` when the caller is
|
||||
/// `System(Play)`. Otherwise, one `Mismatch::RuledWithoutAuthority` per
|
||||
/// `Ruled` fact and per `Mint`, and `Ok` if there are none. Called in
|
||||
/// `reduce`'s `AdmitPatch` arm right after `resolve_patch` and before
|
||||
/// confinement, and re-decided in `apply_effect`'s `PatchAdmitted` arm. The
|
||||
/// resolver stays actor-blind: this runs after resolution, never inside it.
|
||||
fn require_ruler(caller: &CallerId, resolved: &ResolvedPatch) -> Result<(), Vec<Mismatch>> {
|
||||
if caller == &CallerId::System(SystemCapability::Play) {
|
||||
return Ok(());
|
||||
}
|
||||
let mut mismatches = Vec::new();
|
||||
for declared in &resolved.facts {
|
||||
if matches!(declared.fact.standing, FactStanding::Ruled) {
|
||||
mismatches.push(Mismatch::RuledWithoutAuthority {
|
||||
site: Site::Declaration(declared.handle.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
for (index, operation) in resolved.operations.iter().enumerate() {
|
||||
if matches!(operation, ResolvedOp::Mint { .. }) {
|
||||
mismatches.push(Mismatch::RuledWithoutAuthority {
|
||||
site: Site::Operation(index),
|
||||
});
|
||||
}
|
||||
}
|
||||
if mismatches.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(mismatches)
|
||||
}
|
||||
}
|
||||
|
||||
/// The ground one lowered operation touches: the subjects whose position it
|
||||
/// must sit under, the places it names directly, and the routes whose endpoints
|
||||
/// it must sit under. Total over `ResolvedOp`, so a new operation cannot ship
|
||||
|
|
@ -4605,6 +4690,9 @@ fn operation_ground(
|
|||
}
|
||||
| ResolvedOp::Admit {
|
||||
holder, resource, ..
|
||||
}
|
||||
| ResolvedOp::Mint {
|
||||
holder, resource, ..
|
||||
} => (vec![*holder], places(vec![*resource]), Vec::new()),
|
||||
ResolvedOp::Bind { subject, target } | ResolvedOp::Release { subject, target } => {
|
||||
match target {
|
||||
|
|
@ -4774,8 +4862,11 @@ fn apply_effect(
|
|||
let admitted = matches!(
|
||||
(capability, effect),
|
||||
(SystemCapability::Clock, WorldEffect::TimeAdvanced { .. })
|
||||
| (SystemCapability::Play, WorldEffect::TimeAdvanced { .. })
|
||||
| (
|
||||
SystemCapability::Elaborator { .. } | SystemCapability::Consumer { .. },
|
||||
SystemCapability::Elaborator { .. }
|
||||
| SystemCapability::Consumer { .. }
|
||||
| SystemCapability::Play,
|
||||
WorldEffect::PatchAdmitted { .. }
|
||||
)
|
||||
);
|
||||
|
|
@ -4790,7 +4881,12 @@ fn apply_effect(
|
|||
));
|
||||
}
|
||||
WorldEffect::PatchAdmitted { answers, resolved } => {
|
||||
require_answer(state, answers.as_ref(), !resolved.declares_nothing())?;
|
||||
require_answer(state, caller, answers.as_ref(), !resolved.declares_nothing())?;
|
||||
require_ruler(caller, resolved).map_err(|_| {
|
||||
KernelError::Invariant(
|
||||
"admitted patch rules or mints without the play authority".into(),
|
||||
)
|
||||
})?;
|
||||
let confinement =
|
||||
require_patch_author(state, caller, answers.as_ref()).map_err(|_| {
|
||||
KernelError::Invariant(
|
||||
|
|
@ -15830,4 +15926,472 @@ mod clock_tests {
|
|||
);
|
||||
assert_eq!(kernel.state.revision, revision);
|
||||
}
|
||||
|
||||
// ---- Cut 3: the play authority, `Ruled` facts and `Mint` -----------
|
||||
|
||||
fn play_caller() -> CallerId {
|
||||
CallerId::System(SystemCapability::Play)
|
||||
}
|
||||
|
||||
fn ruled_fact_patch(handle: &str) -> WorldPatch {
|
||||
WorldPatch {
|
||||
declarations: vec![Declaration::Fact(FactDeclaration {
|
||||
handle: DraftHandle::new(handle),
|
||||
label: "The Ruling".into(),
|
||||
statement: Statement::new("Stated by the table.").unwrap(),
|
||||
standing: FactStandingRef::Ruled,
|
||||
})],
|
||||
operations: Vec::new(),
|
||||
evidence: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_named(kernel: &WorldKernel, label: &str) -> EntityId {
|
||||
*kernel
|
||||
.state
|
||||
.entities
|
||||
.iter()
|
||||
.find(|(_, record)| record.label == label)
|
||||
.map(|(id, _)| id)
|
||||
.expect("the declared resource")
|
||||
}
|
||||
|
||||
/// Invariant 5, first half, and P3.1: the play authority declares in
|
||||
/// Active with no answer.
|
||||
#[test]
|
||||
fn the_play_authority_declares_in_active_without_an_answer() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "PlayDeclares");
|
||||
let receipt = submit_as(
|
||||
&mut kernel,
|
||||
play_caller(),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: None,
|
||||
patch: shed_under(clockwork.dead_end, "play-shed"),
|
||||
},
|
||||
)
|
||||
.expect("the play authority declares unanswered in Active");
|
||||
assert!(matches!(receipt, SubmitReceipt::Applied(_)));
|
||||
}
|
||||
|
||||
/// Invariant 5, second half: the elaborator gate is unchanged.
|
||||
#[test]
|
||||
fn an_elaborator_declaration_in_active_without_an_answer_is_still_refused() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "ElaboratorUnanswered");
|
||||
let error = submit_as(
|
||||
&mut kernel,
|
||||
elaborator(JurisdictionKey::PlaceSubtree(clockwork.dead_end)),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: None,
|
||||
patch: shed_under(clockwork.dead_end, "shed"),
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(error, KernelError::AnswerRequired), "{error:?}");
|
||||
}
|
||||
|
||||
/// Invariant 5, second half: the owner's own gate is unchanged too. Only
|
||||
/// `Play` is exempted from `AnswerRequired`.
|
||||
#[test]
|
||||
fn the_owner_still_cannot_declare_unanswered_in_active() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "OwnerUnanswered");
|
||||
let snapshot = kernel.snapshot().unwrap();
|
||||
let error = kernel
|
||||
.submit(
|
||||
command(
|
||||
&snapshot,
|
||||
CommandId::new(),
|
||||
CallerId::Principal(owner()),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: None,
|
||||
patch: shed_under(clockwork.dead_end, "shed"),
|
||||
},
|
||||
),
|
||||
&auth_principal(owner()),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(error, KernelError::AnswerRequired), "{error:?}");
|
||||
}
|
||||
|
||||
/// Invariant 5: the play authority reaches the whole world, unconfined to
|
||||
/// a ground. Writing under a place no jurisdiction covers, with no
|
||||
/// answer, is exactly what an elaborator or the owner cannot do.
|
||||
#[test]
|
||||
fn the_play_authority_is_unconfined() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "PlayUnconfined");
|
||||
submit_as(
|
||||
&mut kernel,
|
||||
play_caller(),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: None,
|
||||
patch: shed_under(clockwork.dead_end, "unconfined-shed"),
|
||||
},
|
||||
)
|
||||
.expect("the play authority is confined to no ground");
|
||||
}
|
||||
|
||||
/// P3.6: the play authority cannot act in Draft.
|
||||
#[test]
|
||||
fn the_play_authority_is_refused_in_draft() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let mut kernel = WorldKernel::create(
|
||||
directory.path().join("world.cc"),
|
||||
creation(CommandId::new(), "PlayDraft"),
|
||||
&auth_principal(owner()),
|
||||
)
|
||||
.expect("a created world")
|
||||
.0;
|
||||
let commons = kernel.snapshot().unwrap().places[0].id;
|
||||
let error = submit_as(
|
||||
&mut kernel,
|
||||
play_caller(),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: None,
|
||||
patch: shed_under(commons, "shed"),
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(
|
||||
error,
|
||||
KernelError::WrongPhase {
|
||||
expected: WorldPhase::Active,
|
||||
actual: WorldPhase::Draft,
|
||||
}
|
||||
),
|
||||
"{error:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Invariant 5: the play authority advances time, the clock's precedent.
|
||||
#[test]
|
||||
fn the_play_authority_advances_time() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (mut kernel, _clockwork, active) = clock_kernel(directory.path(), "PlayClock");
|
||||
let receipt = submit_as(
|
||||
&mut kernel,
|
||||
play_caller(),
|
||||
CommandBody::AdvanceTime {
|
||||
minutes: minutes(30),
|
||||
},
|
||||
)
|
||||
.expect("the play authority advances time");
|
||||
assert!(matches!(receipt, SubmitReceipt::Applied(_)));
|
||||
assert!(kernel.state.now > active.now);
|
||||
}
|
||||
|
||||
/// P3.6: the play authority may not approve, activate, exercise, or
|
||||
/// decline. `require_system_capability`, at the top of `reduce`, refuses
|
||||
/// all four before any body-specific arm is read.
|
||||
#[test]
|
||||
fn the_play_authority_cannot_approve_activate_exercise_or_decline() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (mut kernel, clockwork, active) = clock_kernel(directory.path(), "PlayForbidden");
|
||||
let opportunity = opportunity_for(&active, clockwork.reeve);
|
||||
let forbidden = [
|
||||
CommandBody::ApproveDraft,
|
||||
CommandBody::ActivateWorld,
|
||||
CommandBody::ExerciseDecision {
|
||||
opportunity: opportunity.clone(),
|
||||
invocation: DecisionInvocation {
|
||||
affordance: clockwork.deliver,
|
||||
bindings: Vec::new(),
|
||||
proposed: Vec::new(),
|
||||
speech: None,
|
||||
display: None,
|
||||
},
|
||||
},
|
||||
CommandBody::DeclineDecision {
|
||||
opportunity: opportunity.clone(),
|
||||
},
|
||||
];
|
||||
for body in &forbidden {
|
||||
let error = submit_as(&mut kernel, play_caller(), body.clone()).unwrap_err();
|
||||
assert!(matches!(error, KernelError::Unauthorized), "{error:?}");
|
||||
}
|
||||
// The end-to-end check above cannot tell "refused before any
|
||||
// body-specific arm is read" apart from a downstream refusal that
|
||||
// would fail Play just as closed (`require_owner`, for one, refuses
|
||||
// every non-owner caller regardless of what admitted it). Pin
|
||||
// `require_system_capability` directly, so widening its table for
|
||||
// `Play` is caught even though every arm behind it still happens to
|
||||
// refuse `Play` on its own.
|
||||
for body in &forbidden {
|
||||
assert!(
|
||||
matches!(
|
||||
require_system_capability(&play_caller(), body),
|
||||
Err(KernelError::Unauthorized)
|
||||
),
|
||||
"require_system_capability admitted {body:?} for the play authority"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// P3.4: a ruled fact is world truth to every subject that knows it.
|
||||
#[test]
|
||||
fn the_play_authority_rules_a_fact_and_a_witness_knows_it_as_canon() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "PlayRules");
|
||||
let mut patch = ruled_fact_patch("verdict");
|
||||
patch.operations.push(ComponentOp::AcquireKnowledge {
|
||||
subject: Ref::Existing(clockwork.reeve),
|
||||
fact: Ref::Draft(DraftHandle::new("verdict")),
|
||||
source: AuthoredSource::Witnessed,
|
||||
confidence: Confidence::Certain,
|
||||
});
|
||||
submit_as(
|
||||
&mut kernel,
|
||||
play_caller(),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: None,
|
||||
patch,
|
||||
},
|
||||
)
|
||||
.expect("the play authority rules a fact and grants it as knowledge");
|
||||
|
||||
let fact = *kernel
|
||||
.state
|
||||
.facts
|
||||
.iter()
|
||||
.find(|(_, record)| record.statement.as_str() == "Stated by the table.")
|
||||
.map(|(id, _)| id)
|
||||
.expect("the ruled fact");
|
||||
assert_eq!(kernel.state.facts[&fact].standing, FactStanding::Ruled);
|
||||
|
||||
let snapshot = kernel.snapshot().unwrap();
|
||||
let subject = snapshot
|
||||
.subjects
|
||||
.iter()
|
||||
.find(|subject| subject.id == clockwork.reeve)
|
||||
.expect("the reeve's snapshot");
|
||||
let known = subject
|
||||
.knowledge
|
||||
.iter()
|
||||
.find(|held| held.fact == fact)
|
||||
.expect("the reeve knows the ruled fact");
|
||||
assert_eq!(known.standing, FactStandingView::Canonical);
|
||||
}
|
||||
|
||||
/// P3.5: minted quantity balances the ledger, and replay reproduces it.
|
||||
#[test]
|
||||
fn the_play_authority_mints_and_the_ledger_balances() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().join("world.cc");
|
||||
let (world_id, reeve, grain) = {
|
||||
let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "PlayMints");
|
||||
let grain = resource_named(&kernel, "Winter Grain");
|
||||
let before = held(&kernel.state, clockwork.reeve, grain);
|
||||
assert_eq!(before, 0);
|
||||
submit_as(
|
||||
&mut kernel,
|
||||
play_caller(),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: None,
|
||||
patch: WorldPatch {
|
||||
declarations: Vec::new(),
|
||||
operations: vec![ComponentOp::Mint {
|
||||
holder: Ref::Existing(clockwork.reeve),
|
||||
resource: Ref::Existing(grain),
|
||||
qty: Quantity(7),
|
||||
}],
|
||||
evidence: Vec::new(),
|
||||
},
|
||||
},
|
||||
)
|
||||
.expect("the play authority mints");
|
||||
let after = held(&kernel.state, clockwork.reeve, grain);
|
||||
assert_eq!(before + 7, after);
|
||||
(kernel.state.world_id, clockwork.reeve, grain)
|
||||
};
|
||||
let replayed = WorldKernel::open(&path, world_id).expect("the minted store replays");
|
||||
assert_eq!(held(&replayed.state, reeve, grain), 7);
|
||||
}
|
||||
|
||||
/// The evidenced-knowledge clause is unchanged: a receipt cannot vouch for
|
||||
/// an assertion the kernel never evaluated, and a `Ruled` fact is exactly
|
||||
/// such an assertion.
|
||||
#[test]
|
||||
fn a_ruled_fact_cannot_be_known_evidenced() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "PlayEvidenced");
|
||||
let mut patch = ruled_fact_patch("verdict");
|
||||
patch.operations.push(ComponentOp::AcquireKnowledge {
|
||||
subject: Ref::Existing(clockwork.reeve),
|
||||
fact: Ref::Draft(DraftHandle::new("verdict")),
|
||||
source: AuthoredSource::Evidenced,
|
||||
confidence: Confidence::Certain,
|
||||
});
|
||||
let error = submit_as(
|
||||
&mut kernel,
|
||||
play_caller(),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: None,
|
||||
patch,
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(&error, KernelError::PatchRejected(set)
|
||||
if set.contains(&Mismatch::EvidencedKnowledgeOfClaim { operation: 0 })),
|
||||
"{error:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The evidence gate on `Canonical` facts and `Admit` is byte-for-byte the
|
||||
/// same predicate at the same sites for every author, `Play` included
|
||||
/// (P3.3). `Play` carries no receipts, so its `Admit` is refused like
|
||||
/// anyone's without one.
|
||||
#[test]
|
||||
fn the_evidence_gate_is_unchanged_for_every_author() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "PlayEvidenceGate");
|
||||
let grain = resource_named(&kernel, "Winter Grain");
|
||||
|
||||
// Without a receipt: `Play`'s `Admit` is refused exactly as anyone's.
|
||||
let error = submit_as(
|
||||
&mut kernel,
|
||||
play_caller(),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: None,
|
||||
patch: WorldPatch {
|
||||
declarations: Vec::new(),
|
||||
operations: vec![ComponentOp::Admit {
|
||||
holder: Ref::Existing(clockwork.reeve),
|
||||
resource: Ref::Existing(grain),
|
||||
qty: Quantity(1),
|
||||
evidence: EvidenceRef::new("vault:uncited"),
|
||||
}],
|
||||
evidence: Vec::new(),
|
||||
},
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(&error, KernelError::PatchRejected(set)
|
||||
if set.contains(&Mismatch::AdmitWithoutEvidence { operation: 0 })),
|
||||
"{error:?}"
|
||||
);
|
||||
|
||||
// With the receipt cited in the same patch, `Play`'s `Admit` is
|
||||
// admitted like anyone unconfined's.
|
||||
submit_as(
|
||||
&mut kernel,
|
||||
play_caller(),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: None,
|
||||
patch: WorldPatch {
|
||||
declarations: Vec::new(),
|
||||
operations: vec![ComponentOp::Admit {
|
||||
holder: Ref::Existing(clockwork.reeve),
|
||||
resource: Ref::Existing(grain),
|
||||
qty: Quantity(1),
|
||||
evidence: EvidenceRef::new("vault:cited"),
|
||||
}],
|
||||
evidence: vec![EvidenceRef::new("vault:cited")],
|
||||
},
|
||||
},
|
||||
)
|
||||
.expect("an evidenced admit is admitted for the play authority too");
|
||||
}
|
||||
|
||||
/// `reduce` decides and `apply_effect` re-decides: a forged
|
||||
/// `PatchAdmitted` under an elaborator caller, carrying a `Mint` the
|
||||
/// resolver would accept (the resolver is actor-blind) and that fully
|
||||
/// satisfies confinement (the minted subject is declared inside the
|
||||
/// elaborator's own jurisdiction), is still refused at apply. `Mint`
|
||||
/// carries no confinement veto of its own — unlike a `Ruled` fact,
|
||||
/// `confine_to_ground` has no redundant clause for it — so only
|
||||
/// `require_ruler`'s re-decision can be refusing this. Follows
|
||||
/// `apply_effect_re_decides_elaborator_authority`.
|
||||
#[test]
|
||||
fn apply_effect_re_decides_the_ruler() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let (kernel, clockwork, _) = clock_kernel(directory.path(), "RedecideRulerMint");
|
||||
let answered = dead_end_boundary(&kernel);
|
||||
let speak = super::tests::speak_entry(&kernel);
|
||||
let command_id = CommandId::issue();
|
||||
let patch = WorldPatch {
|
||||
declarations: vec![
|
||||
Declaration::Entity(EntityDeclaration {
|
||||
handle: DraftHandle::new("loot"),
|
||||
label: "Roadside Loot".into(),
|
||||
kind: EntityKind::Resource,
|
||||
container: None,
|
||||
}),
|
||||
Declaration::Subject(SubjectDeclaration {
|
||||
handle: DraftHandle::new("finder"),
|
||||
label: "The Roadside Finder".into(),
|
||||
kind: SubjectKind::Person,
|
||||
controller: NewController::NarrativePersona,
|
||||
affordances: BTreeSet::from([speak]),
|
||||
position: Some(Ref::Existing(clockwork.dead_end)),
|
||||
}),
|
||||
],
|
||||
operations: vec![ComponentOp::Mint {
|
||||
holder: Ref::Draft(DraftHandle::new("finder")),
|
||||
resource: Ref::Draft(DraftHandle::new("loot")),
|
||||
qty: Quantity(4),
|
||||
}],
|
||||
evidence: Vec::new(),
|
||||
};
|
||||
let resolved = patch::resolve_patch(&kernel.state, command_id, &patch, None, None)
|
||||
.expect("the patch resolves; the resolver is actor-blind");
|
||||
let effect = WorldEffect::PatchAdmitted {
|
||||
answers: Some(PatchAnswer::Boundary(answered)),
|
||||
resolved,
|
||||
};
|
||||
|
||||
let mut candidate = kernel.state.clone();
|
||||
let error = apply_effect(
|
||||
&mut candidate,
|
||||
command_id,
|
||||
&elaborator(JurisdictionKey::PlaceSubtree(clockwork.dead_end)),
|
||||
&effect,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(error, KernelError::Invariant(_)), "{error:?}");
|
||||
assert_eq!(candidate, kernel.state);
|
||||
|
||||
let mut candidate = kernel.state.clone();
|
||||
apply_effect(&mut candidate, command_id, &play_caller(), &effect)
|
||||
.expect("the play authority applies through the same arm");
|
||||
assert_ne!(candidate, kernel.state);
|
||||
}
|
||||
|
||||
/// P3.5, replay half, folded with a ruled fact: reopening the journal
|
||||
/// reproduces both across the digest.
|
||||
#[test]
|
||||
fn a_journal_holding_a_ruled_fact_and_a_mint_replays() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().join("world.cc");
|
||||
let (world_id, expected) = {
|
||||
let (mut kernel, clockwork, _) = clock_kernel(directory.path(), "PlayReplay");
|
||||
let grain = resource_named(&kernel, "Winter Grain");
|
||||
let mut patch = ruled_fact_patch("verdict");
|
||||
patch.operations.push(ComponentOp::Mint {
|
||||
holder: Ref::Existing(clockwork.farmer),
|
||||
resource: Ref::Existing(grain),
|
||||
qty: Quantity(2),
|
||||
});
|
||||
submit_as(
|
||||
&mut kernel,
|
||||
play_caller(),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: None,
|
||||
patch,
|
||||
},
|
||||
)
|
||||
.expect("the play authority rules and mints in one patch");
|
||||
(kernel.state.world_id, kernel.snapshot().unwrap())
|
||||
};
|
||||
let replayed = WorldKernel::open(&path, world_id).expect("the store replays");
|
||||
assert_eq!(replayed.snapshot().unwrap(), expected);
|
||||
drop(replayed);
|
||||
let replayed_again =
|
||||
WorldKernel::open(&path, world_id).expect("the store replays a second time");
|
||||
assert_eq!(replayed_again.snapshot().unwrap(), expected);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -648,6 +648,59 @@ impl SeedPort {
|
|||
}
|
||||
}
|
||||
|
||||
/// The play table's one door. Minted only by Dungeon, which constructs one
|
||||
/// `PlayPort` in its `AppState`; exclusivity is that single construction site,
|
||||
/// pinned by a negative grep, exactly as every other system capability's is.
|
||||
/// No session evidence backs it: it is a system capability like the clock,
|
||||
/// not a principal, and it carries no expiry to hold.
|
||||
#[derive(Clone)]
|
||||
pub struct PlayPort {
|
||||
mailbox: WorldMailbox,
|
||||
}
|
||||
|
||||
impl PlayPort {
|
||||
pub fn new(mailbox: WorldMailbox) -> Self {
|
||||
Self { mailbox }
|
||||
}
|
||||
|
||||
pub async fn snapshot(&self) -> Result<WorldSnapshot, MailboxError> {
|
||||
self.mailbox.snapshot().await
|
||||
}
|
||||
|
||||
pub async fn submit_patch(
|
||||
&self,
|
||||
command_id: CommandId,
|
||||
patch: WorldPatch,
|
||||
) -> Result<SubmitReceipt, MailboxError> {
|
||||
self.mailbox
|
||||
.submit_stamped(
|
||||
command_id,
|
||||
CallerId::System(SystemCapability::Play),
|
||||
CommandBody::AdmitPatch {
|
||||
answers: None,
|
||||
patch,
|
||||
},
|
||||
AuthenticatedCaller::verified_system(SystemCapability::Play),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn advance_time(
|
||||
&self,
|
||||
command_id: CommandId,
|
||||
minutes: TickMinutes,
|
||||
) -> Result<SubmitReceipt, MailboxError> {
|
||||
self.mailbox
|
||||
.submit_stamped(
|
||||
command_id,
|
||||
CallerId::System(SystemCapability::Play),
|
||||
CommandBody::AdvanceTime { minutes },
|
||||
AuthenticatedCaller::verified_system(SystemCapability::Play),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// The consumer ingress's narrowing of the mailbox: one method. The ingress
|
||||
/// has no reason to read the world — it selects no answer, pre-validates
|
||||
/// nothing, and returns a receipt rather than state — so a snapshot method here
|
||||
|
|
|
|||
|
|
@ -300,6 +300,9 @@ pub(crate) enum FactStanding {
|
|||
Canonical { evidence: EvidenceRef },
|
||||
/// Asserted by a subject. The kernel does not evaluate the assertion.
|
||||
Claimed { by: SubjectId },
|
||||
/// Stated by the play table. The kernel does not evaluate it; only `Play`
|
||||
/// may write it.
|
||||
Ruled,
|
||||
}
|
||||
|
||||
/// The proposal-time twin of [`FactStanding`].
|
||||
|
|
@ -308,6 +311,7 @@ pub(crate) enum FactStanding {
|
|||
pub(crate) enum FactStandingRef {
|
||||
Canonical { evidence: EvidenceRef },
|
||||
Claimed { by: Ref<SubjectId> },
|
||||
Ruled,
|
||||
}
|
||||
|
||||
/// One `EntityKind::Fact` row's payload. Write-once: only declaration and
|
||||
|
|
@ -1058,6 +1062,13 @@ pub(crate) enum ComponentOp {
|
|||
qty: Quantity,
|
||||
evidence: EvidenceRef,
|
||||
},
|
||||
/// Creates quantity with no receipt. Only `Play` may write it; every other
|
||||
/// author creates quantity through `Admit` and its evidence.
|
||||
Mint {
|
||||
holder: Ref<SubjectId>,
|
||||
resource: Ref<EntityId>,
|
||||
qty: Quantity,
|
||||
},
|
||||
Bind {
|
||||
subject: Ref<SubjectId>,
|
||||
target: DependencyRef,
|
||||
|
|
@ -1560,6 +1571,11 @@ pub enum Mismatch {
|
|||
OutsideJurisdiction {
|
||||
site: Site,
|
||||
},
|
||||
/// A `Ruled` fact or a `Mint` was written by a caller other than the play
|
||||
/// authority.
|
||||
RuledWithoutAuthority {
|
||||
site: Site,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
|
|
@ -1724,6 +1740,11 @@ pub(crate) enum ResolvedOp {
|
|||
qty: Quantity,
|
||||
evidence: EvidenceRef,
|
||||
},
|
||||
Mint {
|
||||
holder: SubjectId,
|
||||
resource: EntityId,
|
||||
qty: Quantity,
|
||||
},
|
||||
Bind {
|
||||
subject: SubjectId,
|
||||
target: DependencyTarget,
|
||||
|
|
@ -2111,6 +2132,7 @@ fn candidate_effective_authority(
|
|||
enum FactCandidate {
|
||||
Canonical,
|
||||
Claimed(Key<SubjectId>),
|
||||
Ruled,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
|
@ -3183,6 +3205,7 @@ pub(super) fn resolve_patch(
|
|||
match &record.standing {
|
||||
FactStanding::Canonical { .. } => FactCandidate::Canonical,
|
||||
FactStanding::Claimed { by } => FactCandidate::Claimed(Key::Existing(*by)),
|
||||
FactStanding::Ruled => FactCandidate::Ruled,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
|
@ -3346,6 +3369,7 @@ pub(super) fn resolve_patch(
|
|||
&mut mismatches,
|
||||
)
|
||||
.map(FactCandidate::Claimed),
|
||||
FactStandingRef::Ruled => Some(FactCandidate::Ruled),
|
||||
};
|
||||
if let Some(standing) = standing {
|
||||
facts.insert(Key::Draft(fact.handle.clone()), standing);
|
||||
|
|
@ -3959,6 +3983,48 @@ pub(super) fn resolve_patch(
|
|||
candidate_set(&mut holdings, &holder_key, &resource_key, admitted);
|
||||
deltas.entry(resource_key).or_default().admitted += u128::from(qty.0);
|
||||
}
|
||||
ComponentOp::Mint {
|
||||
holder,
|
||||
resource,
|
||||
qty,
|
||||
} => {
|
||||
let holder_key = resolve_subject(
|
||||
Site::Operation(position),
|
||||
holder,
|
||||
&index,
|
||||
&state.subjects,
|
||||
&mut mismatches,
|
||||
);
|
||||
let resource_key = resolve_entity(
|
||||
Site::Operation(position),
|
||||
EntityKind::Resource,
|
||||
resource,
|
||||
&index,
|
||||
&state.entities,
|
||||
&mut mismatches,
|
||||
);
|
||||
if qty.0 == 0 {
|
||||
mismatches.push(Mismatch::ZeroQuantity {
|
||||
operation: position,
|
||||
});
|
||||
}
|
||||
let (Some(holder_key), Some(resource_key)) = (holder_key, resource_key) else {
|
||||
continue;
|
||||
};
|
||||
if qty.0 == 0 {
|
||||
continue;
|
||||
}
|
||||
let Some(minted) =
|
||||
candidate_held(&holdings, &holder_key, &resource_key).checked_add(qty.0)
|
||||
else {
|
||||
mismatches.push(Mismatch::QuantityOverflow {
|
||||
operation: position,
|
||||
});
|
||||
continue;
|
||||
};
|
||||
candidate_set(&mut holdings, &holder_key, &resource_key, minted);
|
||||
deltas.entry(resource_key).or_default().admitted += u128::from(qty.0);
|
||||
}
|
||||
ComponentOp::Bind { subject, target } | ComponentOp::Release { subject, target } => {
|
||||
let bind = matches!(operation, ComponentOp::Bind { .. });
|
||||
let subject_key = resolve_subject(
|
||||
|
|
@ -4968,6 +5034,7 @@ pub(super) fn resolve_patch(
|
|||
FactStandingRef::Claimed { by } => FactStanding::Claimed {
|
||||
by: subject_id_of(&key_of(by)),
|
||||
},
|
||||
FactStandingRef::Ruled => FactStanding::Ruled,
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
|
@ -5146,6 +5213,15 @@ pub(super) fn resolve_patch(
|
|||
qty: *qty,
|
||||
evidence: evidence.clone(),
|
||||
},
|
||||
ComponentOp::Mint {
|
||||
holder,
|
||||
resource,
|
||||
qty,
|
||||
} => ResolvedOp::Mint {
|
||||
holder: subject_id_of(&key_of(holder)),
|
||||
resource: entity_id_of(&key_of(resource)),
|
||||
qty: *qty,
|
||||
},
|
||||
ComponentOp::Bind { subject, target } => ResolvedOp::Bind {
|
||||
subject: subject_id_of(&key_of(subject)),
|
||||
target: target_of(target),
|
||||
|
|
@ -5692,7 +5768,7 @@ pub(crate) const PATCH_TOOLS: &[PatchTool] = &[
|
|||
},
|
||||
PatchTool {
|
||||
name: "declare_fact",
|
||||
description: "Declare a fact: a short authored name and the statement it carries.",
|
||||
description: "Declare a fact: a short authored name and the statement it carries. Only the play table may rule a fact.",
|
||||
fields: &[
|
||||
field("handle", PatchFieldKind::Handle),
|
||||
field("label", PatchFieldKind::Label),
|
||||
|
|
@ -5806,6 +5882,16 @@ pub(crate) const PATCH_TOOLS: &[PatchTool] = &[
|
|||
],
|
||||
shape: PatchToolShape::Operate { variant: "admit" },
|
||||
},
|
||||
PatchTool {
|
||||
name: "mint",
|
||||
description: "Create quantity with no receipt. Only the play table may mint.",
|
||||
fields: &[
|
||||
field("holder", PatchFieldKind::Reference("subject")),
|
||||
field("resource", PatchFieldKind::Reference("resource")),
|
||||
field("qty", PatchFieldKind::Quantity),
|
||||
],
|
||||
shape: PatchToolShape::Operate { variant: "mint" },
|
||||
},
|
||||
PatchTool {
|
||||
name: "bind",
|
||||
description: "Record that a subject depends on a resource, a route, or another subject.",
|
||||
|
|
@ -6278,6 +6364,7 @@ fn composite_schema(shape: CompositeShape) -> Value {
|
|||
)],
|
||||
),
|
||||
("claimed", vec![("by".to_owned(), subject_ref_schema())]),
|
||||
("ruled", vec![]),
|
||||
],
|
||||
),
|
||||
CompositeShape::AudienceRef => tool_schema::external_variant(vec![
|
||||
|
|
@ -6848,7 +6935,7 @@ mod catalog_tests {
|
|||
.iter()
|
||||
.filter(|entry| matches!(entry.shape, PatchToolShape::Operate { .. }))
|
||||
.count();
|
||||
assert_eq!((declarations, operations, PATCH_TOOLS.len()), (7, 30, 39));
|
||||
assert_eq!((declarations, operations, PATCH_TOOLS.len()), (7, 31, 40));
|
||||
|
||||
// Every declaration variant the vocabulary owns is reachable, and the
|
||||
// two payload-carrying entity kinds are not exposed as an `Entity`
|
||||
|
|
@ -6920,6 +7007,7 @@ mod catalog_tests {
|
|||
ComponentOp::Transform { .. } => "transform",
|
||||
ComponentOp::Consume { .. } => "consume",
|
||||
ComponentOp::Admit { .. } => "admit",
|
||||
ComponentOp::Mint { .. } => "mint",
|
||||
ComponentOp::Bind { .. } => "bind",
|
||||
ComponentOp::Release { .. } => "release",
|
||||
ComponentOp::GrantAuthority { .. } => "grant_authority",
|
||||
|
|
@ -6993,7 +7081,7 @@ mod catalog_tests {
|
|||
.count();
|
||||
assert_eq!(
|
||||
(declare_tools, operate_tools, PATCH_TOOLS.len()),
|
||||
(7, 30, 39)
|
||||
(7, 31, 40)
|
||||
);
|
||||
|
||||
let declarations = every_declaration();
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@
|
|||
use chrono::{Duration, Utc};
|
||||
use ghostlight::{
|
||||
CommandBody, CommandId, ControllerMode, ControllerPort, CreateWorldIntent, DecisionInvocation,
|
||||
DecisionOpportunity, KernelError, Lens, LensWeights, MailboxError, Mismatch, PrincipalCommandIntent,
|
||||
Statement, SubjectId, SubmitReceipt, VerifiedPrincipalEvidence, WorldMailbox, WorldPhase,
|
||||
WorldSnapshot,
|
||||
DecisionOpportunity, KernelError, Lens, LensWeights, MailboxError, Mismatch, PlayPort,
|
||||
PrincipalCommandIntent, Statement, SubjectId, SubmitReceipt, VerifiedPrincipalEvidence,
|
||||
WorldMailbox, WorldPatch, WorldPhase, WorldSnapshot,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::BTreeMap;
|
||||
|
|
@ -693,6 +693,50 @@ async fn a_lens_set_spelled_with_or_without_its_zero_weights_commits_nothing() {
|
|||
assert_eq!(world.snapshot().await.lens_weights, moved);
|
||||
}
|
||||
|
||||
/// Cut 3: `PlayPort` is the play authority's only door, and it is a public,
|
||||
/// externally constructible type — Dungeon mints exactly one of these in its
|
||||
/// `AppState`. This is a compile-pass use of the port from outside the crate:
|
||||
/// it mints quantity through `Mint`, the spelling only `Play` may write, on a
|
||||
/// resource it declares in the same patch, unconfined and with no answer.
|
||||
#[tokio::test]
|
||||
async fn play_port_is_the_only_play_minter() {
|
||||
let world = World::active().await;
|
||||
let play = PlayPort::new(world.mailbox.clone());
|
||||
let holder = world
|
||||
.snapshot()
|
||||
.await
|
||||
.subjects
|
||||
.first()
|
||||
.expect("genesis declares at least one subject")
|
||||
.id;
|
||||
let patch: WorldPatch = serde_json::from_value(json!({
|
||||
"declarations": [
|
||||
{
|
||||
"type": "entity",
|
||||
"handle": "loot",
|
||||
"label": "Ruled Loot",
|
||||
"kind": "resource",
|
||||
"container": null,
|
||||
},
|
||||
],
|
||||
"operations": [
|
||||
{
|
||||
"op": "mint",
|
||||
"holder": {"ref": "existing", "value": holder},
|
||||
"resource": {"ref": "draft", "value": "loot"},
|
||||
"qty": 5,
|
||||
},
|
||||
],
|
||||
"evidence": [],
|
||||
}))
|
||||
.expect("a mint operation, off the wire, decodes into a WorldPatch");
|
||||
let receipt = play
|
||||
.submit_patch(CommandId::new(), patch)
|
||||
.await
|
||||
.expect("the play authority mints with no answer, unconfined");
|
||||
assert!(matches!(receipt, SubmitReceipt::Applied(_)));
|
||||
}
|
||||
|
||||
/// A lens name the library does not know cannot be decoded into a command
|
||||
/// body, beside names it does. This is a serde limit, stated as one: the body
|
||||
/// never exists, so no kernel admission is being proven here.
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
},
|
||||
{
|
||||
"name": "declare_fact",
|
||||
"parameters_json": "{\"additionalProperties\":false,\"properties\":{\"handle\":{\"description\":\"a new draft handle, unique within this patch\",\"type\":\"string\"},\"label\":{\"description\":\"a display label\",\"type\":\"string\"},\"standing\":{\"anyOf\":[{\"additionalProperties\":false,\"properties\":{\"evidence\":{\"description\":\"an exact evidence receipt\",\"type\":\"string\"},\"standing\":{\"const\":\"canonical\",\"type\":\"string\"}},\"required\":[\"standing\",\"evidence\"],\"type\":\"object\"},{\"additionalProperties\":false,\"properties\":{\"by\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing subject\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"},\"standing\":{\"const\":\"claimed\",\"type\":\"string\"}},\"required\":[\"standing\",\"by\"],\"type\":\"object\"}]},\"statement\":{\"description\":\"the committed world text this fact carries\",\"type\":\"string\"}},\"required\":[\"handle\",\"label\",\"statement\",\"standing\"],\"type\":\"object\"}"
|
||||
"parameters_json": "{\"additionalProperties\":false,\"properties\":{\"handle\":{\"description\":\"a new draft handle, unique within this patch\",\"type\":\"string\"},\"label\":{\"description\":\"a display label\",\"type\":\"string\"},\"standing\":{\"anyOf\":[{\"additionalProperties\":false,\"properties\":{\"evidence\":{\"description\":\"an exact evidence receipt\",\"type\":\"string\"},\"standing\":{\"const\":\"canonical\",\"type\":\"string\"}},\"required\":[\"standing\",\"evidence\"],\"type\":\"object\"},{\"additionalProperties\":false,\"properties\":{\"by\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing subject\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"},\"standing\":{\"const\":\"claimed\",\"type\":\"string\"}},\"required\":[\"standing\",\"by\"],\"type\":\"object\"},{\"additionalProperties\":false,\"properties\":{\"standing\":{\"const\":\"ruled\",\"type\":\"string\"}},\"required\":[\"standing\"],\"type\":\"object\"}]},\"statement\":{\"description\":\"the committed world text this fact carries\",\"type\":\"string\"}},\"required\":[\"handle\",\"label\",\"statement\",\"standing\"],\"type\":\"object\"}"
|
||||
},
|
||||
{
|
||||
"name": "declare_place",
|
||||
|
|
@ -103,6 +103,10 @@
|
|||
"name": "install_incumbent",
|
||||
"parameters_json": "{\"additionalProperties\":false,\"properties\":{\"incumbent\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing subject\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"},\"institution\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing subject\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"},\"office\":{\"description\":\"the office's name\",\"type\":\"string\"}},\"required\":[\"institution\",\"office\",\"incumbent\"],\"type\":\"object\"}"
|
||||
},
|
||||
{
|
||||
"name": "mint",
|
||||
"parameters_json": "{\"additionalProperties\":false,\"properties\":{\"holder\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing subject\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"},\"qty\":{\"maximum\":18446744073709551615,\"minimum\":1,\"type\":\"integer\"},\"resource\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing resource\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"}},\"required\":[\"holder\",\"resource\",\"qty\"],\"type\":\"object\"}"
|
||||
},
|
||||
{
|
||||
"name": "open_forum",
|
||||
"parameters_json": "{\"additionalProperties\":false,\"properties\":{\"forum\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing subject\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"},\"grievance\":{\"description\":\"the grievance kind\",\"type\":\"string\"},\"standing\":{\"anyOf\":[{\"additionalProperties\":false,\"properties\":{\"over\":{\"const\":\"subject\",\"type\":\"string\"},\"ref\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing subject\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"}},\"required\":[\"over\",\"ref\"],\"type\":\"object\"},{\"additionalProperties\":false,\"properties\":{\"over\":{\"const\":\"place_subtree\",\"type\":\"string\"},\"ref\":{\"additionalProperties\":false,\"properties\":{\"ref\":{\"enum\":[\"draft\",\"existing\"],\"type\":\"string\"},\"value\":{\"description\":\"a draft handle declared in this same patch, or the canonical id of an existing place\",\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"}},\"required\":[\"over\",\"ref\"],\"type\":\"object\"}]}},\"required\":[\"grievance\",\"forum\",\"standing\"],\"type\":\"object\"}"
|
||||
|
|
|
|||
Loading…
Reference in a new issue