eureka-pipeline, huginn-mind, mind-body: a night's cut line is its typed Slept closing

ResolutionOutcome::Slept { line, day_through } is how a Sleep run ends when the ledger took her reply; day_through is the last memory of her day she was shown. CitationRole::DayThrough (day_through) makes the memory an A7 referent and a cites/cited edge; admission refuses Slept on any subject but a Sleep run, a day_through that is not a memory admitted before the batch, and skips the in-force check for that role since the night may have consumed it. The outcome alias carries Slept. sleep.rs close admits the typed outcome and covered_through reads only the Slept closing in force and day_through's admission ordinal: SLEPT, THROUGH and the rationale parse are gone, and a hand closing or a withdrawn Slept covers nothing. outcome_words renders Slept as 'slept: <line>'. Published schemas and the envelope golden regenerated from the derivation.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-11-08 13:59:06 +01:00
commit 2e98327ded
13 changed files with 315 additions and 54 deletions

View file

@ -37,6 +37,7 @@ resolution.fixed-by-some eureka-state:resolution:question.Q1.n1 epiphany.pipelin
resolution.deferred eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581a8446566657272656481a2746f82a46b696e64a8466f6c6c6f775570a26964bb657572656b612d73746174653a666f6c6c6f775f75703a46552d34a9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135
resolution.recorded eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581a85265636f7264656481a6726561736f6ea64e6f7465642ea9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135
resolution.withdrawn eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581a957697468647261776e81a6726561736f6ea94d697374616b656e2ea9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135
resolution.slept eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581a5536c65707482a46c696e65b04c657420746865207265737420676f2eab6461795f7468726f75676882a46b696e64a64d656d6f7279a26964b7657572656b612d73746174653a6d656d6f72793a6d2d31a9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135
ruling.authority-standing eureka-state:ruling:R8 epiphany.pipeline.ruling.v2 EpiphanyPipelineRulingDocument 918aa863616d706169676eac657572656b612d7374617465a56c6162656ca25238a57469746c65b9416e20696e7374616e6365206f776e7320697473206d696e64a7616e7377657273b8657572656b612d73746174653a7175657374696f6e3a5131a663686f696365a141a672756c696e67ba416e20696e7374616e6365206f776e7320697473206d696e642eae6f70657261746f725f71756f7465bd616c6c207265636f6d6d656e646174696f6e732c20676f206168656164a872756c65645f6f6eaa323032362d30392d3135aa707265636564656e74739185a47265706fb147616d6543756c742f4165746865726961a6636f6d6d6974d92861616161616161616161616161616161616161616161616161616161616161616161616161616161a46b696e64a652756c696e67a26964b363756c7463616368653a72756c696e673a5231ae7061796c6f61645f736861323536d94062626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262a9617574686f72697479a85374616e64696e67
ruling.authority-defaulted eureka-state:ruling:R8 epiphany.pipeline.ruling.v2 EpiphanyPipelineRulingDocument 918aa863616d706169676eac657572656b612d7374617465a56c6162656ca25238a57469746c65b9416e20696e7374616e6365206f776e7320697473206d696e64a7616e7377657273b8657572656b612d73746174653a7175657374696f6e3a5131a663686f696365a141a672756c696e67ba416e20696e7374616e6365206f776e7320697473206d696e642eae6f70657261746f725f71756f7465bd616c6c207265636f6d6d656e646174696f6e732c20676f206168656164a872756c65645f6f6eaa323032362d30392d3135aa707265636564656e74739185a47265706fb147616d6543756c742f4165746865726961a6636f6d6d6974d92861616161616161616161616161616161616161616161616161616161616161616161616161616161a46b696e64a652756c696e67a26964b363756c7463616368653a72756c696e673a5231ae7061796c6f61645f736861323536d94062626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262a9617574686f72697479a944656661756c746564
ruling.options-none eureka-state:ruling:R8 epiphany.pipeline.ruling.v2 EpiphanyPipelineRulingDocument 918aa863616d706169676eac657572656b612d7374617465a56c6162656ca25238a57469746c65b9416e20696e7374616e6365206f776e7320697473206d696e64a7616e7377657273c0a663686f696365c0a672756c696e67ba416e20696e7374616e6365206f776e7320697473206d696e642eae6f70657261746f725f71756f7465c0a872756c65645f6f6eaa323032362d30392d3135aa707265636564656e747390a9617574686f72697479a84f70657261746f72

View file

