affidavit 26.6.22

Provenance Layer — receipt assembly and certification (verify a witness against a format standard; never decide honesty).
//! Process discovery over receipts — the genuine `wasm4pm` integration.
//!
//! This is the "discover" half of van der Aalst's discover-then-conform pipeline,
//! run on a receipt. A receipt is OCEL-shaped operation-events; we project it into
//! a `wasm4pm` `EventLog` (each receipt event → one event in a single trace, keyed
//! by activity = `event_type`) and hand it to `wasm4pm`'s real discovery engine.
//!
//! The contribution (COVERAGE.md §1) is that the receipt is BOTH the event log AND
//! the conformance certificate. Here we exercise the log-facet: the same bytes that
//! prove conformance (via admission) are mined for their process model.
//!
//! Integration is genuine (not a stub): `discover_process_tree` calls
//! `wasm4pm::process_tree::discover_simple_process_tree_from_log`. Remove the
//! `wasm4pm` dependency and this module does not compile (failing-when-fake on the
//! integration axis); feed it a receipt whose activities are absent from the output
//! and the witness test fails (failing-when-fake on the capability axis).

use crate::types::Receipt;
use std::collections::HashMap;
use wasm4pm::ilp_discovery::{
    compute_simplicity, discover_ilp_petri_net_from_log, discover_optimized_dfg_from_log,
};
use wasm4pm::models::{AttributeValue, Event, EventLog, Trace};
use wasm4pm::process_tree::discover_simple_process_tree_from_log;

/// The activity key used when projecting receipt events into the event log
/// (the OCEL convention).
pub const ACTIVITY_KEY: &str = "concept:name";

/// Project an affidavit [`Receipt`] into a `wasm4pm` [`EventLog`]: the receipt's
/// ordered events become a single trace, each event's `event_type` becoming its
/// `concept:name` activity attribute. This is the boundary projection from the
/// producer's receipt shape into the court's event-log shape.
pub fn project_to_event_log(receipt: &Receipt) -> EventLog {
    let events: Vec<Event> = receipt
        .events
        .iter()
        .map(|ev| {
            let mut attrs: HashMap<String, AttributeValue> = HashMap::new();
            attrs.insert(
                ACTIVITY_KEY.to_string(),
                AttributeValue::String(ev.event_type.clone()),
            );
            Event { attributes: attrs }
        })
        .collect();

    EventLog {
        attributes: HashMap::new(),
        traces: vec![Trace {
            attributes: HashMap::new(),
            events,
        }],
    }
}

/// Discover a process-tree description from a receipt, using `wasm4pm`'s real
/// discovery engine. Returns the engine's serialized process-tree string.
pub fn discover_process_tree(receipt: &Receipt) -> String {
    let log = project_to_event_log(receipt);
    discover_simple_process_tree_from_log(&log, ACTIVITY_KEY)
}

/// Conformance metrics for a receipt: `(fitness, activity_coverage)`.
///
/// HONEST LABELLING (corrected after the van der Aalst review): only `fitness` is
/// a van-der-Aalst conformance number. The second value is **activity coverage**,
/// NOT van der Aalst precision.
/// - `fitness` = `wasm4pm::token_replay_pure(...).avg_fitness` — a real token-replay
///   number (consumed/produced/missing/remaining), in `[0,1]`. This *is* "a number
///   from replay".
/// - `activity_coverage` = `|log_activities ∩ model_activities| / |model_activities|`
///   (wasm4pm's `calculate_precision`, renamed here to what it actually computes).
///   It is an activity-set coverage ratio, NOT escaping-edges precision; it performs
///   no enablement analysis. We do not claim it is van der Aalst precision and do not
///   attribute it to token replay.
///
/// True van der Aalst precision (escaping edges) and generalization are NOT computed
/// here — see `reference/COVERAGE.md §2.4`.
pub fn conformance_metrics(receipt: &Receipt) -> (f64, f64) {
    let log = project_to_event_log(receipt);
    let (_net, fitness, activity_coverage) = discover_ilp_petri_net_from_log(&log, ACTIVITY_KEY);
    (fitness, activity_coverage)
}

/// Discover the directly-follows graph (DFG) from a receipt and return a summary:
/// `(nodes, edges, start_activities, end_activities)`. Uses `wasm4pm`'s optimized
/// DFG discovery (fitness/simplicity-weighted). The DFG is the most basic process
/// model — activities as nodes, directly-follows relations as weighted edges.
pub fn discover_dfg_summary(receipt: &Receipt) -> (usize, usize, usize, usize) {
    let log = project_to_event_log(receipt);
    let dfg = discover_optimized_dfg_from_log(&log, ACTIVITY_KEY, 1.0, 1.0);
    (
        dfg.nodes.len(),
        dfg.edges.len(),
        dfg.start_activities.len(),
        dfg.end_activities.len(),
    )
}

/// Discover a process model from an **admitted** receipt — the genuine Shape-B
/// fusion (ARDPRD §4): discovery here is *type-gated on admission*. The only way
/// to obtain an [`AdmittedReceipt`] is through [`crate::admission::admit`], which
/// runs the OCEL court + the chain/continuity certify pipeline. So this function
/// *cannot be called* on a receipt that did not pass conformance — admission is a
/// compile-time precondition of discovery. The receipt that is mined IS the same
/// value that proved conformance: the event log and the conformance certificate
/// are one object, and discovery consumes the certificate, not raw bytes.
///
/// # Example: see `examples/discover_shapeb.rs` (run: `cargo run --example discover_shapeb`).
pub fn discover_from_admitted(admitted: &crate::types::AdmittedReceipt) -> String {
    // `&AdmittedReceipt` in the signature is the load-bearing part: a caller with
    // only a `Receipt` (un-adjudicated) cannot reach this path.
    discover_process_tree(&admitted.value)
}

