diff --git a/epiphany-core/src/reasoning_context.rs b/epiphany-core/src/reasoning_context.rs index 1229aa9f..bdea64ba 100644 --- a/epiphany-core/src/reasoning_context.rs +++ b/epiphany-core/src/reasoning_context.rs @@ -28,7 +28,7 @@ pub const MIND_COMMIT_RECEIPT_TYPE: &str = "epiphany.mind_commit_receipt.v1"; pub const DECISION_AUDIT_PROJECTION_SCHEMA_VERSION: &str = "epiphany.decision_audit_projection.v1"; pub const DECISION_AUDIT_INDEX_SCHEMA_VERSION: &str = "epiphany.decision_audit_index.v1"; pub const WORKER_REASONING_PROJECTION_POLICY: &str = - "epiphany.reasoning_projection.worker_launch.v2"; + "epiphany.reasoning_projection.worker_launch.v3"; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct EpiphanyRolePassAuthorityProjection { @@ -154,6 +154,7 @@ impl From for EpiphanyMindPromptProjection { pub struct EpiphanyRoleReasoningProjection { pub authority: EpiphanyRolePassAuthorityProjection, pub mind: EpiphanyMindPromptProjection, + pub modeling_body: Option, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -996,9 +997,7 @@ fn validate_decision_terminal_records( fn role_terminal_semantic_role(basis: &EpiphanyReasoningBasis) -> Result> { Ok(match basis.projection()? { - EpiphanyReasoningProjection::RolePass(projection) => { - Some(projection.authority.role_id) - } + EpiphanyReasoningProjection::RolePass(projection) => Some(projection.authority.role_id), _ => None, }) } @@ -1017,11 +1016,26 @@ pub fn worker_reasoning_basis( EpiphanyWorkerLaunchDocument::Role(document) => { let mind = crate::assemble_mind_view(store_path)?; let source_documents = mind.source_documents.clone(); + let authority: EpiphanyRolePassAuthorityProjection = document.into(); + let modeling_body = if authority.role_id.eq_ignore_ascii_case("modeling") { + Some(crate::load_repository_body_reasoning_projection( + store_path, + authority + .repository_body_observation_basis + .as_ref() + .ok_or_else(|| { + anyhow!("Modeling reasoning launch has no repository Body basis") + })?, + )?) + } else { + None + }; ( source_documents, EpiphanyReasoningProjection::RolePass(EpiphanyRoleReasoningProjection { - authority: document.into(), + authority, mind: mind.into(), + modeling_body, }), ) } @@ -1109,6 +1123,22 @@ pub fn put_reasoning_basis( return Err(anyhow!("role reasoning basis cites a non-role launch")); }; let current_mind = crate::assemble_mind_view(store_path)?; + let current_modeling_body = if projection + .authority + .role_id + .eq_ignore_ascii_case("modeling") + { + Some(crate::load_repository_body_reasoning_projection( + store_path, + projection + .authority + .repository_body_observation_basis + .as_ref() + .ok_or_else(|| anyhow!("Modeling reasoning basis has no Body basis"))?, + )?) + } else { + None + }; let modeling_body_mismatch = projection .authority .role_id @@ -1117,6 +1147,7 @@ pub fn put_reasoning_basis( != current_mind.repository_body_observation; if projection.authority != document.into() || projection.mind != current_mind.clone().into() + || projection.modeling_body != current_modeling_body || basis.source_documents != current_mind.source_documents || modeling_body_mismatch { @@ -1846,8 +1877,8 @@ mod tests { fn role_document() -> EpiphanyRoleWorkerLaunchDocument { EpiphanyRoleWorkerLaunchDocument { thread_id: "creation-thread".into(), - role_id: "Modeling".into(), - objective: Some("Map the Body".into()), + role_id: "Imagination".into(), + objective: Some("Propose one future".into()), dynamic_prompt_context: Some("typed projection".into()), repository_body_observation_basis: None, proposal_modeling_context: None, @@ -1879,16 +1910,16 @@ mod tests { Ok(EpiphanyRuntimeWorkerLaunchRequest { schema_version: crate::RUNTIME_WORKER_LAUNCH_REQUEST_SCHEMA_VERSION.into(), job_id: "pass-1".into(), - binding_id: crate::EPIPHANY_MODELING_ROLE_BINDING_ID.into(), - role: "Modeling".into(), - authority_scope: "epiphany.role.modeling".into(), - instruction: "Map the Body".into(), + binding_id: crate::EPIPHANY_IMAGINATION_ROLE_BINDING_ID.into(), + role: "Imagination".into(), + authority_scope: "epiphany.role.imagination".into(), + instruction: "Propose one future".into(), output_contract_id: crate::ROLE_WORKER_OUTPUT_CONTRACT_ID.into(), document_kind: "role".into(), launch_document_msgpack: rmp_serde::to_vec_named(&document)?, metadata: Default::default(), organ_launch_contract: crate::default_launch_organ_contract( - "epiphany.role.modeling", + "epiphany.role.imagination", "role", crate::ROLE_WORKER_OUTPUT_CONTRACT_ID, ), @@ -1904,7 +1935,10 @@ mod tests { } fn basis() -> Result { - let authority = EpiphanyRolePassAuthorityProjection::from(role_document()); + let mut document = role_document(); + document.role_id = "Modeling".into(); + document.objective = Some("Map the Body".into()); + let authority = EpiphanyRolePassAuthorityProjection::from(document); let payload_msgpack = rmp_serde::to_vec_named(&crate::EpiphanyMindIdentity { schema_epoch: crate::MIND_SCHEMA_EPOCH.into(), runtime_id: "test-runtime".into(), @@ -1938,6 +1972,7 @@ mod tests { repository_body_observation: None, repo_model: None, }, + modeling_body: None, }); EpiphanyReasoningBasis::new( "pass-1", @@ -2581,9 +2616,9 @@ mod tests { result_id: "worker-result-1".into(), job_id: "pass-1".into(), session_id: "session-1".into(), - role: "Modeling".into(), + role: "Imagination".into(), verdict: "completed".into(), - summary: "Structured Modeling decision".into(), + summary: "Structured Imagination decision".into(), completed_at: "2026-08-14T00:00:03Z".into(), next_safe_move: "Inspect the durable decision context".into(), evidence_refs: Vec::new(), diff --git a/epiphany-core/src/repository_body_observer.rs b/epiphany-core/src/repository_body_observer.rs index 945025ff..ebbde502 100644 --- a/epiphany-core/src/repository_body_observer.rs +++ b/epiphany-core/src/repository_body_observer.rs @@ -109,6 +109,27 @@ pub struct RepositoryBodyObservationBasis { pub scan_finished_at: String, } +/// Exact authenticated Repository Body state sealed into Modeling reasoning. +/// +/// The compact basis identifies the admitted generation. Modeling additionally +/// needs the actual Git identity and complete manifest so it can reason from +/// its Body without laundering internal inspection through Eyes. +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RepositoryBodyReasoningProjection { + pub basis: RepositoryBodyObservationBasis, + pub source_identity_type: String, + pub source_identity_key: String, + pub source_identity_sha256: String, + pub git_top_level: String, + pub object_format: String, + pub head_oid: Option, + pub tree_oid: String, + pub two_scan_outcome: String, + pub manifest_entry_count: u64, + pub manifest_entries: Vec, +} + #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] #[cultcache( type = "epiphany.repository_body.observation", @@ -487,6 +508,41 @@ pub fn load_current_runtime_repository_body_basis( }) } +pub fn load_repository_body_reasoning_projection( + runtime_store: &Path, + basis: &RepositoryBodyObservationBasis, +) -> Result { + validate_repository_body_observation_basis(runtime_store, basis)?; + let route = runtime_repository_body_store_binding(runtime_store)? + .ok_or_else(|| anyhow!("runtime has no repository Body-store binding"))?; + let entries = load_body_envelopes(Path::new(&route.body_store_path))?; + let binding: RepositoryBodyBinding = decode( + find(&entries, BODY_BINDING_TYPE, BODY_BINDING_KEY) + .ok_or_else(|| anyhow!("runtime repository Body store has no Body binding"))?, + )?; + let historical_head = RepositoryBodyHead { + schema_version: BODY_SCHEMA_VERSION.into(), + workspace_id: basis.workspace_id.clone(), + generation: basis.generation, + observation_id: basis.observation_id.clone(), + manifest_root_sha256: basis.manifest_root_sha256.clone(), + }; + let (observation, manifest) = validate_body_chain(&entries, &binding, &historical_head)?; + Ok(RepositoryBodyReasoningProjection { + basis: basis.clone(), + source_identity_type: binding.source_identity_type, + source_identity_key: binding.source_identity_key, + source_identity_sha256: binding.source_identity_sha256, + git_top_level: binding.git_top_level, + object_format: binding.object_format, + head_oid: observation.head_oid, + tree_oid: observation.tree_oid, + two_scan_outcome: observation.two_scan_outcome, + manifest_entry_count: observation.manifest_entry_count, + manifest_entries: manifest.entries, + }) +} + pub fn validate_repository_body_observation_basis( runtime_store: &Path, basis: &RepositoryBodyObservationBasis, @@ -1688,6 +1744,47 @@ mod tests { assert_ne!(b.tree_oid, c.tree_oid); Ok(()) } + + #[test] + fn reasoning_projection_carries_the_exact_authenticated_body_manifest() -> Result<()> { + let d = repo()?; + let state = tempfile::tempdir()?; + let (store, runtime) = bound(d.path(), state.path(), "workspace", "runtime", "swarm")?; + write(&d.path().join("tracked.txt"), "one")?; + run(d.path(), &["add", "."])?; + run(d.path(), &["commit", "-m", "seed"])?; + + let basis = observe_runtime_repository_body_basis(&runtime)?; + let projection = load_repository_body_reasoning_projection(&runtime, &basis)?; + let (binding, _) = load_repository_body_status(&store)?.expect("Body status"); + + assert_eq!(projection.basis, basis); + assert_eq!( + projection.source_identity_type, + binding.source_identity_type + ); + assert_eq!(projection.source_identity_key, binding.source_identity_key); + assert_eq!( + projection.source_identity_sha256, + binding.source_identity_sha256 + ); + assert_eq!(projection.git_top_level, binding.git_top_level); + assert_eq!(projection.object_format, binding.object_format); + assert!(projection.head_oid.is_some()); + assert!(!projection.tree_oid.is_empty()); + assert_eq!(projection.two_scan_outcome, "stable_equal"); + assert_eq!( + projection.manifest_entry_count as usize, + projection.manifest_entries.len() + ); + assert!( + projection + .manifest_entries + .iter() + .any(|entry| entry.path == "tracked.txt") + ); + Ok(()) + } #[test] fn equality_scans_share_one_private_index_session_and_remove_it() -> Result<()> { let d = repo()?;