research and idea kinds: typed documents for what Eyes and Imagination find #2

Merged
eureka merged 6 commits from hands/eureka-body-typed-research into main 2026-11-04 15:01:59 +00:00
25 changed files with 1637 additions and 42 deletions

View file

@ -27,6 +27,8 @@ sample.hand_off yggdrasil:hand_off:thought-cage.gamecult_-epiphany.2026-09-15 ep
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 9189a8696e7374616e6365a979676764726173696ca56c6162656cb7323032362d31302d3331743034313230302d74302d6d30a4736f7274a9436f6d6d6974746564a474657874d936492074616b6520746865206261636b67726f756e6420736861646572733b204c6962627920686f6c64732074686520736f756e64732ea561626f757491ab406c696262793a63686174a66f726967696e81a4526f6f6d82a5726f6f6d73a5737761726daa68656172645f66726f6d91ab406c696262793a63686174a7736f757263657391a3246531b173616c69656e63655f7065726d696c6c65cd02bcac636f6e736f6c69646174657390
sample.research eureka-state:research:p1.scope epiphany.pipeline.research.v2 EpiphanyPipelineResearchDocument 9189a863616d706169676eac657572656b612d7374617465a470617373a27031a56c6162656ca573636f7065a5636c61696dd93654686520656d626564206c696d6974206973203430393620746f6b656e7320776974682073696c656e74207472756e636174696f6e2ea664657461696cd934526561642066726f6d20746865206461656d6f6e277320696e646578206d6f64756c6520616e642070726f626564206c6976652ea8696e666572726564c2a7736f75726365739481a45265706f82a47265706fb147616d6543756c742f4570697068616e79a2617484a470617468d9256372617465732f687567696e6e2d6461656d6f6e2f7372632f696e6465782f6d6f642e7273aa73746172745f6c696e6532a8656e645f6c696e6538a6636f6d6d6974a8356639383232386481a357656283a375726cd92268747470733a2f2f7a657474656c6b617374656e2e64652f61746f6d69636974792fa4776f726bb65a657474656c6b617374656e2061746f6d6963697479ac7265747269657665645f6f6eaa323032362d30392d313581a84f6273657276656481a865766964656e636583a46b696e64a454657374a76c6f6361746f72aa636172676f2074657374a6726573756c74a26f6b81a8446f63756d656e7481a97265666572656e636582a46b696e64a652756c696e67a26964b6657572656b612d73746174653a72756c696e673a5238a67365727665739182a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131ab6f627365727665645f6f6eaa323032362d30392d3135
sample.idea eureka-state:idea:p1.i1 epiphany.pipeline.idea.v2 EpiphanyPipelineIdeaDocument 9189a863616d706169676eac657572656b612d7374617465a470617373a27031a56c6162656ca26931a57469746c65b24f6e65206e6f74652070657220636c61696da469646561d9204b65657020612070617373206173206d616e7920736d616c6c206e6f7465732ea974726164656f666673b74d6f726520646f63756d656e747320746f206c6973742ea872657374735f6f6e9182a46b696e64a85265736561726368a26964be657572656b612d73746174653a72657365617263683a70312e73636f7065a6746f776172649182a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131ab70726f706f7365645f6f6eaa323032362d30392d3135
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

