Emit one turn row's nodes and edges. Triples (all in the caller's transaction): - `session:{id}` (upserted with latest lifecycle metadata) - `provenance:{hash}` typed `vault:ProvenanceGraph` - `session:{id}/turn:{n}` typed `vault:SessionTurn` - session `prov:hadMember` turn - turn `vault:explainedBy` provenance - turn `prov:generated` change (per explained change; full hash in edge metadata) - tu
(
&mut self,
record: &SessionRecord,
turn: &SessionTurn,
previous_turn_number: Option<u32>,
)
| 337 | /// - turn `prov:generated` change (per explained change; full hash in edge metadata) |
| 338 | /// - turn `prov:wasInformedBy` the turn named by `previous_provenance` |
| 339 | pub(crate) fn emit_turn_kg( |
| 340 | &mut self, |
| 341 | record: &SessionRecord, |
| 342 | turn: &SessionTurn, |
| 343 | previous_turn_number: Option<u32>, |
| 344 | ) -> PristineResult<()> { |
| 345 | self.emit_session_node(record)?; |
| 346 | |
| 347 | let prov_id = provenance_node_id(&turn.provenance_hash); |
| 348 | let prov_node = KgNode::new( |
| 349 | &prov_id, |
| 350 | "provenance", |
| 351 | &turn.provenance_hash.to_base32()[..12], |
| 352 | "session_ledger", |
| 353 | ) |
| 354 | .with_metadata(serde_json::json!({ |
| 355 | "rdf_type": entity_type::PROVENANCE, |
| 356 | "session_id": turn.session_id, |
| 357 | "timestamp": turn.timestamp, |
| 358 | })); |
| 359 | self.upsert_kg_node(&prov_node)?; |
| 360 | |
| 361 | let turn_id = turn_node_id(&turn.session_id, turn.turn_number); |
| 362 | let turn_node = KgNode::new( |
| 363 | &turn_id, |
| 364 | "session_turn", |
| 365 | format!("turn {}", turn.turn_number), |
| 366 | "session_ledger", |
| 367 | ) |
| 368 | .with_metadata(serde_json::json!({ |
| 369 | "rdf_type": entity_type::SESSION_TURN, |
| 370 | "turn_number": turn.turn_number, |
| 371 | "timestamp": turn.timestamp, |
| 372 | "goal": turn.goal, |
| 373 | })); |
| 374 | let turn_node = match &turn.goal { |
| 375 | Some(goal) => turn_node.with_summary(goal), |
| 376 | None => turn_node, |
| 377 | }; |
| 378 | self.upsert_kg_node(&turn_node)?; |
| 379 | |
| 380 | self.upsert_kg_edge(&KgEdge::new( |
| 381 | session_node_id(&turn.session_id), |
| 382 | &turn_id, |
| 383 | predicate::HAD_MEMBER, |
| 384 | ))?; |
| 385 | |
| 386 | self.upsert_kg_edge(&KgEdge::new(&turn_id, &prov_id, predicate::EXPLAINED_BY))?; |
| 387 | |
| 388 | if let Some(plan_id) = &turn.plan_id { |
| 389 | let intent_id = format!("intent:{}", plan_id.to_uppercase()); |
| 390 | // The managed plan governs both the session and this concrete turn. |
| 391 | self.upsert_kg_edge(&KgEdge::new( |
| 392 | session_node_id(&turn.session_id), |
| 393 | &intent_id, |
| 394 | predicate::HAD_PLAN, |
| 395 | ))?; |
| 396 | self.upsert_kg_edge(&KgEdge::new(&turn_id, &intent_id, predicate::HAD_PLAN))?; |
no test coverage detected