Watch
1
0
Fork
You've already forked souveraine
0

nervous: gauges and glands are different afferents

A battery at .42 is still .42 when nobody looks — only confidence in an
unrefreshed reading decays. Feeding a fuel gauge into an accumulator would
drain the pack because the reporter went quiet, which is the empty-result
trap wearing a units label. Field keeps exudates; Level keeps readings.

sessiond mounts the somatic types the way machined mounts seed.rs. The
fields are its own — SOMATIC_NERVOUS_SYSTEM.md answers open question 1
with "sessiond, period".
This commit is contained in:
Fimeg 2026-08-15 15:54:10 -04:00
commit 46876865c9
2 changed files with 233 additions and 30 deletions

View file

@ -17,6 +17,18 @@
#[path = "../sessiond/mod.rs"]
pub(crate) mod sessiond;
// The somatic vocabulary, mounted the way machined mounts `identity/seed.rs`.
// It lives under `core/nervous/` because the agent side shares the types, but
// the *fields* are sessiond's — SOMATIC_NERVOUS_SYSTEM.md resolves open
// question 1 with "sessiond, period", and a second holder of the body's state
// would be the fifth blind actor. `use super::belief` inside plexus resolves
// to this crate root here and to `core::nervous` there, so one file serves
// both without a copy.
#[path = "../core/nervous/belief.rs"]
pub(crate) mod belief;
#[path = "../core/nervous/plexus.rs"]
pub(crate) mod plexus;
use std::path::PathBuf;
fn main() -> anyhow::Result<()> {

View file

@ -48,8 +48,9 @@ pub const ALLOSTATIC_THRESHOLD: f32 = 0.7;
#[derive(Debug, Clone)]
pub struct SensorReading {
pub source: String,
/// Secretion amount: positive adds, negative drains. The caller decides
/// the magnitude; the plexus only accumulates.
/// Read as a secretion by an exudate field (positive adds, negative
/// drains) and as the present reading by a gauge. Which one it is comes
/// from how the afferent was declared, never from the reporter.
pub amount: f32,
}
@ -170,15 +171,119 @@ impl Viability {
}
}
/// One named field, its side of the boundary, and its bounds.
/// A measured level, as opposed to a secretion.
///
/// The distinction the ladder already draws between exudate and belief. A
/// battery at .42 is still at .42 when nobody looks, so the *value* must not
/// decay — only confidence in a reading nobody has refreshed. Putting a fuel
/// gauge into an accumulator would make the pack drain because the reporter
/// went quiet, which is the empty-result trap wearing a units label.
#[derive(Debug, Clone)]
struct Level {
name: String,
value: f32,
observed_at: DateTime<Utc>,
prior: Option<(f32, DateTime<Utc>)>,
half_life: Duration,
}
impl Level {
fn new(name: impl Into<String>, half_life: Duration) -> Self {
Self {
name: name.into(),
value: 0.0,
observed_at: DateTime::<Utc>::MIN_UTC,
prior: None,
half_life,
}
}
fn observe(&mut self, value: f32, now: DateTime<Utc>) {
if self.seen() {
self.prior = Some((self.value, self.observed_at));
}
self.value = value.clamp(0.0, 1.0);
self.observed_at = now;
}
fn seen(&self) -> bool {
self.observed_at != DateTime::<Utc>::MIN_UTC
}
fn velocity(&self) -> f32 {
let Some((pv, pt)) = self.prior else {
return 0.0;
};
let dt = (self.observed_at - pt).num_milliseconds() as f32 / 1000.0;
if dt <= 0.0 {
return 0.0;
}
(self.value - pv) / dt
}
fn as_belief(&self, now: DateTime<Utc>) -> Belief<f32> {
if !self.seen() {
return Belief::unknown(now);
}
Belief {
value: Some(self.value),
confidence: 1.0,
trend: Trend::from_velocity(self.velocity()),
persistence: Duration::from_millis(
(now - self.prior.map_or(self.observed_at, |(_, t)| t))
.num_milliseconds()
.max(0) as u64,
),
observed_at: self.observed_at,
half_life: self.half_life,
sources: vec![self.name.clone()],
conflicts: Vec::new(),
}
}
}
/// What a named afferent carries.
#[derive(Debug)]
enum Afferent {
/// Secretions that stack and fade. The weather changed.
Exudate(Field),
/// A gauge. The value stands until re-read; the confidence does not.
Level(Level),
}
/// One named afferent, its side of the boundary, and its bounds.
#[derive(Debug)]
struct FieldEntry {
name: String,
field: Field,
afferent: Afferent,
locus: Locus,
viability: Option<Viability>,
}
impl FieldEntry {
fn belief(&self, now: DateTime<Utc>) -> Belief<f32> {
match &self.afferent {
Afferent::Exudate(f) => f.as_belief(now),
Afferent::Level(l) => l.as_belief(now),
}
}
/// The present magnitude, for the viability arithmetic.
fn magnitude(&self, now: DateTime<Utc>) -> f32 {
match &self.afferent {
Afferent::Exudate(f) => f.level_at(now),
Afferent::Level(l) => l.value,
}
}
fn velocity(&self) -> f32 {
match &self.afferent {
Afferent::Exudate(f) => f.velocity(),
Afferent::Level(l) => l.velocity(),
}
}
}
/// The body's somatic plexus.
#[derive(Debug)]
pub struct SomaticPlexus {
@ -212,7 +317,7 @@ impl SomaticPlexus {
}
}
/// A field reporting the world. Nothing she does moves it.
/// An exudate field reporting the world. Nothing she does moves it.
pub fn with_boundary_field(
mut self,
name: impl Into<String>,
@ -221,7 +326,7 @@ impl SomaticPlexus {
) -> Self {
let name = name.into();
self.fields.push(FieldEntry {
field: Field::new(&name, half_life, now),
afferent: Afferent::Exudate(Field::new(&name, half_life, now)),
name,
locus: Locus::Boundary,
viability: None,
@ -229,8 +334,8 @@ impl SomaticPlexus {
self
}
/// A field inside the boundary. `regulator` is the verb that reaches it;
/// `None` records a §13 defect rather than hiding one.
/// An exudate field inside the boundary. `regulator` is the verb that
/// reaches it; `None` records a §13 defect rather than hiding one.
pub fn with_inner_field(
mut self,
name: impl Into<String>,
@ -241,7 +346,43 @@ impl SomaticPlexus {
) -> Self {
let name = name.into();
self.fields.push(FieldEntry {
field: Field::new(&name, half_life, now),
afferent: Afferent::Exudate(Field::new(&name, half_life, now)),
name,
locus: Locus::Inner {
regulator: regulator.map(str::to_string),
},
viability,
});
self
}
/// A gauge at the boundary — measured, not secreted.
pub fn with_boundary_level(
mut self,
name: impl Into<String>,
half_life: Duration,
) -> Self {
let name = name.into();
self.fields.push(FieldEntry {
afferent: Afferent::Level(Level::new(&name, half_life)),
name,
locus: Locus::Boundary,
viability: None,
});
self
}
/// A gauge inside the boundary — charge, thermal, load.
pub fn with_inner_level(
mut self,
name: impl Into<String>,
half_life: Duration,
regulator: Option<&str>,
viability: Option<Viability>,
) -> Self {
let name = name.into();
self.fields.push(FieldEntry {
afferent: Afferent::Level(Level::new(&name, half_life)),
name,
locus: Locus::Inner {
regulator: regulator.map(str::to_string),
@ -270,8 +411,20 @@ impl SomaticPlexus {
self.fields.iter().find(|e| e.name == name)
}
/// The exudate field by that name, if it is one. Gauges answer `None`
/// here and through [`Self::magnitude`] instead.
pub fn field(&self, name: &str) -> Option<&Field> {
self.entry(name).map(|e| &e.field)
match &self.entry(name)?.afferent {
Afferent::Exudate(f) => Some(f),
Afferent::Level(_) => None,
}
}
/// Present magnitude of any afferent, exudate or gauge. `None` when it
/// has never spoken — which is not zero.
pub fn magnitude(&self, name: &str, now: DateTime<Utc>) -> Option<f32> {
let e = self.entry(name)?;
e.belief(now).is_known().then(|| e.magnitude(now))
}
pub fn locus(&self, name: &str) -> Option<&Locus> {
@ -298,11 +451,11 @@ impl SomaticPlexus {
.collect()
}
/// Beliefs paired with their field names.
/// Beliefs paired with their afferent names.
pub fn named_beliefs(&self, now: DateTime<Utc>) -> Vec<(&str, Belief<f32>)> {
self.fields
.iter()
.map(|e| (e.name.as_str(), e.field.as_belief(now)))
.map(|e| (e.name.as_str(), e.belief(now)))
.collect()
}
@ -315,12 +468,12 @@ impl SomaticPlexus {
pub fn allostatic_pressure(&self, name: &str, now: DateTime<Utc>) -> Option<f32> {
let e = self.entry(name)?;
let v = e.viability.as_ref()?;
if !e.field.as_belief(now).is_known() {
if !e.belief(now).is_known() {
return None;
}
let level = e.field.level_at(now);
let level = e.magnitude(now);
let proximity = v.boundary_proximity(level);
let Some(eta) = v.eta_secs(level, e.field.velocity()) else {
let Some(eta) = v.eta_secs(level, e.velocity()) else {
return Some(proximity);
};
let horizon = ALLOSTATIC_HORIZON.as_secs_f64();
@ -352,10 +505,9 @@ impl SomaticPlexus {
.iter()
.filter_map(|e| {
let v = e.viability.as_ref()?;
e.field
.as_belief(now)
e.belief(now)
.is_known()
.then(|| v.deviation(e.field.level_at(now)))
.then(|| v.deviation(e.magnitude(now)))
})
.sum()
}
@ -422,7 +574,7 @@ impl SomaticPlexus {
.fields
.iter()
.filter_map(|e| {
let belief = e.field.as_belief(now);
let belief = e.belief(now);
if !belief.is_known() {
return None;
}
@ -466,7 +618,7 @@ impl SomaticPlexus {
}
let v = e.viability.as_ref()?;
let eta = v
.eta_secs(e.field.level_at(now), e.field.velocity())
.eta_secs(e.magnitude(now), e.velocity())
.map(|s| s.max(0.0) as u64);
Some((e.name.clone(), pressure, eta))
})
@ -516,7 +668,13 @@ impl NervousNode for SomaticPlexus {
self.health.insert(input.source.clone(), SourceHealth::Live);
if let Some(e) = self.fields.iter_mut().find(|e| e.name == input.source) {
e.field.secrete(input.amount, now);
// The afferent's declared kind decides what the number means, so
// a reporter never has to know whether it is feeding a gauge or
// a gland.
match &mut e.afferent {
Afferent::Exudate(f) => f.secrete(input.amount, now),
Afferent::Level(l) => l.observe(input.amount, now),
}
}
}
@ -527,7 +685,7 @@ impl NervousNode for SomaticPlexus {
}
fn beliefs(&self, now: DateTime<Utc>) -> Vec<Belief<f32>> {
self.fields.iter().map(|e| e.field.as_belief(now)).collect()
self.fields.iter().map(|e| e.belief(now)).collect()
}
fn notable(&mut self) -> Vec<SomaticEvent> {
@ -558,26 +716,23 @@ mod tests {
.with_boundary_field("motion", Duration::from_secs(60), now)
.with_boundary_field("light", Duration::from_secs(120), now)
.with_boundary_field("touch", Duration::from_secs(10), now)
.with_inner_field(
.with_inner_level(
"charge",
Duration::from_secs(300),
Some("doze"),
Some(Viability::floor(0.2)),
now,
)
.with_inner_field(
.with_inner_level(
"thermal",
Duration::from_secs(120),
Some("doze"),
Some(Viability::ceiling(0.8)),
now,
)
.with_inner_field(
.with_inner_level(
"port_mode",
Duration::from_secs(600),
Some("set_usb_mode"),
None,
now,
)
.with_expected("proximity")
.with_expected("light")
@ -613,6 +768,41 @@ mod tests {
assert!(late < 0.15, "two half-lives leaves little: {late}");
}
#[test]
fn a_gauge_holds_its_value_while_an_exudate_fades() {
let mut p = phone_plexus();
p.ingest(read("charge", 0.42), t0());
p.ingest(read("proximity", 0.5), t0());
// Ten minutes of silence. The battery is still where it was; the
// secretion is not.
let charge = p.magnitude("charge", after(600)).unwrap();
let prox = p.magnitude("proximity", after(600)).unwrap();
assert!(
(charge - 0.42).abs() < 0.001,
"a pack does not drain because nobody looked: {charge}"
);
assert!(prox < 0.05, "an exudate fades: {prox}");
}
#[test]
fn an_unread_gauge_loses_confidence_but_not_its_reading() {
let mut p = phone_plexus();
p.ingest(read("charge", 0.42), t0());
let fresh = p.named_beliefs(t0());
let stale = p.named_beliefs(after(900));
let pick = |bs: &Vec<(&str, Belief<f32>)>, at: DateTime<Utc>| {
let b = bs.iter().find(|(n, _)| *n == "charge").unwrap().1.clone();
(b.value.unwrap(), b.confidence_at(at))
};
let (v0, c0) = pick(&fresh, t0());
let (v1, c1) = pick(&stale, after(900));
assert_eq!(v0, v1, "the reading stands");
assert!(c1 < c0 * 0.2, "trust in it does not: {c0} -> {c1}");
}
#[test]
fn unsecreted_fields_report_unknown_not_zero() {
let p = phone_plexus();
@ -822,7 +1012,8 @@ mod tests {
p.ingest(read("charge", 0.9), t0());
let resting = p.allostatic_pressure("charge", after(1)).unwrap();
p.ingest(read("charge", -0.35), after(2));
// A gauge is *told* its new reading; it is not decremented.
p.ingest(read("charge", 0.55), after(2));
let draining = p.allostatic_pressure("charge", after(2)).unwrap();
assert!(
draining > resting,
@ -834,7 +1025,7 @@ mod tests {
fn viability_threat_fires_on_the_edge_only() {
let mut p = phone_plexus();
p.ingest(read("charge", 0.9), t0());
p.ingest(read("charge", -0.6), after(1));
p.ingest(read("charge", 0.30), after(1));
p.tick(after(1));
let threats: Vec<_> = p