@ -1096,6 +1096,9 @@ pub enum ResolutionOutcome {
Deferred { to: PipelineRef },
Recorded { reason: Line },
Withdrawn { reason: Line },
/// How a Sleep run ended when the ledger took her reply: `line` is her one line, `day_through` the last memory
/// of her day she was shown (none when the day was empty). It is the one place a night's cut line is kept.
Slept { line: Line, day_through: Option<PipelineRef> },
}
impl Bounded for ResolutionOutcome {
@ -1109,6 +1112,10 @@ impl Bounded for ResolutionOutcome {
}
Self::Deferred { to } => to.validate(&format!("{field}.to")),
Self::Recorded { reason } | Self::Withdrawn { reason } => reason.validate(&format!("{field}.reason")),
Self::Slept { line, day_through } => {
line.validate(&format!("{field}.line"))?;
day_through.validate(&format!("{field}.day_through"))
}
}
}
}
@ -1892,6 +1899,7 @@ mod tests {
out.push(("resolution.deferred".into(), resolution(ResolutionOutcome::Deferred { to: by(PipelineKind::FollowUp, "follow_up", "FU-4") })));
out.push(("resolution.recorded".into(), resolution(ResolutionOutcome::Recorded { reason: "Noted.".into() })));
out.push(("resolution.withdrawn".into(), resolution(ResolutionOutcome::Withdrawn { reason: "Mistaken.".into() })));
out.push(("resolution.slept".into(), resolution(ResolutionOutcome::Slept { line: "Let the rest go.".into(), day_through: Some(by(PipelineKind::Memory, "memory", "m-1")) })));
for (name, authority) in [("standing", RulingAuthority::Standing), ("defaulted", RulingAuthority::Defaulted)] {
let mut r = ruling_sample();
r.authority = authority;
@ -3069,6 +3077,16 @@ mod tests {
Err(PipelineRefusal::FieldBound { field: "resolution.outcome.reason".into(), limit: 1000, actual: 1001 })
);
}
// A night's closing is bounded on both of its fields: her line like any `Line`, and the memory it names
// like any referent.
let run = PipelineRef { kind: PipelineKind::Run, id: id("run", "night") };
let memory = PipelineRef { kind: PipelineKind::Memory, id: id("memory", "m-1") };
let slept = |line: Line, day_through: Option<PipelineRef>| resolved(run.clone(), ResolutionOutcome::Slept { line, day_through });
assert_eq!(slept("ok".into(), Some(memory.clone())), Ok(()));
assert_eq!(slept("ok".into(), None), Ok(()));
assert_eq!(slept(Line("a".repeat(1001)), None), Err(PipelineRefusal::FieldBound { field: "resolution.outcome.line".into(), limit: 1000, actual: 1001 }));
refused("resolution.outcome.day_through.id", slept("ok".into(), Some(PipelineRef { kind: PipelineKind::Memory, id: id("ruling", "R8") })));
}
/// Ruling B: a fix names the tree where the finding stopped being true,

View file

