From c8c936ede15799b2265902141662b4cc94e23a74 Mon Sep 17 00:00:00 2001 From: bellman Date: Thu, 14 May 2026 18:00:15 +0900 Subject: [PATCH] omx(team): auto-checkpoint worker-3 [6] --- rust/crates/runtime/src/g004_conformance.rs | 319 ++++++++++++++++++ rust/crates/runtime/src/lib.rs | 1 + .../fixtures/g004_contract_bundle.valid.json | 81 +++++ rust/crates/runtime/tests/g004_conformance.rs | 79 +++++ 4 files changed, 480 insertions(+) create mode 100644 rust/crates/runtime/src/g004_conformance.rs create mode 100644 rust/crates/runtime/tests/fixtures/g004_contract_bundle.valid.json create mode 100644 rust/crates/runtime/tests/g004_conformance.rs diff --git a/rust/crates/runtime/src/g004_conformance.rs b/rust/crates/runtime/src/g004_conformance.rs new file mode 100644 index 00000000..ab0a7da8 --- /dev/null +++ b/rust/crates/runtime/src/g004_conformance.rs @@ -0,0 +1,319 @@ +//! Machine-checkable conformance helpers for G004 event/report contract bundles. +//! +//! The harness intentionally validates JSON-shaped artifacts instead of owning the +//! lane-event, report, or approval-token implementations. This keeps it usable by +//! independent implementation lanes and by golden fixtures produced outside the +//! runtime crate. + +use serde_json::Value; + +const BUNDLE_SCHEMA_VERSION: &str = "g004.contract.bundle.v1"; +const REPORT_SCHEMA_VERSION: &str = "g004.report.v1"; + +/// A single conformance validation failure. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct G004ConformanceError { + /// JSON pointer-ish path to the invalid field. + pub path: String, + /// Human-readable reason the field failed validation. + pub message: String, +} + +impl G004ConformanceError { + fn new(path: impl Into, message: impl Into) -> Self { + Self { + path: path.into(), + message: message.into(), + } + } +} + +/// Validate a G004 golden contract bundle. +/// +/// The bundle shape is deliberately small and cross-lane: +/// - `laneEvents[]` must expose stable event identity, ordering/provenance, and +/// terminal dedupe fingerprints. +/// - `reports[]` must expose schema identity, content hash, projection/redaction +/// provenance, capability negotiation, fact/hypothesis/negative-evidence +/// labels, confidence, and field-level delta attribution. +/// - `approvalTokens[]` must expose owner/scope, delegation chain, one-time-use, +/// and replay-prevention fields. +#[must_use] +pub fn validate_g004_contract_bundle(bundle: &Value) -> Vec { + let mut errors = Vec::new(); + + require_string_eq( + bundle, + "/schemaVersion", + BUNDLE_SCHEMA_VERSION, + &mut errors, + ); + validate_lane_events(bundle.get("laneEvents"), "/laneEvents", &mut errors); + validate_reports(bundle.get("reports"), "/reports", &mut errors); + validate_approval_tokens( + bundle.get("approvalTokens"), + "/approvalTokens", + &mut errors, + ); + + errors +} + +#[must_use] +pub fn is_g004_contract_bundle_valid(bundle: &Value) -> bool { + validate_g004_contract_bundle(bundle).is_empty() +} + +fn validate_lane_events( + value: Option<&Value>, + path: &str, + errors: &mut Vec, +) { + let Some(events) = non_empty_array(value, path, errors) else { + return; + }; + + let mut previous_seq = None; + for (index, event) in events.iter().enumerate() { + let base = format!("{path}/{index}"); + require_non_empty_string(event, &format!("{base}/event"), errors); + require_non_empty_string(event, &format!("{base}/status"), errors); + require_non_empty_string(event, &format!("{base}/emittedAt"), errors); + require_non_empty_string(event, &format!("{base}/metadata/provenance"), errors); + require_non_empty_string(event, &format!("{base}/metadata/emitterIdentity"), errors); + require_non_empty_string(event, &format!("{base}/metadata/environmentLabel"), errors); + + match get_path(event, "/metadata/seq").and_then(Value::as_u64) { + Some(seq) => { + if let Some(previous) = previous_seq { + if seq <= previous { + errors.push(G004ConformanceError::new( + format!("{base}/metadata/seq"), + "sequence must be strictly increasing", + )); + } + } + previous_seq = Some(seq); + } + None => errors.push(G004ConformanceError::new( + format!("{base}/metadata/seq"), + "required u64 field missing", + )), + } + + if is_terminal_event_value(event.get("event")) { + require_non_empty_string(event, &format!("{base}/metadata/eventFingerprint"), errors); + } + } +} + +fn validate_reports(value: Option<&Value>, path: &str, errors: &mut Vec) { + let Some(reports) = non_empty_array(value, path, errors) else { + return; + }; + + for (index, report) in reports.iter().enumerate() { + let base = format!("{path}/{index}"); + require_string_eq( + report, + &format!("{base}/schemaVersion"), + REPORT_SCHEMA_VERSION, + errors, + ); + require_non_empty_string(report, &format!("{base}/reportId"), errors); + require_non_empty_string(report, &format!("{base}/identity/contentHash"), errors); + require_non_empty_string(report, &format!("{base}/projection/provenance"), errors); + require_non_empty_string(report, &format!("{base}/redaction/provenance"), errors); + non_empty_array( + get_path(report, "/consumerCapabilities"), + &format!("{base}/consumerCapabilities"), + errors, + ); + validate_findings( + get_path(report, "/findings"), + &format!("{base}/findings"), + errors, + ); + validate_field_deltas( + get_path(report, "/fieldDeltas"), + &format!("{base}/fieldDeltas"), + errors, + ); + } +} + +fn validate_findings(value: Option<&Value>, path: &str, errors: &mut Vec) { + let Some(findings) = non_empty_array(value, path, errors) else { + return; + }; + + for (index, finding) in findings.iter().enumerate() { + let base = format!("{path}/{index}"); + require_one_of( + finding, + &format!("{base}/kind"), + &["fact", "hypothesis", "negative_evidence"], + errors, + ); + require_one_of( + finding, + &format!("{base}/confidence"), + &["low", "medium", "high"], + errors, + ); + require_non_empty_string(finding, &format!("{base}/statement"), errors); + } +} + +fn validate_field_deltas( + value: Option<&Value>, + path: &str, + errors: &mut Vec, +) { + let Some(deltas) = non_empty_array(value, path, errors) else { + return; + }; + + for (index, delta) in deltas.iter().enumerate() { + let base = format!("{path}/{index}"); + require_non_empty_string(delta, &format!("{base}/field"), errors); + require_non_empty_string(delta, &format!("{base}/previousHash"), errors); + require_non_empty_string(delta, &format!("{base}/currentHash"), errors); + require_non_empty_string(delta, &format!("{base}/attribution"), errors); + } +} + +fn validate_approval_tokens( + value: Option<&Value>, + path: &str, + errors: &mut Vec, +) { + let Some(tokens) = non_empty_array(value, path, errors) else { + return; + }; + + for (index, token) in tokens.iter().enumerate() { + let base = format!("{path}/{index}"); + require_non_empty_string(token, &format!("{base}/tokenId"), errors); + require_non_empty_string(token, &format!("{base}/owner"), errors); + require_non_empty_string(token, &format!("{base}/scope"), errors); + require_non_empty_string(token, &format!("{base}/issuedAt"), errors); + require_bool_true(token, &format!("{base}/oneTimeUse"), errors); + require_non_empty_string(token, &format!("{base}/replayPreventionNonce"), errors); + validate_delegation_chain( + get_path(token, "/delegationChain"), + &format!("{base}/delegationChain"), + errors, + ); + } +} + +fn validate_delegation_chain( + value: Option<&Value>, + path: &str, + errors: &mut Vec, +) { + let Some(chain) = non_empty_array(value, path, errors) else { + return; + }; + + for (index, hop) in chain.iter().enumerate() { + let base = format!("{path}/{index}"); + require_non_empty_string(hop, &format!("{base}/from"), errors); + require_non_empty_string(hop, &format!("{base}/to"), errors); + require_non_empty_string(hop, &format!("{base}/action"), errors); + require_non_empty_string(hop, &format!("{base}/at"), errors); + } +} + +fn non_empty_array<'a>( + value: Option<&'a Value>, + path: &str, + errors: &mut Vec, +) -> Option<&'a Vec> { + match value.and_then(Value::as_array) { + Some(array) if !array.is_empty() => Some(array), + Some(_) => { + errors.push(G004ConformanceError::new(path, "array must not be empty")); + None + } + None => { + errors.push(G004ConformanceError::new( + path, + "required array field missing", + )); + None + } + } +} + +fn require_string_eq( + root: &Value, + path: &str, + expected: &str, + errors: &mut Vec, +) { + match get_path(root, path).and_then(Value::as_str) { + Some(actual) if actual == expected => {} + Some(actual) => errors.push(G004ConformanceError::new( + path, + format!("expected '{expected}', got '{actual}'"), + )), + None => errors.push(G004ConformanceError::new( + path, + "required string field missing", + )), + } +} + +fn require_non_empty_string(root: &Value, path: &str, errors: &mut Vec) { + match get_path(root, path).and_then(Value::as_str) { + Some(value) if !value.trim().is_empty() => {} + Some(_) => errors.push(G004ConformanceError::new(path, "string must not be empty")), + None => errors.push(G004ConformanceError::new( + path, + "required string field missing", + )), + } +} + +fn require_one_of( + root: &Value, + path: &str, + allowed: &[&str], + errors: &mut Vec, +) { + match get_path(root, path).and_then(Value::as_str) { + Some(value) if allowed.contains(&value) => {} + Some(value) => errors.push(G004ConformanceError::new( + path, + format!("'{value}' is not one of {}", allowed.join(", ")), + )), + None => errors.push(G004ConformanceError::new( + path, + "required string field missing", + )), + } +} + +fn require_bool_true(root: &Value, path: &str, errors: &mut Vec) { + match get_path(root, path).and_then(Value::as_bool) { + Some(true) => {} + Some(false) => errors.push(G004ConformanceError::new(path, "must be true")), + None => errors.push(G004ConformanceError::new( + path, + "required boolean field missing", + )), + } +} + +fn is_terminal_event_value(value: Option<&Value>) -> bool { + matches!( + value.and_then(Value::as_str), + Some("lane.finished" | "lane.failed" | "lane.merged" | "lane.superseded" | "lane.closed") + ) +} + +fn get_path<'a>(root: &'a Value, path: &str) -> Option<&'a Value> { + root.pointer(path) +} diff --git a/rust/crates/runtime/src/lib.rs b/rust/crates/runtime/src/lib.rs index 2d18c744..2cc2aa02 100644 --- a/rust/crates/runtime/src/lib.rs +++ b/rust/crates/runtime/src/lib.rs @@ -13,6 +13,7 @@ mod config; pub mod config_validate; mod conversation; mod file_ops; +pub mod g004_conformance; mod git_context; pub mod green_contract; mod hooks; diff --git a/rust/crates/runtime/tests/fixtures/g004_contract_bundle.valid.json b/rust/crates/runtime/tests/fixtures/g004_contract_bundle.valid.json new file mode 100644 index 00000000..50ff0fb1 --- /dev/null +++ b/rust/crates/runtime/tests/fixtures/g004_contract_bundle.valid.json @@ -0,0 +1,81 @@ +{ + "schemaVersion": "g004.contract.bundle.v1", + "laneEvents": [ + { + "event": "lane.started", + "status": "running", + "emittedAt": "2026-05-14T00:00:00Z", + "metadata": { + "seq": 1, + "provenance": "live_lane", + "emitterIdentity": "worker-1", + "environmentLabel": "team-g004" + } + }, + { + "event": "lane.finished", + "status": "completed", + "emittedAt": "2026-05-14T00:00:10Z", + "metadata": { + "seq": 2, + "provenance": "live_lane", + "emitterIdentity": "worker-1", + "environmentLabel": "team-g004", + "eventFingerprint": "terminal-fp-001" + } + } + ], + "reports": [ + { + "schemaVersion": "g004.report.v1", + "reportId": "report-g004-fixture", + "identity": { "contentHash": "sha256:report-content" }, + "projection": { "provenance": "runtime.event_projection.v1" }, + "redaction": { "provenance": "runtime.redaction_policy.v1" }, + "consumerCapabilities": ["facts", "field_deltas", "redaction_provenance"], + "findings": [ + { + "kind": "fact", + "confidence": "high", + "statement": "lane event reached terminal state" + }, + { + "kind": "hypothesis", + "confidence": "medium", + "statement": "consumer can reconcile the terminal fingerprint" + }, + { + "kind": "negative_evidence", + "confidence": "high", + "statement": "no duplicate terminal event appears in this fixture" + } + ], + "fieldDeltas": [ + { + "field": "/laneEvents/1/status", + "previousHash": "sha256:running", + "currentHash": "sha256:completed", + "attribution": "worker-1 terminal reconciliation" + } + ] + } + ], + "approvalTokens": [ + { + "tokenId": "approval-token-fixture", + "owner": "leader-fixed", + "scope": "g004.contract.bundle.fixture", + "issuedAt": "2026-05-14T00:00:01Z", + "oneTimeUse": true, + "replayPreventionNonce": "nonce-fixture-001", + "delegationChain": [ + { + "from": "leader-fixed", + "to": "worker-3", + "action": "validate-g004-contract-fixture", + "at": "2026-05-14T00:00:02Z" + } + ] + } + ] +} diff --git a/rust/crates/runtime/tests/g004_conformance.rs b/rust/crates/runtime/tests/g004_conformance.rs new file mode 100644 index 00000000..c7343c70 --- /dev/null +++ b/rust/crates/runtime/tests/g004_conformance.rs @@ -0,0 +1,79 @@ +use runtime::g004_conformance::{ + is_g004_contract_bundle_valid, validate_g004_contract_bundle, +}; +use serde_json::{json, Value}; + +fn valid_bundle() -> Value { + serde_json::from_str(include_str!("fixtures/g004_contract_bundle.valid.json")) + .expect("valid fixture JSON should parse") +} + +#[test] +fn valid_g004_contract_bundle_fixture_passes_conformance() { + let fixture = valid_bundle(); + + let errors = validate_g004_contract_bundle(&fixture); + + assert!(errors.is_empty(), "unexpected conformance errors: {errors:?}"); + assert!(is_g004_contract_bundle_valid(&fixture)); +} + +#[test] +fn g004_conformance_reports_machine_readable_paths_for_contract_gaps() { + let invalid = json!({ + "schemaVersion": "g004.contract.bundle.v1", + "laneEvents": [ + { + "event": "lane.finished", + "status": "completed", + "emittedAt": "2026-05-14T00:00:10Z", + "metadata": { + "seq": 1, + "provenance": "live_lane", + "emitterIdentity": "worker-1", + "environmentLabel": "team-g004" + } + } + ], + "reports": [ + { + "schemaVersion": "g004.report.v1", + "reportId": "report-with-gaps", + "identity": { "contentHash": "sha256:report-content" }, + "projection": { "provenance": "runtime.event_projection.v1" }, + "redaction": { "provenance": "runtime.redaction_policy.v1" }, + "consumerCapabilities": [], + "findings": [ + { + "kind": "guess", + "confidence": "certain", + "statement": "bad labels should be rejected" + } + ], + "fieldDeltas": [] + } + ], + "approvalTokens": [ + { + "tokenId": "approval-token-fixture", + "owner": "leader-fixed", + "scope": "g004.contract.bundle.fixture", + "issuedAt": "2026-05-14T00:00:01Z", + "oneTimeUse": false, + "replayPreventionNonce": "nonce-fixture-001", + "delegationChain": [] + } + ] + }); + + let errors = validate_g004_contract_bundle(&invalid); + let paths: Vec<&str> = errors.iter().map(|error| error.path.as_str()).collect(); + + assert!(paths.contains(&"/laneEvents/0/metadata/eventFingerprint")); + assert!(paths.contains(&"/reports/0/consumerCapabilities")); + assert!(paths.contains(&"/reports/0/findings/0/kind")); + assert!(paths.contains(&"/reports/0/findings/0/confidence")); + assert!(paths.contains(&"/reports/0/fieldDeltas")); + assert!(paths.contains(&"/approvalTokens/0/oneTimeUse")); + assert!(paths.contains(&"/approvalTokens/0/delegationChain")); +}