diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index 8832c851..3fdd1d38 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -204,6 +204,7 @@ pub use surfaces::EpiphanyRoleFindingInterpretation; pub use surfaces::EpiphanyRoleResultRoleId; pub use surfaces::EpiphanyRoleSelfPersistenceReview; pub use surfaces::EpiphanyRoleSelfPersistenceStatus; +pub use surfaces::EpiphanyRoleStatePatchDocument; pub use surfaces::EpiphanyRoleWorkerLaunchDocument; pub use surfaces::EpiphanyScene; pub use surfaces::EpiphanySceneAction; diff --git a/epiphany-core/src/surfaces/mod.rs b/epiphany-core/src/surfaces/mod.rs index f84f491a..0781e333 100644 --- a/epiphany-core/src/surfaces/mod.rs +++ b/epiphany-core/src/surfaces/mod.rs @@ -9,6 +9,7 @@ mod reorient; mod role_board; mod role_result; mod scene; +mod state_patch; mod worker_launch; pub use coordinator::EpiphanyCoordinatorAction; @@ -123,6 +124,7 @@ pub use scene::EpiphanySceneStateStatus; pub use scene::EpiphanySceneStatusCount; pub use scene::EpiphanySceneSubgoal; pub use scene::derive_scene; +pub use state_patch::EpiphanyRoleStatePatchDocument; pub use worker_launch::EpiphanyReorientWorkerLaunchDocument; pub use worker_launch::EpiphanyRoleWorkerLaunchDocument; pub use worker_launch::EpiphanyWorkerLaunchDocument; diff --git a/epiphany-core/src/surfaces/role_result.rs b/epiphany-core/src/surfaces/role_result.rs index bf7a559c..5d014303 100644 --- a/epiphany-core/src/surfaces/role_result.rs +++ b/epiphany-core/src/surfaces/role_result.rs @@ -38,7 +38,7 @@ pub struct EpiphanyRoleFindingInterpretation { pub open_questions: Vec, pub evidence_gaps: Vec, pub risks: Vec, - pub state_patch: Option, + pub state_patch: Option, pub self_patch: Option, pub self_persistence: Option, pub job_error: Option, @@ -109,11 +109,20 @@ pub struct EpiphanyReorientAcceptanceBundle { pub fn interpret_role_finding( role_id: EpiphanyRoleResultRoleId, raw_result: &serde_json::Value, - state_patch_parse_error: Option, + _legacy_state_patch_parse_error: Option, job_error: Option, item_error: Option, ) -> EpiphanyRoleFindingInterpretation { - let state_patch = raw_result.get("statePatch").cloned(); + let state_patch_result = raw_result + .get("statePatch") + .cloned() + .map(serde_json::from_value::); + let state_patch = state_patch_result + .as_ref() + .and_then(|result| result.as_ref().ok().cloned()); + let state_patch_parse_error = state_patch_result + .as_ref() + .and_then(|result| result.as_ref().err().map(ToString::to_string)); let (self_patch, self_persistence) = raw_result .get("selfPatch") .map(|patch| decode_role_self_patch(role_id, patch)) @@ -437,49 +446,51 @@ fn decode_role_self_patch( (decoded, Some(review)) } -pub fn modeling_role_state_patch_policy_errors(patch: &serde_json::Value) -> Vec { +pub fn modeling_role_state_patch_policy_errors( + patch: &EpiphanyRoleStatePatchDocument, +) -> Vec { let mut errors = Vec::new(); - if has_field(patch, "objective") { + if patch.objective.is_some() { errors .push("objective changes are not allowed through modeling role acceptance".to_string()); } - if has_field(patch, "activeSubgoalId") || has_field(patch, "subgoals") { + if patch.active_subgoal_id.is_some() || patch.subgoals.is_some() { errors.push("subgoal changes are not allowed through modeling role acceptance".to_string()); } - if has_field(patch, "invariants") { + if patch.invariants.is_some() { errors .push("invariant changes are not allowed through modeling role acceptance".to_string()); } - if has_field(patch, "jobBindings") { + if patch.job_bindings.is_some() { errors.push( "job binding changes are not allowed through modeling role acceptance".to_string(), ); } - if non_empty_array_field(patch, "acceptanceReceipts") { + if !patch.acceptance_receipts.is_empty() { errors.push( "acceptance receipt changes are owned by roleAccept, not worker statePatch".to_string(), ); } - if non_empty_array_field(patch, "runtimeLinks") { + if !patch.runtime_links.is_empty() { errors.push( "runtime link changes are owned by launch/read-back surfaces, not worker statePatch" .to_string(), ); } - if has_field(patch, "planning") { + if patch.planning.is_some() { errors .push("planning changes are not allowed through modeling role acceptance".to_string()); } - if has_field(patch, "churn") || has_field(patch, "mode") { + if patch.churn.is_some() || patch.mode.is_some() { errors.push( "churn or mode changes are not allowed through modeling role acceptance".to_string(), ); } - if !has_field(patch, "graphs") - && !has_field(patch, "graphFrontier") - && !has_field(patch, "graphCheckpoint") - && !has_field(patch, "scratch") - && !has_field(patch, "investigationCheckpoint") + if patch.graphs.is_none() + && patch.graph_frontier.is_none() + && patch.graph_checkpoint.is_none() + && patch.scratch.is_none() + && patch.investigation_checkpoint.is_none() { errors.push( "statePatch must include a modeling field: graphs, graphFrontier, graphCheckpoint, scratch, or investigationCheckpoint" @@ -489,106 +500,89 @@ pub fn modeling_role_state_patch_policy_errors(patch: &serde_json::Value) -> Vec errors } -pub fn imagination_role_state_patch_policy_errors(patch: &serde_json::Value) -> Vec { +pub fn imagination_role_state_patch_policy_errors( + patch: &EpiphanyRoleStatePatchDocument, +) -> Vec { let mut errors = Vec::new(); - if has_field(patch, "objective") { + if patch.objective.is_some() { errors.push( "objective changes are not allowed through imagination role acceptance".to_string(), ); } - if has_field(patch, "activeSubgoalId") || has_field(patch, "subgoals") { + if patch.active_subgoal_id.is_some() || patch.subgoals.is_some() { errors.push( "subgoal changes are not allowed through imagination role acceptance".to_string(), ); } - if has_field(patch, "invariants") { + if patch.invariants.is_some() { errors.push( "invariant changes are not allowed through imagination role acceptance".to_string(), ); } - if has_field(patch, "graphs") - || has_field(patch, "graphFrontier") - || has_field(patch, "graphCheckpoint") - || has_field(patch, "investigationCheckpoint") + if patch.graphs.is_some() + || patch.graph_frontier.is_some() + || patch.graph_checkpoint.is_some() + || patch.investigation_checkpoint.is_some() { errors.push( "graph or checkpoint changes are not allowed through imagination role acceptance" .to_string(), ); } - if has_field(patch, "scratch") { + if patch.scratch.is_some() { errors.push( "scratch changes are not allowed through imagination role acceptance".to_string(), ); } - if has_field(patch, "jobBindings") { + if patch.job_bindings.is_some() { errors.push( "job binding changes are not allowed through imagination role acceptance".to_string(), ); } - if non_empty_array_field(patch, "acceptanceReceipts") { + if !patch.acceptance_receipts.is_empty() { errors.push( "acceptance receipt changes are owned by roleAccept, not worker statePatch".to_string(), ); } - if non_empty_array_field(patch, "runtimeLinks") { + if !patch.runtime_links.is_empty() { errors.push( "runtime link changes are owned by launch/read-back surfaces, not worker statePatch" .to_string(), ); } - if has_field(patch, "churn") || has_field(patch, "mode") { + if patch.churn.is_some() || patch.mode.is_some() { errors.push( "churn or mode changes are not allowed through imagination role acceptance".to_string(), ); } - let Some(planning) = patch.get("planning") else { + let Some(planning) = patch.planning.as_ref() else { errors.push("statePatch must include planning changes".to_string()); return errors; }; - let objective_drafts = get_field(planning, "objectiveDrafts", "objective_drafts") - .and_then(serde_json::Value::as_array) - .cloned() - .unwrap_or_default(); - if objective_drafts.is_empty() { + if planning.objective_drafts.is_empty() { errors.push("planning patch must include at least one objective draft".to_string()); } - if !objective_drafts.iter().any(|draft| { - draft - .get("status") - .and_then(serde_json::Value::as_str) - .is_some_and(|status| status.eq_ignore_ascii_case("draft")) - }) { + if !planning + .objective_drafts + .iter() + .any(|draft| draft.status.eq_ignore_ascii_case("draft")) + { errors.push( "planning patch must include at least one objective draft with status draft" .to_string(), ); } - for draft in &objective_drafts { - let id = draft - .get("id") - .and_then(serde_json::Value::as_str) - .unwrap_or(""); - if !draft - .get("acceptanceCriteria") - .or_else(|| draft.get("acceptance_criteria")) - .and_then(serde_json::Value::as_array) - .is_some_and(|items| !items.is_empty()) - { + for draft in &planning.objective_drafts { + if draft.acceptance_criteria.is_empty() { errors.push(format!( "planning objective draft {:?} must include acceptance criteria", - id + draft.id )); } - if !draft - .get("reviewGates") - .or_else(|| draft.get("review_gates")) - .and_then(serde_json::Value::as_array) - .is_some_and(|items| !items.is_empty()) - { + if draft.review_gates.is_empty() { errors.push(format!( "planning objective draft {:?} must include review gates", - id + draft.id )); } } @@ -597,10 +591,10 @@ pub fn imagination_role_state_patch_policy_errors(patch: &serde_json::Value) -> fn modeling_role_state_patch_error( raw_result: &serde_json::Value, - state_patch: Option<&serde_json::Value>, + state_patch: Option<&EpiphanyRoleStatePatchDocument>, parse_error: Option, ) -> Option { - let Some(value) = raw_result.get("statePatch") else { + if raw_result.get("statePatch").is_none() { return Some("modeling result is not reviewable: missing required statePatch".to_string()); }; if let Some(error) = parse_error { @@ -608,7 +602,10 @@ fn modeling_role_state_patch_error( "modeling result is not reviewable: invalid statePatch ({error})" )); } - let errors = modeling_role_state_patch_policy_errors(state_patch.unwrap_or(value)); + let Some(state_patch) = state_patch else { + return Some("modeling result is not reviewable: invalid statePatch".to_string()); + }; + let errors = modeling_role_state_patch_policy_errors(state_patch); if errors.is_empty() { None } else { @@ -621,10 +618,10 @@ fn modeling_role_state_patch_error( fn imagination_role_state_patch_error( raw_result: &serde_json::Value, - state_patch: Option<&serde_json::Value>, + state_patch: Option<&EpiphanyRoleStatePatchDocument>, parse_error: Option, ) -> Option { - let Some(value) = raw_result.get("statePatch") else { + if raw_result.get("statePatch").is_none() { return Some( "imagination result is not reviewable: missing required statePatch".to_string(), ); @@ -634,7 +631,10 @@ fn imagination_role_state_patch_error( "imagination result is not reviewable: invalid statePatch ({error})" )); } - let errors = imagination_role_state_patch_policy_errors(state_patch.unwrap_or(value)); + let Some(state_patch) = state_patch else { + return Some("imagination result is not reviewable: invalid statePatch".to_string()); + }; + let errors = imagination_role_state_patch_policy_errors(state_patch); if errors.is_empty() { None } else { @@ -816,25 +816,6 @@ fn json_string_array_field(value: &serde_json::Value, key: &str) -> Vec .unwrap_or_default() } -fn has_field(value: &serde_json::Value, key: &str) -> bool { - value.get(key).is_some_and(|field| !field.is_null()) -} - -fn get_field<'a>( - value: &'a serde_json::Value, - camel_key: &str, - snake_key: &str, -) -> Option<&'a serde_json::Value> { - value.get(camel_key).or_else(|| value.get(snake_key)) -} - -fn non_empty_array_field(value: &serde_json::Value, key: &str) -> bool { - value - .get(key) - .and_then(serde_json::Value::as_array) - .is_some_and(|items| !items.is_empty()) -} - #[cfg(test)] mod tests { use super::*; @@ -1004,11 +985,15 @@ mod tests { &serde_json::json!({ "statePatch": { "planning": { - "objectiveDrafts": [{ + "objective_drafts": [{ "id": "draft-imagination-test", + "title": "Draft from imagination", + "summary": "Reviewable objective draft.", + "scope": {"includes": ["typed state"], "excludes": []}, "status": "draft", - "acceptanceCriteria": ["A criterion"], - "reviewGates": ["human review"] + "lane_plan": {}, + "acceptance_criteria": ["A criterion"], + "review_gates": ["human review"] }] } } @@ -1154,6 +1139,7 @@ mod tests { })); } } +use super::state_patch::EpiphanyRoleStatePatchDocument; use crate::agent_memory::AgentSelfPatch; use crate::agent_memory::decode_agent_self_patch; use crate::agent_memory::review_agent_self_patch_contract; diff --git a/epiphany-core/src/surfaces/state_patch.rs b/epiphany-core/src/surfaces/state_patch.rs new file mode 100644 index 00000000..77e39966 --- /dev/null +++ b/epiphany-core/src/surfaces/state_patch.rs @@ -0,0 +1,56 @@ +use codex_protocol::protocol::EpiphanyAcceptanceReceipt; +use codex_protocol::protocol::EpiphanyChurnState; +use codex_protocol::protocol::EpiphanyEvidenceRecord; +use codex_protocol::protocol::EpiphanyGraphCheckpoint; +use codex_protocol::protocol::EpiphanyGraphFrontier; +use codex_protocol::protocol::EpiphanyGraphs; +use codex_protocol::protocol::EpiphanyInvariant; +use codex_protocol::protocol::EpiphanyInvestigationCheckpoint; +use codex_protocol::protocol::EpiphanyJobBinding; +use codex_protocol::protocol::EpiphanyModeState; +use codex_protocol::protocol::EpiphanyObservation; +use codex_protocol::protocol::EpiphanyPlanningState; +use codex_protocol::protocol::EpiphanyRuntimeLink; +use codex_protocol::protocol::EpiphanyScratchPad; +use codex_protocol::protocol::EpiphanySubgoal; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EpiphanyRoleStatePatchDocument { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub objective: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub active_subgoal_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub subgoals: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub invariants: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub graphs: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub graph_frontier: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub graph_checkpoint: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub scratch: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub investigation_checkpoint: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub job_bindings: Option>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub acceptance_receipts: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub runtime_links: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub observations: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub evidence: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub churn: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub mode: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub planning: Option, +} diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 49565bff..4c5bc0b9 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -391,14 +391,17 @@ inventory and ledger now exist: `runtime_job_result_to_role_json` or `runtime_job_result_to_reorient_json`. `epiphany-core` owns typed role/reorient runtime-result interpreters and the app-server maps those interpretations directly to protocol projections. +- Fourth cut landed: role `statePatch` is no longer raw + `serde_json::Value` inside `EpiphanyRoleFindingInterpretation`. + `epiphany-core` owns `EpiphanyRoleStatePatchDocument`, policy checks read + typed fields directly, and the app-server maps the typed document to the + legacy `ThreadEpiphanyUpdatePatch` without JSON round-tripping. Continue at the protocol edge. Replace `ThreadEpiphanyJobLaunchParams.input_json` / `output_schema_json` with CultNet -typed launch-intent messages, then type role `statePatch` map/planning/graph -patch documents instead of carrying `serde_json::Value` through role finding -interpretation. Do not resume Rider, Unity, Aquarium, Face, dogfood, planning, -app, skill, marketplace, or bridge expansion until this organ is being cut -cleanly. +typed launch-intent messages, then seal MCP JSON behind a CultNet MCP adapter. +Do not resume Rider, Unity, Aquarium, Face, dogfood, planning, app, skill, +marketplace, or bridge expansion until this organ is being cut cleanly. The Phase 6 freshness slice is landed. It exposes read-only `thread/epiphany/freshness` from live retrieval summaries plus graph diff --git a/notes/json-contamination-ledger.md b/notes/json-contamination-ledger.md index 959522a7..6d62b906 100644 --- a/notes/json-contamination-ledger.md +++ b/notes/json-contamination-ledger.md @@ -59,7 +59,7 @@ Rule: JSON is allowed at external schema/wire boundaries. Inside Epiphany, live 1. Done: purge `selfPatch` as internal JSON. Role-result interpretation now stores the typed `AgentSelfPatch` document and shares the agent-memory contract; JSON remains only at the legacy app-server projection. 2. Partly done: core worker launch `input_json` and `output_schema_json` are replaced by typed launch documents and output contract ids. Remaining cut is the protocol edge. -3. Replace role `statePatch` JSON with typed map/planning/graph patch documents. +3. Done: replace role `statePatch` JSON with typed map/planning/graph patch documents. `EpiphanyRoleFindingInterpretation` now carries `EpiphanyRoleStatePatchDocument`; app-server maps it to the legacy protocol patch without JSON round-tripping. 4. Done: replace runtime job result JSON projections. Runtime-spine results now project through typed role/reorient interpreters instead of `runtime_job_result_to_role_json` and `runtime_job_result_to_reorient_json`. 5. Seal MCP JSON behind a CultNet MCP adapter. 6. Audit heartbeat cognition values and either type them or explicitly expire them. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index c2f2a706..7d85c681 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index 6703f6bf..5e16b647 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -278,5 +278,5 @@ rejected_paths: reason: making Epiphany cleaner inside Codex still preserves the wrong organ; Codex must be starved down to OpenAI subscription auth/model transport, while Epiphany data and subsystem communication move to typed CultCache/CultNet. current_status: - summary: The Codex starvation pass has three receipts. `notes/codex-auth-spine-inventory.md` maps the minimal vendored OpenAI auth/model-call spine; `notes/json-contamination-ledger.md` classifies the major Epiphany JSON uses by allowed edge, quarantine, or internal contamination. Role-result `selfPatch` now uses typed `AgentSelfPatch` internally. Core worker launch now uses `EpiphanyWorkerLaunchDocument` plus `output_contract_id` instead of `input_json` / `output_schema_json`. Runtime job results now project through typed role/reorient interpreters instead of JSON round-tripping. Legacy app-server JSON remains only at hostile ingress/protocol quarantine pending CultNet replacement. - next_action: Continue the liberation pass at the protocol edge: replace `ThreadEpiphanyJobLaunchParams.input_json` / `output_schema_json` with CultNet typed launch-intent messages, then type role `statePatch` map/planning/graph patch documents instead of carrying `serde_json::Value` through role finding interpretation. + summary: The Codex starvation pass has four receipts. `notes/codex-auth-spine-inventory.md` maps the minimal vendored OpenAI auth/model-call spine; `notes/json-contamination-ledger.md` classifies the major Epiphany JSON uses by allowed edge, quarantine, or internal contamination. Role-result `selfPatch` and `statePatch` now use typed Epiphany documents internally. Core worker launch now uses `EpiphanyWorkerLaunchDocument` plus `output_contract_id` instead of `input_json` / `output_schema_json`. Runtime job results now project through typed role/reorient interpreters instead of JSON round-tripping. Legacy app-server JSON remains only at hostile ingress/protocol quarantine pending CultNet replacement. + next_action: Continue the liberation pass at the protocol edge: replace `ThreadEpiphanyJobLaunchParams.input_json` / `output_schema_json` with CultNet typed launch-intent messages, then seal MCP JSON behind a CultNet MCP adapter instead of letting Codex app/tool plumbing own it. diff --git a/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs b/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs index 4014ff2a..2e427373 100644 --- a/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs +++ b/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs @@ -563,6 +563,7 @@ use epiphany_core::EpiphanyRoleFindingInterpretation; use epiphany_core::EpiphanyRoleResultRoleId; use epiphany_core::EpiphanyRoleSelfPersistenceReview as CoreEpiphanyRoleSelfPersistenceReview; use epiphany_core::EpiphanyRoleSelfPersistenceStatus as CoreEpiphanyRoleSelfPersistenceStatus; +use epiphany_core::EpiphanyRoleStatePatchDocument; use epiphany_core::EpiphanyRoleWorkerLaunchDocument; use epiphany_core::EpiphanyRuntimeJobSnapshot; use epiphany_core::EpiphanyRuntimeJobStatus; @@ -13345,31 +13346,21 @@ fn map_epiphany_role_finding( job_error: Option, item_error: Option, ) -> ThreadEpiphanyRoleFinding { - let state_patch_result = raw_result - .get("statePatch") - .cloned() - .map(serde_json::from_value::); - let state_patch = state_patch_result - .as_ref() - .and_then(|result| result.as_ref().ok().cloned()); - let state_patch_parse_error = state_patch_result - .as_ref() - .and_then(|result| result.as_ref().err().map(ToString::to_string)); let finding = interpret_role_finding( map_core_role_result_role_id(role_id), &raw_result, - state_patch_parse_error, + None, job_error, item_error, ); - map_protocol_role_finding(role_id, finding, state_patch) + map_protocol_role_finding(role_id, finding) } fn map_protocol_role_finding( role_id: ThreadEpiphanyRoleId, finding: EpiphanyRoleFindingInterpretation, - state_patch: Option, ) -> ThreadEpiphanyRoleFinding { + let state_patch = finding.state_patch.map(map_protocol_update_patch); ThreadEpiphanyRoleFinding { role_id, verdict: finding.verdict, @@ -13399,6 +13390,52 @@ fn map_protocol_role_finding( } } +fn map_protocol_update_patch(patch: EpiphanyRoleStatePatchDocument) -> ThreadEpiphanyUpdatePatch { + ThreadEpiphanyUpdatePatch { + objective: patch.objective, + active_subgoal_id: patch.active_subgoal_id, + subgoals: patch.subgoals, + invariants: patch.invariants, + graphs: patch.graphs, + graph_frontier: patch.graph_frontier, + graph_checkpoint: patch.graph_checkpoint, + scratch: patch.scratch, + investigation_checkpoint: patch.investigation_checkpoint, + job_bindings: patch.job_bindings, + acceptance_receipts: patch.acceptance_receipts, + runtime_links: patch.runtime_links, + observations: patch.observations, + evidence: patch.evidence, + churn: patch.churn, + mode: patch.mode, + planning: patch.planning, + } +} + +fn core_state_patch_from_protocol( + patch: &ThreadEpiphanyUpdatePatch, +) -> EpiphanyRoleStatePatchDocument { + EpiphanyRoleStatePatchDocument { + objective: patch.objective.clone(), + active_subgoal_id: patch.active_subgoal_id.clone(), + subgoals: patch.subgoals.clone(), + invariants: patch.invariants.clone(), + graphs: patch.graphs.clone(), + graph_frontier: patch.graph_frontier.clone(), + graph_checkpoint: patch.graph_checkpoint.clone(), + scratch: patch.scratch.clone(), + investigation_checkpoint: patch.investigation_checkpoint.clone(), + job_bindings: patch.job_bindings.clone(), + acceptance_receipts: patch.acceptance_receipts.clone(), + runtime_links: patch.runtime_links.clone(), + observations: patch.observations.clone(), + evidence: patch.evidence.clone(), + churn: patch.churn.clone(), + mode: patch.mode.clone(), + planning: patch.planning.clone(), + } +} + fn map_core_role_result_role_id(role_id: ThreadEpiphanyRoleId) -> EpiphanyRoleResultRoleId { match role_id { ThreadEpiphanyRoleId::Implementation => EpiphanyRoleResultRoleId::Implementation, @@ -13642,7 +13679,6 @@ fn load_epiphany_role_result_from_runtime_spine_job( map_protocol_role_finding( role_id, interpret_role_runtime_job_result(map_core_role_result_role_id(role_id), result), - None, ) }); let note = render_epiphany_role_result_note(role_id, status, finding.as_ref(), None); @@ -14777,13 +14813,11 @@ fn parse_role_finding_state_patch( } fn imagination_role_accept_patch_errors(patch: &ThreadEpiphanyUpdatePatch) -> Vec { - let value = serde_json::to_value(patch).unwrap_or_else(|_| serde_json::Value::Null); - imagination_role_state_patch_policy_errors(&value) + imagination_role_state_patch_policy_errors(&core_state_patch_from_protocol(patch)) } fn modeling_role_accept_patch_errors(patch: &ThreadEpiphanyUpdatePatch) -> Vec { - let value = serde_json::to_value(patch).unwrap_or_else(|_| serde_json::Value::Null); - modeling_role_state_patch_policy_errors(&value) + modeling_role_state_patch_policy_errors(&core_state_patch_from_protocol(patch)) } fn role_finding_summary(finding: &ThreadEpiphanyRoleFinding) -> String {