memory: a persistent kind for what a Mind keeps (eureka-pipeline, huginn-mind)

epiphany.pipeline.memory.v2, instance-rooted `<instance>:memory:<label>`:
PipelineMemory with seven sorts (Asked, Decided, Committed, Learned, Did,
Idea, Rapport), her own text, who it is about, where it was learned, whether
directly, what it rests on, a salience that only ranks, and the memories a
consolidating one replaces.

Admission refuses a foreign instance and resolves a memory only Deferred to a
follow-up (an ask taken on), Recorded, Withdrawn or Superseded by a memory.
Memories have their own lineage so supersession stays within one mind. The
read side selects by sort, about (multi-valued) and direct; the index carries
the text; `consolidates` is a citation role.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-31 15:15:40 +01:00
commit 152e315e5d
23 changed files with 693 additions and 41 deletions

View file

@ -26,6 +26,7 @@ sample.stewardship yggdrasil:stewardship:gamecult_-epiphany.n1 epiphany.pipeline
sample.hand_off yggdrasil:hand_off:thought-cage.gamecult_-epiphany.2026-09-15 epiphany.pipeline.hand_off.v2 EpiphanyPipelineHandOffDocument 9186ad66726f6d5f696e7374616e6365a979676764726173696cab746f5f696e7374616e6365ac74686f756768742d63616765a47265706fb147616d6543756c742f4570697068616e79a9646f63756d656e747392ac657572656b612d7374617465b6657572656b612d73746174653a72756c696e673a5238a6726561736f6ed92854686520776f726b73746174696f6e206d696e642074616b6573207468652063616d706169676e2ea968616e6465645f6f6eaa323032362d30392d3135
sample.run yggdrasil:run:mind-2026-10-09-1 epiphany.pipeline.run.v2 EpiphanyPipelineRunDocument 9189a8696e7374616e6365a979676764726173696ca56c6162656cb16d696e642d323032362d31302d30392d31ab6f706572617465645f6279a44d696e64a47475726ea753656c6652756ea4686f7374a979676764726173696caa737461727465645f6f6eaa323032362d30392d3135aa6275646765745f757364a3372e35a6636c61696d739182a46b696e64a743757453706563a26964d925657572656b612d73746174653a6375745f737065633a6375742d72756e2d6b696e642e7232a963616d706169676e7391ac657572656b612d7374617465
sample.pulse yggdrasil:pulse:2026-10-31t041200-t0 epiphany.pipeline.pulse.v2 EpiphanyPipelinePulseDocument 9185a8696e7374616e6365a979676764726173696ca56c6162656cb4323032362d31302d3331743034313230302d7430b174656d706f5f766f74655f626561745f73cd0258b0757267656e63795f7065726d696c6c65ccfaa46e6f7465d92254686520737761726d2069732069646c653b20612073686f7274657220626561742e
sample.memory yggdrasil:memory:2026-10-31t041200-t0-m0 epiphany.pipeline.memory.v2 EpiphanyPipelineMemoryDocument 918aa8696e7374616e6365a979676764726173696ca56c6162656cb7323032362d31302d3331743034313230302d74302d6d30a4736f7274a9436f6d6d6974746564a474657874d936492074616b6520746865206261636b67726f756e6420736861646572733b204c6962627920686f6c64732074686520736f756e64732ea561626f757491ab406c696262793a63686174aa6c6561726e65645f696ea5737761726da6646972656374c2a7736f757263657391a3246531b173616c69656e63655f7065726d696c6c65cd02bcac636f6e736f6c69646174657390
resolution.superseded eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581aa5375706572736564656481a262799282a46b696e64a652756c696e67a26964b6657572656b612d73746174653a72756c696e673a523882a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135
resolution.fixed-by-none eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581a5466978656482a6636f6d6d6974a83566393832323864a26279c0a9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135
resolution.fixed-by-some eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581a5466978656482a6636f6d6d6974a83566393832323864a2627982a46b696e64a94375745265706f7274a26964d921657572656b612d73746174653a6375745f7265706f72743a6375742d33612e6831a9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135

View file

