Skip to content

Commit 41875c7

Browse files
feat(evidence): produce the evidence manifest instead of only reading it (#10561)
EvidenceManifest had a reader, a consumer, and no producer: every `runs evidence` report advertised an `evidence_manifest` member that was structurally always absent, because nothing in the repository ever built one. - Derive a manifest from the run record when no producer attached one, with conservative status mapping (an unowned status label is `unknown`, a pass carrying a critical blocker is `blocked`) and a reviewability-graded confidence. - Add `EvidenceManifest.source`, stamped by the reader from the resolution path, so an authored assertion is never confused with a derived reading. Additive and optional; the type has no `deny_unknown_fields`. - Fail closed on structurally empty manifests via `EvidenceManifest::validate`, reported through `evidence_manifest_errors` rather than published as an empty judgement. - Register `homeboy/evidence-manifest/v1` in the contract registry and wire `homeboy contract validate` so an external producer can discover the schema and check a candidate before attaching it. Refs #10306 Co-authored-by: chubes-bot <266378653+homeboy-ci[bot]@users.noreply.github.com>
1 parent e07cbb1 commit 41875c7

8 files changed

Lines changed: 1034 additions & 35 deletions

File tree

crates/homeboy-cli/src/command_contract/registry.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,14 @@ pub const CONTRACT_REGISTRY: &[ContractRegistryEntry] = &[
186186
summary: "Captures the selected agent runtime provider, materialization, secrets, readiness, and capabilities.",
187187
rust_type: "homeboy::agents::agent_task_provider::ResolvedAgentRuntimeExecutionContract",
188188
},
189+
ContractRegistryEntry {
190+
schema_id: crate::core::evidence_manifest::EVIDENCE_MANIFEST_SCHEMA,
191+
name: "evidence-manifest",
192+
title: "Evidence manifest",
193+
owner: "homeboy-core",
194+
summary: "Portable interpretation of a body of evidence: state, summary, confidence, blocking conditions, and tracker/pull-request/run/artifact references.",
195+
rust_type: "homeboy::core::evidence_manifest::EvidenceManifest",
196+
},
189197
ContractRegistryEntry {
190198
schema_id: crate::core::artifact_ref::ARTIFACT_REF_SCHEMA,
191199
name: "reviewer-facing-artifact-ref",

crates/homeboy-cli/src/commands/contract/mod.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use crate::command_contract::{
2121
use crate::commands::{adapter, CmdResult};
2222
use crate::core::artifact_ref::{validate_reviewer_facing_artifact_ref, ArtifactReference};
2323
use crate::core::artifacts::{validate_artifact_postprocess_plan, ArtifactManifest};
24+
use crate::core::evidence_manifest::{EvidenceManifest, EVIDENCE_MANIFEST_SCHEMA};
2425
use crate::core::host_mutation_lifecycle::{
2526
HostMutationLifecycle, HostMutationRevertStrategy, HostMutationStatus,
2627
HOST_MUTATION_LIFECYCLE_SCHEMA,
@@ -1291,6 +1292,7 @@ static CONTRACT_SCHEMAS: &[ContractSchema] = &[
12911292
contract_schema!(RUNNER_EXECUTION_RECORD_SCHEMA, RunnerExecutionRecord),
12921293
contract_schema!(PATH_MATERIALIZATION_PLAN_SCHEMA, PathMaterializationPlan),
12931294
contract_schema!(RUN_OUTCOME_ENVELOPE_SCHEMA, RunOutcomeEnvelope),
1295+
contract_schema!(EVIDENCE_MANIFEST_SCHEMA, EvidenceManifest),
12941296
];
12951297

12961298
trait ContractValidation: DeserializeOwned {
@@ -1333,6 +1335,7 @@ impl_contract_validation! {
13331335
ResourceCleanupIntentContract => RESOURCE_CLEANUP_INTENT_SCHEMA, |value| value.validate();
13341336
ResourceLifecycleIndex => RESOURCE_LIFECYCLE_INDEX_SCHEMA, |value| value.validate();
13351337
HostMutationLifecycle => HOST_MUTATION_LIFECYCLE_SCHEMA, |value| value.validate();
1338+
EvidenceManifest => EVIDENCE_MANIFEST_SCHEMA, |value| validate_evidence_manifest(&value);
13361339
Value => FUZZ_WORKLOAD_SCHEMA, |value| {
13371340
FuzzWorkload::from_value(value).map_err(|message| {
13381341
homeboy::core::Error::new(
@@ -1349,6 +1352,24 @@ impl_contract_validation! {
13491352
};
13501353
}
13511354

1355+
/// Bridge the evidence manifest's own structural validation into the shared
1356+
/// contract-validation error envelope, so a producer can check a manifest with
1357+
/// `homeboy contract validate` before attaching it to a run instead of finding
1358+
/// out from `evidence_manifest_errors` after the fact.
1359+
fn validate_evidence_manifest(manifest: &EvidenceManifest) -> homeboy::core::Result<()> {
1360+
manifest.validate().map_err(|message| {
1361+
homeboy::core::Error::new(
1362+
homeboy::core::ErrorCode::ValidationInvalidArgument,
1363+
"Contract validation failed",
1364+
serde_json::json!({
1365+
"schema": EVIDENCE_MANIFEST_SCHEMA,
1366+
"valid": false,
1367+
"error": message,
1368+
}),
1369+
)
1370+
})
1371+
}
1372+
13521373
fn deserialize_contract<T: DeserializeOwned>(
13531374
raw: &str,
13541375
schema_id: &'static str,
@@ -1465,6 +1486,63 @@ mod tests {
14651486
})
14661487
}
14671488

1489+
/// The evidence manifest is an inbound contract: a producer outside this
1490+
/// repository attaches one to a run. That is only usable if the producer can
1491+
/// discover the schema and check a candidate before attaching it.
1492+
#[test]
1493+
fn evidence_manifest_is_discoverable_and_validatable() {
1494+
let contract =
1495+
registered_contract("evidence-manifest").expect("evidence manifest registry entry");
1496+
assert_eq!(contract.schema_id, EVIDENCE_MANIFEST_SCHEMA);
1497+
1498+
let dir = TempDir::new().unwrap();
1499+
let file = write_json(
1500+
&dir,
1501+
"manifest.json",
1502+
json!({
1503+
"schema": EVIDENCE_MANIFEST_SCHEMA,
1504+
"status": { "state": "blocked" },
1505+
"interpretation": {
1506+
"summary": "One scenario regressed.",
1507+
"confidence": "medium"
1508+
},
1509+
"blocking_conditions": [{
1510+
"kind": "coverage_gap",
1511+
"summary": "Missing scenario.",
1512+
"severity": "warning"
1513+
}]
1514+
}),
1515+
);
1516+
1517+
let output = validate_file(EVIDENCE_MANIFEST_SCHEMA, file).expect("valid manifest");
1518+
assert!(output.valid);
1519+
}
1520+
1521+
#[test]
1522+
fn evidence_manifest_validation_rejects_an_empty_interpretation() {
1523+
let dir = TempDir::new().unwrap();
1524+
let file = write_json(
1525+
&dir,
1526+
"manifest.json",
1527+
json!({
1528+
"schema": EVIDENCE_MANIFEST_SCHEMA,
1529+
"status": { "state": "passed" },
1530+
"interpretation": { "summary": "" }
1531+
}),
1532+
);
1533+
1534+
let err = validate_file(EVIDENCE_MANIFEST_SCHEMA, file).expect_err("invalid manifest");
1535+
assert_eq!(err.details["valid"], json!(false));
1536+
assert!(
1537+
err.details["error"]
1538+
.as_str()
1539+
.expect("error detail")
1540+
.contains("interpretation.summary"),
1541+
"{:?}",
1542+
err.details
1543+
);
1544+
}
1545+
14681546
#[test]
14691547
fn command_registry_export_covers_contract_command() {
14701548
let export = command_registry_export();

crates/homeboy-cli/src/commands/runs/evidence.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,16 @@ mod tests {
356356
assert_eq!(manifest.schema, "homeboy/evidence-manifest/v1");
357357
assert_eq!(manifest.tracker_refs[0].id, "Extra-Chill/homeboy#123");
358358
assert_eq!(manifest.blocking_conditions[0].kind, "review_needed");
359+
// An attached manifest is surfaced verbatim, stamped with where it
360+
// was found rather than replaced by a derived reading.
361+
assert_eq!(
362+
manifest.source,
363+
Some(homeboy::core::evidence_manifest::EvidenceManifestSource::RunMetadata)
364+
);
365+
assert_eq!(
366+
manifest.interpretation.summary,
367+
"Evidence is blocked on reviewer confirmation."
368+
);
359369
assert!(output.evidence_manifest_errors.is_empty());
360370
let lifecycle_event = output
361371
.agent_task_lifecycle_event
@@ -372,6 +382,65 @@ mod tests {
372382
});
373383
}
374384

385+
/// Before this wiring the manifest member was structurally always absent:
386+
/// nothing in the repository produced one, so `runs evidence` advertised an
387+
/// interpretation layer it never populated. A run nobody attached a manifest
388+
/// to must still carry one, marked as Homeboy's own reading.
389+
#[test]
390+
fn evidence_command_derives_a_manifest_when_no_producer_attached_one() {
391+
with_isolated_home(|home| {
392+
let _xdg = XdgGuard::unset();
393+
let _public_artifact_base = EnvGuard::unset(PUBLIC_ARTIFACT_BASE_URL_ENV);
394+
let artifact_root = home.path().join("agent-readable-artifacts");
395+
homeboy::core::set_artifact_root_override(Some(artifact_root));
396+
let store = ObservationStore::open_initialized().expect("store");
397+
let run = store
398+
.start_run(sample_run(
399+
"bench",
400+
"homeboy",
401+
"studio",
402+
serde_json::json!({ "gate_failures": ["p95_ms exceeded"] }),
403+
))
404+
.expect("run");
405+
store
406+
.finish_run(&run.id, RunStatus::Fail, None)
407+
.expect("finish run");
408+
409+
let (output, _) = evidence(&run.id).expect("evidence");
410+
let RunsOutput::Evidence(output) = output else {
411+
panic!("expected evidence output");
412+
};
413+
414+
let manifest = output.evidence_manifest.expect("derived evidence manifest");
415+
manifest
416+
.validate()
417+
.expect("derived manifest is contract-valid");
418+
assert_eq!(
419+
manifest.source,
420+
Some(homeboy::core::evidence_manifest::EvidenceManifestSource::Derived)
421+
);
422+
assert_eq!(
423+
manifest.status.state,
424+
homeboy::core::evidence_manifest::EvidenceManifestState::Failed
425+
);
426+
assert_eq!(manifest.id.as_deref(), Some(run.id.as_str()));
427+
assert_eq!(manifest.run_refs[0].id, run.id);
428+
assert_eq!(manifest.run_refs[0].kind.as_deref(), Some("bench"));
429+
assert_eq!(
430+
manifest.run_refs[0].component_id.as_deref(),
431+
Some("homeboy")
432+
);
433+
let gate = manifest
434+
.blocking_conditions
435+
.iter()
436+
.find(|condition| condition.kind == "gate_failure")
437+
.expect("gate failure blocker");
438+
assert_eq!(gate.summary, "p95_ms exceeded");
439+
assert!(output.evidence_manifest_errors.is_empty());
440+
homeboy::core::set_artifact_root_override(None);
441+
});
442+
}
443+
375444
#[test]
376445
fn evidence_command_surfaces_static_html_preview_entrypoints() {
377446
with_isolated_home(|home| {

0 commit comments

Comments
 (0)