@ -631,6 +631,46 @@ fn memory_is_well_formed(memory: &PipelineMemory, at: &str) -> Result<(), Pipeli
Ok(())
}
/// A research note states a claim and stands on at least one source that
/// names something: a Repo source a file and a range that does not end before
/// it starts, a Web source its address and work, an Observed one its
/// locator and result, a Document one a note other than this one. A claim
/// with no source is not research. An inferred note names what it inferred
/// from in those sources. Two notes citing each other cannot be seen from one
/// document; that refusal would need the image, so it is not made here.
fn research_is_well_formed(research: &PipelineResearch, at: &str) -> Result<(), PipelineRefusal> {
if research.claim.0.trim().is_empty() {
return Err(format_error(&format!("{at}.claim"), "an empty claim"));
}
if research.sources.is_empty() {
return Err(format_error(&format!("{at}.sources"), "a research note that names no source"));
}
let own_key = format!("{}:research:{}.{}", research.campaign.0, research.pass.0, research.label.0);
let blank = |text: &str| text.trim().is_empty();
for (index, source) in research.sources.iter().enumerate() {
let (field, empty) = match source {
ResearchSource::Repo { at, .. } if blank(&at.path.0) => ("at.path", "a repo source pinning no file"),
ResearchSource::Repo { at, .. } if at.end_line < at.start_line => ("at.end_line", "a repo source whose range ends before it starts"),
ResearchSource::Web { url, .. } if blank(&url.0) => ("url", "a web source with no address"),
ResearchSource::Web { work, .. } if blank(&work.0) => ("work", "a web source naming no work"),
ResearchSource::Observed { evidence } if blank(&evidence.locator.0) => ("evidence.locator", "an observation naming nothing observed"),
ResearchSource::Observed { evidence } if blank(&evidence.result.0) => ("evidence.result", "an observation with no result"),
ResearchSource::Document { reference } if reference.kind == PipelineKind::Research && reference.id.0 == own_key => ("reference", "a note that is its own source"),
_ => continue,
};
return Err(format_error(&format!("{at}.sources[{index}].{field}"), empty));
}
Ok(())
}
/// An idea says something. It may serve no fork and rest on nothing.
fn idea_is_well_formed(idea: &PipelineIdea, at: &str) -> Result<(), PipelineRefusal> {
if idea.idea.0.trim().is_empty() {
return Err(format_error(&format!("{at}.idea"), "an empty idea"));
}
Ok(())
}
unit_enums! {
EvidenceKind { Command, Test, Mutation, Probe, SourceRead, Capture }
ClaimOutcome { Holds, Falsified, Unproven }
@ -793,6 +833,28 @@ value_types! {
instance: Slug, label: Label, sort: MemorySort, text: Line, about: Vec<Short>[8], origin: MemoryOrigin,
sources: Vec<Short>[8], salience_permille: u16, consolidates: Vec<PipelineRef>[16],
} => memory_is_well_formed
/// One claim a pass found, with the sources it stands on, grouped by `pass`.
/// What is so, never what to do: a recommendation is an idea. `inferred`
/// marks a claim read from its sources rather than seen in them. It stores
/// no status: whether a note is current, superseded or wrong is its
/// resolutions'. `serves` names the question, cut or campaign document it
/// was gathered for. The pass's method and what it did not search is its
/// first note, labelled `scope`. Campaign-rooted:
/// `<campaign>:research:<pass>.<label>`.
pub struct PipelineResearch {
campaign: Slug, pass: Label, label: Label, claim: Line, detail: Para, inferred: bool,
sources: Vec<ResearchSource>[16], serves: Vec<PipelineRef>[8], observed_on: Date,
} => research_is_well_formed
/// One thing that could be, including options rejected and ideas no cut
/// uses yet. `rests_on` is what it stands on (a reader selects the ideas
/// resting on a stale note with `cites`); `toward` is the fork, ruling or
/// follow-up it was made for, and empty is legal. It stores no status:
/// taken up is its Deferred resolution, rejected its Recorded one.
/// Campaign-rooted: `<campaign>:idea:<pass>.<label>`.
pub struct PipelineIdea {
campaign: Slug, pass: Label, label: Label, title: Title, idea: Para, tradeoffs: Para,
rests_on: Vec<PipelineRef>[16], toward: Vec<PipelineRef>[8], proposed_on: Date,
} => idea_is_well_formed
}
/// A reference to another document. The id is parsed as a full pipeline id of
@ -872,6 +934,39 @@ impl Bounded for MemoryOrigin {
}
}
/// Where a research claim comes from. A `Repo` pin and a `Web` page are
/// outside the document set; an `Observed` result reuses a finding's evidence;
/// a `Document` is the one source that is a citation edge.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum ResearchSource {
/// A pinned file range in a repo.
Repo { repo: OrgRepo, at: DocRef },
/// A page read on the web: its address, the work it belongs to, and when.
Web { url: Line, work: Short, retrieved_on: Date },
/// A command, probe, read or capture the pass ran itself.
Observed { evidence: Evidence },
/// Another document in the mind.
Document { reference: PipelineRef },
}
impl Bounded for ResearchSource {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
match self {
Self::Repo { repo, at } => {
repo.validate(&format!("{field}.repo"))?;
at.validate(&format!("{field}.at"))
}
Self::Web { url, work, retrieved_on } => {
url.validate(&format!("{field}.url"))?;
work.validate(&format!("{field}.work"))?;
retrieved_on.validate(&format!("{field}.retrieved_on"))
}
Self::Observed { evidence } => evidence.validate(&format!("{field}.evidence")),
Self::Document { reference } => reference.validate(&format!("{field}.reference")),
}
}
}
/// How a subject was resolved. Every referent is a parsed `PipelineRef`, so a
/// resolution names its records by ids of the kinds they declare, validated
/// where every other referent is; a `Fixed` commit is a `Sha` whose referent
@ -936,7 +1031,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
/// sixteen per-kind value documents, and the envelope is the wire's
/// eighteen 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")]
@ -1059,6 +1154,10 @@ pipeline_kinds! {
"epiphany.pipeline.pulse.v2", "EpiphanyPipelinePulseDocument";
Memory(PipelineMemory) => EpiphanyPipelineMemoryDocument, "memory",
"epiphany.pipeline.memory.v2", "EpiphanyPipelineMemoryDocument";
Research(PipelineResearch) => EpiphanyPipelineResearchDocument, "research",
"epiphany.pipeline.research.v2", "EpiphanyPipelineResearchDocument";
Idea(PipelineIdea) => EpiphanyPipelineIdeaDocument, "idea",
"epiphany.pipeline.idea.v2", "EpiphanyPipelineIdeaDocument";
}
/// Parses a full document id: the grammar read backwards. A key and an id are
@ -1256,6 +1355,8 @@ pub fn pipeline_key(document: &PipelineDocument) -> Result<String, PipelineRefus
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::Research(value) => (campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[&value.pass.0, &value.label.0])?),
D::Idea(value) => (campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[&value.pass.0, &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])?),
@ -1509,6 +1610,24 @@ mod tests {
text: "I take the background shaders; Libby holds the sounds.".into(), about: vec![s("@libby:chat")],
origin: MemoryOrigin::Room { rooms: s("swarm"), heard_from: vec![s("@libby:chat")] }, sources: vec![s("$e1")], salience_permille: 700, consolidates: vec![],
}), format!("{INSTANCE}:memory:2026-10-31t041200-t0-m0")),
(D::Research(PipelineResearch {
campaign: slug(CAMPAIGN), pass: l("p1"), label: l("scope"),
claim: "The embed limit is 4096 tokens with silent truncation.".into(),
detail: "Read from the daemon's index module and probed live.".into(), inferred: false,
sources: vec![
ResearchSource::Repo { repo: repo(), at: DocRef { path: s("crates/huginn-daemon/src/index/mod.rs"), start_line: 50, end_line: 56, commit: sha() } },
ResearchSource::Web { url: "https://zettelkasten.de/atomicity/".into(), work: s("Zettelkasten atomicity"), retrieved_on: date() },
ResearchSource::Observed { evidence: evidence() },
ResearchSource::Document { reference: PipelineRef { kind: PipelineKind::Ruling, id: id("ruling", "R8") } },
],
serves: vec![PipelineRef { kind: PipelineKind::Question, id: id("question", "Q1") }], observed_on: date(),
}), format!("{CAMPAIGN}:research:p1.scope")),
(D::Idea(PipelineIdea {
campaign: slug(CAMPAIGN), pass: l("p1"), label: l("i1"), title: t("One note per claim"),
idea: "Keep a pass as many small notes.".into(), tradeoffs: "More documents to list.".into(),
rests_on: vec![PipelineRef { kind: PipelineKind::Research, id: id("research", "p1.scope") }],
toward: vec![PipelineRef { kind: PipelineKind::Question, id: id("question", "Q1") }], proposed_on: date(),
}), format!("{CAMPAIGN}:idea:p1.i1")),
]
}
@ -1870,6 +1989,70 @@ mod tests {
assert!(matches!(validated(&|memory| memory.text = Line("x".repeat(1001))), Err(PipelineRefusal::FieldBound { limit: 1000, .. })));
}
fn research_sample() -> PipelineResearch {
let PipelineDocument::Research(research) = samples().remove(16).0 else { unreachable!() };
research
}
fn idea_sample() -> PipelineIdea {
let PipelineDocument::Idea(idea) = samples().remove(17).0 else { unreachable!() };
idea
}
/// A research note has a claim and at least one source; each source is bounded.
#[test]
fn research_needs_a_source_and_a_claim() {
let validated = |change: &dyn Fn(&mut PipelineResearch)| {
let mut research = research_sample();
change(&mut research);
PipelineDocument::Research(research).validate()
};
assert_eq!(validated(&|_| {}), Ok(()));
let bad = |result: Result<(), PipelineRefusal>, field: &str| {
assert!(matches!(&result, Err(PipelineRefusal::InvalidFormat { field: found, .. }) if found == field), "{field}: {result:?}");
};
bad(validated(&|research| research.sources = vec![]), "research.sources");
bad(validated(&|research| research.claim = " ".into()), "research.claim");
bad(validated(&|research| research.claim = "".into()), "research.claim");
let source_bad = |source: ResearchSource, field: &str| {
bad(validated(&|research| research.sources = vec![source.clone()]), &format!("research.sources[0].{field}"));
};
source_bad(ResearchSource::Repo { repo: OrgRepo("GameCult/Huginn".into()), at: DocRef { path: s(" "), start_line: 1, end_line: 2, commit: sha() } }, "at.path");
source_bad(ResearchSource::Repo { repo: OrgRepo("GameCult/Huginn".into()), at: DocRef { path: s("a.rs"), start_line: 5, end_line: 4, commit: sha() } }, "at.end_line");
assert_eq!(validated(&|research| research.sources = vec![ResearchSource::Repo { repo: OrgRepo("GameCult/Huginn".into()), at: DocRef { path: s("a.rs"), start_line: 5, end_line: 5, commit: sha() } }]), Ok(()));
source_bad(ResearchSource::Web { url: " ".into(), work: s("w"), retrieved_on: date() }, "url");
source_bad(ResearchSource::Web { url: "https://x.test/".into(), work: s(""), retrieved_on: date() }, "work");
source_bad(ResearchSource::Observed { evidence: Evidence { kind: EvidenceKind::Probe, locator: " ".into(), result: "found".into() } }, "evidence.locator");
source_bad(ResearchSource::Observed { evidence: Evidence { kind: EvidenceKind::Probe, locator: "ran it".into(), result: "".into() } }, "evidence.result");
let own = research_sample();
let cites = |label: &str| PipelineRef { kind: PipelineKind::Research, id: Short(format!("{}:research:{}.{label}", own.campaign.0, own.pass.0)) };
source_bad(ResearchSource::Document { reference: cites(&own.label.0) }, "reference");
assert_eq!(validated(&|research| research.sources = vec![ResearchSource::Document { reference: cites("another") }]), Ok(()));
assert_eq!(validated(&|research| research.inferred = true), Ok(()));
assert!(matches!(validated(&|research| research.sources = vec![research_sample().sources.remove(2); 17]), Err(PipelineRefusal::FieldBound { limit: 16, .. })));
assert!(validated(&|research| research.sources = vec![ResearchSource::Web { url: Line("x".repeat(1001)), work: s("w"), retrieved_on: date() }]).is_err());
}
/// A blank idea is refused; an idea that serves and rests on nothing is admitted.
#[test]
fn idea_needs_text_and_may_serve_nothing() {
let validated = |change: &dyn Fn(&mut PipelineIdea)| {
let mut idea = idea_sample();
change(&mut idea);
PipelineDocument::Idea(idea).validate()
};
assert_eq!(validated(&|idea| (idea.rests_on, idea.toward) = (vec![], vec![])), Ok(()));
assert!(matches!(validated(&|idea| idea.idea = " ".into()), Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "idea.idea"));
}
/// Both are keyed under their campaign by pass and label.
#[test]
fn research_and_idea_are_keyed_by_pass_and_label() -> Result<()> {
assert_eq!(pipeline_key(&PipelineDocument::Research(research_sample()))?, format!("{CAMPAIGN}:research:p1.scope"));
assert_eq!(pipeline_key(&PipelineDocument::Idea(idea_sample()))?, format!("{CAMPAIGN}:idea:p1.i1"));
Ok(())
}
/// A memory's origin is bounded field by field, and a private one names whom she heard it from.
#[test]
fn a_memory_origin_is_bounded_and_a_direct_one_names_who_she_heard_it_from() {
@ -3457,11 +3640,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(), 16, "sixteen kinds");
assert_eq!(PipelineKind::ALL.len(), 18, "eighteen 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 sixteen kinds, by name"
"the document schema lists exactly the eighteen kinds, by name"
);
let refusal_schema = serde_json::to_value(schemars::schema_for!(PipelineRefusal))?;

View file