/// Admission-gated quality metrics — the conformance analogue of
/// [`discover_from_admitted`]. Takes `&AdmittedReceipt`, so the metrics can only
/// be computed for a receipt that passed the OCEL court + chain verifier. This is
/// the path the binary's `conformance` verb uses, keeping the type-gate live in
/// production (not only in `tests/reference_pipeline.rs`).
///
/// # Example: see `examples/discover_shapeb.rs` (run: `cargo run --example discover_shapeb`).
pub fn quality_metrics_from_admitted(admitted: &crate::types::AdmittedReceipt) -> (f64, f64, f64) {
    quality_metrics(&admitted.value)
}

/// Returns `(fitness, activity_coverage, simplicity)`. HONEST LABELLING:
/// - `fitness` — van der Aalst token-replay fitness (real number from replay).
/// - `activity_coverage` — an activity-set coverage ratio, **NOT** van der Aalst
///   precision (no enablement / escaping-edges analysis). Named for what it is.
/// - `simplicity` — `wasm4pm::compute_simplicity` over the discovered net's
///   `(places, transitions, arcs)` — the Occam dimension.
///
/// So of van der Aalst's four quality dimensions, exactly TWO are genuinely
/// computed here (fitness, simplicity). Precision (escaping edges) and
/// generalization are NOT computed — `wasm4pm::generalization` is wasm-handle
/// gated, and the crate ships no escaping-edges precision callable from here.
/// See reference/COVERAGE.md §2.4.
pub fn quality_metrics(receipt: &Receipt) -> (f64, f64, f64) {
    let log = project_to_event_log(receipt);
    let (net, fitness, activity_coverage) = discover_ilp_petri_net_from_log(&log, ACTIVITY_KEY);
    let simplicity = compute_simplicity(net.places.len(), net.transitions.len(), net.arcs.len());
    (fitness, activity_coverage, simplicity)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ocel::{build_event, object_ref, SeqCounter};

    fn receipt_with(activities: &[&str]) -> Receipt {
        let mut asm = crate::chain::ChainAssembler::new();
        let mut counter = SeqCounter::new();
        for (i, act) in activities.iter().enumerate() {
            let ev = build_event(
                *act,
                vec![object_ref(&format!("obj-{i}"), "artifact")],
                act.as_bytes(),
                &mut counter,
            )
            .expect("build event");
            asm.append(ev).expect("append");
        }
        asm.finalize()
    }

    #[test]
    fn discovers_a_model_mentioning_the_receipt_activities() {
        // A receipt with three distinct activities. wasm4pm's discovery must
        // surface those activities in the discovered model. If discovery were a
        // stub returning a constant, the activity names would be absent and this
        // fails — failing-when-fake on the capability axis.
        let receipt = receipt_with(&["create", "transform", "release"]);
        let model = discover_process_tree(&receipt);

        assert!(
            model.contains("create"),
            "discovered model must mention 'create'; got: {model}"
        );
        assert!(
            model.contains("transform"),
            "discovered model must mention 'transform'; got: {model}"
        );
        assert!(
            model.contains("release"),
            "discovered model must mention 'release'; got: {model}"
        );
    }

    #[test]
    fn projection_preserves_event_count() {
        let receipt = receipt_with(&["a", "b", "c", "d"]);
        let log = project_to_event_log(&receipt);
        assert_eq!(log.traces.len(), 1, "one receipt → one trace");
        assert_eq!(
            log.traces[0].events.len(),
            4,
            "every receipt event projects to one log event"
        );
    }

    #[test]
    fn conformance_metrics_are_real_numbers_from_replay() {
        // A sequential receipt. wasm4pm discovers a net and replays the log:
        // fitness is a real token-replay number in [0,1]; the second value is
        // activity coverage (NOT van der Aalst precision — honest labelling).
        // Failing-when-fake: a stubbed metric returning a constant outside [0,1]
        // (or NaN) fails the bounds check; removing wasm4pm fails to compile.
        let receipt = receipt_with(&["create", "transform", "release"]);
        let (fitness, activity_coverage) = conformance_metrics(&receipt);

        assert!(
            (0.0..=1.0).contains(&fitness),
            "fitness must be a real number in [0,1]; got {fitness}"
        );
        assert!(
            (0.0..=1.0).contains(&activity_coverage),
            "activity_coverage must be a real number in [0,1]; got {activity_coverage}"
        );
        // A model discovered FROM this very log must fit it well — the log is, by
        // construction, replayable on its own discovered net.
        assert!(
            fitness > 0.5,
            "a log replayed on its own discovered net should fit well; got {fitness}"
        );
    }

    #[test]
    fn simplicity_is_a_real_number_from_the_discovered_net() {
        let receipt = receipt_with(&["create", "transform", "release"]);
        let (_f, _p, simplicity) = quality_metrics(&receipt);
        assert!(
            (0.0..=1.0).contains(&simplicity),
            "simplicity must be a real number in [0,1]; got {simplicity}"
        );
    }
}