@ -32,7 +32,7 @@ use chrono::{DateTime, Utc};
use cultcache_rs::{CultCache, CultCacheEnvelope};
use eureka_pipeline::{
ClaimOutcome, FindingConfidence, Line, PipelineDocument, PipelineKind, PipelineMemory, PipelineRef, PipelineRefusal,
PipelineResolution, PipelineRun, PipelineStewardship, ResolutionOutcome, RulingAuthority, Short, Slug, pipeline_key,
PipelineResolution, PipelineRun, PipelineStewardship, ResolutionOutcome, RulingAuthority, RunTurn, Short, Slug, pipeline_key,
validate_pipeline_write_envelope,
};
use schemars::JsonSchema;
@ -709,6 +709,7 @@ fn outcome_name(outcome: &ResolutionOutcome) -> &'static str {
ResolutionOutcome::Deferred { .. } => "Deferred",
ResolutionOutcome::Recorded { .. } => "Recorded",
ResolutionOutcome::Withdrawn { .. } => "Withdrawn",
ResolutionOutcome::Slept { .. } => "Slept",
}
}
@ -743,6 +744,7 @@ 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::Run, O::Slept { day_through, .. }) => day_through.as_ref().is_none_or(|memory| memory.kind == K::Memory),
(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),
@ -833,8 +835,9 @@ fn resolution_rule(docs: &Docs<'_>, resolution: &PipelineResolution) -> Result<(
{
run_rule(docs, &reinstating.subject.id.0, run, false)?;
}
for (_, referent) in outcome_citations(&resolution.outcome) {
if !docs.in_force(referent.kind, &referent.id.0) {
for (role, referent) in outcome_citations(&resolution.outcome) {
// The last memory a night was shown may have been consumed by that night's own batches.
if role != CitationRole::DayThrough && !docs.in_force(referent.kind, &referent.id.0) {
return Err(MindRefusal::CitesResolvedDocument { kind: referent.kind, id: referent.id.0 });
}
}
@ -863,6 +866,18 @@ fn resolution_rule(docs: &Docs<'_>, resolution: &PipelineResolution) -> Result<(
return Err(incompatible());
}
}
ResolutionOutcome::Slept { day_through, .. } => {
// A night's closing is a Sleep run's, and the memory it names was admitted before this batch (every
// memory in the image is hers: A5).
if !matches!(docs.find(K::Run, subject_id), Some(D::Run(run)) if run.turn == RunTurn::Sleep) {
return Err(incompatible());
}
if let Some(memory) = day_through
&& docs.in_image(K::Memory, &memory.id.0).is_none()
{
return Err(incompatible());
}
}
_ => {}
}
Ok(())
@ -2719,6 +2734,46 @@ mod tests {
assert_eq!(refusal(admit(&mut mind, vec![mind_run("s2", RunTurn::Sleep, &[])])), live("s"), "one night at a time");
}
/// A Slept closing ends a Sleep run, and only that; the memory it names was admitted before it, and may have
/// been consumed since.
#[test]
fn a_slept_closing_is_a_sleep_runs_and_names_an_earlier_memory_of_hers() {
let incompatible = |subject_kind: K| MindRefusal::IncompatibleResolution { subject_kind, outcome: "Slept".into() };
let m = |label: &str| r(K::Memory, &memory_key(label));
let slept = |subject: PipelineRef, through: Option<PipelineRef>| resolution(subject, ResolutionOutcome::Slept { line: "ok".into(), day_through: through });
let mut mind = with_specs();
committed(admit(&mut mind, vec![D::Ruling(ruling("R1")), memory("m1", MemorySort::Idea, "one", &[]), memory("m2", MemorySort::Idea, "two", &[])]));
committed(admit(&mut mind, vec![mind_run("p", RunTurn::PersonaTurn, &[])]));
assert_eq!(refusal(admit(&mut mind, vec![slept(r(K::Run, &run_key("p")), None)])), incompatible(K::Run), "a persona run is not a night");
committed(admit(&mut mind, vec![recorded("p")]));
committed(admit(&mut mind, vec![mind_run("self", RunTurn::SelfRun, &[])]));
assert_eq!(refusal(admit(&mut mind, vec![slept(r(K::Run, &run_key("self")), None)])), incompatible(K::Run), "a Self run is not a night");
committed(admit(&mut mind, vec![recorded("self")]));
committed(admit(&mut mind, vec![mind_run("s", RunTurn::Sleep, &[])]));
let night = || r(K::Run, &run_key("s"));
assert_eq!(refusal(admit(&mut mind, vec![slept(m("m1"), None)])), incompatible(K::Memory), "a memory is not slept");
let ruling_ref = r(K::Ruling, &id("ruling", "R1"));
assert_eq!(refusal(admit(&mut mind, vec![slept(night(), Some(ruling_ref))])), incompatible(K::Run), "the cut line is a memory");
let same_batch = vec![memory("m3", MemorySort::Idea, "three", &[]), slept(night(), Some(m("m3")))];
assert_eq!(refusal(admit(&mut mind, same_batch)), incompatible(K::Run), "a memory of this very batch was not shown");
assert_eq!(
refusal(admit(&mut mind, vec![slept(night(), Some(m("nope")))])),
MindRefusal::MissingReference { kind: K::Memory, id: memory_key("nope") },
"a memory that does not exist"
);
// A memory the night itself consumed is still the last it was shown.
committed(admit(&mut mind, consolidation("m4", &["m1"])));
committed(admit(&mut mind, vec![slept(night(), Some(m("m1")))]));
assert_eq!(
refusal(admit(&mut mind, vec![resolution_n(night(), 2, ResolutionOutcome::Slept { line: "again".into(), day_through: None })])),
MindRefusal::AlreadyResolved { subject: run_key("s") },
"the first closing in force is the only one"
);
// An empty day names no memory.
committed(admit(&mut mind, vec![mind_run("s2", RunTurn::Sleep, &[])]));
committed(admit(&mut mind, vec![slept(r(K::Run, &run_key("s2")), None)]));
}
fn research_key(pass: &str, label: &str) -> String {
format!("{CAMPAIGN}:research:{pass}.{label}")
}

View file

@ -397,10 +397,11 @@ pub enum CitationRole {
Toward,
Run,
Reads,
DayThrough,
}
impl CitationRole {
pub const ALL: [CitationRole; 23] = [
pub const ALL: [CitationRole; 24] = [
Self::RaisedIn,
Self::Answers,
Self::Rulings,
@ -424,6 +425,7 @@ impl CitationRole {
Self::Toward,
Self::Run,
Self::Reads,
Self::DayThrough,
];
/// The wire spelling, equal to the referring field's name.
@ -452,6 +454,7 @@ impl CitationRole {
Self::Toward => "toward",
Self::Run => "run",
Self::Reads => "reads",
Self::DayThrough => "day_through",
}
}
@ -470,7 +473,7 @@ impl CitationRole {
Self::CutReport | Self::Findings => K::Verdict,
Self::Verdict => K::Finding,
Self::Source => K::FollowUp,
Self::Subject | Self::SupersededBy | Self::ResolvedBy | Self::DeferredTo => K::Resolution,
Self::Subject | Self::SupersededBy | Self::ResolvedBy | Self::DeferredTo | Self::DayThrough => K::Resolution,
Self::Documents => K::HandOff,
Self::Claims => K::Run,
Self::Consolidates => K::Memory,
@ -494,7 +497,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::Consolidates | Self::DayThrough => vec![K::Memory],
Self::Run => vec![K::Run],
Self::RaisedIn
| Self::Source
@ -530,6 +533,7 @@ pub(crate) fn outcome_citations(outcome: &ResolutionOutcome) -> Vec<(CitationRol
ResolutionOutcome::Answered { by } => vec![(CitationRole::ResolvedBy, by.clone())],
ResolutionOutcome::Fixed { by: Some(by), .. } => vec![(CitationRole::ResolvedBy, by.clone())],
ResolutionOutcome::Deferred { to } => vec![(CitationRole::DeferredTo, to.clone())],
ResolutionOutcome::Slept { day_through, .. } => day_through.iter().map(|memory| (CitationRole::DayThrough, memory.clone())).collect(),
ResolutionOutcome::Fixed { by: None, .. } | ResolutionOutcome::Recorded { .. } | ResolutionOutcome::Withdrawn { .. } => {
vec![]
}

View file

@ -1796,6 +1796,10 @@ mod tests {
let (older, newer) = (format!("{INSTANCE}:memory:m1"), format!("{INSTANCE}:memory:m2"));
committed(admit(&mut mind, vec![memory("m1", MemorySort::Idea, "one", &[])]));
committed(admit(&mut mind, crate::fixtures::consolidation("m2", &["m1"])));
let night_id = format!("{INSTANCE}:run:night");
committed(admit(&mut mind, vec![crate::fixtures::mind_run("night", eureka_pipeline::RunTurn::Sleep, &[])]));
committed(admit(&mut mind, vec![resolution(r(K::Run, &night_id), ResolutionOutcome::Slept { line: "ok".into(), day_through: Some(r(K::Memory, &older)) })]));
let night_closed = format!("{INSTANCE}:resolution:run.night.n1");
let (note, way) = (id("research", "p.a"), id("idea", "p.i"));
let D::Research(mut note_document) = research("p", "a", "A fact.") else { unreachable!() };
note_document.serves = vec![r(K::Question, &q1)];
@ -1835,6 +1839,7 @@ mod tests {
(CitationRole::Subject, &f2_closed, &f2),
(CitationRole::Subject, &spec_closed, &spec),
(CitationRole::Subject, &older_closed, &older),
(CitationRole::Subject, &night_closed, &night_id),
(CitationRole::SupersededBy, &spec_closed, &spec_r2),
(CitationRole::SupersededBy, &older_closed, &newer),
(CitationRole::ResolvedBy, &n1, &r1),
@ -1844,6 +1849,7 @@ mod tests {
(CitationRole::Claims, &run_id, &spec_r2),
(CitationRole::Claims, &run_id, &fu),
(CitationRole::Run, &reply_id, &run_id),
(CitationRole::DayThrough, &night_closed, &older),
(CitationRole::Reads, &thought_id, &f1),
(CitationRole::Reads, &thought_id, &q1),
(CitationRole::Consolidates, &newer, &older),
@ -1852,7 +1858,7 @@ mod tests {
(CitationRole::RestsOn, &way, &note),
(CitationRole::Toward, &way, &q1),
];
assert_eq!(CitationRole::ALL.len(), 23);
assert_eq!(CitationRole::ALL.len(), 24);
for role in CitationRole::ALL {
let selection =
Selection { cited: Some(Incoming { role: role.name().into(), exists: true }), ..Selection::default() };

View file

@ -64,7 +64,7 @@ pub(crate) enum Alias {
}
const FACULTIES: [&str; 7] = ["SelfFaculty", "Imagination", "Hands", "Soul", "Life", "Eyes", "Operator"];
const OUTCOMES: [&str; 6] = ["Superseded", "Answered", "Fixed", "Deferred", "Recorded", "Withdrawn"];
const OUTCOMES: [&str; 7] = ["Superseded", "Answered", "Fixed", "Deferred", "Recorded", "Withdrawn", "Slept"];
const BOOLEANS: [&str; 2] = ["true", "false"];
const MEMORY_ORIGINS: [&str; 5] = ["Room", "Direct", "Wake", "SelfRun", "Sleep"];
/// The one value a presence alias answers with; a row without it answers nothing.
@ -199,6 +199,7 @@ fn outcome_name(outcome: &ResolutionOutcome) -> &'static str {
ResolutionOutcome::Deferred { .. } => "Deferred",
ResolutionOutcome::Recorded { .. } => "Recorded",
ResolutionOutcome::Withdrawn { .. } => "Withdrawn",
ResolutionOutcome::Slept { .. } => "Slept",
}
}
@ -330,7 +331,7 @@ impl Row for SelectionRow {
}
}
/// The organ's closed lists: twenty schemas, twenty aliases, twenty-three roles.
/// The organ's closed lists: twenty schemas, twenty aliases, twenty-four roles.
/// A schema's name is its id, so the substrate's alias matching resolves
/// nothing beyond exact ids.
pub(crate) struct Vocabulary;
@ -489,7 +490,7 @@ mod tests {
(K::Verdict, &["cut_report", "findings"]),
(K::Finding, &["verdict"]),
(K::FollowUp, &["source"]),
(K::Resolution, &["subject", "superseded_by", "resolved_by", "deferred_to"]),
(K::Resolution, &["subject", "superseded_by", "resolved_by", "deferred_to", "day_through"]),
(K::Instance, &[]),
(K::Stewardship, &[]),
(K::HandOff, &["documents"]),

View file

@ -2638,6 +2638,7 @@ pub(crate) fn outcome_words(outcome: &ResolutionOutcome) -> String {
ResolutionOutcome::Answered { by } => format!("answered by {}", by.id.0),
ResolutionOutcome::Fixed { commit, .. } => format!("fixed by {}", commit.0),
ResolutionOutcome::Deferred { to } => format!("deferred to {}", to.id.0),
ResolutionOutcome::Slept { line, .. } => format!("slept: {}", line.0),
}
}

View file

@ -7089,8 +7089,15 @@ impl Rig {
let label = label_of(run);
let view = self.mind.view(K::Resolution, &format!("{INSTANCE}:resolution:run.{label}.n1")).expect("the night is closed");
let PipelineDocument::Resolution(resolution) = view.document else { unreachable!() };
let ResolutionOutcome::Recorded { reason } = resolution.outcome else { panic!("a night is closed Recorded") };
reason.0
outcome_words(&resolution.outcome)
}
/// The memory the Sleep run's closing names as the last of her day she was shown.
fn night_through(&self, run: &PipelineRef) -> Option<String> {
let view = self.mind.view(K::Resolution, &format!("{INSTANCE}:resolution:run.{}.n1", label_of(run))).expect("the night is closed");
let PipelineDocument::Resolution(resolution) = view.document else { unreachable!() };
let ResolutionOutcome::Slept { day_through, .. } = resolution.outcome else { panic!("a night that slept closes Slept") };
day_through.map(|memory| memory.id.0)
}
fn the_night(&self) -> Outcome {
@ -7120,7 +7127,7 @@ fn the_night_line_holds_the_day_whole_and_prescribes_nothing() {
// An earlier night of hers, opened 30 hours before this one, closed.
rig.mind.admit_at(at(NIGHT_AT_MS - 30 * HOUR_MS));
rig.mind.committed(vec![crate::testkit::run("slept-before", RunTurn::Sleep, RunOperator::Mind, &[])]);
rig.mind.committed(vec![crate::testkit::close(crate::testkit::run_ref("slept-before"), ResolutionOutcome::Recorded { reason: "slept: ok".into() })]);
rig.mind.committed(vec![crate::testkit::close(crate::testkit::run_ref("slept-before"), ResolutionOutcome::Slept { line: "ok".into(), day_through: None })]);
// Her day: after that night opened. A Sleep-origin memory of it is not part of it.
rig.remembered("d1", MemorySort::Decided, "Ship on Friday.", &["@op:chat"], room(), 20, &["$e1"]);
rig.remembered("d2", MemorySort::Learned, "A private thing.", &["@kiko:chat"], dm("!dm:chat", &["@kiko:chat"]), 10, &[]);
@ -7228,6 +7235,7 @@ fn a_decoded_night_admits_whole_operations_and_the_run_closes_with_her_line() {
assert_eq!(rig.the_memory(&format!("{tag}-c0")).origin, MemoryOrigin::Sleep { run: tag.as_str().into() }, "a public consolidation is formed in sleep");
assert_eq!(rig.the_memory(&format!("{tag}-r0")).origin, MemoryOrigin::Sleep { run: tag.as_str().into() });
assert_eq!(rig.night_closing(&run), "slept: I looked at the release again and let the rest go.");
assert_eq!(rig.night_through(&run), Some(memory_id("wrong")), "the closing names the last memory of her day she was shown");
// She reads it on waking, as the line of her last runs.
let lines = recent_runs(&rig.mind, &Slug(INSTANCE.into())).unwrap();
assert!(lines[0].starts_with(&format!("{tag} Sleep: slept: I looked")), "{lines:?}");

View file

@ -196,29 +196,22 @@ fn sleeps(mind: &dyn MindPort) -> Result<Vec<Night>> {
Ok(found)
}
const SLEPT: &str = "slept: ";
const THROUGH: &str = "; day through ordinal ";
/// Where the newest of `earlier` that closed `slept:` left her day: the admission ordinal of the last memory of
/// the day it was shown (its own run's ordinal when it was shown none). A night that failed, or whose run never
/// closed, left its day uncovered, so it is not an anchor.
/// Where the newest of `earlier` whose closing in force is `Slept` left her day: the admission ordinal of the
/// memory it names as the last of the day it was shown, in force or not (its own run's ordinal when it was shown
/// none). A night with no such closing (it failed, its run never closed, a hand closed it, or its `Slept` closing
/// was withdrawn) left its day uncovered, so it is not an anchor.
fn covered_through(mind: &dyn MindPort, earlier: &[&Night]) -> Result<Option<u64>> {
for night in earlier.iter().rev() {
let closings = Selection { cites: Some(citing(&night.run, Some("subject"))), ..of_kind(PipelineKind::Resolution) };
let latest = documents(mind, &closings)?
.into_iter()
.filter_map(|view| match view.document {
PipelineDocument::Resolution(resolution) => Some(resolution),
_ => None,
})
.max_by_key(|resolution| resolution.sequence);
if let Some(resolution) = latest
&& let ResolutionOutcome::Recorded { reason } = &resolution.outcome
&& reason.0.as_str().starts_with(SLEPT)
{
let through = resolution.rationale.0.rsplit_once(THROUGH).and_then(|(_, ordinal)| ordinal.trim().parse().ok());
return Ok(Some(through.unwrap_or(night.ordinal)));
}
let closings = in_force(any_of(Selection { cites: Some(citing(&night.run, Some("subject"))), ..of_kind(PipelineKind::Resolution) }, "outcome", &["Slept"]));
let slept = documents(mind, &closings)?.into_iter().find_map(|view| match view.document {
PipelineDocument::Resolution(PipelineResolution { outcome: ResolutionOutcome::Slept { day_through, .. }, .. }) => Some(day_through),
_ => None,
});
let Some(through) = slept else { continue };
let Some(memory) = through else { return Ok(Some(night.ordinal)) };
let named = Selection { keys: Some(vec![memory.id.0.clone()]), ..of_kind(PipelineKind::Memory) };
let held = headers(mind, &named)?.into_iter().next().ok_or_else(|| anyhow!("the last memory a night was shown could not be read"))?;
return Ok(Some(held.admission.ordinal));
}
Ok(None)
}
@ -233,7 +226,7 @@ fn is_sleep_made(held: &Held) -> bool {
matches!(held.memory.origin, MemoryOrigin::Sleep { .. }) || !held.memory.consolidates.is_empty()
}
/// The night, derived. Her day: her memories in force admitted after what her newest night that closed `slept:`
/// The night, derived. Her day: her memories in force admitted after what her newest night whose closing in force is `Slept`
/// covered (a night that failed covered nothing), else in the 24 hours before her first night opened; formed
/// otherwise than in sleep, oldest first, at most `DAY_MAX`, the rest waiting for the next night. Her open Asked
/// and Committed items older than the day. A tide of `TIDE` older memories of other sorts, from an offset that
@ -475,8 +468,8 @@ pub(crate) fn pack(units: Vec<Vec<PipelineDocument>>) -> Vec<Vec<PipelineDocumen
/// How a night ended.
enum Finish {
/// `through` is the ordinal of the last memory of the day the night was shown, if it was shown any.
Slept { reply: SleepReply, through: Option<u64> },
/// `through` is the last memory of the day the night was shown, if it was shown any.
Slept { reply: SleepReply, through: Option<PipelineRef> },
Failed { refusals: u32, why: String },
}
@ -552,7 +545,7 @@ impl<C: ControlSource, S: ControlSource> SessionInbox for SleepInbox<'_, C, S> {
match decode(&text, self.context) {
Ok(reply) => match self.admit(&reply) {
Ok(()) => {
self.finish = Some(Finish::Slept { reply, through: self.context.day.last().map(|held| held.ordinal) });
self.finish = Some(Finish::Slept { reply, through: self.context.day.last().map(|held| reference(&held.id)) });
Ok((true, false))
}
Err(error) => {
@ -565,33 +558,35 @@ impl<C: ControlSource, S: ControlSource> SessionInbox for SleepInbox<'_, C, S> {
}
}
/// What a night closes with: the reason (her line, or the failure) and the rationale (the counts, and where her
/// day was covered through).
fn closing(finish: &Finish) -> (String, String) {
/// What a night closes with: the outcome (`Slept` with her line and where her day was covered through, or
/// `Recorded` with the failure) and the rationale (the counts, display only: nothing reads it).
fn closing(finish: &Finish) -> (ResolutionOutcome, String) {
match finish {
Finish::Slept { reply, through } => (
format!("{SLEPT}{}", reply.night),
ResolutionOutcome::Slept { line: reply.night.as_str().into(), day_through: through.clone() },
format!(
"kept {}, consolidated {} from {}, retired {}, formed {}{}",
"kept {}, consolidated {} from {}, retired {}, formed {}",
reply.keep.len(),
reply.consolidate.len(),
reply.consolidate.iter().map(|consolidation| consolidation.consumes.len()).sum::<usize>(),
reply.retire.len(),
reply.remember.len(),
through.map_or(String::new(), |ordinal| format!("{THROUGH}{ordinal}"))
reply.remember.len()
),
),
Finish::Failed { refusals, why } => (format!("failed after {refusals} refusals: {why}"), "The night ended without a reply the ledger took.".to_string()),
Finish::Failed { refusals, why } => (
ResolutionOutcome::Recorded { reason: format!("failed after {refusals} refusals: {why}").as_str().into() },
"The night ended without a reply the ledger took.".to_string(),
),
}
}
/// Closes the night's run Recorded with her line and the counts, or the failure.
/// Closes the night's run: `Slept` with her line and the cut line, or `Recorded` with the failure.
fn close(organs: &Organs, run: &PipelineRef, finish: &Finish) -> Result<()> {
let (reason, rationale) = closing(finish);
let (outcome, rationale) = closing(finish);
let resolution = PipelineResolution {
subject: run.clone(),
sequence: next_sequence(organs.mind, run)?,
outcome: ResolutionOutcome::Recorded { reason: reason.as_str().into() },
outcome,
rationale: rationale.as_str().into(),
resolved_on: date(organs.clock.now()),
};

View file

@ -246,7 +246,7 @@ fn night_over(earlier: usize, memories: &[(String, MemorySort, MemoryOrigin, i64
let mut events: Vec<(i64, Vec<Vec<PipelineDocument>>)> = Vec::new();
for n in 0..earlier {
let label = format!("before{n}");
let closed = crate::testkit::close(run_ref(&label), ResolutionOutcome::Recorded { reason: "slept: ok".into() });
let closed = crate::testkit::close(run_ref(&label), ResolutionOutcome::Slept { line: "ok".into(), day_through: None });
events.push((T0 - 100 * 3_600_000 + n as i64, vec![vec![run(&label, RunTurn::Sleep, RunOperator::Mind, &[])], vec![closed]]));
}
for (label, sort, origin, offset) in memories {
@ -265,18 +265,29 @@ fn night_over(earlier: usize, memories: &[(String, MemorySort, MemoryOrigin, i64
/// A night's own ending, admitted: slept through what it was shown, or failed.
fn close_night(mind: &TestMind, run: &PipelineRef, finish: &Finish) {
let (reason, rationale) = closing(finish);
let (outcome, rationale) = closing(finish);
mind.committed(vec![PipelineDocument::Resolution(PipelineResolution {
subject: run.clone(),
sequence: 1,
outcome: ResolutionOutcome::Recorded { reason: reason.as_str().into() },
outcome,
rationale: rationale.as_str().into(),
resolved_on: eureka_pipeline::Date("2026-10-09".into()),
})]);
}
fn slept_over(context: &Context) -> Finish {
Finish::Slept { reply: SleepReply { night: "n".into(), keep: vec![], consolidate: vec![], retire: vec![], remember: vec![] }, through: context.day.last().map(|held| held.ordinal) }
Finish::Slept { reply: SleepReply { night: "n".into(), keep: vec![], consolidate: vec![], retire: vec![], remember: vec![] }, through: context.day.last().map(|held| reference(&held.id)) }
}
/// A closing someone other than the organ wrote: a Recorded one, whatever its words say.
fn closed_by_hand(mind: &TestMind, run: &PipelineRef, reason: &str, rationale: &str) {
mind.committed(vec![PipelineDocument::Resolution(PipelineResolution {
subject: run.clone(),
sequence: 1,
outcome: ResolutionOutcome::Recorded { reason: reason.into() },
rationale: rationale.into(),
resolved_on: eureka_pipeline::Date("2026-10-09".into()),
})]);
}
fn failed() -> Finish {
@ -406,6 +417,73 @@ fn a_night_that_failed_or_never_ran_covers_nothing_so_its_day_comes_back() {
assert_eq!(day_ids(&fifth), vec![id("g0"), id("g1"), id("g2")]);
}
#[test]
fn a_hand_written_closing_moves_no_cut_line() {
// Closed Recorded with the words and the numbers the old reader trusted: the day is owed all the same.
let days: Vec<_> = (0..3).map(|n| (format!("g{n}"), MemorySort::Learned, wake(), 10 + n as i64)).collect();
let (mind, first) = night_over(0, &days);
mind.admit_at(at(T0 + 2000));
closed_by_hand(&mind, &first, "slept: closed by hand", "kept 0; day through ordinal 999999999");
let (_, next) = next_night(&mind, "second", 3000);
assert_eq!(day_ids(&next), vec![id("g0"), id("g1"), id("g2")], "nothing was shown, so nothing is dropped");
// A hundred-memory day closed by hand covers none of it; a Slept close over what was then shown covers it.
let days: Vec<_> = (0..100).map(|n| (format!("day{n:03}"), MemorySort::Learned, wake(), 10 + n as i64)).collect();
let (mind, first) = night_over(0, &days);
mind.admit_at(at(T0 + 2000));
closed_by_hand(&mind, &first, "slept: closed by hand", "kept 96 (by hand)");
let (second, owed) = next_night(&mind, "second", 3000);
assert_eq!(day_ids(&owed), (0..96).map(|n| id(&format!("day{n:03}"))).collect::<Vec<_>>(), "the overflow is not dropped by a hand");
mind.admit_at(at(T0 + 3600));
close_night(&mind, &second, &slept_over(&owed));
let (_, third) = next_night(&mind, "third", 4000);
assert_eq!(day_ids(&third), (96..100).map(|n| id(&format!("day{n:03}"))).collect::<Vec<_>>());
}
#[test]
fn a_slept_nights_closing_names_the_last_memory_it_was_shown_even_when_that_night_consumed_it() {
let days: Vec<_> = (0..100).map(|n| (format!("day{n:03}"), MemorySort::Learned, wake(), 10 + n as i64)).collect();
let (mind, tonight) = night_over(0, &days);
let shown = context(&mind, &tonight, at(T0 + 5000)).unwrap();
let (outcome, rationale) = closing(&slept_over(&shown));
assert_eq!(outcome, ResolutionOutcome::Slept { line: "n".into(), day_through: Some(crate::testkit::reference(eureka_pipeline::PipelineKind::Memory, &id("day095"))) });
assert_eq!(rationale, "kept 0, consolidated 0 from 0, retired 0, formed 0", "the rationale is the counts and says nothing of the cut line");
// The night folds the last memory it was shown into a consolidation, so day095 leaves force.
mind.admit_at(at(T0 + 2000));
let PipelineDocument::Memory(mut merged) = memory_from("merged", MemorySort::Learned, "Merged.", &[], wake(), 500) else { unreachable!() };
let consumed = crate::testkit::reference(eureka_pipeline::PipelineKind::Memory, &id("day095"));
merged.consolidates = vec![consumed.clone()];
let by = crate::testkit::reference(eureka_pipeline::PipelineKind::Memory, &id("merged"));
mind.committed(vec![PipelineDocument::Memory(merged), crate::testkit::close(consumed, ResolutionOutcome::Superseded { by: vec![by] })]);
mind.admit_at(at(T0 + 2500));
close_night(&mind, &tonight, &slept_over(&shown));
let (_, next) = next_night(&mind, "tomorrow", 3000);
assert_eq!(day_ids(&next), (96..100).map(|n| id(&format!("day{n:03}"))).collect::<Vec<_>>(), "the cut line is read from the memory that is no longer in force");
}
#[test]
fn a_slept_closing_that_was_withdrawn_covers_nothing() {
let days: Vec<_> = (0..3).map(|n| (format!("f{n}"), MemorySort::Learned, wake(), 10 + n as i64)).collect();
let (mind, first) = night_over(0, &days);
let shown = context(&mind, &first, at(T0 + 5000)).unwrap();
mind.admit_at(at(T0 + 2000));
close_night(&mind, &first, &slept_over(&shown));
// The closing is withdrawn (the run is live again) and the run ends in failure: no Slept closing stands.
mind.admit_at(at(T0 + 2100));
let closing_ref = crate::testkit::reference(eureka_pipeline::PipelineKind::Resolution, &format!("{INSTANCE}:resolution:run.tonight.n1"));
mind.committed(vec![crate::testkit::close(closing_ref, crate::testkit::withdrawn())]);
mind.admit_at(at(T0 + 2200));
mind.committed(vec![PipelineDocument::Resolution(PipelineResolution {
subject: first.clone(),
sequence: 2,
outcome: ResolutionOutcome::Recorded { reason: "failed after 3 refusals: no reply the ledger took".into() },
rationale: "The night ended without a reply the ledger took.".into(),
resolved_on: eureka_pipeline::Date("2026-10-09".into()),
})]);
let (_, next) = next_night(&mind, "second", 3000);
assert_eq!(day_ids(&next), vec![id("f0"), id("f1"), id("f2")], "a withdrawn Slept closing leaves its day owed");
}
#[test]
fn the_first_nights_day_is_the_twenty_four_hours_before_it_opened_to_the_second() {
let hour = 3_600_000;

View file

@ -193,6 +193,37 @@
"Withdrawn"
],
"type": "object"
},
{
"additionalProperties": false,
"description": "How a Sleep run ended when the ledger took her reply: `line` is her one line, `day_through` the last memory\nof her day she was shown (none when the day was empty). It is the one place a night's cut line is kept.",
"properties": {
"Slept": {
"properties": {
"day_through": {
"anyOf": [
{
"$ref": "#/$defs/PipelineRef"
},
{
"type": "null"
}
]
},
"line": {
"$ref": "#/$defs/Line"
}
},
"required": [
"line"
],
"type": "object"
}
},
"required": [
"Slept"
],
"type": "object"
}
]
},

View file

@ -2609,6 +2609,37 @@
"required": [
"Withdrawn"
]
},
{
"description": "How a Sleep run ended when the ledger took her reply: `line` is her one line, `day_through` the last memory\nof her day she was shown (none when the day was empty). It is the one place a night's cut line is kept.",
"type": "object",
"properties": {
"Slept": {
"type": "object",
"properties": {
"day_through": {
"anyOf": [
{
"$ref": "#/$defs/PipelineRef"
},
{
"type": "null"
}
]
},
"line": {
"$ref": "#/$defs/Line"
}
},
"required": [
"line"
]
}
},
"additionalProperties": false,
"required": [
"Slept"
]
}
]
},

View file

@ -262,7 +262,8 @@
"rests_on",
"toward",
"run",
"reads"
"reads",
"day_through"
]
},
"ClaimOutcome": {
@ -5325,6 +5326,37 @@
"required": [
"Withdrawn"
]
},
{
"description": "How a Sleep run ended when the ledger took her reply: `line` is her one line, `day_through` the last memory\nof her day she was shown (none when the day was empty). It is the one place a night's cut line is kept.",
"type": "object",
"properties": {
"Slept": {
"type": "object",
"properties": {
"day_through": {
"anyOf": [
{
"$ref": "#/$defs/PipelineRef"
},
{
"type": "null"
}
]
},
"line": {
"$ref": "#/$defs/Line"
}
},
"required": [
"line"
]
}
},
"additionalProperties": false,
"required": [
"Slept"
]
}
]
},