@ -17,6 +17,8 @@ use crate::trouble::Trouble;
const GAP_HOME: &str = "eureka-substrate";
/// How many neighbours `precedent` asks the index for.
const PRECEDENT_TOP_K: u32 = 10;
/// How many neighbours `known` asks the index for.
const KNOWN_TOP_K: u32 = 10;
#[derive(Args, Clone, Debug, Default)]
pub struct Paging {
@ -107,6 +109,26 @@ pub enum Recipe {
},
/// The substrate's known gaps.
Gaps(Paging),
/// Research and ideas nearest some words, in every campaign, closed ones included.
Known {
text: String,
#[command(flatten)]
page: Paging,
},
/// One pass's research and ideas, whole, in no promised order (a pass spanning batches is grouped by batch).
Pass {
campaign: String,
pass: String,
#[command(flatten)]
page: Paging,
},
/// Ideas in force.
Ideas {
#[arg(required = true)]
campaigns: Vec<String>,
#[command(flatten)]
page: Paging,
},
}
/// What a recipe asks the mind: the selection, the semantic part when it has
@ -140,6 +162,9 @@ impl Recipe {
(precedent(&campaign), Some(SemanticQuery { text: Line(text), top_k: PRECEDENT_TOP_K }), page)
}
Self::Gaps(page) => (gaps(), None, page),
Self::Known { text, page } => (known(), Some(SemanticQuery { text: Line(text), top_k: KNOWN_TOP_K }), page),
Self::Pass { campaign, pass, page } => (pass_of(&campaign, &pass), None, page),
Self::Ideas { campaigns, page } => (ideas(&campaigns), None, page),
};
Ok(Ask { selection, semantic, page })
}
@ -248,6 +273,21 @@ fn precedent(campaign: &str) -> Value {
})
}
fn known() -> Value {
json!({ "schemas": [schema(PipelineKind::Research), schema(PipelineKind::Idea)] })
}
fn pass_of(campaign: &str, pass: &str) -> Value {
json!({
"schemas": [schema(PipelineKind::Research), schema(PipelineKind::Idea)],
"fields": [root(&[campaign.to_string()]), { "index": "pass", "op": "any_of", "values": [pass] }],
})
}
fn ideas(campaigns: &[String]) -> Value {
json!({ "schemas": [schema(PipelineKind::Idea)], "fields": [root(campaigns), in_force()] })
}
fn gaps() -> Value {
json!({
"schemas": [schema(PipelineKind::FollowUp)],
@ -279,6 +319,8 @@ pub fn help() -> Vec<(&'static str, String)> {
"{}\nsemantic: {{ \"text\": \"<text>\", \"top_k\": {PRECEDENT_TOP_K} }}",
selection(precedent(&c()))
);
let known_help = format!("{}
semantic: {{ \"text\": \"<text>\", \"top_k\": {KNOWN_TOP_K} }}", selection(known()));
vec![
("campaigns", selection(campaigns())),
("target", selection(target(&c()))),
@ -293,5 +335,8 @@ pub fn help() -> Vec<(&'static str, String)> {
("ledger", selection(ledger(&c()))),
("precedent", precedent_help),
("gaps", selection(gaps())),
("known", known_help),
("pass", selection(pass_of(&c(), "<pass>"))),
("ideas", selection(ideas(&[c()]))),
]
}

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, memory,
hand_off, run, pulse, memory, research, idea,
);
/// The schema named, or the names when none is: the exit code is 2 for a name

View file

@ -784,11 +784,24 @@ fn the_recipes_send_exactly_the_pages_selections() {
vec!["gaps"],
json!({ "schemas": [schema("follow_up")], "fields": [root(&["eureka-substrate"]), in_force] }),
),
(vec!["known", "the words"], json!({ "schemas": [schema("research"), schema("idea")] })),
(
vec!["pass", c, "p1"],
json!({
"schemas": [schema("research"), schema("idea")],
"fields": [root(&[c]), { "index": "pass", "op": "any_of", "values": ["p1"] }],
}),
),
(vec!["ideas", c], json!({ "schemas": [schema("idea")], "fields": [root(&[c]), in_force] })),
];
for (args, literal) in cases {
let (selection, semantic) = sent(&args);
assert_eq!(selection, page_literal(literal), "{args:?}");
let expected = (args[0] == "precedent").then(|| ("the question in plain words".to_string(), 10));
let expected = match args[0] {
"precedent" => Some(("the question in plain words".to_string(), 10)),
"known" => Some(("the words".to_string(), 10)),
_ => None,
};
assert_eq!(semantic, expected, "{args:?}");
}
}
@ -1109,11 +1122,12 @@ 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(), 17, "{names:?}");
assert_eq!(names.len(), 19, "{names:?}");
assert_eq!(names[0], "campaign");
assert_eq!(names[14], "pulse");
assert_eq!(names[15], "memory");
assert_eq!(names[16], "selection");
assert_eq!((names[16], names[17]), ("research", "idea"));
assert_eq!(names[18], "selection");
for kind in PipelineKind::ALL {
assert!(names.contains(&kind.name()), "{}", kind.name());
}
@ -1159,7 +1173,7 @@ fn help_is_the_schema_surface() {
fn every_recipe_has_help() {
for recipe in [
"campaigns", "target", "rulings", "open", "findings", "unreported", "blocked", "unjudged", "history", "cut", "ledger",
"precedent", "gaps",
"precedent", "gaps", "known", "pass", "ideas",
] {
let ran = run(&[], &[recipe, "--help"], None);
assert_eq!(ran.code, 0, "{recipe}");

View file

@ -623,7 +623,7 @@ fn check(docs: &Docs<'_>, staged: &Staged, mind: &Slug) -> Result<(), MindRefusa
let replay = docs.in_image(K::Run, key) == Some(&staged.document);
run_rule(docs, key, run, replay)
}
D::FollowUp(_) | D::Instance(_) | D::Pulse(_) | D::Memory(_) => Ok(()),
D::FollowUp(_) | D::Instance(_) | D::Pulse(_) | D::Memory(_) | D::Research(_) | D::Idea(_) => Ok(()),
}
}
@ -722,6 +722,11 @@ fn matrix(subject: PipelineKind, outcome: &ResolutionOutcome) -> bool {
(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::Research, O::Superseded { by }) => all(by, K::Research),
(K::Research, O::Recorded { .. } | O::Withdrawn { .. }) => true,
(K::Idea, O::Deferred { to }) => matches!(to.kind, K::Question | K::CutSpec | K::FollowUp | K::Ruling),
(K::Idea, O::Superseded { by }) => all(by, K::Idea),
(K::Idea, O::Recorded { .. } | O::Withdrawn { .. }) => true,
(K::Resolution, O::Withdrawn { .. }) => true,
_ => false,
}
@ -2545,6 +2550,176 @@ mod tests {
committed(admit(&mut mind, vec![consolidating(r(K::Memory, &memory_key("m1")))]));
}
fn research_key(pass: &str, label: &str) -> String {
format!("{CAMPAIGN}:research:{pass}.{label}")
}
fn idea_key(pass: &str, label: &str) -> String {
format!("{CAMPAIGN}:idea:{pass}.{label}")
}
/// Research is superseded by research, recorded or withdrawn; an idea is deferred to a
/// question, cut spec, follow-up or ruling, superseded by an idea, recorded or withdrawn.
/// Their neighbours are refused.
#[test]
fn research_and_idea_resolution_matrix() {
let incompatible = |kind: K, outcome: &str| MindRefusal::IncompatibleResolution { subject_kind: kind, outcome: outcome.into() };
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"))),
research("p", "a", "One fact."),
research("p", "b", "A later fact."),
idea("p", "i1", "One way."),
idea("p", "i2", "Another way."),
question_n(1),
run("w", &[spec_ref("1")]),
]));
mind
};
let note = || r(K::Research, &research_key("p", "a"));
let way = || r(K::Idea, &idea_key("p", "i1"));
let spec = || r(K::CutSpec, &id("cut_spec", "cut-1.r1"));
let ruling_ref = || r(K::Ruling, &id("ruling", "R1"));
for outcome in [superseded(&[r(K::Research, &research_key("p", "b"))]), recorded_outcome(), withdrawn()] {
committed(admit(&mut open(), vec![resolution(note(), outcome)]));
}
for outcome in [
ResolutionOutcome::Deferred { to: spec() },
ResolutionOutcome::Deferred { to: ruling_ref() },
ResolutionOutcome::Deferred { to: r(K::Question, &id("question", "Q1")) },
ResolutionOutcome::Deferred { to: r(K::FollowUp, &id("follow_up", "FU")) },
superseded(&[r(K::Idea, &idea_key("p", "i2"))]),
recorded_outcome(),
withdrawn(),
] {
committed(admit(&mut open(), vec![resolution(way(), outcome)]));
}
let refused_note = [
(ResolutionOutcome::Deferred { to: spec() }, "Deferred"),
(superseded(&[r(K::Idea, &idea_key("p", "i2"))]), "Superseded"),
(ResolutionOutcome::Answered { by: ruling_ref() }, "Answered"),
(ResolutionOutcome::Fixed { commit: sha(), by: None }, "Fixed"),
];
for (outcome, name) in refused_note {
assert_eq!(refusal(admit(&mut open(), vec![resolution(note(), outcome)])), incompatible(K::Research, name), "{name}");
}
let held_run = || r(K::Run, &run_key("w"));
let refused_idea = [
(ResolutionOutcome::Answered { by: ruling_ref() }, "Answered"),
(superseded(&[spec()]), "Superseded"),
(ResolutionOutcome::Deferred { to: held_run() }, "Deferred"),
(ResolutionOutcome::Deferred { to: r(K::Research, &research_key("p", "b")) }, "Deferred"),
(ResolutionOutcome::Fixed { commit: sha(), by: None }, "Fixed"),
];
for (outcome, name) in refused_idea {
assert_eq!(refusal(admit(&mut open(), vec![resolution(way(), outcome)])), incompatible(K::Idea, name), "{name}");
}
}
fn recorded_outcome() -> ResolutionOutcome {
ResolutionOutcome::Recorded { reason: "why".into() }
}
/// A rejected option lands with its Recorded resolution in one batch, and an idea that
/// serves nothing and rests on nothing is admitted.
#[test]
fn a_rejected_option_lands_with_its_resolution_in_one_batch() {
let mut mind = with_specs();
committed(admit(&mut mind, vec![
idea("p", "no", "A road not taken."),
resolution(r(K::Idea, &idea_key("p", "no")), recorded_outcome()),
]));
assert!(!docs_of(&mind).in_force(K::Idea, &idea_key("p", "no")));
}
/// Research supersedes across campaigns (Lineage::Global); the
/// note stays open until its closure lands.
#[test]
fn a_later_pass_in_another_campaign_supersedes_research() {
let mut mind = seeded();
committed(admit(&mut mind, vec![research("p", "a", "Old fact.")]));
let mut batch = rival_revision(1);
let D::Research(mut later) = research("q", "a", "New fact.") else { panic!("research") };
later.campaign = RIVAL.into();
batch.push(D::Research(later));
batch.push(resolution(
r(K::Research, &research_key("p", "a")),
superseded(&[r(K::Research, &format!("{RIVAL}:research:q.a"))]),
));
committed(admit(&mut mind, batch));
assert!(!docs_of(&mind).in_force(K::Research, &research_key("p", "a")));
assert!(docs_of(&mind).in_force(K::Research, &format!("{RIVAL}:research:q.a")));
}
/// A withdrawn closure reopens the note: Recorded closes it, withdrawing that record puts it back in force.
#[test]
fn withdrawing_a_notes_resolution_reopens_it() {
let mut mind = seeded();
committed(admit(&mut mind, vec![research("p", "a", "A fact."), idea("p", "i", "A way.")]));
for (kind, key) in [(K::Research, research_key("p", "a")), (K::Idea, idea_key("p", "i"))] {
let (_, landed) = committed(admit(&mut mind, vec![resolution(r(kind, &key), recorded_outcome())]));
assert!(!docs_of(&mind).in_force(kind, &key), "{kind:?} is closed");
let closure = landed.into_iter().find(|write| write.kind == K::Resolution).expect("the resolution landed");
committed(admit(&mut mind, vec![resolution(r(K::Resolution, &closure.id.0), withdrawn())]));
assert!(docs_of(&mind).in_force(kind, &key), "{kind:?} is open again");
}
}
/// A question may be raised in a research note, and only in one the mind holds.
#[test]
fn question_raised_in_a_research_note() {
let mut mind = seeded();
let D::Question(mut raised) = question("Q1", &["A", "B"], "A") else { panic!("question") };
raised.raised_in = Some(r(K::Research, &research_key("p", "a")));
assert_eq!(
refusal(admit(&mut mind, vec![D::Question(raised.clone())])),
MindRefusal::MissingReference { kind: K::Research, id: research_key("p", "a") }
);
committed(admit(&mut mind, vec![research("p", "a", "A fact.")]));
committed(admit(&mut mind, vec![D::Question(raised)]));
assert!(docs_of(&mind).in_force(K::Question, &id("question", "Q1")));
}
/// Every referent of a research note and an idea must be held; a pass.label beyond the key's
/// local bound is refused whole.
#[test]
fn research_and_idea_name_held_referents_and_keep_their_key_in_bound() {
let mut mind = with_specs();
let missing = id("question", "Q404");
let D::Research(mut serving) = research("p", "a", "A fact.") else { panic!("research") };
serving.serves = vec![r(K::Question, &missing)];
assert_eq!(
refusal(admit(&mut mind, vec![D::Research(serving)])),
MindRefusal::MissingReference { kind: K::Question, id: missing.clone() }
);
let D::Idea(mut resting) = idea("p", "i", "A way.") else { panic!("idea") };
resting.rests_on = vec![r(K::Research, &research_key("p", "gone"))];
assert_eq!(
refusal(admit(&mut mind, vec![D::Idea(resting)])),
MindRefusal::MissingReference { kind: K::Research, id: research_key("p", "gone") }
);
let D::Research(mut document) = research("p", "a", "A fact.") else { panic!("research") };
document.sources = vec![eureka_pipeline::ResearchSource::Document { reference: r(K::Question, &missing) }];
// A Document source is held to its referent, as an edge.
assert_eq!(
refusal(admit(&mut mind, vec![D::Research(document)])),
MindRefusal::MissingReference { kind: K::Question, id: missing }
);
let long = "x".repeat(40);
assert!(matches!(
refusal(admit(&mut mind, vec![research(&long, &"y".repeat(30), "Too long a key.")])),
MindRefusal::Document(_)
));
assert!(docs_of(&mind).find(K::Research, &research_key("p", "a")).is_none(), "a refused batch commits nothing");
committed(admit(&mut mind, vec![idea("p", "i", "Rests on nothing."), research("p", "a", "A fact.")]));
let D::Idea(mut resting) = idea("p", "j", "Rests on a note.") else { panic!("idea") };
resting.rests_on = vec![r(K::Research, &research_key("p", "a"))];
resting.toward = vec![r(K::Idea, &idea_key("p", "i"))];
committed(admit(&mut mind, vec![D::Idea(resting)]));
}
/// A run ends: Recorded or Withdrawn, and no other outcome.
#[test]
fn a_run_resolves_recorded_or_withdrawn_and_nothing_else() {

View file

@ -8,7 +8,7 @@
use cultcache_rs::CultCacheEnvelope;
use eureka_pipeline::{
Label, OrgRepo, PipelineCampaign, PipelineDocument, PipelineKind, PipelineRef, PipelineResolution, PipelineRun, PipelineStewardship, ResolutionOutcome,
RunOperator, Short, Slug,
ResearchSource, RunOperator, Short, Slug,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -321,7 +321,7 @@ pub(crate) enum Lineage<'a> {
/// 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
/// A ruling, a follow-up, a research note or an idea: operator directions and re-homed work cut
/// across campaigns.
Global,
}
@ -333,7 +333,7 @@ pub(crate) fn line_of(document: &PipelineDocument) -> Option<Lineage<'_>> {
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::Ruling(_) | D::FollowUp(_) | D::Research(_) | D::Idea(_) => Some(Lineage::Global),
D::Campaign(_)
| D::Question(_)
| D::CutReport(_)
@ -388,10 +388,14 @@ pub enum CitationRole {
Documents,
Claims,
Consolidates,
Serves,
Sources,
RestsOn,
Toward,
}
impl CitationRole {
pub const ALL: [CitationRole; 17] = [
pub const ALL: [CitationRole; 21] = [
Self::RaisedIn,
Self::Answers,
Self::Rulings,
@ -409,6 +413,10 @@ impl CitationRole {
Self::Documents,
Self::Claims,
Self::Consolidates,
Self::Serves,
Self::Sources,
Self::RestsOn,
Self::Toward,
];
/// The wire spelling, equal to the referring field's name.
@ -431,6 +439,10 @@ impl CitationRole {
Self::Documents => "documents",
Self::Claims => "claims",
Self::Consolidates => "consolidates",
Self::Serves => "serves",
Self::Sources => "sources",
Self::RestsOn => "rests_on",
Self::Toward => "toward",
}
}
@ -453,6 +465,8 @@ impl CitationRole {
Self::Documents => K::HandOff,
Self::Claims => K::Run,
Self::Consolidates => K::Memory,
Self::Serves | Self::Sources => K::Research,
Self::RestsOn | Self::Toward => K::Idea,
}
}
@ -476,7 +490,11 @@ impl CitationRole {
| Self::SupersededBy
| Self::ResolvedBy
| Self::DeferredTo
| Self::Documents => PipelineKind::ALL.to_vec(),
| Self::Documents
| Self::Serves
| Self::Sources
| Self::RestsOn
| Self::Toward => PipelineKind::ALL.to_vec(),
}
}
}
@ -549,6 +567,17 @@ pub(crate) fn citations(document: &PipelineDocument) -> Vec<(CitationRole, Pipel
),
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::Research(research) => {
edges.extend(research.serves.iter().map(|to| (R::Serves, to.clone())));
edges.extend(research.sources.iter().filter_map(|source| match source {
ResearchSource::Document { reference } => Some((R::Sources, reference.clone())),
_ => None,
}));
}
D::Idea(idea) => {
edges.extend(idea.rests_on.iter().map(|to| (R::RestsOn, to.clone())));
edges.extend(idea.toward.iter().map(|to| (R::Toward, to.clone())));
}
D::Campaign(_) | D::Target(_) | D::Instance(_) | D::Stewardship(_) | D::Pulse(_) => {}
}
edges

