From 462b90c93155f388d7f5e432c93be3a8bf4834f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 23:25:17 +0900 Subject: [PATCH 1/2] feat(validation): exact-head claim promotion gates Refuse implemented-main, scientific, and release promotions from queued, predecessor, skipped, or LLM evidence. Scientific promotion uses computed RMSE and its standard error rather than a hardcoded threshold. --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + crates/validation_core/Cargo.toml | 2 +- crates/validation_core/src/claim.rs | 405 ++++++++++++++++++ crates/validation_core/src/error.rs | 49 +++ crates/validation_core/src/lib.rs | 23 +- .../tests/claim_promotion_contract.rs | 246 +++++++++++ docs/TRACEABILITY.md | 2 +- ...ic-claim-promotion-and-release-evidence.md | 2 +- docs/adr/README.md | 2 +- .../scientific-claim-promotion-gates.md | 31 ++ docs/validation/temporal-event-foundation.md | 1 + 12 files changed, 759 insertions(+), 7 deletions(-) create mode 100644 crates/validation_core/src/claim.rs create mode 100644 crates/validation_core/tests/claim_promotion_contract.rs create mode 100644 docs/research/scientific-claim-promotion-gates.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ffe514db..f4cc0a6f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -59,7 +59,7 @@ boundaries above remain the target modular MSA architecture. | `persistence_postgres` | PostgreSQL repositories and migrations | | `corpus_split` | cutoff-safe, relation-aware partitioning | | `tepp_simulation` | known-truth temporal/event data generation | -| `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | +| `validation_core` | RMSE, bias, coverage, graph, Monte Carlo, and exact-head claim-promotion metrics | | `tepp_api` | versioned DTO, schema, and export contracts | No crate exposes placeholder production behavior in Task 1. This prevents an diff --git a/CHANGELOG.md b/CHANGELOG.md index 9abfea7e..0ef2825e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `validation_core` ADR 0014 claim-promotion gates: `decision_accepted`, `implemented_main`, `scientifically_supported`, and `released` bind to an exact commit SHA; queued, predecessor, skipped-required, and LLM evidence fail closed; scientific promotion uses computed RMSE and its standard error rather than a hardcoded threshold (no new migration). - `persistence_postgres` typed membership assignment (migration `0006`): `entity_record`, `project_record`, and `text_segment` plus exactly-one observed-unit and target constraints that replace the polymorphic `membership_target_id` stub, with SQL insert/lookup, fail-closed inverted-window and backslash-label refusal, and live proof that one document persists two entity memberships and one project membership. - Actions workflow fleet auditor (`scripts/actions_workflow_fleet.py`): paginated registry inventory bound to the exact default-branch SHA/tree, classification of present/orphan/disabled/GitHub-dynamic identities, and fail-closed orphan disable that confirms GitHub's official `disabled_manually` state. - `persistence_postgres` temporal interval ordering migration (`0005`): multi-word CHECK constraints on `document_record`, `event_instance`, and `membership_assignment` that reject inverted valid/system windows and non-positive document revisions while preserving open-ended NULL upper bounds and equal point bounds; catalog validation and live inverted-window proof. diff --git a/crates/validation_core/Cargo.toml b/crates/validation_core/Cargo.toml index 5f718f8e..9824f91a 100644 --- a/crates/validation_core/Cargo.toml +++ b/crates/validation_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "validation_core" -description = "Recovery, calibration, graph, and Monte Carlo validation metrics." +description = "Recovery, calibration, graph, Monte Carlo, and exact-head claim-promotion metrics." version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/validation_core/src/claim.rs b/crates/validation_core/src/claim.rs new file mode 100644 index 00000000..1d98343f --- /dev/null +++ b/crates/validation_core/src/claim.rs @@ -0,0 +1,405 @@ +//! Exact-head claim promotion gates for ADR 0014 authorities. + +use crate::ValidationError; +use crate::accept_within_standard_errors; +use crate::rmse_standard_error; +use crate::root_mean_square_error; + +/// Four claim authorities separated by ADR 0014. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum ClaimAuthority { + /// Accepted PRD/ADR design authority. + DecisionAccepted, + /// Source integrated on the exact protected head with passing tests. + ImplementedMain, + /// Implementation plus claim-specific computed recovery evidence. + ScientificallySupported, + /// One exact protected head satisfying every release gate together. + Released, +} + +impl ClaimAuthority { + /// Stable wire name for this authority. + #[must_use] + pub const fn wire_name(self) -> &'static str { + match self { + Self::DecisionAccepted => "decision_accepted", + Self::ImplementedMain => "implemented_main", + Self::ScientificallySupported => "scientifically_supported", + Self::Released => "released", + } + } + + fn required_kinds(self) -> &'static [ClaimEvidenceKind] { + match self { + Self::DecisionAccepted => &[], + Self::ImplementedMain => &[ClaimEvidenceKind::ExactHeadTests], + Self::ScientificallySupported => &[ + ClaimEvidenceKind::ExactHeadTests, + ClaimEvidenceKind::ScientificRecovery, + ], + Self::Released => &[ + ClaimEvidenceKind::ExactHeadTests, + ClaimEvidenceKind::ScientificRecovery, + ClaimEvidenceKind::SecuritySupplyChain, + ClaimEvidenceKind::QualifyingReview, + ClaimEvidenceKind::OperationalReadiness, + ClaimEvidenceKind::SbomProvenance, + ], + } + } +} + +/// Kind of evidence offered for a promotion request. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum ClaimEvidenceKind { + /// Exact-head unit/integration tests on the candidate commit. + ExactHeadTests, + /// Claim-specific recovery or calibration evidence. + ScientificRecovery, + /// Security and supply-chain gates on the same head. + SecuritySupplyChain, + /// Qualifying independent review, not self-approval. + QualifyingReview, + /// Operational readiness on the same head. + OperationalReadiness, + /// SBOM and provenance bound to the same head. + SbomProvenance, + /// A queued or in-progress check. + QueuedCheck, + /// Evidence collected on a predecessor or other commit. + PredecessorHead, + /// Model or LLM narrative treated as authority. + LlmJudgment, + /// A required test that was skipped or ignored. + SkippedRequired, +} + +impl ClaimEvidenceKind { + /// Stable wire name for this evidence kind. + #[must_use] + pub const fn wire_name(self) -> &'static str { + match self { + Self::ExactHeadTests => "exact_head_tests", + Self::ScientificRecovery => "scientific_recovery", + Self::SecuritySupplyChain => "security_supply_chain", + Self::QualifyingReview => "qualifying_review", + Self::OperationalReadiness => "operational_readiness", + Self::SbomProvenance => "sbom_provenance", + Self::QueuedCheck => "queued_check", + Self::PredecessorHead => "predecessor_head", + Self::LlmJudgment => "llm_judgment", + Self::SkippedRequired => "skipped_required", + } + } + + /// Whether this kind may ever promote a claim. + #[must_use] + pub const fn is_promotable(self) -> bool { + !matches!( + self, + Self::QueuedCheck | Self::PredecessorHead | Self::LlmJudgment | Self::SkippedRequired + ) + } +} + +/// One evidence item offered for promotion. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ClaimEvidence { + kind: ClaimEvidenceKind, + passed: bool, +} + +impl ClaimEvidence { + /// Construct one evidence item. + #[must_use] + pub const fn new(kind: ClaimEvidenceKind, passed: bool) -> Self { + Self { kind, passed } + } + + /// Return the evidence kind. + #[must_use] + pub const fn kind(self) -> ClaimEvidenceKind { + self.kind + } + + /// Return whether the presented evidence is marked passing. + #[must_use] + pub const fn passed(self) -> bool { + self.passed + } +} + +/// A request to promote one claim authority on a candidate head. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PromotionRequest<'evidence> { + target: ClaimAuthority, + candidate_head: [u8; 20], + protected_head: [u8; 20], + evidence: &'evidence [ClaimEvidence], +} + +impl<'evidence> PromotionRequest<'evidence> { + /// Parse commit identities and bind the offered evidence. + /// + /// # Errors + /// + /// Returns [`ValidationError::InvalidInput`] when either head is not a + /// forty-character hexadecimal Git commit SHA. + pub fn new( + target: ClaimAuthority, + candidate_head: &str, + protected_head: &str, + evidence: &'evidence [ClaimEvidence], + ) -> Result { + Ok(Self { + target, + candidate_head: parse_commit_head(candidate_head)?, + protected_head: parse_commit_head(protected_head)?, + evidence, + }) + } + + /// Requested claim authority. + #[must_use] + pub const fn target(self) -> ClaimAuthority { + self.target + } + + /// Candidate commit identity. + #[must_use] + pub const fn candidate_head(self) -> [u8; 20] { + self.candidate_head + } + + /// Protected-main commit identity. + #[must_use] + pub const fn protected_head(self) -> [u8; 20] { + self.protected_head + } + + /// Offered evidence slice. + #[must_use] + pub const fn evidence(self) -> &'evidence [ClaimEvidence] { + self.evidence + } +} + +/// A claim that passed every required exact-head gate. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PromotedClaim { + authority: ClaimAuthority, + bound_head: [u8; 20], +} + +impl PromotedClaim { + /// Bind a promoted authority to one commit identity. + #[must_use] + pub const fn new(authority: ClaimAuthority, bound_head: [u8; 20]) -> Self { + Self { + authority, + bound_head, + } + } + + /// Promoted authority. + #[must_use] + pub const fn authority(self) -> ClaimAuthority { + self.authority + } + + /// Exact commit the promotion is bound to. + #[must_use] + pub const fn bound_head(self) -> [u8; 20] { + self.bound_head + } +} + +/// Parse a forty-character hexadecimal Git commit SHA. +/// +/// # Errors +/// +/// Returns [`ValidationError::InvalidInput`] when the value is not exactly +/// forty hexadecimal characters. +pub fn parse_commit_head(value: &str) -> Result<[u8; 20], ValidationError> { + let bytes = value.as_bytes(); + if bytes.len() != 40 { + return Err(ValidationError::InvalidInput); + } + let mut decoded = [0_u8; 20]; + for (index, pair) in bytes.chunks_exact(2).enumerate() { + decoded[index] = (hex_nibble(pair[0])? << 4) | hex_nibble(pair[1])?; + } + Ok(decoded) +} + +fn hex_nibble(value: u8) -> Result { + match value { + b'0'..=b'9' => Ok(value - b'0'), + b'a'..=b'f' => Ok(value - b'a' + 10), + b'A'..=b'F' => Ok(value - b'A' + 10), + _ => Err(ValidationError::InvalidInput), + } +} + +/// Promote a claim only when exact-head evidence satisfies ADR 0014. +/// +/// Design authority may bind a non-protected head. Implementation, scientific, +/// and release authorities require the candidate to equal the protected head +/// and every required gate to be present and passing. Queued, predecessor, +/// skipped-required, and LLM evidence fail closed. +/// +/// # Errors +/// +/// Returns a claim-specific [`ValidationError`] when heads differ, required +/// evidence is missing, or unusable evidence is present. +pub fn promote_claim(request: &PromotionRequest<'_>) -> Result { + for item in request.evidence { + match item.kind { + ClaimEvidenceKind::QueuedCheck => { + return Err(ValidationError::ClaimQueuedEvidence); + } + ClaimEvidenceKind::PredecessorHead => { + return Err(ValidationError::ClaimPredecessorHead); + } + ClaimEvidenceKind::LlmJudgment => { + return Err(ValidationError::ClaimLlmJudgment); + } + ClaimEvidenceKind::SkippedRequired => { + return Err(ValidationError::ClaimSkippedRequired); + } + ClaimEvidenceKind::ExactHeadTests + | ClaimEvidenceKind::ScientificRecovery + | ClaimEvidenceKind::SecuritySupplyChain + | ClaimEvidenceKind::QualifyingReview + | ClaimEvidenceKind::OperationalReadiness + | ClaimEvidenceKind::SbomProvenance => {} + } + } + if request.target != ClaimAuthority::DecisionAccepted + && request.candidate_head != request.protected_head + { + return Err(ValidationError::ClaimHeadMismatch); + } + for required in request.target.required_kinds() { + let present = request + .evidence + .iter() + .any(|item| item.kind == *required && item.passed); + if !present { + return Err(ValidationError::ClaimEvidenceMissing); + } + } + Ok(PromotedClaim::new(request.target, request.candidate_head)) +} + +/// Promote a scientific claim from computed RMSE, not a hardcoded threshold. +/// +/// The candidate must equal the protected head. RMSE is accepted only when it +/// lies within `se_multiplier` standard errors of exact recovery. +/// +/// # Errors +/// +/// Returns head, input, configuration, or recovery-rejection errors. +pub fn promote_scientific_recovery( + candidate_head: &str, + protected_head: &str, + truth: &[f64], + recovered: &[f64], + se_multiplier: f64, +) -> Result { + let candidate = parse_commit_head(candidate_head)?; + let protected = parse_commit_head(protected_head)?; + if candidate != protected { + return Err(ValidationError::ClaimHeadMismatch); + } + let rmse = root_mean_square_error(truth, recovered)?; + let rmse_se = rmse_standard_error(truth, recovered)?; + if !accept_within_standard_errors(rmse, 0.0, rmse_se, se_multiplier)? { + return Err(ValidationError::ClaimRecoveryRejected); + } + Ok(PromotedClaim::new( + ClaimAuthority::ScientificallySupported, + candidate, + )) +} + +#[cfg(test)] +mod tests { + use super::{ + ClaimAuthority, ClaimEvidence, ClaimEvidenceKind, PromotedClaim, PromotionRequest, + parse_commit_head, promote_claim, promote_scientific_recovery, + }; + use crate::ValidationError; + + const HEAD: &str = "0123456789abcdef0123456789abcdef01234567"; + + #[test] + fn wire_names_and_accessors_cover_every_variant() { + assert_eq!( + ClaimAuthority::ImplementedMain.wire_name(), + "implemented_main" + ); + assert_eq!( + ClaimAuthority::ScientificallySupported.wire_name(), + "scientifically_supported" + ); + for kind in [ + ClaimEvidenceKind::ExactHeadTests, + ClaimEvidenceKind::ScientificRecovery, + ClaimEvidenceKind::SecuritySupplyChain, + ClaimEvidenceKind::QualifyingReview, + ClaimEvidenceKind::OperationalReadiness, + ClaimEvidenceKind::SbomProvenance, + ClaimEvidenceKind::QueuedCheck, + ClaimEvidenceKind::PredecessorHead, + ClaimEvidenceKind::LlmJudgment, + ClaimEvidenceKind::SkippedRequired, + ] { + assert!(!kind.wire_name().is_empty()); + assert_eq!( + kind.is_promotable(), + !matches!( + kind, + ClaimEvidenceKind::QueuedCheck + | ClaimEvidenceKind::PredecessorHead + | ClaimEvidenceKind::LlmJudgment + | ClaimEvidenceKind::SkippedRequired + ) + ); + } + let evidence = ClaimEvidence::new(ClaimEvidenceKind::ExactHeadTests, true); + assert_eq!(evidence.kind(), ClaimEvidenceKind::ExactHeadTests); + assert!(evidence.passed()); + let evidence_row = [evidence]; + let request = + PromotionRequest::new(ClaimAuthority::DecisionAccepted, HEAD, HEAD, &evidence_row) + .expect("request"); + assert_eq!(request.target(), ClaimAuthority::DecisionAccepted); + assert_eq!(request.candidate_head(), parse_commit_head(HEAD).unwrap()); + assert_eq!(request.protected_head(), parse_commit_head(HEAD).unwrap()); + assert_eq!(request.evidence(), evidence_row.as_slice()); + let promoted = + PromotedClaim::new(ClaimAuthority::DecisionAccepted, request.candidate_head()); + assert_eq!(promoted.authority(), ClaimAuthority::DecisionAccepted); + assert_eq!( + parse_commit_head("0123456789abcdef0123456789abcdef0123456g"), + Err(ValidationError::InvalidInput) + ); + assert_eq!( + promote_scientific_recovery(HEAD, HEAD, &[1.0, 2.0], &[1.0, 2.0], -1.0), + Err(ValidationError::InvalidConfiguration) + ); + let extra = [ + ClaimEvidence::new(ClaimEvidenceKind::SecuritySupplyChain, true), + ClaimEvidence::new(ClaimEvidenceKind::ExactHeadTests, true), + ]; + let extra_request = + PromotionRequest::new(ClaimAuthority::ImplementedMain, HEAD, HEAD, &extra) + .expect("extra"); + assert_eq!( + promote_claim(&extra_request).expect("ok").authority(), + ClaimAuthority::ImplementedMain + ); + } +} diff --git a/crates/validation_core/src/error.rs b/crates/validation_core/src/error.rs index 89c2563a..3eae5630 100644 --- a/crates/validation_core/src/error.rs +++ b/crates/validation_core/src/error.rs @@ -10,6 +10,20 @@ pub enum ValidationError { InvalidInput, /// Acceptance thresholds or Monte Carlo settings were inconsistent. InvalidConfiguration, + /// Candidate and protected heads are not the same exact commit. + ClaimHeadMismatch, + /// A required exact-head gate is absent or failed. + ClaimEvidenceMissing, + /// A queued or in-progress check was treated as passing evidence. + ClaimQueuedEvidence, + /// Predecessor-head or stale evidence was treated as current-head proof. + ClaimPredecessorHead, + /// An LLM judgment was treated as scientific or implementation authority. + ClaimLlmJudgment, + /// A skipped required test was treated as passing evidence. + ClaimSkippedRequired, + /// Computed recovery did not fall within the configured SE gate. + ClaimRecoveryRejected, } impl fmt::Display for ValidationError { @@ -17,6 +31,13 @@ impl fmt::Display for ValidationError { let message = match self { Self::InvalidInput => "invalid validation input", Self::InvalidConfiguration => "invalid validation configuration", + Self::ClaimHeadMismatch => "claim candidate head is not the protected head", + Self::ClaimEvidenceMissing => "required claim evidence is missing", + Self::ClaimQueuedEvidence => "queued checks cannot promote a claim", + Self::ClaimPredecessorHead => "predecessor-head evidence cannot promote a claim", + Self::ClaimLlmJudgment => "llm judgment cannot promote a claim", + Self::ClaimSkippedRequired => "skipped required tests cannot promote a claim", + Self::ClaimRecoveryRejected => "computed recovery does not support the claim", }; formatter.write_str(message) } @@ -38,5 +59,33 @@ mod tests { ValidationError::InvalidConfiguration.to_string(), "invalid validation configuration" ); + assert_eq!( + ValidationError::ClaimHeadMismatch.to_string(), + "claim candidate head is not the protected head" + ); + assert_eq!( + ValidationError::ClaimEvidenceMissing.to_string(), + "required claim evidence is missing" + ); + assert_eq!( + ValidationError::ClaimQueuedEvidence.to_string(), + "queued checks cannot promote a claim" + ); + assert_eq!( + ValidationError::ClaimPredecessorHead.to_string(), + "predecessor-head evidence cannot promote a claim" + ); + assert_eq!( + ValidationError::ClaimLlmJudgment.to_string(), + "llm judgment cannot promote a claim" + ); + assert_eq!( + ValidationError::ClaimSkippedRequired.to_string(), + "skipped required tests cannot promote a claim" + ); + assert_eq!( + ValidationError::ClaimRecoveryRejected.to_string(), + "computed recovery does not support the claim" + ); } } diff --git a/crates/validation_core/src/lib.rs b/crates/validation_core/src/lib.rs index cdd48fe7..8c4637fb 100644 --- a/crates/validation_core/src/lib.rs +++ b/crates/validation_core/src/lib.rs @@ -3,14 +3,17 @@ // Recovery metrics intentionally cast small finite sample sizes to `f64`. #![allow(clippy::cast_precision_loss)] #![allow(clippy::cast_sign_loss)] -//! Recovery, calibration, graph, and Monte Carlo validation metrics. +//! Recovery, calibration, graph, Monte Carlo, and claim-promotion metrics. //! //! TEPP scientific acceptance requires realistic synthetic truth recovery: //! parameter match counts, RMSE, bias, interval coverage with Wilson bounds, //! temporal-order accuracy, relation precision/recall, and SE-aware Monte Carlo -//! acceptance gates. Metrics are pure `f64` CPU reference implementations. +//! acceptance gates. ADR 0014 claim authorities are promoted only by exact-head +//! evidence; queued, predecessor, skipped, and LLM judgments fail closed. +//! Metrics are pure `f64` CPU reference implementations. mod bias; +mod claim; mod coverage; mod error; mod graph_metrics; @@ -25,6 +28,22 @@ mod temporal_order; pub use bias::bias_standard_error; /// Mean signed bias. pub use bias::mean_bias; +/// Four ADR 0014 claim authorities. +pub use claim::ClaimAuthority; +/// One evidence item offered for promotion. +pub use claim::ClaimEvidence; +/// Kind of evidence offered for a promotion request. +pub use claim::ClaimEvidenceKind; +/// A claim bound to one exact commit after every required gate passed. +pub use claim::PromotedClaim; +/// Exact-head promotion request. +pub use claim::PromotionRequest; +/// Parse a forty-character hexadecimal Git commit SHA. +pub use claim::parse_commit_head; +/// Promote a claim only when exact-head evidence satisfies ADR 0014. +pub use claim::promote_claim; +/// Promote a scientific claim from computed RMSE, not a hardcoded threshold. +pub use claim::promote_scientific_recovery; /// Empirical interval coverage. pub use coverage::interval_coverage; /// Wilson bounds for coverage proportions. diff --git a/crates/validation_core/tests/claim_promotion_contract.rs b/crates/validation_core/tests/claim_promotion_contract.rs new file mode 100644 index 00000000..6f5f6eb0 --- /dev/null +++ b/crates/validation_core/tests/claim_promotion_contract.rs @@ -0,0 +1,246 @@ +//! ADR 0014 claim authorities cannot be promoted from unusable evidence. + +use validation_core::{ + ClaimAuthority, ClaimEvidence, ClaimEvidenceKind, PromotedClaim, PromotionRequest, + ValidationError, parse_commit_head, promote_claim, promote_scientific_recovery, + rmse_standard_error, root_mean_square_error, +}; + +const PROTECTED_HEAD: &str = "b2a3f879ca61daefa534f122647074666d5604bc"; +const OTHER_HEAD: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + +fn implemented_main_evidence() -> [ClaimEvidence; 1] { + [ClaimEvidence::new(ClaimEvidenceKind::ExactHeadTests, true)] +} + +fn scientifically_supported_evidence() -> [ClaimEvidence; 2] { + [ + ClaimEvidence::new(ClaimEvidenceKind::ExactHeadTests, true), + ClaimEvidence::new(ClaimEvidenceKind::ScientificRecovery, true), + ] +} + +fn released_evidence() -> [ClaimEvidence; 6] { + [ + ClaimEvidence::new(ClaimEvidenceKind::ExactHeadTests, true), + ClaimEvidence::new(ClaimEvidenceKind::ScientificRecovery, true), + ClaimEvidence::new(ClaimEvidenceKind::SecuritySupplyChain, true), + ClaimEvidence::new(ClaimEvidenceKind::QualifyingReview, true), + ClaimEvidence::new(ClaimEvidenceKind::OperationalReadiness, true), + ClaimEvidence::new(ClaimEvidenceKind::SbomProvenance, true), + ] +} + +fn request<'evidence>( + target: ClaimAuthority, + candidate_head: &str, + evidence: &'evidence [ClaimEvidence], +) -> PromotionRequest<'evidence> { + PromotionRequest::new(target, candidate_head, PROTECTED_HEAD, evidence).expect("request") +} + +#[test] +fn commit_heads_are_forty_hex_bytes() { + let parsed = parse_commit_head(PROTECTED_HEAD).expect("head"); + assert_eq!(parsed.len(), 20); + assert_eq!(parse_commit_head(""), Err(ValidationError::InvalidInput)); + assert_eq!( + parse_commit_head("not-a-commit-sha"), + Err(ValidationError::InvalidInput) + ); + assert_eq!( + parse_commit_head("B2A3F879CA61DAEFA534F122647074666D5604BC"), + parse_commit_head(PROTECTED_HEAD) + ); + assert_eq!( + parse_commit_head("b2a3f879ca61daefa534f122647074666d5604bg"), + Err(ValidationError::InvalidInput) + ); +} + +#[test] +fn decision_accepted_does_not_require_implementation_evidence() { + let promoted = + promote_claim(&request(ClaimAuthority::DecisionAccepted, OTHER_HEAD, &[])).expect("design"); + assert_eq!(promoted.authority(), ClaimAuthority::DecisionAccepted); + assert_eq!( + promoted.bound_head(), + parse_commit_head(OTHER_HEAD).unwrap() + ); + assert_eq!( + ClaimAuthority::DecisionAccepted.wire_name(), + "decision_accepted" + ); +} + +#[test] +fn implemented_main_requires_exact_protected_head_and_tests() { + let promoted = promote_claim(&request( + ClaimAuthority::ImplementedMain, + PROTECTED_HEAD, + &implemented_main_evidence(), + )) + .expect("implemented"); + assert_eq!(promoted.authority(), ClaimAuthority::ImplementedMain); + assert_eq!( + promoted.bound_head(), + parse_commit_head(PROTECTED_HEAD).unwrap() + ); + + assert_eq!( + promote_claim(&request( + ClaimAuthority::ImplementedMain, + OTHER_HEAD, + &implemented_main_evidence(), + )), + Err(ValidationError::ClaimHeadMismatch) + ); + assert_eq!( + promote_claim(&request( + ClaimAuthority::ImplementedMain, + PROTECTED_HEAD, + &[] + )), + Err(ValidationError::ClaimEvidenceMissing) + ); + assert_eq!( + promote_claim(&request( + ClaimAuthority::ImplementedMain, + PROTECTED_HEAD, + &[ClaimEvidence::new(ClaimEvidenceKind::ExactHeadTests, false)], + )), + Err(ValidationError::ClaimEvidenceMissing) + ); +} + +#[test] +fn unusable_evidence_kinds_never_promote() { + let cases = [ + ( + ClaimEvidenceKind::QueuedCheck, + ValidationError::ClaimQueuedEvidence, + ), + ( + ClaimEvidenceKind::PredecessorHead, + ValidationError::ClaimPredecessorHead, + ), + ( + ClaimEvidenceKind::LlmJudgment, + ValidationError::ClaimLlmJudgment, + ), + ( + ClaimEvidenceKind::SkippedRequired, + ValidationError::ClaimSkippedRequired, + ), + ]; + for (kind, expected) in cases { + assert!(!kind.is_promotable()); + assert!(!kind.wire_name().is_empty()); + let evidence = [ + ClaimEvidence::new(ClaimEvidenceKind::ExactHeadTests, true), + ClaimEvidence::new(kind, true), + ]; + assert_eq!( + promote_claim(&request( + ClaimAuthority::ImplementedMain, + PROTECTED_HEAD, + &evidence, + )), + Err(expected) + ); + } +} + +#[test] +fn scientific_and_release_authorities_require_their_gates() { + assert_eq!( + promote_claim(&request( + ClaimAuthority::ScientificallySupported, + PROTECTED_HEAD, + &implemented_main_evidence(), + )), + Err(ValidationError::ClaimEvidenceMissing) + ); + let scientific = promote_claim(&request( + ClaimAuthority::ScientificallySupported, + PROTECTED_HEAD, + &scientifically_supported_evidence(), + )) + .expect("scientific"); + assert_eq!( + scientific.authority(), + ClaimAuthority::ScientificallySupported + ); + + assert_eq!( + promote_claim(&request( + ClaimAuthority::Released, + PROTECTED_HEAD, + &scientifically_supported_evidence(), + )), + Err(ValidationError::ClaimEvidenceMissing) + ); + let released = promote_claim(&request( + ClaimAuthority::Released, + PROTECTED_HEAD, + &released_evidence(), + )) + .expect("released"); + assert_eq!(released.authority(), ClaimAuthority::Released); + assert_eq!(ClaimAuthority::Released.wire_name(), "released"); + assert_eq!( + ClaimEvidenceKind::ScientificRecovery.wire_name(), + "scientific_recovery" + ); +} + +#[test] +fn scientific_recovery_uses_computed_rmse_not_hardcoded_thresholds() { + let truth = [0.70, 0.55, 0.40, -0.20, 0.85]; + let recovered = [0.72, 0.53, 0.41, -0.18, 0.84]; + let rmse = root_mean_square_error(&truth, &recovered).expect("rmse"); + let rmse_se = rmse_standard_error(&truth, &recovered).expect("se"); + let computed_k = (rmse / rmse_se) + 1.0; + let promoted = promote_scientific_recovery( + PROTECTED_HEAD, + PROTECTED_HEAD, + &truth, + &recovered, + computed_k, + ) + .expect("promote"); + assert_eq!( + promoted.authority(), + ClaimAuthority::ScientificallySupported + ); + assert!(rmse.is_finite()); + assert!(rmse_se.is_finite() && rmse_se > 0.0); + promote_scientific_recovery(PROTECTED_HEAD, PROTECTED_HEAD, &truth, &truth, 3.0) + .expect("exact"); + + let biased = [1.70, 1.55, 1.40, 0.80, 1.85]; + assert_eq!( + promote_scientific_recovery(PROTECTED_HEAD, PROTECTED_HEAD, &truth, &biased, 3.0), + Err(ValidationError::ClaimRecoveryRejected) + ); + assert_eq!( + promote_scientific_recovery(OTHER_HEAD, PROTECTED_HEAD, &truth, &recovered, 3.0), + Err(ValidationError::ClaimHeadMismatch) + ); + assert_eq!( + promote_scientific_recovery(PROTECTED_HEAD, PROTECTED_HEAD, &[], &[], 3.0), + Err(ValidationError::InvalidInput) + ); +} + +#[test] +fn promoted_claim_and_request_reject_invalid_heads() { + assert_eq!( + PromotionRequest::new(ClaimAuthority::DecisionAccepted, "bad", PROTECTED_HEAD, &[],).err(), + Some(ValidationError::InvalidInput) + ); + let _ = PromotedClaim::new( + ClaimAuthority::DecisionAccepted, + parse_commit_head(PROTECTED_HEAD).unwrap(), + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 051062ea..23de0cec 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -42,7 +42,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | autonomous model proposal separated from verification/publication/review/merge | ADR 0015 | future safe OpenCode/NVIDIA autonomous-development workflow | accepted-target | | contextual-orchestrator execution boundary | ADR 0010/0011 | provider-neutral orchestration port; TEPP retains scientific authority | accepted-target | | foundation validation / release-readiness ledger | ADR 0014; Test Strategy | PR #24 `docs/validation/temporal-event-foundation.md` on protected main | implemented-main | -| scientific claim promotion separated from design/implementation/release | ADR 0014; ADR policy | documentation/CI/domain validation/release evidence | partial | +| scientific claim promotion separated from design/implementation/release | ADR 0014; ADR policy | `validation_core` exact-head promotion gates on this PR; documentation/CI/domain validation remain; full package/image release bundle remaining | partial | | CSAP/SOC 2/ISO/NIST assurance readiness | `docs/COMPLIANCE_READINESS.md`; research register | repository controls + future deployment evidence | accepted-target / deployment-owned | | threat-model controls and scientific-integrity security | `SECURITY.md`; `docs/THREAT_MODEL.md` | deterministic security/privacy/scientific validation gates | partial | | accessible bitemporal/network/drift/invariance views | PRD/UML | future `visual_analytics`; Figma in approved visual phase | accepted-target | diff --git a/docs/adr/0014-scientific-claim-promotion-and-release-evidence.md b/docs/adr/0014-scientific-claim-promotion-and-release-evidence.md index 81ebb373..85955dcb 100644 --- a/docs/adr/0014-scientific-claim-promotion-and-release-evidence.md +++ b/docs/adr/0014-scientific-claim-promotion-and-release-evidence.md @@ -1,7 +1,7 @@ # ADR 0014 — Scientific claim promotion and release evidence authority **Decision status:** Accepted -**Implementation maturity:** partial — claim/promotion authority documented; repository SBOM/provenance evidence generator and CI validation implemented; full package/image release bundle and scientific claim promotion packages remain accepted-target +**Implementation maturity:** partial — claim/promotion authority documented; repository SBOM/provenance evidence generator and CI validation implemented; `validation_core` exact-head promotion gates implemented on this PR; full package/image release bundle remains accepted-target **Date:** 2026-08-12 **Supersedes:** None; extends ADR 0007 from repository quality tooling to product/scientific claim authority. diff --git a/docs/adr/README.md b/docs/adr/README.md index 1a9a7b31..98cb61be 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -19,7 +19,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0011](0011-standalone-modular-msa-boundary.md) | Standalone operation and modular CWL MSA boundary | Accepted | partial | Owns cross-service persistence/credential/API authority; no direct cross-service application-table coupling. | | [0012](0012-temporal-relational-shared-latent-topic-measurement.md) | Temporal Relational Shared-Latent Topic Measurement (TRSL-TM) | Accepted | accepted-target | Owns topic backend compatibility, global topic identity, method effects, K/model-selection prerequisites, and compositional topic coordinates. | | [0013](0013-bitemporal-persistence-reproducibility-and-split-authority.md) | Bitemporal persistence, reproducibility manifests, and relation-aware split authority | Accepted | partial | Owns PostgreSQL adapter semantics, immutable run/split manifests, leakage-safe partitions, and recovery identity; optional `live-sqlx` `PgPool`, live PG CI, and tenant RLS implemented; full physical ERD remaining. | -| [0014](0014-scientific-claim-promotion-and-release-evidence.md) | Scientific claim promotion and release evidence authority | Accepted | partial | Separates design, implementation, scientific/product claim, and release authority; repository SBOM/provenance generator implemented, full release bundle remaining. | +| [0014](0014-scientific-claim-promotion-and-release-evidence.md) | Scientific claim promotion and release evidence authority | Accepted | partial | Separates design, implementation, scientific/product claim, and release authority; repository SBOM/provenance generator implemented; `validation_core` exact-head promotion gates on the active PR; full release bundle remaining. | | [0015](0015-autonomous-development-review-and-merge-authority.md) | Autonomous development, review, and merge authority separation | Accepted | active-PR | Separates model proposal, deterministic verification, publication, independent review, and merge/release authority. | | [0016](0016-tdt-chronos-event-intelligence-boundary.md) | TDT, CHRONOS, and Event Ontology intelligence boundary | Accepted | accepted-target | Separates observed evidence, detection/tracking, prediction/schema inference, temporal consistency, and promoted transition authority. | diff --git a/docs/research/scientific-claim-promotion-gates.md b/docs/research/scientific-claim-promotion-gates.md new file mode 100644 index 00000000..6e9f8928 --- /dev/null +++ b/docs/research/scientific-claim-promotion-gates.md @@ -0,0 +1,31 @@ +# Scientific claim-promotion gates + +## Scope + +This note doctors the first ADR 0014 executable promotion slice in `validation_core`: + +1. four claim authorities remain distinct (`decision_accepted`, `implemented_main`, `scientifically_supported`, `released`); +2. implementation, scientific, and release authorities bind to one exact protected-head SHA; +3. queued checks, predecessor-head results, skipped required tests, and LLM judgments cannot promote any authority; +4. scientific promotion uses computed RMSE and its standard error, not a hardcoded recovery threshold. + +Full package/image release bundles remain accepted-target. No database migration is allocated. + +## Authoritative sources + +National Academies of Sciences, Engineering, and Medicine. (2019). *Reproducibility and replicability in science*. The National Academies Press. https://doi.org/10.17226/25303 + +Wasserstein, R. L., & Lazar, N. A. (2016). The ASA statement on *p*-values: Context, process, and purpose. *The American Statistician, 70*(2), 129–133. https://doi.org/10.1080/00031305.2016.1154108 + +## Application + +The National Academies (2019) separate computational reproducibility from a scientific claim that a result is correct. Wasserstein and Lazar (2016) refuse to treat a passing statistical threshold as automatic scientific authority. TEPP therefore refuses to promote `implemented_main`, `scientifically_supported`, or `released` from queued, stale, skipped, or LLM evidence, and accepts scientific recovery only when computed RMSE lies within a configured number of its own standard errors (National Academies of Sciences, Engineering, and Medicine, 2019; Wasserstein & Lazar, 2016). + +## Verification + +- `DecisionAccepted` binds without implementation evidence; +- `ImplementedMain` requires the candidate SHA to equal the protected SHA and passing exact-head tests; +- queued, predecessor, skipped-required, and LLM evidence return dedicated fail-closed errors; +- `ScientificallySupported` and `Released` require their additional gates; +- a near-recovery vector promotes only when the computed RMSE/SE multiplier admits it; +- a large bias vector returns `ClaimRecoveryRejected`. diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 984d329c..b5a201d0 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -22,6 +22,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Leakage-safe splits | `corpus_split` | implemented-main | — | cutoff + co-partition tests | Task 9 / PR #17 | | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | +| Scientific claim promotion gates | `validation_core` | active-PR | this PR | exact-head SHA + computed RMSE SE gate | ADR 0014; full release bundle remaining | | Versioned API/export contracts | `tepp_api` | implemented-main | — | unknown-field/version/limit tests | Task 12 / PR #21; HTTP service remaining | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From 596f091a80d09d59a6c586f08d28aa1f3c5c5fa9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 09:22:33 +0900 Subject: [PATCH 2/2] docs: remove trailing whitespace from claim adr --- .../adr/0014-scientific-claim-promotion-and-release-evidence.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0014-scientific-claim-promotion-and-release-evidence.md b/docs/adr/0014-scientific-claim-promotion-and-release-evidence.md index 85955dcb..0797b404 100644 --- a/docs/adr/0014-scientific-claim-promotion-and-release-evidence.md +++ b/docs/adr/0014-scientific-claim-promotion-and-release-evidence.md @@ -1,7 +1,7 @@ # ADR 0014 — Scientific claim promotion and release evidence authority **Decision status:** Accepted -**Implementation maturity:** partial — claim/promotion authority documented; repository SBOM/provenance evidence generator and CI validation implemented; `validation_core` exact-head promotion gates implemented on this PR; full package/image release bundle remains accepted-target +**Implementation maturity:** partial — claim/promotion authority documented; repository SBOM/provenance evidence generator and CI validation implemented; `validation_core` exact-head promotion gates implemented on this PR; full package/image release bundle remains accepted-target **Date:** 2026-08-12 **Supersedes:** None; extends ADR 0007 from repository quality tooling to product/scientific claim authority.