@ -606,6 +606,26 @@ fn pulse_is_well_formed(pulse: &PipelinePulse, at: &str) -> Result<(), PipelineR
Ok(())
}
/// The most thousandths a memory's salience may carry.
pub const SALIENCE_MAX_PERMILLE: u16 = 1000;
/// A memory says something (its text is not empty), its salience is in
/// thousandths, and it consolidates only memories.
fn memory_is_well_formed(memory: &PipelineMemory, at: &str) -> Result<(), PipelineRefusal> {
if memory.text.0.trim().is_empty() {
return Err(format_error(&format!("{at}.text"), "an empty memory"));
}
if memory.salience_permille > SALIENCE_MAX_PERMILLE {
return Err(format_error(&format!("{at}.salience_permille"), &memory.salience_permille.to_string()));
}
for (index, entry) in memory.consolidates.iter().enumerate() {
if entry.kind != PipelineKind::Memory {
return Err(format_error(&format!("{at}.consolidates[{index}].kind"), entry.kind.name()));
}
}
Ok(())
}
unit_enums! {
EvidenceKind { Command, Test, Mutation, Probe, SourceRead, Capture }
ClaimOutcome { Holds, Falsified, Unproven }
@ -615,6 +635,7 @@ unit_enums! {
FindingOrigin { Introduced, PreExisting }
RunOperator { Operator, Mind }
RunTurn { PersonaTurn, SelfRun }
MemorySort { Asked, Decided, Committed, Learned, Did, Idea, Rapport }
}
value_types! {
@ -754,6 +775,20 @@ value_types! {
pub struct PipelinePulse {
instance: Slug, label: Label, tempo_vote_beat_s: Option<u32>, tempo_vote_withdrawn: bool = absent, urgency_permille: Option<u16>, note: Line,
} => pulse_is_well_formed
/// One thing a Mind keeps for herself, in her own mind: what she was asked,
/// decided, committed to, learned or did, an idea, or the rapport between
/// her and another Mind or person. `text` is her own words. `about` names
/// the Matrix users it concerns (Minds included), `learned_in` where she
/// learned it (a room alias, a DM's room id, or `self run <label>`),
/// `direct` whether it came in a direct message, and `sources` the event
/// ids, commits or documents it rests on; the organ fills the last three,
/// not the model. `salience_permille` ranks it and decides nothing.
/// `consolidates` names the memories a consolidating memory replaces.
/// Instance-rooted: the key is `<instance>:memory:<label>`.
pub struct PipelineMemory {
instance: Slug, label: Label, sort: MemorySort, text: Line, about: Vec<Short>[8], learned_in: Short, direct: bool,
sources: Vec<Short>[8], salience_permille: u16, consolidates: Vec<PipelineRef>[16],
} => memory_is_well_formed
}
/// A reference to another document. The id is parsed as a full pipeline id of
@ -856,7 +891,7 @@ macro_rules! pipeline_kinds {
/// because a value is a named map of its own and an internal tag would
/// have to be merged into it; the tag stays beside the value instead.
/// This is not a published schema: `schemas/cultnet` publishes the
/// fifteen per-kind value documents, and the envelope is the wire's
/// sixteen per-kind value documents, and the envelope is the wire's
/// shape, not a document's.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
@ -977,6 +1012,8 @@ pipeline_kinds! {
"epiphany.pipeline.run.v2", "EpiphanyPipelineRunDocument";
Pulse(PipelinePulse) => EpiphanyPipelinePulseDocument, "pulse",
"epiphany.pipeline.pulse.v2", "EpiphanyPipelinePulseDocument";
Memory(PipelineMemory) => EpiphanyPipelineMemoryDocument, "memory",
"epiphany.pipeline.memory.v2", "EpiphanyPipelineMemoryDocument";
}
/// Parses a full document id: the grammar read backwards. A key and an id are
@ -1173,6 +1210,7 @@ pub fn pipeline_key(document: &PipelineDocument) -> Result<String, PipelineRefus
// A run hangs off its instance too: its label is its whole local.
D::Run(value) => ("run.instance", value.instance.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Pulse(value) => ("pulse.instance", value.instance.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Memory(value) => ("memory.instance", value.instance.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Target(value) => (campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[&format!("r{}", value.revision)])?),
D::Question(value) => (campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Ruling(value) => (campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
@ -1421,6 +1459,11 @@ mod tests {
instance: slug(INSTANCE), label: l("2026-10-31t041200-t0"), tempo_vote_beat_s: Some(600), tempo_vote_withdrawn: false, urgency_permille: Some(250),
note: "The swarm is idle; a shorter beat.".into(),
}), format!("{INSTANCE}:pulse:2026-10-31t041200-t0")),
(D::Memory(PipelineMemory {
instance: slug(INSTANCE), label: l("2026-10-31t041200-t0-m0"), sort: MemorySort::Committed,
text: "I take the background shaders; Libby holds the sounds.".into(), about: vec![s("@libby:chat")],
learned_in: s("swarm"), direct: false, sources: vec![s("$e1")], salience_permille: 700, consolidates: vec![],
}), format!("{INSTANCE}:memory:2026-10-31t041200-t0-m0")),
]
}
@ -1753,6 +1796,42 @@ mod tests {
}
}
fn memory_sample() -> PipelineMemory {
let PipelineDocument::Memory(memory) = samples().remove(15).0 else { unreachable!() };
memory
}
/// A memory says something, its salience is in thousandths, and it consolidates only memories.
#[test]
fn a_memory_says_something_in_bounds_and_consolidates_only_memories() {
let validated = |change: &dyn Fn(&mut PipelineMemory)| {
let mut memory = memory_sample();
change(&mut memory);
PipelineDocument::Memory(memory).validate()
};
assert_eq!(validated(&|_| {}), Ok(()));
assert_eq!(validated(&|memory| memory.salience_permille = 1000), Ok(()));
let bad = |outcome: Result<(), PipelineRefusal>, expected: &str| {
assert!(matches!(outcome, Err(PipelineRefusal::InvalidFormat { field, .. }) if field == expected), "{expected}");
};
bad(validated(&|memory| memory.text = " ".into()), "memory.text");
bad(validated(&|memory| memory.text = "".into()), "memory.text");
bad(validated(&|memory| memory.salience_permille = 1001), "memory.salience_permille");
bad(validated(&|memory| memory.consolidates = vec![PipelineRef { kind: PipelineKind::Ruling, id: id("ruling", "R8") }]), "memory.consolidates[0].kind");
let own = PipelineRef { kind: PipelineKind::Memory, id: Short(format!("{INSTANCE}:memory:older")) };
assert_eq!(validated(&|memory| memory.consolidates = vec![own.clone()]), Ok(()));
assert!(matches!(validated(&|memory| memory.about = vec![s("@a:chat"); 9]), Err(PipelineRefusal::FieldBound { limit: 8, .. })));
assert!(matches!(validated(&|memory| memory.sources = vec![s("$e"); 9]), Err(PipelineRefusal::FieldBound { limit: 8, .. })));
assert!(matches!(validated(&|memory| memory.text = Line("x".repeat(1001))), Err(PipelineRefusal::FieldBound { limit: 1000, .. })));
}
/// A memory is instance-rooted and its label is its whole local.
#[test]
fn a_memory_is_keyed_under_its_instance_by_its_label() -> Result<()> {
assert_eq!(pipeline_key(&PipelineDocument::Memory(memory_sample()))?, format!("{INSTANCE}:memory:2026-10-31t041200-t0-m0"));
Ok(())
}
/// Bounds: a run's campaigns and claims stop at their maximums.
#[test]
fn a_runs_lists_are_bounded() {
@ -3307,11 +3386,11 @@ mod tests {
};
let document_schema = serde_json::to_value(schemars::schema_for!(PipelineDocument))?;
let variants = document_schema["oneOf"].as_array().expect("the document schema is a oneOf");
assert_eq!(PipelineKind::ALL.len(), 15, "fifteen kinds");
assert_eq!(PipelineKind::ALL.len(), 16, "sixteen kinds");
assert_eq!(
variants.iter().map(tag).collect::<Vec<_>>(),
PipelineKind::ALL.iter().map(|kind| kind.name().to_owned()).collect::<Vec<_>>(),
"the document schema lists exactly the fifteen kinds, by name"
"the document schema lists exactly the sixteen kinds, by name"
);
let refusal_schema = serde_json::to_value(schemars::schema_for!(PipelineRefusal))?;

View file

@ -20,7 +20,7 @@ macro_rules! kinds {
kinds!(
campaign, target, question, ruling, cut_spec, cut_report, verdict, finding, follow_up, resolution, instance, stewardship,
hand_off, run, pulse,
hand_off, run, pulse, memory,
);
/// The schema named, or the names when none is: the exit code is 2 for a name

View file

@ -974,10 +974,11 @@ fn schema_prints_the_published_files() {
let names = run(&[], &["schema"], None);
assert_eq!(names.code, 0);
let names: Vec<&str> = names.out.lines().collect();
assert_eq!(names.len(), 16, "{names:?}");
assert_eq!(names.len(), 17, "{names:?}");
assert_eq!(names[0], "campaign");
assert_eq!(names[14], "pulse");
assert_eq!(names[15], "selection");
assert_eq!(names[15], "memory");
assert_eq!(names[16], "selection");
for kind in PipelineKind::ALL {
assert!(names.contains(&kind.name()), "{}", kind.name());
}

View file

@ -312,6 +312,7 @@ fn refuse_foreign_instance(document: &PipelineDocument, mind: &Slug) -> Result<(
PipelineDocument::Stewardship(value) if value.instance != *mind => Err(foreign(&value.instance)),
PipelineDocument::Run(value) if value.instance != *mind => Err(foreign(&value.instance)),
PipelineDocument::Pulse(value) if value.instance != *mind => Err(foreign(&value.instance)),
PipelineDocument::Memory(value) if value.instance != *mind => Err(foreign(&value.instance)),
PipelineDocument::HandOff(value) if value.from_instance != *mind && value.to_instance != *mind => {
Err(foreign(&value.from_instance))
}
@ -621,7 +622,7 @@ fn check(docs: &Docs, staged: &Staged, mind: &Slug) -> Result<(), MindRefusal> {
let replay = docs.in_image(K::Run, key) == Some(&staged.document);
run_rule(docs, key, run, replay)
}
D::FollowUp(_) | D::Instance(_) | D::Pulse(_) => Ok(()),
D::FollowUp(_) | D::Instance(_) | D::Pulse(_) | D::Memory(_) => Ok(()),
}
}
@ -716,6 +717,9 @@ fn matrix(subject: PipelineKind, outcome: &ResolutionOutcome) -> bool {
(K::Stewardship, O::Superseded { by }) => all(by, K::Stewardship),
(K::Stewardship, O::Withdrawn { .. }) => true,
(K::Run, O::Recorded { .. } | O::Withdrawn { .. }) => true,
(K::Memory, O::Deferred { to }) => to.kind == K::FollowUp,
(K::Memory, O::Recorded { .. } | O::Withdrawn { .. }) => true,
(K::Memory, O::Superseded { by }) => all(by, K::Memory),
(K::Resolution, O::Withdrawn { .. }) => true,
_ => false,
}
@ -843,7 +847,7 @@ mod tests {
use crate::receipt::{DocumentVersion, Faculty, HuginnCommitReceipt};
use crate::store::test_stores::{MemoryStore, RefusingStore, SwapCommand};
use cultcache_rs::DatabaseEntry;
use eureka_pipeline::{PIPELINE_SCHEMA_EPOCH, PipelineDocument as D, PipelineKind as K, RunTurn};
use eureka_pipeline::{MemorySort, PIPELINE_SCHEMA_EPOCH, PipelineDocument as D, PipelineKind as K, RunTurn};
use sha2::{Digest, Sha256};
fn receipt_count<S: MindStore>(mind: &Mind<S>) -> usize {
@ -2462,6 +2466,82 @@ mod tests {
committed(admit(&mut mind, vec![pulse(INSTANCE)]));
}
fn memory_key(label: &str) -> String {
format!("{INSTANCE}:memory:{label}")
}
/// A memory is her own: one of another mind is a ForeignInstance, and her own is admitted.
#[test]
fn a_memory_of_another_mind_is_a_foreign_instance_and_her_own_is_admitted() {
let mut mind = with_specs();
let D::Memory(mut foreign) = memory("m1", MemorySort::Asked, "Prioritise the fork.", &["@op:chat"]) else { panic!() };
foreign.instance = slug(OTHER_INSTANCE);
assert_eq!(
refusal(admit(&mut mind, vec![D::Memory(foreign)])),
MindRefusal::ForeignInstance { declared: OTHER_INSTANCE.into(), mind: INSTANCE.into() }
);
committed(admit(&mut mind, vec![memory("m1", MemorySort::Asked, "Prioritise the fork.", &["@op:chat"])]));
}
/// A memory is taken on (Deferred to a follow-up), recorded, withdrawn or superseded by a
/// memory; Fixed, Answered, and Deferred or Superseded to anything else are refused.
#[test]
fn a_memory_resolves_deferred_to_a_follow_up_recorded_withdrawn_or_superseded_by_a_memory_only() {
let incompatible = |outcome: &str| MindRefusal::IncompatibleResolution { subject_kind: K::Memory, outcome: outcome.into() };
let subject = || r(K::Memory, &memory_key("m1"));
let follow = || r(K::FollowUp, &id("follow_up", "FU"));
let open = || {
let mut mind = with_specs();
committed(admit(&mut mind, vec![
D::Ruling(ruling("R1")),
follow_up("FU", r(K::CutSpec, &id("cut_spec", "cut-1.r1"))),
memory("m1", MemorySort::Asked, "Prioritise the fork.", &[]),
memory("m2", MemorySort::Idea, "A newer way.", &[]),
]));
mind
};
for outcome in [
ResolutionOutcome::Deferred { to: follow() },
ResolutionOutcome::Recorded { reason: "done".into() },
withdrawn(),
superseded(&[r(K::Memory, &memory_key("m2"))]),
] {
committed(admit(&mut open(), vec![resolution(subject(), outcome)]));
}
let refused = [
(ResolutionOutcome::Deferred { to: r(K::CutSpec, &id("cut_spec", "cut-1.r1")) }, "Deferred"),
(ResolutionOutcome::Fixed { commit: sha(), by: None }, "Fixed"),
(ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) }, "Answered"),
(superseded(&[r(K::Ruling, &id("ruling", "R1"))]), "Superseded"),
];
for (outcome, name) in refused {
assert_eq!(refusal(admit(&mut open(), vec![resolution(subject(), outcome)])), incompatible(name), "{name}");
}
}
/// A memory consolidates memories of this mind: a ruling is refused by the leaf, another mind's
/// memory is a missing reference, her own is cited.
#[test]
fn a_memory_consolidates_only_memories_in_this_mind() {
let mut mind = with_specs();
committed(admit(&mut mind, vec![D::Ruling(ruling("R1")), memory("m1", MemorySort::Idea, "One.", &[])]));
let consolidating = |cites: PipelineRef| {
let D::Memory(mut memory) = memory("m2", MemorySort::Idea, "One and more.", &[]) else { panic!() };
memory.consolidates = vec![cites];
D::Memory(memory)
};
assert!(matches!(
refusal(admit(&mut mind, vec![consolidating(r(K::Ruling, &id("ruling", "R1")))])),
MindRefusal::Document(PipelineRefusal::InvalidFormat { .. })
));
let elsewhere = format!("{OTHER_INSTANCE}:memory:m1");
assert_eq!(
refusal(admit(&mut mind, vec![consolidating(r(K::Memory, &elsewhere))])),
MindRefusal::MissingReference { kind: K::Memory, id: elsewhere }
);
committed(admit(&mut mind, vec![consolidating(r(K::Memory, &memory_key("m1")))]));
}
/// A run ends: Recorded or Withdrawn, and no other outcome.
#[test]
fn a_run_resolves_recorded_or_withdrawn_and_nothing_else() {

View file

@ -317,6 +317,9 @@ pub(crate) enum Lineage<'a> {
Cut(&'a Slug, &'a Label),
/// A stewardship: the instance and the repo.
Assignment(&'a Slug, &'a OrgRepo),
/// A memory: the instance whose mind holds it, so a consolidating memory
/// replaces only her own.
Memories(&'a Slug),
/// A ruling or a follow-up: operator directions and re-homed work cut
/// across campaigns.
Global,
@ -328,6 +331,7 @@ pub(crate) fn line_of(document: &PipelineDocument) -> Option<Lineage<'_>> {
D::Target(target) => Some(Lineage::Campaign(&target.campaign)),
D::CutSpec(spec) => Some(Lineage::Cut(&spec.campaign, &spec.cut)),
D::Stewardship(stewardship) => Some(Lineage::Assignment(&stewardship.instance, &stewardship.repo)),
D::Memory(memory) => Some(Lineage::Memories(&memory.instance)),
D::Ruling(_) | D::FollowUp(_) => Some(Lineage::Global),
D::Campaign(_)
| D::Question(_)
@ -382,10 +386,11 @@ pub enum CitationRole {
DeferredTo,
Documents,
Claims,
Consolidates,
}
impl CitationRole {
pub const ALL: [CitationRole; 16] = [
pub const ALL: [CitationRole; 17] = [
Self::RaisedIn,
Self::Answers,
Self::Rulings,
@ -402,6 +407,7 @@ impl CitationRole {
Self::DeferredTo,
Self::Documents,
Self::Claims,
Self::Consolidates,
];
/// The wire spelling, equal to the referring field's name.
@ -423,6 +429,7 @@ impl CitationRole {
Self::DeferredTo => "deferred_to",
Self::Documents => "documents",
Self::Claims => "claims",
Self::Consolidates => "consolidates",
}
}
@ -444,6 +451,7 @@ impl CitationRole {
Self::Subject | Self::SupersededBy | Self::ResolvedBy | Self::DeferredTo => K::Resolution,
Self::Documents => K::HandOff,
Self::Claims => K::Run,
Self::Consolidates => K::Memory,
}
}
@ -460,6 +468,7 @@ impl CitationRole {
Self::Findings => vec![K::Finding],
Self::Verdict => vec![K::Verdict],
Self::Claims => vec![K::CutSpec, K::Finding, K::FollowUp],
Self::Consolidates => vec![K::Memory],
Self::RaisedIn
| Self::Source
| Self::Subject
@ -538,6 +547,7 @@ pub(crate) fn citations(document: &PipelineDocument) -> Vec<(CitationRole, Pipel
hand_off.documents.iter().filter_map(|to| kind_of_id(&to.0).map(|kind| (R::Documents, cited(kind, to)))),
),
D::Run(run) => edges.extend(run.claims.iter().map(|to| (R::Claims, to.clone()))),
D::Memory(memory) => edges.extend(memory.consolidates.iter().map(|to| (R::Consolidates, to.clone()))),
D::Campaign(_) | D::Target(_) | D::Instance(_) | D::Stewardship(_) | D::Pulse(_) => {}
}
edges

View file

@ -6,9 +6,9 @@ use chrono::{DateTime, TimeZone, Utc};
use cultcache_rs::CultCacheEnvelope;
use eureka_pipeline::{
AuthorityMap, ClaimOutcome, CodeLocation, CommitRange, CutVerification, Date, DocRef, Evidence, EvidenceKind,
FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, MutationRecord, OrgRepo, PipelineCampaign,
FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, MemorySort, MutationRecord, OrgRepo, PipelineCampaign,
PipelineCutReport, PipelineCutSpec, PipelineDocument, PipelineFinding, PipelineFollowUp, PipelineHandOff,
PipelineInstance, PipelineKind, PipelineQuestion, PipelineRef, PipelineResolution, PipelineRuling,
PipelineInstance, PipelineKind, PipelineMemory, PipelineQuestion, PipelineRef, PipelineResolution, PipelineRuling,
PipelineRun, PipelineStewardship, PipelineTarget, PipelineVerdict, Promise, QuestionOption, ReportCommit,
ResolutionOutcome, RulingAuthority, RunOperator, RunTurn, Sha, Short, Slug, StructuralDelta, TargetInvariant, VerdictClaim,
};
@ -354,6 +354,22 @@ fn run_of(label: &str, operated_by: RunOperator, turn: RunTurn, claims: &[Pipeli
})
}
/// One of her memories: the sort and the text, about `about`, learned in a room, not direct.
pub(crate) fn memory(label: &str, sort: MemorySort, text: &str, about: &[&str]) -> PipelineDocument {
PipelineDocument::Memory(PipelineMemory {
instance: slug(INSTANCE),
label: l(label),
sort,
text: text.into(),
about: about.iter().map(|who| s(who)).collect(),
learned_in: s("swarm"),
direct: false,
sources: vec![],
salience_permille: 500,
consolidates: vec![],
})
}
pub(crate) fn provenance(faculty: Faculty) -> PipelineProvenance {
PipelineProvenance { faculty, agent: s("claude"), session: s("session-1"), tool: s("admit") }
}

View file

@ -86,6 +86,7 @@ pub fn index_text(document: &PipelineDocument) -> Option<String> {
}
D::Resolution(resolution) => lines.push(&resolution.rationale.0),
D::HandOff(hand_off) => lines.push(&hand_off.reason.0),
D::Memory(memory) => lines.push(&memory.text.0),
D::Instance(_) | D::Stewardship(_) | D::Run(_) | D::Pulse(_) => {}
}
lines.retain(|line| !line.is_empty());
@ -155,7 +156,7 @@ mod tests {
use crate::receipt::Faculty;
use crate::store::test_stores::MemoryStore;
use eureka_pipeline::{
ClaimOutcome, Deviation, FindingConfidence, PipelineDocument as D, PipelineKind as K, RulingAuthority,
ClaimOutcome, Deviation, FindingConfidence, MemorySort, PipelineDocument as D, PipelineKind as K, RulingAuthority,
};
use std::collections::BTreeMap;
@ -201,6 +202,7 @@ mod tests {
D::Finding(finding("1", 1, "F1", FindingConfidence::Confirmed)),
],
));
committed(admit(&mut mind, vec![memory("M1", MemorySort::Idea, "A shared idea.", &[])]));
let (_, writes) = committed(admit(&mut mind, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[])]));
let hand_off_key = writes.iter().find(|write| write.kind == K::HandOff).unwrap().id.0.clone();
(mind, hand_off_key)
@ -231,6 +233,7 @@ mod tests {
// The withdrawn closure of Q1, admitted by hand.
(id("resolution", "resolution.question.Q1.n1.n1"), "Resolved."),
(hand_off_key, "The workstation mind takes the campaign."),
(format!("{INSTANCE}:memory:M1"), "A shared idea."),
];
let held = texts(&mind);
for (key, text) in &expected {

View file

@ -72,8 +72,8 @@ fn require_grammatical_slug(field: &str, declared: &Slug) -> Result<(), MindRefu
})
}
/// A cache that knows the seventeen types a mind's store may hold: the leaf's
/// fifteen through its registrar, the epoch record, the commit receipt and
/// A cache that knows the nineteen types a mind's store may hold: the leaf's
/// sixteen through its registrar, the epoch record, the commit receipt and
/// the persona entry. It is the cache every envelope is prepared against.
pub(crate) fn schema_cache() -> Result<CultCache, MindRefusal> {
let mut cache = CultCache::new();
@ -171,7 +171,7 @@ impl<S: MindStore> Mind<S> {
/// Fail-closed, in this order, nothing attached until every step passes:
/// pull the raw envelopes; if anything is stored, exactly one epoch
/// record at the current epoch; every type is one of the seventeen; and
/// record at the current epoch; every type is one of the nineteen; and
/// exactly one `instance` document naming the declared instance; then
/// register, attach and pull. The epoch gate runs first so a store
/// written at a foreign epoch is refused as `ForeignEpoch`, never as

View file

@ -595,7 +595,7 @@ mod tests {
Mind::open_with(planted(with_one), &yggdrasil).err(),
Some(MindRefusal::ForeignStore { r#type: HuginnPersonaEntry::TYPE.into() })
);
// The opener still refuses a type that is not one of the seventeen.
// The opener still refuses a type that is not one of the nineteen.
let mut stranger = base();
let mut foreign = entry(INSTANCE, persona(INSTANCE, T1));
foreign.r#type = "gamecult.persona_state.v1".into();

View file

@ -27,7 +27,7 @@ use cultnet_rs::{
Cursor, EdgeAnchor, EdgeMatch, Evaluation, LIMIT_MAX, LIMIT_MIN, PROJECTION_DOCUMENT, Row, Selection, select, validate,
};
use eureka_pipeline::{
ClaimOutcome, CommitRange, Date, FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, OrgRepo,
ClaimOutcome, CommitRange, Date, FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, MemorySort, OrgRepo,
PipelineDocument, PipelineKind, PipelineRef, PipelineResolution, ResolutionOutcome, RulingAuthority, RunOperator, RunTurn, Sha,
Short, Slug, Title, pipeline_key,
};
@ -142,6 +142,7 @@ pub enum PipelineFacts {
HandOff { from_instance: Slug, to_instance: Slug, repo: OrgRepo, handed_on: Date },
Run { label: Label, operated_by: RunOperator, turn: RunTurn, started_on: Date, claims: Vec<PipelineRef> },
Pulse { label: Label, tempo_vote_beat_s: Option<u32>, #[serde(default)] tempo_vote_withdrawn: bool, urgency_permille: Option<u16> },
Memory { label: Label, sort: MemorySort, about: Vec<Short>, direct: bool, learned_in: Short, salience_permille: u16 },
}
impl PipelineFacts {
@ -246,6 +247,14 @@ impl PipelineFacts {
tempo_vote_withdrawn: value.tempo_vote_withdrawn,
urgency_permille: value.urgency_permille,
},
D::Memory(value) => Self::Memory {
label: value.label.clone(),
sort: value.sort,
about: value.about.clone(),
direct: value.direct,
learned_in: value.learned_in.clone(),
salience_permille: value.salience_permille,
},
}
}
}
@ -749,7 +758,7 @@ mod tests {
use crate::store::test_stores::MemoryStore;
use chrono::{TimeZone, Utc};
use eureka_pipeline::{
ClaimOutcome, FindingConfidence, PipelineDocument as D, PipelineKind as K, PipelineRefusal, ResolutionOutcome,
ClaimOutcome, FindingConfidence, MemorySort, PipelineDocument as D, PipelineKind as K, PipelineRefusal, ResolutionOutcome,
};
fn view(mind: &Mind<MemoryStore>, kind: K, key: &str) -> PipelineDocumentView {
@ -1486,6 +1495,41 @@ mod tests {
assert!(invalid("fields[0].values", "false")(&refused(&mind, &outside)));
}
/// A memory is selected by its sort, by each user it is about (a memory about two users answers
/// for both) and by whether it was learned in a direct message; a value outside the domain is
/// refused, and its header carries the triage facts but no text.
#[test]
fn memories_are_selected_by_sort_about_and_direct() {
let mut mind = seeded();
let D::Memory(mut direct) = memory("dm", MemorySort::Asked, "Prioritise the fork.", &["@op:chat"]) else { panic!() };
direct.direct = true;
committed(admit(&mut mind, vec![
D::Memory(direct),
memory("plan", MemorySort::Committed, "I take the shaders.", &["@aqua:chat", "@libby:chat"]),
memory("bond", MemorySort::Rapport, "Libby asks sharp questions.", &["@libby:chat"]),
]));
let key = |label: &str| format!("{INSTANCE}:memory:{label}");
let asking = |alias: &str, values: &[&str]| {
let mut found = ids(&mind.query(&with(of_kinds(&[K::Memory]), any_of(alias, values))).unwrap());
found.sort();
found
};
assert_eq!(asking("sort", &["Asked"]), vec![key("dm")]);
assert_eq!(asking("sort", &["Committed", "Rapport"]), vec![key("bond"), key("plan")]);
assert_eq!(asking("about", &["@libby:chat"]), vec![key("bond"), key("plan")]);
assert_eq!(asking("about", &["@aqua:chat", "@op:chat"]), vec![key("dm"), key("plan")]);
assert_eq!(asking("direct", &["true"]), vec![key("dm")]);
assert_eq!(asking("direct", &["false"]), vec![key("bond"), key("plan")]);
for sort in MemorySort::NAMES {
assert!(mind.query(&with(of_kinds(&[K::Memory]), any_of("sort", &[sort]))).is_ok(), "{sort}");
}
assert!(invalid("fields[0].values", "Opinion")(&refused(&mind, &with(of_kinds(&[K::Memory]), any_of("sort", &["Opinion"])))));
assert!(invalid("fields[0].values", "")(&refused(&mind, &with(of_kinds(&[K::Memory]), any_of("about", &[""])))));
let page = mind.query(&with(of_kinds(&[K::Memory]), any_of("sort", &["Rapport"]))).unwrap();
let PipelinePageItems::Headers(found) = page.items else { panic!("headers") };
assert!(matches!(&found[0].facts, PipelineFacts::Memory { sort: MemorySort::Rapport, direct: false, salience_permille: 500, .. }), "{:?}", found[0].facts);
}
/// A withdrawal pulse admitted into the mind is read back as a withdrawal: its
/// facts say so, `voted` selects it, and a later plain vote is not one.
#[test]
@ -1592,6 +1636,11 @@ mod tests {
let handoff = landed.iter().find(|write| write.kind == K::HandOff).expect("the hand-off landed").id.0.clone();
let (_, landed) = committed(admit(&mut mind, vec![run("w1", &[r(K::CutSpec, &id("cut_spec", "cut-1.r2")), r(K::FollowUp, &fu)])]));
let run_id = landed[0].id.0.clone();
let (older, newer) = (format!("{INSTANCE}:memory:m1"), format!("{INSTANCE}:memory:m2"));
committed(admit(&mut mind, vec![memory("m1", MemorySort::Idea, "one", &[])]));
let D::Memory(mut consolidating) = memory("m2", MemorySort::Idea, "two", &[]) else { unreachable!() };
consolidating.consolidates = vec![r(K::Memory, &older)];
committed(admit(&mut mind, vec![D::Memory(consolidating)]));
let (n1, f1_closed, f2_closed) = (
id("resolution", "question.Q1.n1"),
@ -1625,8 +1674,9 @@ mod tests {
(CitationRole::Documents, &handoff, &q1),
(CitationRole::Claims, &run_id, &spec_r2),
(CitationRole::Claims, &run_id, &fu),
(CitationRole::Consolidates, &newer, &older),
];
assert_eq!(CitationRole::ALL.len(), 16);
assert_eq!(CitationRole::ALL.len(), 17);
for role in CitationRole::ALL {
let selection =
Selection { cited: Some(Incoming { role: role.name().into(), exists: true }), ..Selection::default() };

View file

@ -13,7 +13,7 @@ use std::collections::BTreeMap;
use cultnet_rs::{Row, RowSet, Selection};
use eureka_pipeline::{
FindingConfidence, FindingOrigin, FindingSeverity, Label, OrgRepo, PipelineDocument, PipelineKind, PipelineRef, ResolutionOutcome,
FindingConfidence, FindingOrigin, FindingSeverity, Label, MemorySort, OrgRepo, PipelineDocument, PipelineKind, PipelineRef, ResolutionOutcome,
RulingAuthority, RunOperator, RunTurn, Slug,
};
@ -52,6 +52,12 @@ pub(crate) enum Alias {
Voted,
/// `true` on a pulse that declares an urgency.
Urged,
/// A memory's `sort`, from its fields.
Sort,
/// The Matrix users a memory is about, one value each.
About,
/// `true` or `false` on a memory: whether it was learned in a direct message.
Direct,
}
const FACULTIES: [&str; 7] = ["SelfFaculty", "Imagination", "Hands", "Soul", "Life", "Eyes", "Operator"];
@ -61,7 +67,7 @@ const BOOLEANS: [&str; 2] = ["true", "false"];
const PRESENT: [&str; 1] = ["true"];
impl Alias {
const ALL: [Alias; 16] = [
const ALL: [Alias; 19] = [
Self::Root,
Self::InForce,
Self::Faculty,
@ -78,6 +84,9 @@ impl Alias {
Self::Ran,
Self::Voted,
Self::Urged,
Self::Sort,
Self::About,
Self::Direct,
];
fn name(self) -> &'static str {
@ -98,6 +107,9 @@ impl Alias {
Self::Ran => "ran",
Self::Voted => "voted",
Self::Urged => "urged",
Self::Sort => "sort",
Self::About => "about",
Self::Direct => "direct",
}
}
@ -119,6 +131,7 @@ impl Alias {
Self::Outcome => kind == K::Resolution,
Self::Turn | Self::OperatedBy | Self::Ran => kind == K::Run,
Self::Voted | Self::Urged => kind == K::Pulse,
Self::Sort | Self::About | Self::Direct => kind == K::Memory,
}
}
@ -136,7 +149,9 @@ impl Alias {
Self::Turn => Some(RunTurn::NAMES),
Self::OperatedBy => Some(RunOperator::NAMES),
Self::Ran | Self::Voted | Self::Urged => Some(&PRESENT),
Self::Root | Self::Repo | Self::Cut => None,
Self::Sort => Some(MemorySort::NAMES),
Self::Direct => Some(&BOOLEANS),
Self::Root | Self::Repo | Self::Cut | Self::About => None,
}
}
@ -147,6 +162,7 @@ impl Alias {
Self::Root => Slug(value.into()).validate_slug().is_ok(),
Self::Repo => OrgRepo(value.into()).validate_org_repo().is_ok(),
Self::Cut => Label(value.into()).validate_label().is_ok(),
Self::About => !value.is_empty() && value.len() <= 200,
closed => closed.closed().is_some_and(|set| set.contains(&value)),
}
}
@ -211,7 +227,7 @@ impl SelectionRow {
D::FollowUp(follow_up) => vec![follow_up.repo.identity()],
D::Stewardship(stewardship) => vec![stewardship.repo.identity()],
D::HandOff(hand_off) => vec![hand_off.repo.identity()],
D::Target(_) | D::Question(_) | D::Ruling(_) | D::Verdict(_) | D::Finding(_) | D::Instance(_) | D::Resolution(_) | D::Run(_) | D::Pulse(_) => {
D::Target(_) | D::Question(_) | D::Ruling(_) | D::Verdict(_) | D::Finding(_) | D::Instance(_) | D::Resolution(_) | D::Run(_) | D::Pulse(_) | D::Memory(_) => {
Vec::new()
}
},
@ -247,6 +263,11 @@ impl SelectionRow {
put(Alias::Voted, if pulse.tempo_vote_beat_s.is_some() || pulse.tempo_vote_withdrawn { vec!["true".into()] } else { Vec::new() });
put(Alias::Urged, if pulse.urgency_permille.is_some() { vec!["true".into()] } else { Vec::new() });
}
D::Memory(memory) => {
put(Alias::Sort, vec![format!("{:?}", memory.sort)]);
put(Alias::About, memory.about.iter().map(|who| who.0.clone()).collect());
put(Alias::Direct, vec![memory.direct.to_string()]);
}
_ => {}
}
Self { kind: held.kind, key: held.key.clone(), ordinal: view.admission.ordinal, values, refs, view }
@ -279,7 +300,7 @@ impl Row for SelectionRow {
}
}
/// The organ's closed lists: fifteen schemas, sixteen aliases, sixteen roles.
/// The organ's closed lists: sixteen schemas, nineteen aliases, seventeen roles.
/// A schema's name is its id, so the substrate's alias matching resolves
/// nothing beyond exact ids.
pub(crate) struct Vocabulary;
@ -325,7 +346,7 @@ fn invalid(field: impl Into<String>, value: &str, message: impl Into<String>) ->
/// because `OrgRepo` identity is case-insensitive.
///
/// Refuses, naming the field the client sent: a `schemas` entry that is not
/// one of the fifteen ids; a `keys` entry that is not a full id whose kind
/// one of the sixteen ids; a `keys` entry that is not a full id whose kind
/// segment reads and passes the leaf's ref grammar; an `any_of` value outside
/// its alias's domain; and a `cites.target` whose key fails the ref grammar
/// for its schema's kind (V24's rule, at this door). Whether the target
@ -392,7 +413,7 @@ mod tests {
/// refused at the substrate's door.
#[test]
fn each_kind_declares_exactly_its_aliases_and_none_is_numeric() {
let table: [(K, &[&str]); 15] = [
let table: [(K, &[&str]); 16] = [
(K::Campaign, &["repo"]),
(K::Target, &[]),
(K::Question, &[]),
@ -408,6 +429,7 @@ mod tests {
(K::HandOff, &["repo"]),
(K::Run, &["turn", "operated_by", "ran"]),
(K::Pulse, &["voted", "urged"]),
(K::Memory, &["sort", "about", "direct"]),
];
for (kind, extra) in table {
let want = [&["root", "in_force", "faculty"][..], extra].concat();
@ -420,10 +442,10 @@ mod tests {
/// Each role is declared by the one kind whose field carries it, and its
/// targets are the kinds that field is typed to, "any" spelled as all
/// fifteen.
/// sixteen.
#[test]
fn each_kind_declares_the_roles_it_carries_and_their_targets() {
let table: [(K, &[&str]); 15] = [
let table: [(K, &[&str]); 16] = [
(K::Campaign, &[]),
(K::Target, &[]),
(K::Question, &["raised_in"]),
@ -439,13 +461,14 @@ mod tests {
(K::HandOff, &["documents"]),
(K::Run, &["claims"]),
(K::Pulse, &[]),
(K::Memory, &["consolidates"]),
];
for (kind, roles) in table {
assert_eq!(sorted(Vocabulary.declared_roles(kind.type_id())), strings(roles), "{kind:?}");
}
assert_eq!(Vocabulary.target_leaves("answers"), vec![K::Question.type_id().to_string()]);
assert_eq!(Vocabulary.target_leaves("rulings"), vec![K::Ruling.type_id().to_string()]);
assert_eq!(Vocabulary.target_leaves("subject").len(), 15);
assert_eq!(Vocabulary.target_leaves("subject").len(), 16);
assert_eq!(
sorted(Vocabulary.target_leaves("claims")),
sorted([K::CutSpec, K::Finding, K::FollowUp].iter().map(|kind| kind.type_id().to_string()).collect())

View file

@ -12,7 +12,7 @@ compares file to derivation byte for byte.
## The pipeline catalogue
`index.json` is the publication manifest for every schema here: the fourteen
`index.json` is the publication manifest for every schema here: the sixteen
`epiphany.pipeline.*.v2` schemas, `gamecult.persona_state.v0` and the two
`huginn.mind_*` wire schemas. The pipeline
schemas are derived from the value types in `crates/eureka-pipeline` and checked

View file

@ -172,7 +172,8 @@
"Stewardship",
"HandOff",
"Run",
"Pulse"
"Pulse",
"Memory"
],
"type": "string"
},

View file

@ -66,7 +66,8 @@
"Stewardship",
"HandOff",
"Run",
"Pulse"
"Pulse",
"Memory"
],
"type": "string"
},

View file

@ -0,0 +1,137 @@
{
"$defs": {
"Label": {
"description": "A key label: `[A-Za-z0-9_-]{1,64}`.",
"maxLength": 64,
"type": "string"
},
"Line": {
"description": "Text of at most 1,000 UTF-8 bytes.",
"maxLength": 1000,
"type": "string"
},
"MemorySort": {
"enum": [
"Asked",
"Decided",
"Committed",
"Learned",
"Did",
"Idea",
"Rapport"
],
"type": "string"
},
"PipelineKind": {
"enum": [
"Campaign",
"Target",
"Question",
"Ruling",
"CutSpec",
"CutReport",
"Verdict",
"Finding",
"FollowUp",
"Resolution",
"Instance",
"Stewardship",
"HandOff",
"Run",
"Pulse",
"Memory"
],
"type": "string"
},
"PipelineRef": {
"description": "A reference to another document. The id is parsed as a full pipeline id of\nthe declared `kind`, so the kind and the id's kind segment cannot disagree.",
"properties": {
"id": {
"$ref": "#/$defs/Short"
},
"kind": {
"$ref": "#/$defs/PipelineKind"
}
},
"required": [
"kind",
"id"
],
"type": "object"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"maxLength": 200,
"type": "string"
},
"Slug": {
"description": "A campaign slug or key segment: label parts joined by `.`.",
"maxLength": 64,
"type": "string"
}
},
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.memory.v2.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "One thing a Mind keeps for herself, in her own mind: what she was asked,\ndecided, committed to, learned or did, an idea, or the rapport between\nher and another Mind or person. `text` is her own words. `about` names\nthe Matrix users it concerns (Minds included), `learned_in` where she\nlearned it (a room alias, a DM's room id, or `self run <label>`),\n`direct` whether it came in a direct message, and `sources` the event\nids, commits or documents it rests on; the organ fills the last three,\nnot the model. `salience_permille` ranks it and decides nothing.\n`consolidates` names the memories a consolidating memory replaces.\nInstance-rooted: the key is `<instance>:memory:<label>`.",
"properties": {
"about": {
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 8,
"type": "array"
},
"consolidates": {
"items": {
"$ref": "#/$defs/PipelineRef"
},
"maxItems": 16,
"type": "array"
},
"direct": {
"type": "boolean"
},
"instance": {
"$ref": "#/$defs/Slug"
},
"label": {
"$ref": "#/$defs/Label"
},
"learned_in": {
"$ref": "#/$defs/Short"
},
"salience_permille": {
"format": "uint16",
"maximum": 65535,
"minimum": 0,
"type": "integer"
},
"sort": {
"$ref": "#/$defs/MemorySort"
},
"sources": {
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 8,
"type": "array"
},
"text": {
"$ref": "#/$defs/Line"
}
},
"required": [
"instance",
"label",
"sort",
"text",
"about",
"learned_in",
"direct",
"sources",
"salience_permille",
"consolidates"
],
"title": "PipelineMemory",
"type": "object"
}

View file

@ -36,7 +36,8 @@
"Stewardship",
"HandOff",
"Run",
"Pulse"
"Pulse",
"Memory"
],
"type": "string"
},

View file

@ -31,7 +31,8 @@
"Stewardship",
"HandOff",
"Run",
"Pulse"
"Pulse",
"Memory"
],
"type": "string"
},

View file

@ -69,7 +69,8 @@
"Stewardship",
"HandOff",
"Run",
"Pulse"
"Pulse",
"Memory"
],
"type": "string"
},

View file

@ -26,7 +26,8 @@
"Stewardship",
"HandOff",
"Run",
"Pulse"
"Pulse",
"Memory"
],
"type": "string"
},

View file

@ -520,6 +520,18 @@
"type": "string",
"maxLength": 1000
},
"MemorySort": {
"type": "string",
"enum": [
"Asked",
"Decided",
"Committed",
"Learned",
"Did",
"Idea",
"Rapport"
]
},
"MutationRecord": {
"description": "One mutation a report ran: a key-safe label a verdict claim can name,\nthe exact edit, and the tree it was applied to.",
"type": "object",
@ -863,7 +875,7 @@
]
},
"PipelineDocument": {
"description": "The envelope enum, adjacently tagged on the kind name: a document\non a wire is `{ kind, value }`, and `kind` is exactly what\n`PipelineKind::name()` returns, so a reader dispatches on the kind\nsegment it already knows from the key and needs no second registry\nmapping variant spellings to kinds. Adjacent rather than internal\nbecause a value is a named map of its own and an internal tag would\nhave to be merged into it; the tag stays beside the value instead.\nThis is not a published schema: `schemas/cultnet` publishes the\nfifteen per-kind value documents, and the envelope is the wire's\nshape, not a document's.",
"description": "The envelope enum, adjacently tagged on the kind name: a document\non a wire is `{ kind, value }`, and `kind` is exactly what\n`PipelineKind::name()` returns, so a reader dispatches on the kind\nsegment it already knows from the key and needs no second registry\nmapping variant spellings to kinds. Adjacent rather than internal\nbecause a value is a named map of its own and an internal tag would\nhave to be merged into it; the tag stays beside the value instead.\nThis is not a published schema: `schemas/cultnet` publishes the\nsixteen per-kind value documents, and the envelope is the wire's\nshape, not a document's.",
"oneOf": [
{
"type": "object",
@ -1104,6 +1116,22 @@
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "memory"
},
"value": {
"$ref": "#/$defs/PipelineMemory"
}
},
"required": [
"kind",
"value"
]
}
]
},
@ -1302,7 +1330,71 @@
"Stewardship",
"HandOff",
"Run",
"Pulse"
"Pulse",
"Memory"
]
},
"PipelineMemory": {
"description": "One thing a Mind keeps for herself, in her own mind: what she was asked,\ndecided, committed to, learned or did, an idea, or the rapport between\nher and another Mind or person. `text` is her own words. `about` names\nthe Matrix users it concerns (Minds included), `learned_in` where she\nlearned it (a room alias, a DM's room id, or `self run <label>`),\n`direct` whether it came in a direct message, and `sources` the event\nids, commits or documents it rests on; the organ fills the last three,\nnot the model. `salience_permille` ranks it and decides nothing.\n`consolidates` names the memories a consolidating memory replaces.\nInstance-rooted: the key is `<instance>:memory:<label>`.",
"type": "object",
"properties": {
"about": {
"type": "array",
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 8
},
"consolidates": {
"type": "array",
"items": {
"$ref": "#/$defs/PipelineRef"
},
"maxItems": 16
},
"direct": {
"type": "boolean"
},
"instance": {
"$ref": "#/$defs/Slug"
},
"label": {
"$ref": "#/$defs/Label"
},
"learned_in": {
"$ref": "#/$defs/Short"
},
"salience_permille": {
"type": "integer",
"format": "uint16",
"maximum": 65535,
"minimum": 0
},
"sort": {
"$ref": "#/$defs/MemorySort"
},
"sources": {
"type": "array",
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 8
},
"text": {
"$ref": "#/$defs/Line"
}
},
"required": [
"instance",
"label",
"sort",
"text",
"about",
"learned_in",
"direct",
"sources",
"salience_permille",
"consolidates"
]
},
"PipelineProvenance": {

View file

@ -228,7 +228,8 @@
"resolved_by",
"deferred_to",
"documents",
"claims"
"claims",
"consolidates"
]
},
"ClaimOutcome": {
@ -671,6 +672,18 @@
"type": "string",
"maxLength": 1000
},
"MemorySort": {
"type": "string",
"enum": [
"Asked",
"Decided",
"Committed",
"Learned",
"Did",
"Idea",
"Rapport"
]
},
"MindRefusal": {
"description": "Every way a mind refuses. `Document` wraps the leaf's four (bounds,\nformats, key identity, foreign type); the rest are the organ's: store\nidentity, epoch, ownership, batch shape, references, and every cross-field\nand cross-document rule of admission.\n\nThe two sequenced kinds refuse in the same pair of shapes: an\n`OutOfSequence` when the writer's `sequence` is not the previous plus one,\nand `AlreadyResolved`/`AlreadyStewarded` when a record of that scope is\nstill in force.",
"oneOf": [
@ -2161,7 +2174,7 @@
]
},
"PipelineDocument": {
"description": "The envelope enum, adjacently tagged on the kind name: a document\non a wire is `{ kind, value }`, and `kind` is exactly what\n`PipelineKind::name()` returns, so a reader dispatches on the kind\nsegment it already knows from the key and needs no second registry\nmapping variant spellings to kinds. Adjacent rather than internal\nbecause a value is a named map of its own and an internal tag would\nhave to be merged into it; the tag stays beside the value instead.\nThis is not a published schema: `schemas/cultnet` publishes the\nfifteen per-kind value documents, and the envelope is the wire's\nshape, not a document's.",
"description": "The envelope enum, adjacently tagged on the kind name: a document\non a wire is `{ kind, value }`, and `kind` is exactly what\n`PipelineKind::name()` returns, so a reader dispatches on the kind\nsegment it already knows from the key and needs no second registry\nmapping variant spellings to kinds. Adjacent rather than internal\nbecause a value is a named map of its own and an internal tag would\nhave to be merged into it; the tag stays beside the value instead.\nThis is not a published schema: `schemas/cultnet` publishes the\nsixteen per-kind value documents, and the envelope is the wire's\nshape, not a document's.",
"oneOf": [
{
"type": "object",
@ -2402,6 +2415,22 @@
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "memory"
},
"value": {
"$ref": "#/$defs/PipelineMemory"
}
},
"required": [
"kind",
"value"
]
}
]
},
@ -3119,6 +3148,56 @@
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "memory"
},
"value": {
"type": "object",
"properties": {
"about": {
"type": "array",
"items": {
"$ref": "#/$defs/Short"
}
},
"direct": {
"type": "boolean"
},
"label": {
"$ref": "#/$defs/Label"
},
"learned_in": {
"$ref": "#/$defs/Short"
},
"salience_permille": {
"type": "integer",
"format": "uint16",
"maximum": 65535,
"minimum": 0
},
"sort": {
"$ref": "#/$defs/MemorySort"
}
},
"required": [
"label",
"sort",
"about",
"direct",
"learned_in",
"salience_permille"
]
}
},
"required": [
"kind",
"value"
]
}
]
},
@ -3317,7 +3396,71 @@
"Stewardship",
"HandOff",
"Run",
"Pulse"
"Pulse",
"Memory"
]
},
"PipelineMemory": {
"description": "One thing a Mind keeps for herself, in her own mind: what she was asked,\ndecided, committed to, learned or did, an idea, or the rapport between\nher and another Mind or person. `text` is her own words. `about` names\nthe Matrix users it concerns (Minds included), `learned_in` where she\nlearned it (a room alias, a DM's room id, or `self run <label>`),\n`direct` whether it came in a direct message, and `sources` the event\nids, commits or documents it rests on; the organ fills the last three,\nnot the model. `salience_permille` ranks it and decides nothing.\n`consolidates` names the memories a consolidating memory replaces.\nInstance-rooted: the key is `<instance>:memory:<label>`.",
"type": "object",
"properties": {
"about": {
"type": "array",
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 8
},
"consolidates": {
"type": "array",
"items": {
"$ref": "#/$defs/PipelineRef"
},
"maxItems": 16
},
"direct": {
"type": "boolean"
},
"instance": {
"$ref": "#/$defs/Slug"
},
"label": {
"$ref": "#/$defs/Label"
},
"learned_in": {
"$ref": "#/$defs/Short"
},
"salience_permille": {
"type": "integer",
"format": "uint16",
"maximum": 65535,
"minimum": 0
},
"sort": {
"$ref": "#/$defs/MemorySort"
},
"sources": {
"type": "array",
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 8
},
"text": {
"$ref": "#/$defs/Line"
}
},
"required": [
"instance",
"label",
"sort",
"text",
"about",
"learned_in",
"direct",
"sources",
"salience_permille",
"consolidates"
]
},
"PipelinePageItems": {

View file

@ -177,6 +177,17 @@
"title": "Epiphany Pipeline Pulse v2",
"path": "epiphany.pipeline.pulse.v2.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.memory.v2.schema.json",
"kind": "document_payload",
"wireContracts": [
"cultnet.schema.v0"
],
"schemaVersion": "epiphany.pipeline.memory.v2",
"documentType": "epiphany.pipeline.memory.v2",
"title": "Epiphany Pipeline Memory v2",
"path": "epiphany.pipeline.memory.v2.schema.json"
},
{
"schemaId": "huginn.mind_request.v1",
"kind": "wire_message",