View file

@ -369,6 +369,36 @@ pub(crate) fn memory(label: &str, sort: MemorySort, text: &str, about: &[&str])
})
}
/// One research note of pass `pass`: a claim standing on one observed source, serving nothing.
pub(crate) fn research(pass: &str, label: &str, claim: &str) -> PipelineDocument {
PipelineDocument::Research(eureka_pipeline::PipelineResearch {
campaign: slug(CAMPAIGN),
pass: l(pass),
label: l(label),
claim: claim.into(),
detail: "".into(),
inferred: false,
sources: vec![eureka_pipeline::ResearchSource::Observed { evidence: evidence() }],
serves: vec![],
observed_on: date(),
})
}
/// One idea of pass `pass`, resting on nothing and made for no fork.
pub(crate) fn idea(pass: &str, label: &str, text: &str) -> PipelineDocument {
PipelineDocument::Idea(eureka_pipeline::PipelineIdea {
campaign: slug(CAMPAIGN),
pass: l(pass),
label: l(label),
title: eureka_pipeline::Title(label.into()),
idea: text.into(),
tradeoffs: "".into(),
rests_on: vec![],
toward: vec![],
proposed_on: date(),
})
}
pub(crate) fn provenance(faculty: Faculty) -> PipelineProvenance {
PipelineProvenance { faculty, agent: s("claude"), session: s("session-1"), tool: s("admit") }
}

