pipeline: a follow-up records the origin of the Persona turn that admitted it
PipelineFollowUp gains origin (absent is not written, so the golden sample is unchanged); MemoryOrigin's doc says it is a brief's too; the header facts carry it; schemas regenerated. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
26303f045b
commit
5eb1452ca4
10 changed files with 205 additions and 7 deletions
|
|
@ -784,9 +784,13 @@ value_types! {
|
|||
severity: FindingSeverity, claim: Line, invariants: Vec<Label>[8], locations: Vec<CodeLocation>[16],
|
||||
failure_scenario: Para, evidence: Vec<Evidence>[16], precedents: Vec<ForeignRef>[8], origin: FindingOrigin,
|
||||
}
|
||||
/// A follow-up. `origin` is where the Persona turn that admitted it was
|
||||
/// asked, as the organ derived it: a brief records the conversation whose
|
||||
/// message started it. Absent on follow-ups no Persona turn admitted and
|
||||
/// on briefs admitted before it existed.
|
||||
pub struct PipelineFollowUp {
|
||||
campaign: Slug, label: Label, source: PipelineRef, repo: OrgRepo, locations: Vec<CodeLocation>[16],
|
||||
item: Line, why_it_can_wait: Line, owner: Short,
|
||||
item: Line, why_it_can_wait: Line, owner: Short, origin: Option<MemoryOrigin> = absent,
|
||||
}
|
||||
/// The record of how a subject was closed and by what. `sequence` is per
|
||||
/// subject and set by the writer, `1` for the first, and it is the last
|
||||
|
|
@ -914,8 +918,9 @@ impl PipelineRef {
|
|||
}
|
||||
}
|
||||
|
||||
/// Where a memory came from and whom she heard it from. The organ derives it
|
||||
/// from the batch of the turn that wrote the memory; the model never writes it.
|
||||
/// Where a Persona turn's batch came from and whom she heard it from: a
|
||||
/// memory's origin and a brief's. The organ derives it from the batch of the
|
||||
/// turn; the model never writes it.
|
||||
/// `about` names whom a memory concerns, this names the room and the voices.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum MemoryOrigin {
|
||||
|
|
@ -1598,6 +1603,7 @@ mod tests {
|
|||
source: PipelineRef { kind: PipelineKind::Finding, id: id("finding", "cut-3a.s2.F4") },
|
||||
repo: repo(), locations: vec![location()], item: "Per-kind admission rules.".into(),
|
||||
why_it_can_wait: "The organ owns admission.".into(), owner: s("Hands"),
|
||||
origin: None,
|
||||
}), format!("{CAMPAIGN}:follow_up:FU-4")),
|
||||
(D::Resolution(PipelineResolution {
|
||||
subject: PipelineRef { kind: PipelineKind::Question, id: id("question", "Q1") }, sequence: 1,
|
||||
|
|
@ -3604,6 +3610,30 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn follow_up_origin_absent_is_not_written_and_a_direct_origin_round_trips() -> Result<()> {
|
||||
let sample = samples()
|
||||
.into_iter()
|
||||
.find_map(|(document, _)| if let PipelineDocument::FollowUp(follow_up) = document { Some(follow_up) } else { None })
|
||||
.expect("a follow_up sample");
|
||||
let json = serde_json::to_value(PipelineDocument::FollowUp(sample.clone()))?;
|
||||
assert!(json["value"].get("origin").is_none(), "an absent origin is not written");
|
||||
assert!(!rmp_serde::to_vec_named(&PipelineDocument::FollowUp(sample.clone()))?.windows(6).any(|window| window == b"origin"));
|
||||
let PipelineDocument::FollowUp(decoded) = serde_json::from_value::<PipelineDocument>(json)? else { unreachable!() };
|
||||
assert_eq!(decoded.origin, None);
|
||||
|
||||
let mut direct = sample;
|
||||
direct.origin = Some(MemoryOrigin::Direct { room: "!dm:chat".into(), with: vec!["@sample:chat".into()] });
|
||||
let document = PipelineDocument::FollowUp(direct);
|
||||
document.validate()?;
|
||||
let json = serde_json::to_value(&document)?;
|
||||
assert_eq!(serde_json::from_value::<PipelineDocument>(json)?, document);
|
||||
let packed = rmp_serde::to_vec_named(&document)?;
|
||||
assert_eq!(rmp_serde::from_slice::<PipelineDocument>(&packed)?, document);
|
||||
assert!(packed.windows(6).any(|window| window == b"origin"), "the origin key is written");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_repos_bounded_and_distinct() {
|
||||
let with_repos = |repos: Vec<OrgRepo>| {
|
||||
|
|
|
|||
|
|
@ -290,6 +290,7 @@ pub(crate) fn follow_up(label: &str, source: PipelineRef) -> PipelineDocument {
|
|||
item: "Per-kind admission rules.".into(),
|
||||
why_it_can_wait: "The organ owns admission.".into(),
|
||||
owner: s("Hands"),
|
||||
origin: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -140,7 +140,16 @@ pub enum PipelineFacts {
|
|||
invariants: Vec<Label>,
|
||||
range: CommitRange,
|
||||
},
|
||||
FollowUp { label: Label, source: PipelineRef, repo: OrgRepo, owner: Short, item: Line },
|
||||
FollowUp {
|
||||
label: Label,
|
||||
source: PipelineRef,
|
||||
repo: OrgRepo,
|
||||
owner: Short,
|
||||
item: Line,
|
||||
/// Where the Persona turn that admitted it was asked; absent on follow-ups no turn admitted.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
origin: Option<MemoryOrigin>,
|
||||
},
|
||||
Resolution { subject: PipelineRef, sequence: u32, outcome: ResolutionOutcome, resolved_on: Date },
|
||||
Instance { instance: Slug, display_name: Short, host: Short, created_at: Date },
|
||||
Stewardship { instance: Slug, repo: OrgRepo, sequence: u32, assigned_on: Date, note: Line },
|
||||
|
|
@ -216,6 +225,7 @@ impl PipelineFacts {
|
|||
repo: value.repo.clone(),
|
||||
owner: value.owner.clone(),
|
||||
item: value.item.clone(),
|
||||
origin: value.origin.clone(),
|
||||
},
|
||||
D::Resolution(value) => Self::Resolution {
|
||||
subject: value.subject.clone(),
|
||||
|
|
@ -1632,6 +1642,27 @@ mod tests {
|
|||
assert!(matches!(&found[0].facts, PipelineFacts::Memory { sort: MemorySort::Rapport, origin: MemoryOrigin::Room { .. }, salience_permille: 500, .. }), "{:?}", found[0].facts);
|
||||
}
|
||||
|
||||
/// A follow-up's origin reaches its header facts when it has one, and is absent when it has none.
|
||||
#[test]
|
||||
fn a_follow_up_header_carries_the_origin_its_turn_recorded() {
|
||||
let mut mind = seeded();
|
||||
let q1 = id("question", "Q1");
|
||||
let D::FollowUp(mut asked) = follow_up("FU-dm", r(K::Question, &q1)) else { panic!() };
|
||||
asked.origin = Some(MemoryOrigin::Direct { room: s("!dm:chat"), with: vec![s("@op:chat")] });
|
||||
committed(admit(&mut mind, vec![question_n(1), D::FollowUp(asked), follow_up("FU-old", r(K::Question, &q1))]));
|
||||
let PipelinePageItems::Headers(found) = mind.query(&of_kinds(&[K::FollowUp])).unwrap().items else { panic!("headers") };
|
||||
let origin_of = |label: &str| {
|
||||
found.iter().find_map(|header| match &header.facts {
|
||||
PipelineFacts::FollowUp { label: found, origin, .. } if found.0 == label => Some(origin.clone()),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
assert_eq!(origin_of("FU-dm"), Some(Some(MemoryOrigin::Direct { room: s("!dm:chat"), with: vec![s("@op:chat")] })));
|
||||
assert_eq!(origin_of("FU-old"), Some(None));
|
||||
let wire = serde_json::to_value(found.iter().find(|header| matches!(&header.facts, PipelineFacts::FollowUp { origin: None, .. })).unwrap()).unwrap();
|
||||
assert!(!wire.to_string().contains("\"origin\""), "an absent origin is not written");
|
||||
}
|
||||
|
||||
/// A withdrawal pulse admitted into the mind is read back as a withdrawal: its
|
||||
/// facts say so, `voted` selects it, and a later plain vote is not one.
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1905,6 +1905,7 @@ fn start_self_run(
|
|||
item: ask.brief.as_str().into(),
|
||||
why_it_can_wait: "Started from chat; the Self run that claims it works it now.".into(),
|
||||
owner: "self-run".into(),
|
||||
origin: None,
|
||||
});
|
||||
let claim = PipelineRef { kind: PipelineKind::FollowUp, id: pipeline_key(&follow_up).map_err(|_| anyhow!("the follow-up could not be keyed"))?.as_str().into() };
|
||||
let mut documents = vec![follow_up];
|
||||
|
|
|
|||
|
|
@ -1160,6 +1160,7 @@ fn open_follow_up(label: &str, source: &PipelineRef, item: &str) -> PipelineDocu
|
|||
item: item.into(),
|
||||
why_it_can_wait: "It can.".into(),
|
||||
owner: "Hands".into(),
|
||||
origin: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -510,6 +510,7 @@ pub(crate) fn follow_up(label: &str, source: &PipelineRef) -> PipelineDocument {
|
|||
item: "Later.".into(),
|
||||
why_it_can_wait: "It can.".into(),
|
||||
owner: "Hands".into(),
|
||||
origin: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,95 @@
|
|||
"maxLength": 1000,
|
||||
"type": "string"
|
||||
},
|
||||
"MemoryOrigin": {
|
||||
"description": "Where a Persona turn's batch came from and whom she heard it from: a\nmemory's origin and a brief's. The organ derives it from the batch of the\nturn; the model never writes it.\n`about` names whom a memory concerns, this names the room and the voices.",
|
||||
"oneOf": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "A turn in rooms: their aliases comma-joined, and the batch's other senders.",
|
||||
"properties": {
|
||||
"Room": {
|
||||
"properties": {
|
||||
"heard_from": {
|
||||
"items": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
"maxItems": 8,
|
||||
"type": "array"
|
||||
},
|
||||
"rooms": {
|
||||
"$ref": "#/$defs/Short"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"rooms",
|
||||
"heard_from"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Room"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "A direct message: the DM's room id, and the others who spoke in it (whom\nshe heard it from in confidence).",
|
||||
"properties": {
|
||||
"Direct": {
|
||||
"properties": {
|
||||
"room": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
"with": {
|
||||
"items": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
"maxItems": 8,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"room",
|
||||
"with"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Direct"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"const": "Wake",
|
||||
"description": "A wake: no one spoke.",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "A Self run of hers, by its label.",
|
||||
"properties": {
|
||||
"SelfRun": {
|
||||
"properties": {
|
||||
"run": {
|
||||
"$ref": "#/$defs/Short"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"run"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"SelfRun"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"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,
|
||||
|
|
@ -109,6 +198,7 @@
|
|||
},
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.follow_up.v2.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "A follow-up. `origin` is where the Persona turn that admitted it was\nasked, as the organ derived it: a brief records the conversation whose\nmessage started it. Absent on follow-ups no Persona turn admitted and\non briefs admitted before it existed.",
|
||||
"properties": {
|
||||
"campaign": {
|
||||
"$ref": "#/$defs/Slug"
|
||||
|
|
@ -126,6 +216,16 @@
|
|||
"maxItems": 16,
|
||||
"type": "array"
|
||||
},
|
||||
"origin": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/MemoryOrigin"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
"type": "string"
|
||||
},
|
||||
"MemoryOrigin": {
|
||||
"description": "Where a memory came from and whom she heard it from. The organ derives it\nfrom the batch of the turn that wrote the memory; the model never writes it.\n`about` names whom a memory concerns, this names the room and the voices.",
|
||||
"description": "Where a Persona turn's batch came from and whom she heard it from: a\nmemory's origin and a brief's. The organ derives it from the batch of the\nturn; the model never writes it.\n`about` names whom a memory concerns, this names the room and the voices.",
|
||||
"oneOf": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
|
|
|
|||
|
|
@ -578,7 +578,7 @@
|
|||
"maxLength": 1000
|
||||
},
|
||||
"MemoryOrigin": {
|
||||
"description": "Where a memory came from and whom she heard it from. The organ derives it\nfrom the batch of the turn that wrote the memory; the model never writes it.\n`about` names whom a memory concerns, this names the room and the voices.",
|
||||
"description": "Where a Persona turn's batch came from and whom she heard it from: a\nmemory's origin and a brief's. The organ derives it from the batch of the\nturn; the model never writes it.\n`about` names whom a memory concerns, this names the room and the voices.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "A turn in rooms: their aliases comma-joined, and the batch's other senders.",
|
||||
|
|
@ -1405,6 +1405,7 @@
|
|||
]
|
||||
},
|
||||
"PipelineFollowUp": {
|
||||
"description": "A follow-up. `origin` is where the Persona turn that admitted it was\nasked, as the organ derived it: a brief records the conversation whose\nmessage started it. Absent on follow-ups no Persona turn admitted and\non briefs admitted before it existed.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"campaign": {
|
||||
|
|
@ -1423,6 +1424,16 @@
|
|||
},
|
||||
"maxItems": 16
|
||||
},
|
||||
"origin": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/MemoryOrigin"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -705,7 +705,7 @@
|
|||
"maxLength": 1000
|
||||
},
|
||||
"MemoryOrigin": {
|
||||
"description": "Where a memory came from and whom she heard it from. The organ derives it\nfrom the batch of the turn that wrote the memory; the model never writes it.\n`about` names whom a memory concerns, this names the room and the voices.",
|
||||
"description": "Where a Persona turn's batch came from and whom she heard it from: a\nmemory's origin and a brief's. The organ derives it from the batch of the\nturn; the model never writes it.\n`about` names whom a memory concerns, this names the room and the voices.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "A turn in rooms: their aliases comma-joined, and the batch's other senders.",
|
||||
|
|
@ -3083,6 +3083,17 @@
|
|||
"label": {
|
||||
"$ref": "#/$defs/Label"
|
||||
},
|
||||
"origin": {
|
||||
"description": "Where the Persona turn that admitted it was asked; absent on follow-ups no turn admitted.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/MemoryOrigin"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
|
|
@ -3582,6 +3593,7 @@
|
|||
]
|
||||
},
|
||||
"PipelineFollowUp": {
|
||||
"description": "A follow-up. `origin` is where the Persona turn that admitted it was\nasked, as the organ derived it: a brief records the conversation whose\nmessage started it. Absent on follow-ups no Persona turn admitted and\non briefs admitted before it existed.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"campaign": {
|
||||
|
|
@ -3600,6 +3612,16 @@
|
|||
},
|
||||
"maxItems": 16
|
||||
},
|
||||
"origin": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/MemoryOrigin"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue