diff --git a/CHANGELOG.md b/CHANGELOG.md index 9855c676..123a11c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,9 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve - **Rust Holmes validation gate review fixes**: Law evidence validation now continues artifact checks when structure validation emits warning-only diagnostics, rejects duplicate artifact roles after workspace-relative path - normalization, and validates artifact `sha256` digests with artifact-specific - diagnostics instead of allowing malformed digest anchors into later - traceability gates. + normalization, requires schema versions on every present artifact reference, + and validates artifact `sha256` digests with artifact-specific diagnostics + instead of allowing malformed digest anchors into later traceability gates. - **Rust Holmes assurance review fixes**: The new Holmes artifact locator now returns stable Holmes diagnostics for invalid and escaping paths, rejects platform-specific backslash and drive-path input before normalization, the diff --git a/crates/wesley-holmes/src/domain/evidence.rs b/crates/wesley-holmes/src/domain/evidence.rs index 255a06ad..ba345de8 100644 --- a/crates/wesley-holmes/src/domain/evidence.rs +++ b/crates/wesley-holmes/src/domain/evidence.rs @@ -13,7 +13,7 @@ use super::versioning::{ArtifactFamily, VersionRegistry}; pub struct ArtifactRef { /// Workspace-relative artifact path. pub path: String, - /// Optional artifact-local schema version. + /// Artifact-local schema version required for present artifact references. #[serde(skip_serializing_if = "Option::is_none")] pub schema_version: Option, /// Optional expected SHA-256 digest. @@ -31,6 +31,12 @@ impl ArtifactRef { } } + /// Attach an artifact-local schema version. + pub fn with_schema_version(mut self, schema_version: impl Into) -> Self { + self.schema_version = Some(schema_version.into()); + self + } + fn is_blank(&self) -> bool { self.path.trim().is_empty() } @@ -335,17 +341,17 @@ impl HolmesLawEvidenceBundle { ); } - if let Some(schema_version) = artifact.schema_version.as_deref() { - let schema_field = format!("{}.schemaVersion", bundle_artifact.field_path); - match version_registry.classify(bundle_artifact.family, Some(schema_version)) { - Ok(version_check) => diagnostics.extend( - version_check - .diagnostics - .into_iter() - .map(|diagnostic| diagnostic.at_field(schema_field.clone())), - ), - Err(diagnostic) => diagnostics.push(diagnostic.at_field(schema_field)), - } + let schema_field = format!("{}.schemaVersion", bundle_artifact.field_path); + match version_registry + .classify(bundle_artifact.family, artifact.schema_version.as_deref()) + { + Ok(version_check) => diagnostics.extend( + version_check + .diagnostics + .into_iter() + .map(|diagnostic| diagnostic.at_field(schema_field.clone())), + ), + Err(diagnostic) => diagnostics.push(diagnostic.at_field(schema_field)), } if let Some(sha256) = artifact.sha256.as_deref() { diff --git a/crates/wesley-holmes/tests/foundation.rs b/crates/wesley-holmes/tests/foundation.rs index 2fd588fc..770cefaf 100644 --- a/crates/wesley-holmes/tests/foundation.rs +++ b/crates/wesley-holmes/tests/foundation.rs @@ -223,6 +223,35 @@ fn artifact_sha256_fields_must_be_canonical_when_present() { ); } +#[test] +fn artifact_schema_versions_are_required_for_every_present_artifact_reference() { + let mut bundle = valid_evidence_bundle(); + bundle.artifacts.law_diff.schema_version = None; + bundle.artifacts.law_coverage.schema_version = None; + bundle.artifacts.law_capabilities.schema_version = None; + bundle.artifacts.contract_bundle_manifest.schema_version = None; + bundle.artifacts.policy = Some(ArtifactRef::new("evidence/policy.json")); + + let result = bundle.validate_structure(&VersionRegistry::default()); + + assert_eq!(result.status, LawEvidenceValidationStatus::Invalid); + assert_diagnostic( + &result.diagnostics, + HolmesDiagnosticCode::HlawSchemaVersionMissing, + ); + assert_diagnostic_field(&result.diagnostics, "artifacts.lawDiff.schemaVersion"); + assert_diagnostic_field(&result.diagnostics, "artifacts.lawCoverage.schemaVersion"); + assert_diagnostic_field( + &result.diagnostics, + "artifacts.lawCapabilities.schemaVersion", + ); + assert_diagnostic_field( + &result.diagnostics, + "artifacts.contractBundleManifest.schemaVersion", + ); + assert_diagnostic_field(&result.diagnostics, "artifacts.policy.schemaVersion"); +} + #[test] fn law_evidence_validator_reports_artifact_availability_size_and_read_errors() { let bundle = valid_evidence_bundle(); @@ -505,10 +534,10 @@ fn valid_evidence_bundle() -> HolmesLawEvidenceBundle { schema_version: "1.0.0".to_owned(), bundle_id: "bundle-001".to_owned(), artifacts: LawEvidenceArtifacts { - law_diff: ArtifactRef::new("evidence/law-diff.json"), - law_coverage: ArtifactRef::new("evidence/law-coverage.json"), - law_capabilities: ArtifactRef::new("evidence/law-capabilities.json"), - contract_bundle_manifest: ArtifactRef::new("evidence/bundle-manifest.json"), + law_diff: artifact_ref("evidence/law-diff.json"), + law_coverage: artifact_ref("evidence/law-coverage.json"), + law_capabilities: artifact_ref("evidence/law-capabilities.json"), + contract_bundle_manifest: artifact_ref("evidence/bundle-manifest.json"), policy: None, report: None, witness: None, @@ -523,6 +552,10 @@ fn valid_evidence_bundle() -> HolmesLawEvidenceBundle { } } +fn artifact_ref(path: &str) -> ArtifactRef { + ArtifactRef::new(path).with_schema_version("1.0.0") +} + fn assert_diagnostic(diagnostics: &[wesley_holmes::HolmesDiagnostic], code: HolmesDiagnosticCode) { assert!( diagnostics.iter().any(|diagnostic| diagnostic.code == code),