View file

@ -85,6 +85,22 @@ 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::Research(research) => {
lines.push(&research.claim.0);
lines.push(&research.detail.0);
for source in &research.sources {
match source {
eureka_pipeline::ResearchSource::Web { work, .. } => lines.push(&work.0),
eureka_pipeline::ResearchSource::Observed { evidence } => lines.push(&evidence.result.0),
eureka_pipeline::ResearchSource::Repo { .. } | eureka_pipeline::ResearchSource::Document { .. } => {}
}
}
}
D::Idea(idea) => {
lines.push(&idea.title.0);
lines.push(&idea.idea.0);
lines.push(&idea.tradeoffs.0);
}
D::Instance(_) | D::Stewardship(_) | D::Run(_) | D::Pulse(_) => {}
}
lines.retain(|line| !line.is_empty());
@ -205,6 +221,30 @@ mod tests {
(mind, hand_off_key)
}
/// Research is indexed by claim, detail, each Web source's work and each Observed result;
/// an idea by title, idea and tradeoffs. Neither moved the text version.
#[test]
fn index_text_of_research_and_idea() {
let D::Research(mut note) = research("p", "a", "The limit is 4096.") else { unreachable!() };
note.detail = "Read from the module.".into();
note.sources = vec![
eureka_pipeline::ResearchSource::Web { url: "https://example.org".into(), work: s("A paper"), retrieved_on: date() },
eureka_pipeline::ResearchSource::Observed { evidence: evidence() },
eureka_pipeline::ResearchSource::Document { reference: r(K::Question, &id("question", "Q1")) },
];
let observed = evidence().result.0;
assert_eq!(index_text(&D::Research(note)).unwrap(), format!("The limit is 4096.
Read from the module.
A paper
{observed}"));
let D::Idea(mut way) = idea("p", "i", "Keep notes small.") else { unreachable!() };
way.tradeoffs = "More documents.".into();
assert_eq!(index_text(&D::Idea(way)).unwrap(), "i
Keep notes small.
More documents.");
assert_eq!(INDEX_TEXT_VERSION, 1);
}
fn texts(mind: &Mind<MemoryStore>) -> BTreeMap<String, String> {
mind.index_entries(None).unwrap().into_iter().map(|entry| (entry.id.id.0, entry.text)).collect()
}

View file

@ -75,8 +75,8 @@ fn require_grammatical_slug(field: &str, declared: &Slug) -> Result<(), MindRefu
})
}
/// 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
/// A cache that knows the twenty-one types a mind's store may hold: the leaf's
/// eighteen 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();
@ -128,7 +128,7 @@ fn fold(envelopes: &Envelopes, instance: &Slug) -> (Result<Image, MindRefusal>,
}
/// One instance's mind: the declared instance, the store, the registry of
/// the nineteen schemas, the store's envelopes as last pulled, and the image
/// the twenty-one schemas, the store's envelopes as last pulled, and the image
/// folded from them. The envelopes are the store's bytes and the image is a
/// pure fold of the receipts among them: both advance with each commit and
/// are rebuilt from the store on a lost swap, a store error and a restart.
@ -212,7 +212,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 nineteen; and
/// record at the current epoch; every type is one of the twenty-one; 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

