1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! State struct from SPEC.md.
use crate::base::canonical::map_as_pairs;
use crate::base::hash::Hash256;
use crate::base::time::Time;
use crate::record::author::ActorId;
use crate::record::evidence::Evidence;
use crate::record::kind::RefType;
use crate::record::payloads::UsageOutcomeCounts;
use crate::record::record::{Record, ScopeId};
use crate::record::refs::{RecordId, Ref};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
/// Effective state derived from committed records and verdicts.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct State {
/// Ids of every record whose verdict was Accept; rejected records never
/// appear anywhere in state.
pub accepted_records: BTreeSet<RecordId>,
/// Requests not yet terminal: a request leaves on an explicit terminal
/// event - a Response with `closes_request` (only valid with no open
/// actions) or a Refusal targeting it directly. Completion is never
/// inferred from a transient zero count of open actions.
pub active_requests: BTreeSet<RecordId>,
/// (actor_id, action_class, scope) → latest accepted Capability record;
/// the lookup key Actions are checked against.
#[serde(with = "map_as_pairs")]
pub active_capabilities: BTreeMap<(ActorId, String, ScopeId), RecordId>,
/// Exact approvals: SHA-256(canonical((action_author_id, ActionData)))
/// → Approval record id; consulted first for Ask-mode actions. The
/// hash binds the acting author together with the action content, and
/// entries are removed when consumed (exact approvals are single-use).
#[serde(with = "map_as_pairs")]
pub valid_approvals: BTreeMap<Hash256, RecordId>,
/// Class approvals: (action_class, scope, Some(actor) | None for
/// wildcard) → Approval record id; consulted after exact matches.
#[serde(with = "map_as_pairs")]
pub class_approvals: BTreeMap<(String, ScopeId, Option<ActorId>), RecordId>,
/// Accepted Actions not yet closed by a Result or an action-targeting
/// Refusal.
pub open_actions: BTreeSet<RecordId>,
/// Action id → owning request id; used to decrement the request's open
/// count when the action closes.
#[serde(with = "map_as_pairs")]
pub action_to_request: BTreeMap<RecordId, RecordId>,
/// SHA-256(canonical((summary_type, subject, scope))) → latest accepted
/// Summary record id for that identity.
#[serde(with = "map_as_pairs")]
pub active_summaries: BTreeMap<Hash256, RecordId>,
/// Targets of accepted Replace refs. Superseded but never deleted;
/// excluded from context selection.
pub replaced_records: BTreeSet<RecordId>,
/// Per-request count of still-open actions (the entry is removed at
/// zero for map hygiene). Reaching zero does NOT complete the request:
/// completion is only ever the explicit closing Response or a
/// request-targeting Refusal. This count gates closing (a closing
/// Response is invalid while it is non-zero).
#[serde(with = "map_as_pairs")]
pub request_open_action_count: BTreeMap<RecordId, u64>,
/// (used_record, role) → tallied usage outcomes, fed back into context
/// as usage feedback.
#[serde(with = "map_as_pairs")]
pub usage_counts: BTreeMap<(RecordId, String), UsageOutcomeCounts>,
/// Maps request_id → active plan record_id.
#[serde(with = "map_as_pairs")]
pub active_plans: BTreeMap<RecordId, RecordId>,
/// Request id → count of accepted Responses so far; a Response's
/// `turn_index` must equal this count (gap-free, in order).
#[serde(with = "map_as_pairs")]
pub response_turns: BTreeMap<RecordId, u32>,
/// Capability record id → its (actor, action_class, scope) key. Reverse
/// index so a Retraction can deactivate the capability in O(log n)
/// (linear-cost replay, SPEC §12.3).
#[serde(with = "map_as_pairs")]
pub capability_index: BTreeMap<RecordId, (ActorId, String, ScopeId)>,
/// Exact-approval record id → the action hash it approves. Reverse
/// index for retraction-time deactivation.
#[serde(with = "map_as_pairs")]
pub exact_approval_index: BTreeMap<RecordId, Hash256>,
/// Class-approval record id → its (action_class, scope, actor) key.
/// Reverse index for retraction-time deactivation.
#[serde(with = "map_as_pairs")]
pub class_approval_index: BTreeMap<RecordId, (String, ScopeId, Option<ActorId>)>,
/// Targets of accepted Retractions: records whose content was asserted
/// WRONG. They stay in the log (append-only) but are excluded from
/// context selection, and epistemic dependence on them taints.
pub retracted_records: BTreeSet<RecordId>,
/// Records that epistemically depend - transitively, via `Use`/`Require`
/// refs - on a retracted record. Tainted records replay and verify
/// normally; taint marks their content unreliable, not the history
/// invalid.
pub tainted_records: BTreeSet<RecordId>,
/// Reverse epistemic-dependence index: target → accepted records holding
/// a `Use`/`Require` ref to it. Lets a late retraction taint dependents
/// committed before it.
#[serde(with = "map_as_pairs")]
pub epistemic_dependents: BTreeMap<RecordId, BTreeSet<RecordId>>,
/// Logical time of the last record (subject or verdict) folded into this
/// state; 0 for an empty log.
pub applied_up_to: Time,
}
impl State {
/// The evidence a ref contributes to weakest-link derivation, or None
/// when the ref does not participate. Only `Use`/`Require` refs are
/// epistemic dependence and contribute (degraded to the floor,
/// `Assumed`, when the target is retracted or tainted).
/// `Cause`/`Replace` refs are provenance, not epistemic dependence
/// (SPEC §7.1): they never affect derived evidence, exactly as they
/// never propagate taint - a Result exists because of its Action
/// without resting on the action's claim.
pub fn ref_evidence(&self, ref_: &Ref, target: &Record) -> Option<Evidence> {
match ref_.type_ {
RefType::Use | RefType::Require => {
if !self.accepted_records.contains(&target.id)
|| self.retracted_records.contains(&target.id)
|| self.tainted_records.contains(&target.id)
{
// Rejected, retracted, or tainted content contributes
// the floor: depending on it is an unverified
// assumption.
Some(Evidence::Assumed)
} else {
Some(target.evidence)
}
}
RefType::Cause | RefType::Replace => None,
}
}
}