harn-vm 0.10.168

Async bytecode virtual machine for the Harn programming language
//! Content-free diagnostic of the existing canonical publication replay.
//!
//! This reads a completed product run without launching another agent. Private
//! prose remains in the journal; only source linkage and owner decisions leave
//! this seam. Missing metadata is explicit, never inferred from visible text.

use std::path::Path;

use harn_session_store::{SessionStore, StoreError, StoreResult};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

pub use crate::llm::assistant_publication::Publication as AssistantPublicationDisposition;

/// Only the transcript owner's explicit flag establishes harness authorship.
/// An unmarked assistant still needs dispatch metadata; it is not proof that
/// a model was called.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AssistantPublicationOrigin {
    UnmarkedAssistant,
    HarnessBookkeeping,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct AssistantPublicationRecord {
    pub source_event_id: Option<String>,
    pub origin: AssistantPublicationOrigin,
    pub call_role: Option<String>,
    pub call_stage: Option<String>,
    pub stage_metadata_present: bool,
    pub stage_metadata_valid: bool,
    pub metadata_present: bool,
    pub disposition: Option<AssistantPublicationDisposition>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SessionPublicationEvidence {
    pub schema: String,
    pub session_id: String,
    pub last_event_id: Option<u64>,
    pub chain_root_hash: Option<String>,
    pub stored_event_count: usize,
    pub assistant_message_count: usize,
    pub bookkeeping_count: usize,
    pub missing_source_count: usize,
    pub missing_call_role_count: usize,
    pub missing_call_stage_metadata_count: usize,
    pub unrecognized_call_stage_metadata_count: usize,
    pub missing_metadata_count: usize,
    pub unrecognized_metadata_count: usize,
    pub pending_count: usize,
    pub published_count: usize,
    pub withheld_count: usize,
    pub records: Vec<AssistantPublicationRecord>,
}

/// Open only an existing canonical store. Absence remains `None`; it cannot
/// create a database and then report a measured empty result.
pub async fn read_project_session_publication_evidence(
    project_root: &Path,
    session_id: &str,
) -> StoreResult<Option<SessionPublicationEvidence>> {
    let Some(store) = crate::stdlib::session_store::open_existing_canonical_store(project_root)
        .map_err(|error| StoreError::Backend(error.to_string()))?
    else {
        return Ok(None);
    };
    read_session_publication_evidence(&store, session_id).await
}

/// Replay through the transcript owner, with complete stable read coverage.
pub async fn read_session_publication_evidence(
    store: &dyn SessionStore,
    session_id: &str,
) -> StoreResult<Option<SessionPublicationEvidence>> {
    if session_id.trim().is_empty() {
        return Err(StoreError::Conflict(
            "publication evidence requires a nonempty session identity".into(),
        ));
    }
    let before = match store.describe(session_id).await {
        Ok(meta) => meta,
        Err(StoreError::NotFound(_)) => return Ok(None),
        Err(error) => return Err(error),
    };
    let events = store.read_all(session_id).await?;
    let after = store.describe(session_id).await?;
    validate_coverage(session_id, &before, &after, &events)?;
    let hydrated = crate::agent_session_journal::hydrate_events(events);
    let records: Vec<_> = hydrated
        .messages
        .iter()
        .enumerate()
        .filter(|(_, message)| {
            message.get("role").and_then(serde_json::Value::as_str) == Some("assistant")
        })
        .map(|(index, message)| {
            let source_event_id = hydrated
                .source_event_ids
                .get(index)
                .cloned()
                .flatten()
                .filter(|source| !source.trim().is_empty());
            if message
                .get(crate::llm::agent_result_projection::BOOKKEEPING_TURN_KEY)
                .and_then(serde_json::Value::as_bool)
                == Some(true)
            {
                return AssistantPublicationRecord {
                    source_event_id,
                    origin: AssistantPublicationOrigin::HarnessBookkeeping,
                    call_role: None,
                    call_stage: None,
                    stage_metadata_present: false,
                    stage_metadata_valid: false,
                    metadata_present: false,
                    disposition: None,
                };
            }
            let (metadata_present, disposition) =
                crate::llm::assistant_publication::diagnostic_disposition(message);
            AssistantPublicationRecord {
                source_event_id,
                origin: AssistantPublicationOrigin::UnmarkedAssistant,
                call_role: message
                    .get("_harn")
                    .filter(|facts| {
                        facts.get("kind").and_then(serde_json::Value::as_str) == Some("assistant")
                    })
                    .and_then(|facts| facts.get("call_role"))
                    .and_then(serde_json::Value::as_str)
                    .filter(|role| !role.trim().is_empty())
                    .map(str::to_owned),
                call_stage: message
                    .get("_harn")
                    .and_then(|facts| facts.get("call_stage"))
                    .and_then(serde_json::Value::as_str)
                    .map(str::to_owned),
                stage_metadata_present: message
                    .get("_harn")
                    .is_some_and(|facts| facts.get("call_stage").is_some()),
                stage_metadata_valid: message.get("_harn").is_some_and(|facts| {
                    facts.get("kind").and_then(serde_json::Value::as_str) == Some("assistant")
                        && facts.get("call_stage").is_some_and(|stage| {
                            stage.is_null()
                                || stage.as_str().is_some_and(|stage| !stage.trim().is_empty())
                        })
                }),
                metadata_present,
                disposition,
            }
        })
        .collect();
    let dispatch_records: Vec<_> = records
        .iter()
        .filter(|record| record.origin == AssistantPublicationOrigin::UnmarkedAssistant)
        .collect();
    Ok(Some(SessionPublicationEvidence {
        schema: "harn.session_publication_evidence.v1".into(),
        session_id: session_id.into(),
        last_event_id: after.last_event_id,
        chain_root_hash: after.chain_root_hash,
        stored_event_count: after.event_count,
        assistant_message_count: records.len(),
        bookkeeping_count: records.len() - dispatch_records.len(),
        missing_source_count: records
            .iter()
            .filter(|record| record.source_event_id.is_none())
            .count(),
        missing_call_role_count: dispatch_records
            .iter()
            .filter(|record| record.call_role.is_none())
            .count(),
        missing_call_stage_metadata_count: dispatch_records
            .iter()
            .filter(|record| !record.stage_metadata_present)
            .count(),
        unrecognized_call_stage_metadata_count: dispatch_records
            .iter()
            .filter(|record| record.stage_metadata_present && !record.stage_metadata_valid)
            .count(),
        missing_metadata_count: dispatch_records
            .iter()
            .filter(|record| !record.metadata_present)
            .count(),
        unrecognized_metadata_count: dispatch_records
            .iter()
            .filter(|record| record.metadata_present && record.disposition.is_none())
            .count(),
        pending_count: dispatch_records
            .iter()
            .filter(|record| record.disposition == Some(AssistantPublicationDisposition::Pending))
            .count(),
        published_count: dispatch_records
            .iter()
            .filter(|record| record.disposition == Some(AssistantPublicationDisposition::Published))
            .count(),
        withheld_count: dispatch_records
            .iter()
            .filter(|record| record.disposition == Some(AssistantPublicationDisposition::Withheld))
            .count(),
        records,
    }))
}

fn validate_coverage(
    session_id: &str,
    before: &harn_session_store::SessionMeta,
    after: &harn_session_store::SessionMeta,
    events: &[harn_session_store::StoredEvent],
) -> StoreResult<()> {
    if before.id != session_id
        || after.id != session_id
        || events.iter().any(|event| event.session_id != session_id)
        || before.event_count != after.event_count
        || before.last_event_id != after.last_event_id
        || before.chain_root_hash != after.chain_root_hash
        || events.len() != after.event_count
        || events.last().map(|event| event.event_id) != after.last_event_id
    {
        return Err(StoreError::Conflict(
            "canonical publication evidence has incomplete or changing event coverage".into(),
        ));
    }
    Ok(())
}

#[cfg(test)]
mod tests;