@ -148,6 +148,8 @@ pub enum PipelineFacts {
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>, origin: MemoryOrigin, salience_permille: u16 },
Research { pass: Label, label: Label, claim: Line, inferred: bool, observed_on: Date },
Idea { pass: Label, label: Label, title: Title, proposed_on: Date },
}
impl PipelineFacts {
@ -259,6 +261,19 @@ impl PipelineFacts {
origin: value.origin.clone(),
salience_permille: value.salience_permille,
},
D::Research(value) => Self::Research {
pass: value.pass.clone(),
label: value.label.clone(),
claim: value.claim.clone(),
inferred: value.inferred,
observed_on: value.observed_on.clone(),
},
D::Idea(value) => Self::Idea {
pass: value.pass.clone(),
label: value.label.clone(),
title: value.title.clone(),
proposed_on: value.proposed_on.clone(),
},
}
}
}
@ -1389,6 +1404,27 @@ mod tests {
assert!(invalid("fields[0].values", "1.2")(&refused(&mind, &with(Selection::default(), any_of("cut", &["1.2"])))));
}
/// The `pass` alias answers a pass's research and its ideas, and nothing of another pass.
#[test]
fn the_pass_alias_selects_a_passs_research_and_ideas() {
let mut mind = seeded();
committed(admit(&mut mind, vec![
research("alpha", "scope", "A fact."),
idea("alpha", "way", "A way."),
research("beta", "scope", "Another fact."),
idea("beta", "way", "Another way."),
question_n(1),
]));
let in_pass = |pass: &str| {
let mut found = ids(&mind.query(&with(Selection::default(), any_of("pass", &[pass]))).unwrap());
found.sort();
found
};
assert_eq!(in_pass("alpha"), vec![id("idea", "alpha.way"), id("research", "alpha.scope")]);
assert_eq!(in_pass("beta"), vec![id("idea", "beta.way"), id("research", "beta.scope")]);
assert!(in_pass("gamma").is_empty(), "a pass nothing was admitted under matches nothing");
}
#[test]
fn enum_values_are_the_variant_names() {
let mind = worked();
@ -1705,6 +1741,17 @@ mod tests {
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 (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)];
note_document.sources = vec![
eureka_pipeline::ResearchSource::Document { reference: r(K::Finding, &f1) },
eureka_pipeline::ResearchSource::Repo { repo: OrgRepo(REPO.into()), at: eureka_pipeline::DocRef { path: s("a.rs"), start_line: 1, end_line: 2, commit: sha() } },
];
let D::Idea(mut way_document) = idea("p", "i", "A way.") else { unreachable!() };
way_document.rests_on = vec![r(K::Research, &note)];
way_document.toward = vec![r(K::Question, &q1)];
committed(admit(&mut mind, vec![D::Research(note_document), D::Idea(way_document)]));
let (n1, f1_closed, f2_closed) = (
id("resolution", "question.Q1.n1"),
@ -1739,8 +1786,12 @@ mod tests {
(CitationRole::Claims, &run_id, &spec_r2),
(CitationRole::Claims, &run_id, &fu),
(CitationRole::Consolidates, &newer, &older),
(CitationRole::Serves, &note, &q1),
(CitationRole::Sources, &note, &f1),
(CitationRole::RestsOn, &way, &note),
(CitationRole::Toward, &way, &q1),
];
assert_eq!(CitationRole::ALL.len(), 17);
assert_eq!(CitationRole::ALL.len(), 21);
for role in CitationRole::ALL {
let selection =
Selection { cited: Some(Incoming { role: role.name().into(), exists: true }), ..Selection::default() };

View file

@ -59,6 +59,8 @@ pub(crate) enum Alias {
/// A memory's origin kind: `Room`, `Direct`, `Wake` or `SelfRun`. Named `memory_origin` because
/// `origin` already selects a finding's origin.
MemoryOrigin,
/// A research note's or an idea's `pass` label: a pass lists whole by root and pass.
Pass,
}
const FACULTIES: [&str; 7] = ["SelfFaculty", "Imagination", "Hands", "Soul", "Life", "Eyes", "Operator"];
@ -75,7 +77,7 @@ const PRESENT: [&str; 1] = ["true"];
pub(crate) const STATUS_ALIASES: [Alias; 2] = [Alias::InForce, Alias::Ran];
impl Alias {
pub(crate) const ALL: [Alias; 19] = [
pub(crate) const ALL: [Alias; 20] = [
Self::Root,
Self::InForce,
Self::Faculty,
@ -95,6 +97,7 @@ impl Alias {
Self::Sort,
Self::About,
Self::MemoryOrigin,
Self::Pass,
];
pub(crate) fn name(self) -> &'static str {
@ -118,6 +121,7 @@ impl Alias {
Self::Sort => "sort",
Self::About => "about",
Self::MemoryOrigin => "memory_origin",
Self::Pass => "pass",
}
}
@ -140,6 +144,7 @@ impl Alias {
Self::Turn | Self::OperatedBy | Self::Ran => kind == K::Run,
Self::Voted | Self::Urged => kind == K::Pulse,
Self::Sort | Self::About | Self::MemoryOrigin => kind == K::Memory,
Self::Pass => matches!(kind, K::Research | K::Idea),
}
}
@ -159,7 +164,7 @@ impl Alias {
Self::Ran | Self::Voted | Self::Urged => Some(&PRESENT),
Self::Sort => Some(MemorySort::NAMES),
Self::MemoryOrigin => Some(&MEMORY_ORIGINS),
Self::Root | Self::Repo | Self::Cut | Self::About => None,
Self::Root | Self::Repo | Self::Cut | Self::About | Self::Pass => None,
}
}
@ -169,7 +174,7 @@ impl Alias {
match self {
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::Cut | Self::Pass => 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)),
}
@ -239,7 +244,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::Memory(_) => {
D::Target(_) | D::Question(_) | D::Ruling(_) | D::Verdict(_) | D::Finding(_) | D::Instance(_) | D::Resolution(_) | D::Run(_) | D::Pulse(_) | D::Memory(_) | D::Research(_) | D::Idea(_) => {
Vec::new()
}
},
@ -285,6 +290,8 @@ impl SelectionRow {
MemoryOrigin::SelfRun { .. } => "SelfRun",
}.to_string()]);
}
D::Research(research) => put(Alias::Pass, vec![research.pass.0.clone()]),
D::Idea(idea) => put(Alias::Pass, vec![idea.pass.0.clone()]),
_ => {}
}
Self { kind, key: key.to_string(), ordinal: view.admission.ordinal, values, refs, view }
@ -322,7 +329,7 @@ impl Row for SelectionRow {
}
}
/// The organ's closed lists: sixteen schemas, nineteen aliases, seventeen roles.
/// The organ's closed lists: eighteen schemas, twenty aliases, twenty-one roles.
/// A schema's name is its id, so the substrate's alias matching resolves
/// nothing beyond exact ids.
pub(crate) struct Vocabulary;
@ -368,7 +375,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 sixteen ids; a `keys` entry that is not a full id whose kind
/// one of the eighteen 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
@ -435,7 +442,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]); 16] = [
let table: [(K, &[&str]); 18] = [
(K::Campaign, &["repo"]),
(K::Target, &[]),
(K::Question, &[]),
@ -452,6 +459,8 @@ mod tests {
(K::Run, &["turn", "operated_by", "ran"]),
(K::Pulse, &["voted", "urged"]),
(K::Memory, &["sort", "about", "memory_origin"]),
(K::Research, &["pass"]),
(K::Idea, &["pass"]),
];
for (kind, extra) in table {
let want = [&["root", "in_force", "faculty"][..], extra].concat();
@ -464,10 +473,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
/// sixteen.
/// eighteen.
#[test]
fn each_kind_declares_the_roles_it_carries_and_their_targets() {
let table: [(K, &[&str]); 16] = [
let table: [(K, &[&str]); 18] = [
(K::Campaign, &[]),
(K::Target, &[]),
(K::Question, &["raised_in"]),
@ -484,13 +493,15 @@ mod tests {
(K::Run, &["claims"]),
(K::Pulse, &[]),
(K::Memory, &["consolidates"]),
(K::Research, &["serves", "sources"]),
(K::Idea, &["rests_on", "toward"]),
];
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(), 16);
assert_eq!(Vocabulary.target_leaves("subject").len(), 18);
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 sixteen
`index.json` is the publication manifest for every schema here: the eighteen
`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

@ -173,7 +173,9 @@
"HandOff",
"Run",
"Pulse",
"Memory"
"Memory",
"Research",
"Idea"
],
"type": "string"
},

View file

@ -67,7 +67,9 @@
"HandOff",
"Run",
"Pulse",
"Memory"
"Memory",
"Research",
"Idea"
],
"type": "string"
},

View file

@ -0,0 +1,127 @@
{
"$defs": {
"Date": {
"description": "A calendar date, `YYYY-MM-DD`.",
"maxLength": 10,
"type": "string"
},
"Label": {
"description": "A key label: `[A-Za-z0-9_-]{1,64}`.",
"maxLength": 64,
"type": "string"
},
"Para": {
"description": "Text of at most 4,000 UTF-8 bytes; longer narrative is cited by `DocRef`.",
"maxLength": 4000,
"type": "string"
},
"PipelineKind": {
"enum": [
"Campaign",
"Target",
"Question",
"Ruling",
"CutSpec",
"CutReport",
"Verdict",
"Finding",
"FollowUp",
"Resolution",
"Instance",
"Stewardship",
"HandOff",
"Run",
"Pulse",
"Memory",
"Research",
"Idea"
],
"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"
},
"Title": {
"description": "A title: 1 to 200 UTF-8 bytes, enforced by admission; JSON Schema\nlength counts characters. Holds at least one alphanumeric character,\nno control character, and no bidi control character (`U+061C`,\n`U+200E`, `U+200F`, `U+202A`-`U+202E`, `U+2066`-`U+2069`).",
"maxLength": 200,
"minLength": 1,
"type": "string"
}
},
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.idea.v2.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "One thing that could be, including options rejected and ideas no cut\nuses yet. `rests_on` is what it stands on (a reader selects the ideas\nresting on a stale note with `cites`); `toward` is the fork, ruling or\nfollow-up it was made for, and empty is legal. It stores no status:\ntaken up is its Deferred resolution, rejected its Recorded one.\nCampaign-rooted: `<campaign>:idea:<pass>.<label>`.",
"properties": {
"campaign": {
"$ref": "#/$defs/Slug"
},
"idea": {
"$ref": "#/$defs/Para"
},
"label": {
"$ref": "#/$defs/Label"
},
"pass": {
"$ref": "#/$defs/Label"
},
"proposed_on": {
"$ref": "#/$defs/Date"
},
"rests_on": {
"items": {
"$ref": "#/$defs/PipelineRef"
},
"maxItems": 16,
"type": "array"
},
"title": {
"$ref": "#/$defs/Title"
},
"toward": {
"items": {
"$ref": "#/$defs/PipelineRef"
},
"maxItems": 8,
"type": "array"
},
"tradeoffs": {
"$ref": "#/$defs/Para"
}
},
"required": [
"campaign",
"pass",
"label",
"title",
"idea",
"tradeoffs",
"rests_on",
"toward",
"proposed_on"
],
"title": "PipelineIdea",
"type": "object"
}

View file

@ -128,7 +128,9 @@
"HandOff",
"Run",
"Pulse",
"Memory"
"Memory",
"Research",
"Idea"
],
"type": "string"
},

View file

@ -37,7 +37,9 @@
"HandOff",
"Run",
"Pulse",
"Memory"
"Memory",
"Research",
"Idea"
],
"type": "string"
},

View file

@ -0,0 +1,294 @@
{
"$defs": {
"Date": {
"description": "A calendar date, `YYYY-MM-DD`.",
"maxLength": 10,
"type": "string"
},
"DocRef": {
"properties": {
"commit": {
"$ref": "#/$defs/Sha"
},
"end_line": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"path": {
"$ref": "#/$defs/Short"
},
"start_line": {
"format": "uint32",
"minimum": 0,
"type": "integer"
}
},
"required": [
"path",
"start_line",
"end_line",
"commit"
],
"type": "object"
},
"Evidence": {
"properties": {
"kind": {
"$ref": "#/$defs/EvidenceKind"
},
"locator": {
"$ref": "#/$defs/Line"
},
"result": {
"$ref": "#/$defs/Line"
}
},
"required": [
"kind",
"locator",
"result"
],
"type": "object"
},
"EvidenceKind": {
"enum": [
"Command",
"Test",
"Mutation",
"Probe",
"SourceRead",
"Capture"
],
"type": "string"
},
"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"
},
"OrgRepo": {
"description": "A GitHub repository as `Org/Repo`, at most 140 UTF-8 bytes (39 + 1 +\n100). The owner is 1 to 39 `[A-Za-z0-9-]` bytes with no leading or\ntrailing hyphen; the repo is 1 to 100 `[A-Za-z0-9._-]` bytes, is never\n`.` or `..`, and does not end in `.git`, ASCII-case-insensitively.\nIdentity is case-insensitive: `GameCult/Epiphany` and `gamecult/epiphany`\nare the same repository.",
"maxLength": 140,
"type": "string"
},
"Para": {
"description": "Text of at most 4,000 UTF-8 bytes; longer narrative is cited by `DocRef`.",
"maxLength": 4000,
"type": "string"
},
"PipelineKind": {
"enum": [
"Campaign",
"Target",
"Question",
"Ruling",
"CutSpec",
"CutReport",
"Verdict",
"Finding",
"FollowUp",
"Resolution",
"Instance",
"Stewardship",
"HandOff",
"Run",
"Pulse",
"Memory",
"Research",
"Idea"
],
"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"
},
"ResearchSource": {
"description": "Where a research claim comes from. A `Repo` pin and a `Web` page are\noutside the document set; an `Observed` result reuses a finding's evidence;\na `Document` is the one source that is a citation edge.",
"oneOf": [
{
"additionalProperties": false,
"description": "A pinned file range in a repo.",
"properties": {
"Repo": {
"properties": {
"at": {
"$ref": "#/$defs/DocRef"
},
"repo": {
"$ref": "#/$defs/OrgRepo"
}
},
"required": [
"repo",
"at"
],
"type": "object"
}
},
"required": [
"Repo"
],
"type": "object"
},
{
"additionalProperties": false,
"description": "A page read on the web: its address, the work it belongs to, and when.",
"properties": {
"Web": {
"properties": {
"retrieved_on": {
"$ref": "#/$defs/Date"
},
"url": {
"$ref": "#/$defs/Line"
},
"work": {
"$ref": "#/$defs/Short"
}
},
"required": [
"url",
"work",
"retrieved_on"
],
"type": "object"
}
},
"required": [
"Web"
],
"type": "object"
},
{
"additionalProperties": false,
"description": "A command, probe, read or capture the pass ran itself.",
"properties": {
"Observed": {
"properties": {
"evidence": {
"$ref": "#/$defs/Evidence"
}
},
"required": [
"evidence"
],
"type": "object"
}
},
"required": [
"Observed"
],
"type": "object"
},
{
"additionalProperties": false,
"description": "Another document in the mind.",
"properties": {
"Document": {
"properties": {
"reference": {
"$ref": "#/$defs/PipelineRef"
}
},
"required": [
"reference"
],
"type": "object"
}
},
"required": [
"Document"
],
"type": "object"
}
]
},
"Sha": {
"description": "A git commit id: 7-40 lowercase hex characters.",
"maxLength": 40,
"type": "string"
},
"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.research.v2.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "One claim a pass found, with the sources it stands on, grouped by `pass`.\nWhat is so, never what to do: a recommendation is an idea. `inferred`\nmarks a claim read from its sources rather than seen in them. It stores\nno status: whether a note is current, superseded or wrong is its\nresolutions'. `serves` names the question, cut or campaign document it\nwas gathered for. The pass's method and what it did not search is its\nfirst note, labelled `scope`. Campaign-rooted:\n`<campaign>:research:<pass>.<label>`.",
"properties": {
"campaign": {
"$ref": "#/$defs/Slug"
},
"claim": {
"$ref": "#/$defs/Line"
},
"detail": {
"$ref": "#/$defs/Para"
},
"inferred": {
"type": "boolean"
},
"label": {
"$ref": "#/$defs/Label"
},
"observed_on": {
"$ref": "#/$defs/Date"
},
"pass": {
"$ref": "#/$defs/Label"
},
"serves": {
"items": {
"$ref": "#/$defs/PipelineRef"
},
"maxItems": 8,
"type": "array"
},
"sources": {
"items": {
"$ref": "#/$defs/ResearchSource"
},
"maxItems": 16,
"type": "array"
}
},
"required": [
"campaign",
"pass",
"label",
"claim",
"detail",
"inferred",
"sources",
"serves",
"observed_on"
],
"title": "PipelineResearch",
"type": "object"
}

View file

@ -32,7 +32,9 @@
"HandOff",
"Run",
"Pulse",
"Memory"
"Memory",
"Research",
"Idea"
],
"type": "string"
},

View file

@ -70,7 +70,9 @@
"HandOff",
"Run",
"Pulse",
"Memory"
"Memory",
"Research",
"Idea"
],
"type": "string"
},

View file

@ -27,7 +27,9 @@
"HandOff",
"Run",
"Pulse",
"Memory"
"Memory",
"Research",
"Idea"
],
"type": "string"
},

View file

@ -1021,7 +1021,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\nsixteen 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\neighteen per-kind value documents, and the envelope is the wire's\nshape, not a document's.",
"oneOf": [
{
"type": "object",
@ -1278,6 +1278,38 @@
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "research"
},
"value": {
"$ref": "#/$defs/PipelineResearch"
}
},
"required": [
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "idea"
},
"value": {
"$ref": "#/$defs/PipelineIdea"
}
},
"required": [
"kind",
"value"
]
}
]
},
@ -1435,6 +1467,58 @@
"handed_on"
]
},
"PipelineIdea": {
"description": "One thing that could be, including options rejected and ideas no cut\nuses yet. `rests_on` is what it stands on (a reader selects the ideas\nresting on a stale note with `cites`); `toward` is the fork, ruling or\nfollow-up it was made for, and empty is legal. It stores no status:\ntaken up is its Deferred resolution, rejected its Recorded one.\nCampaign-rooted: `<campaign>:idea:<pass>.<label>`.",
"type": "object",
"properties": {
"campaign": {
"$ref": "#/$defs/Slug"
},
"idea": {
"$ref": "#/$defs/Para"
},
"label": {
"$ref": "#/$defs/Label"
},
"pass": {
"$ref": "#/$defs/Label"
},
"proposed_on": {
"$ref": "#/$defs/Date"
},
"rests_on": {
"type": "array",
"items": {
"$ref": "#/$defs/PipelineRef"
},
"maxItems": 16
},
"title": {
"$ref": "#/$defs/Title"
},
"toward": {
"type": "array",
"items": {
"$ref": "#/$defs/PipelineRef"
},
"maxItems": 8
},
"tradeoffs": {
"$ref": "#/$defs/Para"
}
},
"required": [
"campaign",
"pass",
"label",
"title",
"idea",
"tradeoffs",
"rests_on",
"toward",
"proposed_on"
]
},
"PipelineInstance": {
"description": "A mind's identity document. A store is canonical to exactly one\ninstance, and this says which; identity lives in the state, not in a\npath.",
"type": "object",
@ -1477,7 +1561,9 @@
"HandOff",
"Run",
"Pulse",
"Memory"
"Memory",
"Research",
"Idea"
]
},
"PipelineMemory": {
@ -1676,6 +1762,58 @@
"id"
]
},
"PipelineResearch": {
"description": "One claim a pass found, with the sources it stands on, grouped by `pass`.\nWhat is so, never what to do: a recommendation is an idea. `inferred`\nmarks a claim read from its sources rather than seen in them. It stores\nno status: whether a note is current, superseded or wrong is its\nresolutions'. `serves` names the question, cut or campaign document it\nwas gathered for. The pass's method and what it did not search is its\nfirst note, labelled `scope`. Campaign-rooted:\n`<campaign>:research:<pass>.<label>`.",
"type": "object",
"properties": {
"campaign": {
"$ref": "#/$defs/Slug"
},
"claim": {
"$ref": "#/$defs/Line"
},
"detail": {
"$ref": "#/$defs/Para"
},
"inferred": {
"type": "boolean"
},
"label": {
"$ref": "#/$defs/Label"
},
"observed_on": {
"$ref": "#/$defs/Date"
},
"pass": {
"$ref": "#/$defs/Label"
},
"serves": {
"type": "array",
"items": {
"$ref": "#/$defs/PipelineRef"
},
"maxItems": 8
},
"sources": {
"type": "array",
"items": {
"$ref": "#/$defs/ResearchSource"
},
"maxItems": 16
}
},
"required": [
"campaign",
"pass",
"label",
"claim",
"detail",
"inferred",
"sources",
"serves",
"observed_on"
]
},
"PipelineResolution": {
"description": "The record of how a subject was closed and by what. `sequence` is per\nsubject and set by the writer, `1` for the first, and it is the last\npart of the key, so a subject's resolutions share a prefix and a\nwithdrawn one is kept under its subject rather than overwritten.\nWhether a sequence is the previous plus one, and whether an earlier\nresolution still stands, are admission's rules: this crate refuses\nneither `0` nor a gap, exactly as it refuses neither `revision: 0` nor\na `revision` with no predecessor.",
"type": "object",
@ -2009,6 +2147,107 @@
"builds"
]
},
"ResearchSource": {
"description": "Where a research claim comes from. A `Repo` pin and a `Web` page are\noutside the document set; an `Observed` result reuses a finding's evidence;\na `Document` is the one source that is a citation edge.",
"oneOf": [
{
"description": "A pinned file range in a repo.",
"type": "object",
"properties": {
"Repo": {
"type": "object",
"properties": {
"at": {
"$ref": "#/$defs/DocRef"
},
"repo": {
"$ref": "#/$defs/OrgRepo"
}
},
"required": [
"repo",
"at"
]
}
},
"additionalProperties": false,
"required": [
"Repo"
]
},
{
"description": "A page read on the web: its address, the work it belongs to, and when.",
"type": "object",
"properties": {
"Web": {
"type": "object",
"properties": {
"retrieved_on": {
"$ref": "#/$defs/Date"
},
"url": {
"$ref": "#/$defs/Line"
},
"work": {
"$ref": "#/$defs/Short"
}
},
"required": [
"url",
"work",
"retrieved_on"
]
}
},
"additionalProperties": false,
"required": [
"Web"
]
},
{
"description": "A command, probe, read or capture the pass ran itself.",
"type": "object",
"properties": {
"Observed": {
"type": "object",
"properties": {
"evidence": {
"$ref": "#/$defs/Evidence"
}
},
"required": [
"evidence"
]
}
},
"additionalProperties": false,
"required": [
"Observed"
]
},
{
"description": "Another document in the mind.",
"type": "object",
"properties": {
"Document": {
"type": "object",
"properties": {
"reference": {
"$ref": "#/$defs/PipelineRef"
}
},
"required": [
"reference"
]
}
},
"additionalProperties": false,
"required": [
"Document"
]
}
]
},
"ResolutionOutcome": {
"description": "How a subject was resolved. Every referent is a parsed `PipelineRef`, so a\nresolution names its records by ids of the kinds they declare, validated\nwhere every other referent is; a `Fixed` commit is a `Sha` whose referent\nis outside the document set (ruling B). Whether a named document exists,\nand how many may supersede one subject, are admission's rules.",
"oneOf": [

View file

@ -256,7 +256,11 @@
"deferred_to",
"documents",
"claims",
"consolidates"
"consolidates",
"serves",
"sources",
"rests_on",
"toward"
]
},
"ClaimOutcome": {
@ -2319,7 +2323,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\nsixteen 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\neighteen per-kind value documents, and the envelope is the wire's\nshape, not a document's.",
"oneOf": [
{
"type": "object",
@ -2576,6 +2580,38 @@
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "research"
},
"value": {
"$ref": "#/$defs/PipelineResearch"
}
},
"required": [
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "idea"
},
"value": {
"$ref": "#/$defs/PipelineIdea"
}
},
"required": [
"kind",
"value"
]
}
]
},
@ -3339,6 +3375,82 @@
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "research"
},
"value": {
"type": "object",
"properties": {
"claim": {
"$ref": "#/$defs/Line"
},
"inferred": {
"type": "boolean"
},
"label": {
"$ref": "#/$defs/Label"
},
"observed_on": {
"$ref": "#/$defs/Date"
},
"pass": {
"$ref": "#/$defs/Label"
}
},
"required": [
"pass",
"label",
"claim",
"inferred",
"observed_on"
]
}
},
"required": [
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "idea"
},
"value": {
"type": "object",
"properties": {
"label": {
"$ref": "#/$defs/Label"
},
"pass": {
"$ref": "#/$defs/Label"
},
"proposed_on": {
"$ref": "#/$defs/Date"
},
"title": {
"$ref": "#/$defs/Title"
}
},
"required": [
"pass",
"label",
"title",
"proposed_on"
]
}
},
"required": [
"kind",
"value"
]
}
]
},
@ -3496,6 +3608,58 @@
"handed_on"
]
},
"PipelineIdea": {
"description": "One thing that could be, including options rejected and ideas no cut\nuses yet. `rests_on` is what it stands on (a reader selects the ideas\nresting on a stale note with `cites`); `toward` is the fork, ruling or\nfollow-up it was made for, and empty is legal. It stores no status:\ntaken up is its Deferred resolution, rejected its Recorded one.\nCampaign-rooted: `<campaign>:idea:<pass>.<label>`.",
"type": "object",
"properties": {
"campaign": {
"$ref": "#/$defs/Slug"
},
"idea": {
"$ref": "#/$defs/Para"
},
"label": {
"$ref": "#/$defs/Label"
},
"pass": {
"$ref": "#/$defs/Label"
},
"proposed_on": {
"$ref": "#/$defs/Date"
},
"rests_on": {
"type": "array",
"items": {
"$ref": "#/$defs/PipelineRef"
},
"maxItems": 16
},
"title": {
"$ref": "#/$defs/Title"
},
"toward": {
"type": "array",
"items": {
"$ref": "#/$defs/PipelineRef"
},
"maxItems": 8
},
"tradeoffs": {
"$ref": "#/$defs/Para"
}
},
"required": [
"campaign",
"pass",
"label",
"title",
"idea",
"tradeoffs",
"rests_on",
"toward",
"proposed_on"
]
},
"PipelineInstance": {
"description": "A mind's identity document. A store is canonical to exactly one\ninstance, and this says which; identity lives in the state, not in a\npath.",
"type": "object",
@ -3538,7 +3702,9 @@
"HandOff",
"Run",
"Pulse",
"Memory"
"Memory",
"Research",
"Idea"
]
},
"PipelineMemory": {
@ -3905,6 +4071,58 @@
}
]
},
"PipelineResearch": {
"description": "One claim a pass found, with the sources it stands on, grouped by `pass`.\nWhat is so, never what to do: a recommendation is an idea. `inferred`\nmarks a claim read from its sources rather than seen in them. It stores\nno status: whether a note is current, superseded or wrong is its\nresolutions'. `serves` names the question, cut or campaign document it\nwas gathered for. The pass's method and what it did not search is its\nfirst note, labelled `scope`. Campaign-rooted:\n`<campaign>:research:<pass>.<label>`.",
"type": "object",
"properties": {
"campaign": {
"$ref": "#/$defs/Slug"
},
"claim": {
"$ref": "#/$defs/Line"
},
"detail": {
"$ref": "#/$defs/Para"
},
"inferred": {
"type": "boolean"
},
"label": {
"$ref": "#/$defs/Label"
},
"observed_on": {
"$ref": "#/$defs/Date"
},
"pass": {
"$ref": "#/$defs/Label"
},
"serves": {
"type": "array",
"items": {
"$ref": "#/$defs/PipelineRef"
},
"maxItems": 8
},
"sources": {
"type": "array",
"items": {
"$ref": "#/$defs/ResearchSource"
},
"maxItems": 16
}
},
"required": [
"campaign",
"pass",
"label",
"claim",
"detail",
"inferred",
"sources",
"serves",
"observed_on"
]
},
"PipelineResolution": {
"description": "The record of how a subject was closed and by what. `sequence` is per\nsubject and set by the writer, `1` for the first, and it is the last\npart of the key, so a subject's resolutions share a prefix and a\nwithdrawn one is kept under its subject rather than overwritten.\nWhether a sequence is the previous plus one, and whether an earlier\nresolution still stands, are admission's rules: this crate refuses\nneither `0` nor a gap, exactly as it refuses neither `revision: 0` nor\na `revision` with no predecessor.",
"type": "object",
@ -4527,6 +4745,107 @@
"builds"
]
},
"ResearchSource": {
"description": "Where a research claim comes from. A `Repo` pin and a `Web` page are\noutside the document set; an `Observed` result reuses a finding's evidence;\na `Document` is the one source that is a citation edge.",
"oneOf": [
{
"description": "A pinned file range in a repo.",
"type": "object",
"properties": {
"Repo": {
"type": "object",
"properties": {
"at": {
"$ref": "#/$defs/DocRef"
},
"repo": {
"$ref": "#/$defs/OrgRepo"
}
},
"required": [
"repo",
"at"
]
}
},
"additionalProperties": false,
"required": [
"Repo"
]
},
{
"description": "A page read on the web: its address, the work it belongs to, and when.",
"type": "object",
"properties": {
"Web": {
"type": "object",
"properties": {
"retrieved_on": {
"$ref": "#/$defs/Date"
},
"url": {
"$ref": "#/$defs/Line"
},
"work": {
"$ref": "#/$defs/Short"
}
},
"required": [
"url",
"work",
"retrieved_on"
]
}
},
"additionalProperties": false,
"required": [
"Web"
]
},
{
"description": "A command, probe, read or capture the pass ran itself.",
"type": "object",
"properties": {
"Observed": {
"type": "object",
"properties": {
"evidence": {
"$ref": "#/$defs/Evidence"
}
},
"required": [
"evidence"
]
}
},
"additionalProperties": false,
"required": [
"Observed"
]
},
{
"description": "Another document in the mind.",
"type": "object",
"properties": {
"Document": {
"type": "object",
"properties": {
"reference": {
"$ref": "#/$defs/PipelineRef"
}
},
"required": [
"reference"
]
}
},
"additionalProperties": false,
"required": [
"Document"
]
}
]
},
"ResolutionOutcome": {
"description": "How a subject was resolved. Every referent is a parsed `PipelineRef`, so a\nresolution names its records by ids of the kinds they declare, validated\nwhere every other referent is; a `Fixed` commit is a `Sha` whose referent\nis outside the document set (ruling B). Whether a named document exists,\nand how many may supersede one subject, are admission's rules.",
"oneOf": [

View file

@ -188,6 +188,28 @@
"title": "Epiphany Pipeline Memory v2",
"path": "epiphany.pipeline.memory.v2.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.research.v2.schema.json",
"kind": "document_payload",
"wireContracts": [
"cultnet.schema.v0"
],
"schemaVersion": "epiphany.pipeline.research.v2",
"documentType": "epiphany.pipeline.research.v2",
"title": "Epiphany Pipeline Research v2",
"path": "epiphany.pipeline.research.v2.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.idea.v2.schema.json",
"kind": "document_payload",
"wireContracts": [
"cultnet.schema.v0"
],
"schemaVersion": "epiphany.pipeline.idea.v2",
"documentType": "epiphany.pipeline.idea.v2",
"title": "Epiphany Pipeline Idea v2",
"path": "epiphany.pipeline.idea.v2.schema.json"
},
{
"schemaId": "huginn.mind_request.v1",
"kind": "wire_message",