mirror of
https://github.com/instructkr/claw-code.git
synced 2026-05-18 21:41:26 +08:00
omx(team): auto-checkpoint worker-3 [6]
This commit is contained in:
319
rust/crates/runtime/src/g004_conformance.rs
Normal file
319
rust/crates/runtime/src/g004_conformance.rs
Normal file
@@ -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<String>, message: impl Into<String>) -> 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<G004ConformanceError> {
|
||||
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<G004ConformanceError>,
|
||||
) {
|
||||
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<G004ConformanceError>) {
|
||||
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<G004ConformanceError>) {
|
||||
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<G004ConformanceError>,
|
||||
) {
|
||||
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<G004ConformanceError>,
|
||||
) {
|
||||
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<G004ConformanceError>,
|
||||
) {
|
||||
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<G004ConformanceError>,
|
||||
) -> Option<&'a Vec<Value>> {
|
||||
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<G004ConformanceError>,
|
||||
) {
|
||||
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<G004ConformanceError>) {
|
||||
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<G004ConformanceError>,
|
||||
) {
|
||||
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<G004ConformanceError>) {
|
||||
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)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user