Type role state patches

This commit is contained in:
Meta Vens 2026-05-12 23:21:15 +01:00
commit 5886ff58ca
9 changed files with 197 additions and 115 deletions

View file

@ -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;

View file

@ -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;

View file

@ -38,7 +38,7 @@ pub struct EpiphanyRoleFindingInterpretation {
pub open_questions: Vec<String>,
pub evidence_gaps: Vec<String>,
pub risks: Vec<String>,
pub state_patch: Option<serde_json::Value>,
pub state_patch: Option<EpiphanyRoleStatePatchDocument>,
pub self_patch: Option<AgentSelfPatch>,
pub self_persistence: Option<EpiphanyRoleSelfPersistenceReview>,
pub job_error: Option<String>,
@ -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<String>,
_legacy_state_patch_parse_error: Option<String>,
job_error: Option<String>,
item_error: Option<String>,
) -> EpiphanyRoleFindingInterpretation {
let state_patch = raw_result.get("statePatch").cloned();
let state_patch_result = raw_result
.get("statePatch")
.cloned()
.map(serde_json::from_value::<EpiphanyRoleStatePatchDocument>);
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<String> {
pub fn modeling_role_state_patch_policy_errors(
patch: &EpiphanyRoleStatePatchDocument,
) -> Vec<String> {
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<String> {
pub fn imagination_role_state_patch_policy_errors(
patch: &EpiphanyRoleStatePatchDocument,
) -> Vec<String> {
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("<missing>");
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<String>,
) -> Option<String> {
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<String>,
) -> Option<String> {
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<String>
.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;

View file

@ -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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_subgoal_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subgoals: Option<Vec<EpiphanySubgoal>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub invariants: Option<Vec<EpiphanyInvariant>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub graphs: Option<EpiphanyGraphs>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub graph_frontier: Option<EpiphanyGraphFrontier>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub graph_checkpoint: Option<EpiphanyGraphCheckpoint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scratch: Option<EpiphanyScratchPad>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub investigation_checkpoint: Option<EpiphanyInvestigationCheckpoint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub job_bindings: Option<Vec<EpiphanyJobBinding>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub acceptance_receipts: Vec<EpiphanyAcceptanceReceipt>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub runtime_links: Vec<EpiphanyRuntimeLink>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub observations: Vec<EpiphanyObservation>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence: Vec<EpiphanyEvidenceRecord>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub churn: Option<EpiphanyChurnState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<EpiphanyModeState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub planning: Option<EpiphanyPlanningState>,
}

View file

@ -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

View file

@ -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.

Binary file not shown.

View file

@ -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.

View file

@ -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<String>,
item_error: Option<String>,
) -> ThreadEpiphanyRoleFinding {
let state_patch_result = raw_result
.get("statePatch")
.cloned()
.map(serde_json::from_value::<ThreadEpiphanyUpdatePatch>);
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<ThreadEpiphanyUpdatePatch>,
) -> 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<String> {
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<String> {
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 {