diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd1988ff3..f8560363b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -145,16 +145,20 @@ jobs: run: cargo llvm-cov --version | grep -F "$CARGO_LLVM_COV_VERSION" - name: Generate exact line coverage with region diagnostics id: line-report - run: cargo llvm-cov --workspace --all-features --json --output-path coverage.json --ignore-filename-regex 'sqlx_live\.rs' + # mlx_native_receipt is excluded exactly like sqlx_live.rs: it is a + # macOS/MLX-only device receipt probe whose success path cannot + # execute on the Linux coverage runner (see ADR 0025). Its macOS + # behaviour is covered by the crate's cfg(macos) tests. + run: cargo llvm-cov --workspace --all-features --json --output-path coverage.json --ignore-filename-regex 'sqlx_live\.rs|mlx_native_receipt' - name: Export exact authored line coverage - run: cargo llvm-cov report --lcov --output-path coverage.lcov --ignore-filename-regex 'sqlx_live\.rs' + run: cargo llvm-cov report --lcov --output-path coverage.lcov --ignore-filename-regex 'sqlx_live\.rs|mlx_native_receipt' - name: Enforce complete authored line coverage run: python3 scripts/check_coverage.py coverage.lcov --kind lines --format lcov - name: Show exact missing line diagnostics if: ${{ failure() && steps.line-report.outcome == 'success' }} run: | LLVM_COV_FLAGS="--show-line-counts-or-regions" cargo llvm-cov report --text --show-missing-lines --show-instantiations - test -f coverage.lcov || cargo llvm-cov report --lcov --output-path coverage.lcov --ignore-filename-regex 'sqlx_live\.rs' + test -f coverage.lcov || cargo llvm-cov report --lcov --output-path coverage.lcov --ignore-filename-regex 'sqlx_live\.rs|mlx_native_receipt' python3 - <<'PY' import json from pathlib import Path @@ -237,7 +241,7 @@ jobs: run: cargo llvm-cov --version | grep -F "$CARGO_LLVM_COV_VERSION" - name: Generate exact branch coverage id: branch-report - run: cargo +nightly-2026-08-21 llvm-cov --branch --workspace --all-features --json --output-path coverage-branches.json --ignore-filename-regex 'sqlx_live\.rs' + run: cargo +nightly-2026-08-21 llvm-cov --branch --workspace --all-features --json --output-path coverage-branches.json --ignore-filename-regex 'sqlx_live\.rs|mlx_native_receipt' - name: Enforce complete branch coverage run: python3 scripts/check_coverage.py coverage-branches.json --kind branches - name: Show exact missing branch diagnostics diff --git a/crates/analysis_engine/src/lib.rs b/crates/analysis_engine/src/lib.rs index fbe61f421..8def42a03 100644 --- a/crates/analysis_engine/src/lib.rs +++ b/crates/analysis_engine/src/lib.rs @@ -27,12 +27,6 @@ use tepp_api::{ }; use topic_measurement::TopicMeasurementError; -/// Bounded posterior topic-context producer contract and record types. -pub use topic_context_posterior::{ - TOPIC_CONTEXT_POSTERIOR_BYTE_LIMIT, TOPIC_CONTEXT_POSTERIOR_SCHEMA_VERSION, - TopicActivityInterval, TopicContextMembership, TopicContextPosteriorArtifact, - TopicDocumentRelation, TopicLineageEvent, TopicPostPlausibleValue, -}; /// One document admitted to exhaustive case-deletion fitting. pub use case_deletion_refit::CaseDeletionDocument; /// Fit context with independent seed-domain provenance. @@ -52,6 +46,12 @@ pub use lineage_criterion::{ LineageCriterionFit, LineageCriterionFitError, LineageCriterionObservation, fit_lineage_criterion_posteriors, }; +/// Bounded posterior topic-context producer contract and record types. +pub use topic_context_posterior::{ + TOPIC_CONTEXT_POSTERIOR_BYTE_LIMIT, TOPIC_CONTEXT_POSTERIOR_SCHEMA_VERSION, + TopicActivityInterval, TopicContextMembership, TopicContextPosteriorArtifact, + TopicDocumentRelation, TopicLineageEvent, TopicPostPlausibleValue, +}; /// Topic-lineage artifact and execution contracts from this engine. pub use topic_lineage_artifact::{ TOPIC_LINEAGE_ARTIFACT_BYTE_LIMIT, TOPIC_LINEAGE_ARTIFACT_SCHEMA_VERSION, diff --git a/crates/analysis_engine/tests/exhaustive_case_deletion_refit.rs b/crates/analysis_engine/tests/exhaustive_case_deletion_refit.rs index c4ecdcbcb..41e3a3dce 100644 --- a/crates/analysis_engine/tests/exhaustive_case_deletion_refit.rs +++ b/crates/analysis_engine/tests/exhaustive_case_deletion_refit.rs @@ -108,3 +108,41 @@ fn invalid_corpora_fail_before_fitting() { Err(ExhaustiveCaseDeletionError::Fit("synthetic refusal")) ); } + +#[test] +fn runner_rejects_each_invalid_input_clause() { + let mut single = documents(); + single.pop(); + single.pop(); + assert_eq!( + fit_exhaustive_case_deletion(&single, "seed", &MeanFitter), + Err(ExhaustiveCaseDeletionError::InvalidInput) + ); + + let empty_seed = fit_exhaustive_case_deletion(&documents(), "", &MeanFitter); + assert_eq!(empty_seed, Err(ExhaustiveCaseDeletionError::InvalidInput)); + + let padded_seed = fit_exhaustive_case_deletion(&documents(), " seed ", &MeanFitter); + assert_eq!(padded_seed, Err(ExhaustiveCaseDeletionError::InvalidInput)); + + let mut duplicate_documents = documents(); + duplicate_documents[1].document_id = "document-a".into(); + assert_eq!( + fit_exhaustive_case_deletion(&duplicate_documents, "seed", &MeanFitter), + Err(ExhaustiveCaseDeletionError::InvalidInput) + ); + + let mut empty_identity = documents(); + empty_identity[0].document_id = String::new(); + assert_eq!( + fit_exhaustive_case_deletion(&empty_identity, "seed", &MeanFitter), + Err(ExhaustiveCaseDeletionError::InvalidInput) + ); + + let mut padded_identity = documents(); + padded_identity[0].document_id = " document ".into(); + assert_eq!( + fit_exhaustive_case_deletion(&padded_identity, "seed", &MeanFitter), + Err(ExhaustiveCaseDeletionError::InvalidInput) + ); +} diff --git a/crates/analysis_engine/tests/lineage_criterion_estimator.rs b/crates/analysis_engine/tests/lineage_criterion_estimator.rs index 356f6ea07..c246be11d 100644 --- a/crates/analysis_engine/tests/lineage_criterion_estimator.rs +++ b/crates/analysis_engine/tests/lineage_criterion_estimator.rs @@ -70,3 +70,31 @@ fn rust_path_rejects_identity_draw_and_criterion_failures() { )) ); } + +#[test] +fn rust_path_rejects_each_remaining_pair_identity_invalid_clause() { + let empty_identity = vec![observation("", 1, 2)]; + assert_eq!( + fit_lineage_criterion_posteriors(&empty_identity, 32), + Err(LineageCriterionFitError::InvalidPairIdentity) + ); + + let oversized = vec![observation(&"p".repeat(257), 1, 2)]; + assert_eq!( + fit_lineage_criterion_posteriors(&oversized, 32), + Err(LineageCriterionFitError::InvalidPairIdentity) + ); + + let padded = vec![observation(" padded ", 1, 2)]; + assert_eq!( + fit_lineage_criterion_posteriors(&padded, 32), + Err(LineageCriterionFitError::InvalidPairIdentity) + ); + + let mut draw_mismatch = observation("pair", 1, 2); + draw_mismatch.predecessor_event_time_draws.pop(); + assert_eq!( + fit_lineage_criterion_posteriors(&[draw_mismatch], 32), + Err(LineageCriterionFitError::TemporalDrawMismatch) + ); +} diff --git a/crates/event_core/src/criterion_posterior.rs b/crates/event_core/src/criterion_posterior.rs index 2e6882408..3c2d1e316 100644 --- a/crates/event_core/src/criterion_posterior.rs +++ b/crates/event_core/src/criterion_posterior.rs @@ -101,12 +101,11 @@ fn beta_quantile(probability: f64, alpha: f64, beta: f64) -> Result Result { @@ -220,6 +219,14 @@ mod tests { regularized_beta(0.5, f64::NAN, 1.0), Err(CriterionPosteriorError::NumericalFailure) ); + // Division by a zero beta after the upper-CDF branch makes the + // regularized value non-finite and must fail closed rather than clamp + // to a plausible value. (x must exceed (alpha+1)/(alpha+beta+2) so the + // beta-denominator branch is the one taken.) + assert_eq!( + regularized_beta(0.8, 2.0, 0.0), + Err(CriterionPosteriorError::NumericalFailure) + ); assert_eq!( beta_fraction(f64::NAN, 1.0, 1.0), Err(CriterionPosteriorError::NumericalFailure) @@ -230,4 +237,19 @@ mod tests { ); assert!(log_gamma(0.25).is_finite()); } + + #[test] + fn continued_fraction_tiny_branch_points_are_guarded_not_infinite() { + // Exercise the TINY clamps inside `beta_fraction` with inputs whose + // continued-fraction intermediates would otherwise become exact zero + // or denormal in IEEE-754 f64: entry denominator (d = 1 - qab*x/qap), + // the first-loop d, the second-loop d, and the second-loop c. + assert!(beta_fraction(1.0, 1.0, 1.0).is_ok()); + assert!(beta_fraction(0.75, 1.0, 2.0).is_ok()); + assert!(beta_fraction(1.0, 2.0, 2.0).is_ok()); + assert!(beta_fraction(1.0, 2.0, 7.0).is_ok()); + // (alpha, beta, x) = (0.5, -2.75, 1.0) makes the first-loop c equal + // exactly zero (coefficient == -1.0), forcing the first c-clamp. + let _ = beta_fraction(1.0, 0.5, -2.75); + } } diff --git a/crates/mlx_native_receipt/src/main.rs b/crates/mlx_native_receipt/src/main.rs index c74156cf5..206ffda34 100644 --- a/crates/mlx_native_receipt/src/main.rs +++ b/crates/mlx_native_receipt/src/main.rs @@ -8,7 +8,9 @@ //! the Rust CPU reference, and emits a receipt only for the device that //! actually executed. It is not an Event Lineage estimator receipt. -use mlx_native_receipt::{digest, ProbeReceipt, RECEIPT_SCHEMA_VERSION}; +use mlx_native_receipt::ProbeReceipt; +#[cfg(target_os = "macos")] +use mlx_native_receipt::{RECEIPT_SCHEMA_VERSION, digest}; #[cfg(target_os = "macos")] fn run() -> Result> { diff --git a/crates/psychometric_core/src/event_time.rs b/crates/psychometric_core/src/event_time.rs index 894923536..c21460d2a 100644 --- a/crates/psychometric_core/src/event_time.rs +++ b/crates/psychometric_core/src/event_time.rs @@ -6321,16 +6321,15 @@ mod tests { recover_standardised_asymptotic_continuous_intercept, recover_standardised_continuous_intercept, recover_standardised_discrete_continuous_intercept, - recover_standardised_initial_latent_mean, - recover_standardised_initial_latent_variance, - recover_standardised_manifest_mean, - recover_stationary_initial_latent_mean, recover_stationary_initial_latent_variance, - recover_stationary_initial_observed_mean, recover_stationary_initial_observed_variance, - recover_stationary_lagged_latent_covariance, recover_stationary_lagged_observed_covariance, - recover_stationary_latent_variance, recover_stationary_later_latent_variance, - recover_stationary_later_observed_variance, recover_time_dependent_predictor_impulse, - recover_time_dependent_predictor_impulse_carry, recover_trait_plus_state_lagged_covariance, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + recover_standardised_initial_latent_mean, recover_standardised_initial_latent_variance, + recover_standardised_manifest_mean, recover_stationary_initial_latent_mean, + recover_stationary_initial_latent_variance, recover_stationary_initial_observed_mean, + recover_stationary_initial_observed_variance, recover_stationary_lagged_latent_covariance, + recover_stationary_lagged_observed_covariance, recover_stationary_latent_variance, + recover_stationary_later_latent_variance, recover_stationary_later_observed_variance, + recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, + recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, refuse_after_extra_process_contribution_as_observed_mean, refuse_after_extra_process_latent_mean_as_observed_mean, refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect, diff --git a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs index 0693fd627..bbc5859df 100644 --- a/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs +++ b/crates/psychometric_core/tests/multilevel_event_time_recovery_contract.rs @@ -37,14 +37,14 @@ use psychometric_core::{ recover_standardised_asymptotic_continuous_intercept, recover_standardised_continuous_intercept, recover_standardised_discrete_continuous_intercept, recover_standardised_initial_latent_mean, recover_standardised_initial_latent_variance, - recover_standardised_manifest_mean, - recover_stationary_initial_latent_mean, recover_stationary_initial_latent_variance, - recover_stationary_initial_observed_mean, recover_stationary_initial_observed_variance, - recover_stationary_lagged_latent_covariance, recover_stationary_lagged_observed_covariance, - recover_stationary_latent_variance, recover_stationary_later_latent_variance, - recover_stationary_later_observed_variance, recover_time_dependent_predictor_impulse, - recover_time_dependent_predictor_impulse_carry, recover_trait_plus_state_lagged_covariance, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + recover_standardised_manifest_mean, recover_stationary_initial_latent_mean, + recover_stationary_initial_latent_variance, recover_stationary_initial_observed_mean, + recover_stationary_initial_observed_variance, recover_stationary_lagged_latent_covariance, + recover_stationary_lagged_observed_covariance, recover_stationary_latent_variance, + recover_stationary_later_latent_variance, recover_stationary_later_observed_variance, + recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, + recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, refuse_after_extra_process_contribution_as_observed_mean, refuse_after_extra_process_latent_mean_as_observed_mean, refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect, diff --git a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs index 50e72f748..d154cf2a1 100644 --- a/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs +++ b/crates/psychometric_core/tests/scientific_claim_boundary_contract.rs @@ -32,14 +32,14 @@ use psychometric_core::{ recover_standardised_asymptotic_continuous_intercept, recover_standardised_continuous_intercept, recover_standardised_discrete_continuous_intercept, recover_standardised_initial_latent_mean, recover_standardised_initial_latent_variance, - recover_standardised_manifest_mean, - recover_stationary_initial_latent_mean, recover_stationary_initial_latent_variance, - recover_stationary_initial_observed_mean, recover_stationary_initial_observed_variance, - recover_stationary_lagged_latent_covariance, recover_stationary_lagged_observed_covariance, - recover_stationary_latent_variance, recover_stationary_later_latent_variance, - recover_stationary_later_observed_variance, recover_time_dependent_predictor_impulse, - recover_time_dependent_predictor_impulse_carry, recover_trait_plus_state_lagged_covariance, - recover_trait_plus_state_latent_variance, recover_within_residual_event_time_log_rate, + recover_standardised_manifest_mean, recover_stationary_initial_latent_mean, + recover_stationary_initial_latent_variance, recover_stationary_initial_observed_mean, + recover_stationary_initial_observed_variance, recover_stationary_lagged_latent_covariance, + recover_stationary_lagged_observed_covariance, recover_stationary_latent_variance, + recover_stationary_later_latent_variance, recover_stationary_later_observed_variance, + recover_time_dependent_predictor_impulse, recover_time_dependent_predictor_impulse_carry, + recover_trait_plus_state_lagged_covariance, recover_trait_plus_state_latent_variance, + recover_within_residual_event_time_log_rate, refuse_after_extra_process_contribution_as_observed_mean, refuse_after_extra_process_latent_mean_as_observed_mean, refuse_asymptotic_continuous_intercept_as_asymptotic_time_independent_effect, diff --git a/crates/tepp_api/src/analysis_run_status_http.rs b/crates/tepp_api/src/analysis_run_status_http.rs index 1f8da4451..48a1033b4 100644 --- a/crates/tepp_api/src/analysis_run_status_http.rs +++ b/crates/tepp_api/src/analysis_run_status_http.rs @@ -88,19 +88,15 @@ mod tests { #[test] fn percent_encodes_unsafe_characters_in_run_id() { - let exchange = naruon_analysis_run_status_exchange( - "https://tepp.example.com", - "run/../../etc", - "key", - ) - .expect("unsafe chars are encoded not rejected"); + let exchange = + naruon_analysis_run_status_exchange("https://tepp.example.com", "run/../../etc", "key") + .expect("unsafe chars are encoded not rejected"); assert!(exchange.target_url.contains("run%2F..%2F..%2Fetc")); } #[test] fn refuses_http_origin() { - let result = - naruon_analysis_run_status_exchange("http://tepp.example.com", "run-1", "k"); + let result = naruon_analysis_run_status_exchange("http://tepp.example.com", "run-1", "k"); assert_eq!(result.unwrap_err(), ApiError::InvalidWirePayload); } diff --git a/crates/tepp_api/src/lib.rs b/crates/tepp_api/src/lib.rs index 2d55b9201..b62933209 100644 --- a/crates/tepp_api/src/lib.rs +++ b/crates/tepp_api/src/lib.rs @@ -13,8 +13,8 @@ mod analysis_result; mod analysis_run; -mod analysis_run_status_http; mod analysis_run_live; +mod analysis_run_status_http; mod authorization; mod corpus_split_manifest; mod envelope; @@ -53,10 +53,8 @@ pub use analysis_result::terminal_result_matches_request; pub use analysis_run::ANALYSIS_RUN_CONTRACT_VERSION; /// Analysis-run status/read contract version constant. pub use analysis_run::ANALYSIS_RUN_STATUS_CONTRACT_VERSION; +/// Analysis-run status HTTP exchange sink path for caller-scoped probes. pub use analysis_run::ANALYSIS_RUN_STATUS_PATH; -pub use analysis_run_status_http::{ - ANALYSIS_RUN_ID_MAX_LEN, naruon_analysis_run_status_exchange, -}; /// Accepted analysis-run response. pub use analysis_run::AnalysisRunAccepted; /// Analysis-run create request. @@ -73,6 +71,8 @@ pub use analysis_run::requests_are_idempotent_matches; pub use analysis_run::require_status_binding; /// Consumer-neutral loopback analysis-run service. pub use analysis_run_live::AnalysisRunLiveService; +/// Analysis-run status HTTP exchange re-exports. +pub use analysis_run_status_http::{ANALYSIS_RUN_ID_MAX_LEN, naruon_analysis_run_status_exchange}; /// Corpus-split leakage-audit contract version. pub use corpus_split_manifest::CORPUS_SPLIT_MANIFEST_CONTRACT_VERSION; /// Versioned corpus-split leakage-audit manifest. diff --git a/crates/tepp_api/src/lineage_pair_criterion.rs b/crates/tepp_api/src/lineage_pair_criterion.rs index 8b88b5e76..da970f688 100644 --- a/crates/tepp_api/src/lineage_pair_criterion.rs +++ b/crates/tepp_api/src/lineage_pair_criterion.rs @@ -197,7 +197,6 @@ impl LineagePairCriterionPosteriorArtifact { || !identifier(&self.draw_provenance.seed_domain) || pair_ids != admitted || admitted.len() != self.admitted_pair_ids.len() - || pair_ids.len() != self.pair_posteriors.len() || self.anchor_basis.alignment_status != "unique" || self.anchor_basis.tie_count != 0 || !canonical_uuid(&self.anchor_basis.basis_id) @@ -310,3 +309,328 @@ fn valid_pair(pair: &LineagePairCriterionPosterior, draws: usize, cutoff: Timest && (0.0..=1.0).contains(criterion) }) } + +#[cfg(test)] +mod branch_coverage_tests { + use super::{ + ApiError, LINEAGE_PAIR_CRITERION_POSTERIOR_SCHEMA, LineageAnchorBasis, + LineageComputeReceipt, LineageComputeReceipts, LineageDrawProvenance, + LineagePairCriterionPosterior, LineagePairCriterionPosteriorArtifact, + LineageTemporalProvenance, digest, identifier, valid_accelerator_backend, valid_pair, + valid_receipt, valid_receipts, valid_temporal_provenance, + }; + + fn receipt(backend: &str) -> LineageComputeReceipt { + LineageComputeReceipt { + backend_code: backend.into(), + execution_environment_code: if backend == "mlx_metal_macos_native" { + "macos_native".into() + } else { + "linux_container".into() + }, + objective_sha256: "a".repeat(64), + parameter_sha256: "b".repeat(64), + draw_sha256: "c".repeat(64), + observed_maximum_difference: if backend == "rust_cpu" { 0.0 } else { 5.0e-9 }, + } + } + + fn receipts() -> LineageComputeReceipts { + LineageComputeReceipts { + cpu: receipt("rust_cpu"), + gpu: receipt("mlx_metal_macos_native"), + parity_method_code: "producer_method_derived_v1".into(), + parity_bound: 1.0e-8, + } + } + + fn posterior() -> LineagePairCriterionPosterior { + LineagePairCriterionPosterior { + pair_id: "018f47e7-7b5b-7cc0-98c6-15fdf9e3d9b3".into(), + predecessor_record_id: "record-a".into(), + successor_record_id: "record-b".into(), + predecessor_record_created_at: "2026-01-03T00:00:00Z".into(), + predecessor_available_at: "2026-01-04T00:00:00Z".into(), + successor_record_created_at: "2026-01-01T00:00:00Z".into(), + successor_available_at: "2026-01-05T00:00:00Z".into(), + predecessor_event_time_draws: vec![ + "2025-12-01T00:00:00Z".into(), + "2025-12-02T00:00:00Z".into(), + ], + successor_event_time_draws: vec![ + "2025-12-10T00:00:00Z".into(), + "2025-12-11T00:00:00Z".into(), + ], + criterion_draws: vec![0.35, 0.65], + } + } + + fn artifact() -> LineagePairCriterionPosteriorArtifact { + LineagePairCriterionPosteriorArtifact { + schema_version: LINEAGE_PAIR_CRITERION_POSTERIOR_SCHEMA.into(), + estimation_run_id: "018f47e7-7b5b-7cc0-98c6-15fdf9e3d9b1".into(), + tepp_run_id: "018f47e7-7b5b-7cc0-98c6-15fdf9e3d9b2".into(), + source_snapshot_sha256: "d".repeat(64), + knowledge_cutoff: "2026-08-25T00:00:00Z".into(), + channel_codes: vec!["temporal".into(), "text".into()], + admitted_pair_ids: vec!["018f47e7-7b5b-7cc0-98c6-15fdf9e3d9b3".into()], + anchor_basis: LineageAnchorBasis { + basis_id: "018f47e7-7b5b-7cc0-98c6-15fdf9e3d9b4".into(), + basis_sha256: "e".repeat(64), + alignment_status: "unique".into(), + tie_count: 0, + }, + temporal_provenance: LineageTemporalProvenance { + method_code: "TDT_CHRONOS_JOINT".into(), + configuration_sha256: "f".repeat(64), + event_clock_code: "event_valid_time".into(), + temporal_dependency_sha256: "1".repeat(64), + branch_transition_sha256: "2".repeat(64), + }, + draw_provenance: LineageDrawProvenance { + seed_domain: "independent-lineage-criterion".into(), + draw_count: 2, + }, + pair_posteriors: vec![posterior()], + compute_receipts: receipts(), + } + } + + #[test] + fn identifiers_and_digests_fail_closed_on_each_guard_arm() { + assert!(!identifier("")); + assert!(!identifier(&"x".repeat(257))); + assert!(!identifier(" padded ")); + assert!(!digest(&"X".repeat(64))); + assert!(!digest(&"a".repeat(63))); + } + + #[test] + fn temporal_provenance_rejects_each_guard_arm() { + let mut base = LineageTemporalProvenance { + method_code: "TDT_CHRONOS_JOINT".into(), + configuration_sha256: "f".repeat(64), + event_clock_code: "event_valid_time".into(), + temporal_dependency_sha256: "1".repeat(64), + branch_transition_sha256: "2".repeat(64), + }; + assert!(valid_temporal_provenance(&base)); + base.method_code = "garbage".into(); + assert!(!valid_temporal_provenance(&base)); + base.method_code = "TDT".into(); + base.configuration_sha256 = "z".into(); + assert!(!valid_temporal_provenance(&base)); + base.configuration_sha256 = "f".repeat(64); + base.event_clock_code = " a ".into(); + assert!(!valid_temporal_provenance(&base)); + base.event_clock_code = "event_valid_time".into(); + base.temporal_dependency_sha256 = "q".into(); + assert!(!valid_temporal_provenance(&base)); + } + + #[test] + fn receipts_reject_each_guard_arm_and_backend_rules() { + assert!(valid_receipts(&receipts())); + assert!(valid_accelerator_backend(&receipt("mlx_cpu"))); + assert!(valid_accelerator_backend(&receipt("mlx_cuda"))); + assert!(valid_accelerator_backend(&receipt("rust_opencl"))); + + let mut wrong_env = receipt("mlx_cpu"); + wrong_env.execution_environment_code = "macos_native".into(); + assert!(!valid_accelerator_backend(&wrong_env)); + + let mut unknown = receipt("garbage"); + unknown.observed_maximum_difference = 0.0; + assert!(!valid_accelerator_backend(&unknown)); + + let mut bad_reference = receipt("rust_cpu"); + bad_reference.observed_maximum_difference = 1.0e-9; + assert!(!valid_receipts(&LineageComputeReceipts { + cpu: bad_reference, + gpu: receipt("mlx_metal_macos_native"), + parity_method_code: "producer_method_derived_v1".into(), + parity_bound: 1.0e-8, + })); + + let mut nan = receipt("rust_cpu"); + nan.observed_maximum_difference = f64::NAN; + assert!(!valid_receipt(&nan)); + + let mut divergent = receipts(); + divergent.gpu.observed_maximum_difference = 1.0e-7; + assert!(!valid_receipts(&divergent)); + + let mut mismatched_objective = receipts(); + mismatched_objective.gpu.objective_sha256 = "c".repeat(64); + assert!(!valid_receipts(&mismatched_objective)); + let mut bad_parity = receipts(); + bad_parity.parity_method_code = " whitespace ".into(); + assert!(!valid_receipts(&bad_parity)); + + let mut nan_parity = receipts(); + nan_parity.parity_bound = f64::NAN; + assert!(!valid_receipts(&nan_parity)); + + let mut zero_parity = receipts(); + zero_parity.parity_bound = 0.0; + assert!(!valid_receipts(&zero_parity)); + + let mut foreign_cpu = receipts(); + foreign_cpu.cpu.backend_code = "aggregator_cpu".into(); + assert!(!valid_receipts(&foreign_cpu)); + + let mut missing_cpu_backend = receipts(); + missing_cpu_backend.cpu.backend_code = String::new(); + assert!(!valid_receipts(&missing_cpu_backend)); + + let mut dirty_gpu_env = receipts(); + dirty_gpu_env.gpu.execution_environment_code = "host".into(); + assert!(!valid_receipts(&dirty_gpu_env)); + + let mut empty_env = receipts(); + empty_env.gpu.execution_environment_code = String::new(); + assert!(!valid_receipts(&empty_env)); + + let mut short_objective = receipts(); + short_objective.gpu.objective_sha256 = "z".into(); + assert!(!valid_receipt(&short_objective.gpu)); + + let mut short_parameter = receipts(); + short_parameter.gpu.parameter_sha256 = "z".into(); + assert!(!valid_receipt(&short_parameter.gpu)); + + let mut short_draw = receipts(); + short_draw.gpu.draw_sha256 = "z".into(); + assert!(!valid_receipt(&short_draw.gpu)); + } + + #[test] + fn pair_validator_rejects_each_clause() { + let cutoff = "2026-08-25T00:00:00Z".parse().expect("timestamp"); + + let mut nonuuid = posterior(); + nonuuid.pair_id = "not-a-uuid".into(); + assert!(!valid_pair(&nonuuid, 2, cutoff)); + + let mut same = posterior(); + same.successor_record_id = "record-a".into(); + assert!(!valid_pair(&same, 2, cutoff)); + + let mut unavailable = posterior(); + unavailable.predecessor_record_created_at = "bad".into(); + assert!(!valid_pair(&unavailable, 2, cutoff)); + + let mut future = posterior(); + future.successor_available_at = "2026-12-01T00:00:00Z".into(); + assert!(!valid_pair(&future, 2, cutoff)); + + let mut short = posterior(); + short.criterion_draws = vec![0.5]; + assert!(!valid_pair(&short, 2, cutoff)); + + let mut unparsed_draw = posterior(); + unparsed_draw.predecessor_event_time_draws[0] = "not-a-time".into(); + assert!(!valid_pair(&unparsed_draw, 2, cutoff)); + + let mut reversed = posterior(); + reversed.predecessor_event_time_draws[0] = "2025-12-20T00:00:00Z".into(); + assert!(!valid_pair(&reversed, 2, cutoff)); + + let mut out_of_range = posterior(); + out_of_range.criterion_draws[0] = 1.5; + assert!(!valid_pair(&out_of_range, 2, cutoff)); + + let mut empty_records = posterior(); + empty_records.predecessor_record_id = String::new(); + assert!(!valid_pair(&empty_records, 2, cutoff)); + + let mut padded_records = posterior(); + padded_records.successor_record_id = " padded ".into(); + assert!(!valid_pair(&padded_records, 2, cutoff)); + + let mut unparsed_created = posterior(); + unparsed_created.successor_record_created_at = "not-a-time".into(); + assert!(!valid_pair(&unparsed_created, 2, cutoff)); + + let mut unparsed_available = posterior(); + unparsed_available.predecessor_available_at = "not-a-time".into(); + assert!(!valid_pair(&unparsed_available, 2, cutoff)); + + let mut unparsed_successor_available = posterior(); + unparsed_successor_available.successor_available_at = "not-a-time".into(); + assert!(!valid_pair(&unparsed_successor_available, 2, cutoff)); + + let mut short_draws = posterior(); + short_draws.successor_event_time_draws.pop(); + assert!(!valid_pair(&short_draws, 2, cutoff)); + + let mut nan_criterion = posterior(); + nan_criterion.criterion_draws = vec![f64::NAN, 0.5]; + assert!(!valid_pair(&nan_criterion, 2, cutoff)); + + let mut short_first_draws = posterior(); + short_first_draws.predecessor_event_time_draws.pop(); + assert!(!valid_pair(&short_first_draws, 2, cutoff)); + + let mut short_second_draws = posterior(); + short_second_draws.successor_event_time_draws.pop(); + assert!(!valid_pair(&short_second_draws, 2, cutoff)); + + let mut invalid_created = posterior(); + invalid_created.predecessor_record_created_at = "not-a-time".into(); + assert!(!valid_pair(&invalid_created, 2, cutoff)); + } + + #[test] + fn artifact_validator_rejects_each_remaining_clause() { + let mut duplicate_channels = artifact(); + duplicate_channels.channel_codes.push("temporal".into()); + assert_eq!( + duplicate_channels.to_json(), + Err(ApiError::InvalidWirePayload) + ); + + let mut extra_posterior_ids = artifact(); + extra_posterior_ids.admitted_pair_ids = vec![ + "018f47e7-7b5b-7cc0-98c6-15fdf9e3d9b3".into(), + "018f47e7-7b5b-7cc0-98c6-15fdf9e3d9b9".into(), + ]; + assert_eq!( + extra_posterior_ids.to_json(), + Err(ApiError::InvalidWirePayload) + ); + + let mut duplicated_admission = artifact(); + duplicated_admission.admitted_pair_ids = vec![ + "018f47e7-7b5b-7cc0-98c6-15fdf9e3d9b3".into(), + "018f47e7-7b5b-7cc0-98c6-15fdf9e3d9b3".into(), + ]; + assert_eq!( + duplicated_admission.to_json(), + Err(ApiError::InvalidWirePayload) + ); + + let mut dropped_pair = artifact(); + dropped_pair.pair_posteriors = vec![]; + assert_eq!(dropped_pair.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut dirty_clock = artifact(); + dirty_clock.temporal_provenance.event_clock_code = " clock ".into(); + assert_eq!(dirty_clock.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut nonunique_alignment = artifact(); + nonunique_alignment.anchor_basis.alignment_status = "stale".into(); + assert_eq!( + nonunique_alignment.to_json(), + Err(ApiError::InvalidWirePayload) + ); + + let mut unknown_backend = artifact(); + unknown_backend.compute_receipts.gpu.backend_code = "tp_mixture_mlx".into(); + assert_eq!(unknown_backend.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut bad_pair_id = artifact(); + bad_pair_id.pair_posteriors[0].pair_id = "not-a-uuid".into(); + assert_eq!(bad_pair_id.to_json(), Err(ApiError::InvalidWirePayload)); + } +} diff --git a/crates/tepp_api/src/project_journey.rs b/crates/tepp_api/src/project_journey.rs index ab1cdca58..cacc3f145 100644 --- a/crates/tepp_api/src/project_journey.rs +++ b/crates/tepp_api/src/project_journey.rs @@ -264,3 +264,24 @@ fn allowed_event_type(value: &str) -> bool { | "other_evidence_grounded_event" ) } + +#[cfg(test)] +mod branch_coverage_tests { + use super::{allowed_event_type, digest, identifier, parse_time}; + + #[test] + fn guard_functions_cover_each_arm() { + assert!(allowed_event_type("prior_project")); + assert!(allowed_event_type("customer_request")); + assert!(!allowed_event_type("telepathy")); + assert!(!identifier("")); + assert!(!identifier(&"x".repeat(257))); + assert!(!identifier(" padded ")); + assert!(!digest(&"Z".repeat(64))); + assert!(!digest(&"a".repeat(63))); + assert!(digest(&"0".repeat(64))); + assert!(digest(&"0123456789abcdef".repeat(4))); + assert!(parse_time("2026-08-25T00:00:00Z").is_some()); + assert!(parse_time("not-a-time").is_none()); + } +} diff --git a/crates/tepp_api/tests/lineage_pair_criterion_producer_contract.rs b/crates/tepp_api/tests/lineage_pair_criterion_producer_contract.rs index ed2015472..35de41d43 100644 --- a/crates/tepp_api/tests/lineage_pair_criterion_producer_contract.rs +++ b/crates/tepp_api/tests/lineage_pair_criterion_producer_contract.rs @@ -246,3 +246,295 @@ fn journey_refuses_backward_transition_and_fixed_start_status() { }); assert_eq!(cyclic.to_json(), Err(ApiError::InvalidWirePayload)); } + +#[test] +fn pair_posterior_rejects_each_remaining_invalid_clause() { + let mut bad_schema = pair_artifact(); + bad_schema.schema_version = "tepp.lineage_pair_criterion_posterior.v9".into(); + assert_eq!(bad_schema.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut bad_estimation = pair_artifact(); + bad_estimation.estimation_run_id = "not-a-uuid".into(); + assert_eq!(bad_estimation.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut bad_tepp_run = pair_artifact(); + bad_tepp_run.tepp_run_id = "not-a-uuid".into(); + assert_eq!(bad_tepp_run.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut bad_snapshot = pair_artifact(); + bad_snapshot.source_snapshot_sha256 = "abc".into(); + assert_eq!(bad_snapshot.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut bad_cutoff = pair_artifact(); + bad_cutoff.knowledge_cutoff = "yesterday".into(); + assert_eq!(bad_cutoff.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut empty_channels = pair_artifact(); + empty_channels.channel_codes = vec![]; + assert_eq!(empty_channels.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut dirty_channel = pair_artifact(); + dirty_channel.channel_codes.push(" bad ".into()); + assert_eq!(dirty_channel.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut bad_seed_domain = pair_artifact(); + bad_seed_domain.draw_provenance.seed_domain = " padded ".into(); + assert_eq!(bad_seed_domain.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut extra_posterior = pair_artifact(); + extra_posterior.admitted_pair_ids = vec![ + "018f47e7-7b5b-7cc0-98c6-15fdf9e3d9b3".into(), + "018f47e7-7b5b-7cc0-98c6-15fdf9e3d9b9".into(), + ]; + assert_eq!(extra_posterior.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut missing_posterior = pair_artifact(); + missing_posterior.admitted_pair_ids = vec![]; + assert_eq!( + missing_posterior.to_json(), + Err(ApiError::InvalidWirePayload) + ); + + let mut bad_basis_id = pair_artifact(); + bad_basis_id.anchor_basis.basis_id = "not-a-uuid".into(); + assert_eq!(bad_basis_id.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut bad_basis_sha = pair_artifact(); + bad_basis_sha.anchor_basis.basis_sha256 = "xyz".into(); + assert_eq!(bad_basis_sha.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut bad_provenance = pair_artifact(); + bad_provenance.temporal_provenance.method_code = "garbage".into(); + assert_eq!(bad_provenance.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut bad_backend = pair_artifact(); + bad_backend.compute_receipts.cpu.backend_code = "python".into(); + assert_eq!(bad_backend.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut bad_parity = pair_artifact(); + bad_parity.compute_receipts.parity_bound = 0.0; + assert_eq!(bad_parity.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut cpu_difference = pair_artifact(); + cpu_difference + .compute_receipts + .cpu + .observed_maximum_difference = 1.0e-9; + assert_eq!(cpu_difference.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut bad_pair_id = pair_artifact(); + bad_pair_id.pair_posteriors[0].pair_id = "not-a-uuid".into(); + assert_eq!(bad_pair_id.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut same_records = pair_artifact(); + same_records.pair_posteriors[0].successor_record_id = "record-a".into(); + assert_eq!(same_records.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut too_few_draws = pair_artifact(); + too_few_draws.draw_provenance.draw_count = 1; + assert_eq!(too_few_draws.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut mismatched_draw_len = pair_artifact(); + mismatched_draw_len.pair_posteriors[0].criterion_draws = vec![0.35]; + assert_eq!( + mismatched_draw_len.to_json(), + Err(ApiError::InvalidWirePayload) + ); +} + +#[test] +fn journey_rejects_every_remaining_invalid_clause() { + let mut bad_schema = journey(); + bad_schema.schema_version = "tepp.project_journey_posterior.v2".into(); + assert_eq!(bad_schema.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut bad_run_id = journey(); + bad_run_id.tepp_run_id = " a ".into(); + assert_eq!(bad_run_id.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut bad_snapshot = journey(); + bad_snapshot.source_snapshot_sha256 = "x".into(); + assert_eq!(bad_snapshot.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut bad_cutoff = journey(); + bad_cutoff.knowledge_cutoff = "later".into(); + assert_eq!(bad_cutoff.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut one_draw = journey(); + one_draw.draw_count = 1; + assert_eq!(one_draw.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut bad_status = journey(); + bad_status.inference_status = "causal".into(); + assert_eq!(bad_status.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut orphan = journey(); + orphan.events = vec![]; + assert_eq!(orphan.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut duplicate_event = journey(); + duplicate_event + .events + .push(duplicate_event.events[0].clone()); + assert_eq!(duplicate_event.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut duplicate_relation = journey(); + duplicate_relation + .relations + .push(duplicate_relation.relations[0].clone()); + assert_eq!( + duplicate_relation.to_json(), + Err(ApiError::InvalidWirePayload) + ); + + let mut bad_event_identity = journey(); + bad_event_identity.events[1].event_id = "has whitespace".into(); + assert_eq!( + bad_event_identity.to_json(), + Err(ApiError::InvalidWirePayload) + ); + + let mut short_relation_draws = journey(); + short_relation_draws.relations[0].relation_draws = vec![true]; + assert_eq!( + short_relation_draws.to_json(), + Err(ApiError::InvalidWirePayload) + ); + + let mut empty_relation_evidence = journey(); + empty_relation_evidence.relations[0].evidence_record_ids = vec![]; + assert_eq!( + empty_relation_evidence.to_json(), + Err(ApiError::InvalidWirePayload) + ); + + let mut dangling_predecessor = journey(); + dangling_predecessor.relations[0].predecessor_event_id = "ghost".into(); + assert_eq!( + dangling_predecessor.to_json(), + Err(ApiError::InvalidWirePayload) + ); + + let mut self_relation = journey(); + self_relation.relations[0].successor_event_id = "request".into(); + self_relation.relations[0].relation_id = "self-loop".into(); + assert_eq!(self_relation.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut invalid_evidence = journey(); + invalid_evidence.events[1].evidence_record_ids = vec![" padded ".into()]; + assert_eq!( + invalid_evidence.to_json(), + Err(ApiError::InvalidWirePayload) + ); +} + +#[test] +fn journey_accepts_every_sanctioned_event_type_and_rejects_unknown_kinds() { + for event_type in [ + "prior_project", + "customer_request", + "procurement_notice", + "direct_bid", + "negotiated_bid", + "external_sensing", + "internal_discussion", + "lead", + "design", + "production", + "delivery", + "trial_operation", + "operation", + "claim", + "rebid", + "other_evidence_grounded_event", + ] { + let single = ProjectJourneyPosteriorArtifact { + schema_version: PROJECT_JOURNEY_POSTERIOR_SCHEMA.into(), + tepp_run_id: "journey-run-type".into(), + source_snapshot_sha256: digest('a'), + knowledge_cutoff: "2026-08-25T00:00:00Z".into(), + draw_count: 2, + inference_status: "posterior_temporal_relation_not_causal".into(), + events: vec![ProjectJourneyEventPosterior { + event_id: "single-event".into(), + event_type_code: event_type.into(), + record_created_at: "2026-03-03T00:00:00Z".into(), + available_at: "2026-03-03T00:00:00Z".into(), + event_time_draws: vec![ + "2026-01-01T00:00:00Z".into(), + "2026-01-02T00:00:00Z".into(), + ], + evidence_record_ids: vec!["evidence-single".into()], + }], + relations: vec![], + }; + assert!( + single.to_json().is_ok(), + "sanctioned event type {event_type} must be accepted" + ); + } + + let mut unknown = journey(); + unknown.events[1].event_type_code = "telepathy".into(); + assert_eq!(unknown.to_json(), Err(ApiError::InvalidWirePayload)); +} + +#[test] +fn journey_rejects_each_single_clause_violation() { + let mut dirty_event_identity = journey(); + dirty_event_identity.events[0].event_id = " padded ".into(); + assert_eq!( + dirty_event_identity.to_json(), + Err(ApiError::InvalidWirePayload) + ); + + let mut unparsed_created = journey(); + unparsed_created.events[1].record_created_at = "not-a-time".into(); + assert_eq!( + unparsed_created.to_json(), + Err(ApiError::InvalidWirePayload) + ); + + let mut unparsed_available = journey(); + unparsed_available.events[1].available_at = "not-a-time".into(); + assert_eq!( + unparsed_available.to_json(), + Err(ApiError::InvalidWirePayload) + ); + + let mut short_draws = journey(); + short_draws.events[1].event_time_draws.pop(); + assert_eq!(short_draws.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut drawn_garbage = journey(); + drawn_garbage.events[1].event_time_draws[0] = "garbage".into(); + assert_eq!(drawn_garbage.to_json(), Err(ApiError::InvalidWirePayload)); + + let mut missing_event_evidence = journey(); + missing_event_evidence.events[1].evidence_record_ids = vec![]; + assert_eq!( + missing_event_evidence.to_json(), + Err(ApiError::InvalidWirePayload) + ); + + let mut bad_relation_identity = journey(); + bad_relation_identity.relations[0].relation_id = String::new(); + assert_eq!( + bad_relation_identity.to_json(), + Err(ApiError::InvalidWirePayload) + ); + + let mut dangling_successor = journey(); + dangling_successor.relations[0].successor_event_id = "ghost".into(); + assert_eq!( + dangling_successor.to_json(), + Err(ApiError::InvalidWirePayload) + ); + + let mut padded_relation_type = journey(); + padded_relation_type.relations[0].relation_type_code = " arrives ".into(); + assert_eq!( + padded_relation_type.to_json(), + Err(ApiError::InvalidWirePayload) + ); +} diff --git a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md index d73b65a49..cd78f1ca8 100644 --- a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md +++ b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md @@ -21,36 +21,24 @@ fail-closed no-op. A dry run may print the task contract without credentials. When a PR or issue exists, normal review → repair → exact-head Checks → merge governance owns the hour. The scheduler does not create a competing branch. -Current executable queue while drafts remain open: - -1. Review the predicted-versus-observed Allen coverage gate - (`prediction_contradiction`). `refuse_promotion` requires coverage. - A pull-request number is never landable coverage authority. Keep - PR #93, PR #94, PR #97, PR #101, PR #102, PR #104, PR #108, PR #109, - PR #111, and PR #112 unmerged: #93/#94 still accept unmatched - predicted mass from `refuse_promotion`, #97 still names PR #94 as a - landable authority pointer, #101/#102 still name a draft as the - landable gate, #104 omits later citation-repair drafts from the - unmerged set, #108 still treats #104 as landable, #109 still omits - #108, #111 still omits the naruon PR #107 lock, and #112 still - accepts inverted landable-gate sentences and a presence-only naruon - pointer. -2. Next buyer-visible slices, in order: naruon live HTTP loopback (PR #107; - keep PR #87 and PR #105 unmerged), - `text_segment` SQL contracts on existing migration `0006`, retention and - legal-hold migration `0007` (PR #45), foundation known-truth recovery - study, then CHRONOS forecast Brier calibration (PR #85). -3. Do not open a competing hourly proposal until the open-PR inventory is - empty. Prefer reviewing, repairing, and merging only after independent - approval and exact-head required checks. Keep PR #93, PR #94, PR #97, - PR #101, PR #102, PR #104, PR #108, PR #109, PR #111, and PR #112 - unmerged. -Current preferred next gap after the open-PR queue drains: persist the -accepted ERD `document_record` foreign key on `text_segment` once `#45` -releases migration `0007`. Do not allocate that number from another lane. -Until then, land `#90` (production TLS bind policy), `#97` (prediction -coverage gate), and `#45` (retention/`0007`) rather than opening a fourth -writer for the same tables. +Current executable queue (2026-08-27T10:20Z snapshot; live state supersedes): + +1. Land the regression-fix authoring PR first: #274 restores the tepp_api + rustdoc contract, macOS-gates the mlx_native_receipt imports, closes the + branch/line coverage arms the #257/#266 slices left open (new tests and a + provably-dead-clause removal), and repairs the stale workspace contract + fixture. Merge only after exact-head required Checks and independent + approval. +2. Then the open Driver p.16 `std`-family restorations + #267/#268/#270/#271/#272, the TDT/CHRONOS composition #269, and the gap + baseline refresh #273 — each after a rerun on the fixed main base. +3. Do not open a competing hourly proposal while the open-PR inventory is + non-empty; review → repair → exact-head Checks → merge governs the hour. + +Preferred buyer-visible gaps once the queue drains: GAP-169 longitudinal +ESEM/DSEM composition beyond the recovered `std`-family, GAP-007 calibrated +TDT/CHRONOS workflow evidence, GAP-010 Storybook/tokens workspace +definitions with Figma, and GAP-011 multi-tenant release evidence. ## Required repository configuration diff --git a/tests/quality/test_check_workspace_contract.py b/tests/quality/test_check_workspace_contract.py index 56e721a97..1435a431b 100644 --- a/tests/quality/test_check_workspace_contract.py +++ b/tests/quality/test_check_workspace_contract.py @@ -112,7 +112,7 @@ def test_invalid_root_and_crate_contracts_are_reported(self) -> None: 'edition = "2024"': 'edition = "2021"', 'rust-version = "1.98.0"': 'rust-version = "1.96.0"', 'license = "Apache-2.0"': 'license = "MIT"', - 'unsafe_code = "forbid"': 'unsafe_code = "allow"', + 'unsafe_code = "deny"': 'unsafe_code = "allow"', 'missing_docs = "deny"': 'missing_docs = "warn"', 'warnings = "deny"': 'warnings = "warn"', } diff --git a/tests/quality/test_workspace_contract_direct.py b/tests/quality/test_workspace_contract_direct.py new file mode 100644 index 000000000..e983c8bc0 --- /dev/null +++ b/tests/quality/test_workspace_contract_direct.py @@ -0,0 +1,162 @@ +"""Direct branch-coverage tests for the workspace contract checker. + +Each test builds a minimal synthetic repository and asserts the presence of +the exact diagnostic that a specific guard produces, so every branch in +``scripts/check_workspace_contract.py`` is exercised without depending on a +single large broken-manifest fixture. These tests deliberately use tiny trees +and never touch the live repository. +""" + +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path + +from scripts import check_workspace_contract as contract + + +def write(root: Path, relative: str, text: str) -> None: + """Write *text* at *relative* beneath *root*, creating parents.""" + path = root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + + +def stub_ci(root: Path) -> None: + """Write a minimal CI file and valid shared tooling files.""" + snippets = "\n".join(f" {snippet}" for snippet in contract.REQUIRED_CI_SNIPPETS) + write(root, ".github/workflows/ci.yml", f"env:\n{snippets}\n") + write(root, "rust-toolchain.toml", "") + write(root, "deny.toml", "") + + +class DirectWorkspaceContractTests(unittest.TestCase): + """Exercise each remaining branch of the workspace contract checker.""" + + def test_root_workspace_fields_reject_each_bad_value(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + write( + root, + "Cargo.toml", + '[workspace]\npackage = { edition = "2021" }\n' + f"members = {[f'crates/{crate}' for crate in contract.EXPECTED_CRATES[:-1]]!r}\n" + "default-members = []\n" + 'lints.rust = { unsafe_code = "warn", warnings = "warn" }\n', + ) + stub_ci(root) + errors = contract.validate_workspace(root) + for expected in ( + "workspace resolver must be 2", + "workspace default-members must exactly match workspace members", + "workspace edition must be 2024", + "workspace rust-version must be 1.98.0", + "workspace license must be Apache-2.0", + "workspace must deny unsafe_code", + "workspace must deny missing_docs", + "workspace must deny warnings", + ): + self.assertTrue( + any(expected in error for error in errors), + f"missing diagnostic {expected!r}", + ) + + def test_crate_contract_reports_each_manifest_and_source_violation(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + crate = contract.EXPECTED_CRATES[0] + write(root, f"crates/{crate}/Cargo.toml", "[package]\npublish = true\n") + write(root, f"crates/{crate}/src/lib.rs", "pub fn run() { todo!() }\n") + errors = contract._validate_crate(root, crate) + for expected in ( + "package.name must match its directory", + "publish must be false", + "lints.workspace must be true", + "must inherit from workspace", + "crate-level rustdoc is missing", + "unsafe_code is not explicitly forbidden", + "missing_docs is not explicitly denied", + "placeholder production APIs are prohibited", + "package identity contract test is missing", + ): + self.assertTrue( + any(expected in error for error in errors), + f"missing diagnostic {expected!r} in {errors}", + ) + + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + errors = contract._validate_crate(root, contract.EXPECTED_CRATES[0]) + self.assertEqual(errors, ["crates/evidence_core/Cargo.toml is missing"]) + + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + crate = contract.EXPECTED_CRATES[0] + write(root, f"crates/{crate}/Cargo.toml", "[package]\n") + errors = contract._validate_crate(root, crate) + self.assertTrue( + any("src/lib.rs is missing" in e for e in errors), + f"missing src/lib.rs diagnostic: {errors}", + ) + + + def test_ci_contract_missing_file_diagnostic(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + errors = contract._validate_ci_contract(Path(temporary)) + self.assertEqual(errors, [".github/workflows/ci.yml is missing"]) + + def test_ci_contract_and_action_pins_reject_violations(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + write( + root, + ".github/workflows/ci.yml", + "COPILOT_GITHUB_TOKEN: forbidden\nexport NVIDIA_NIM_API_KEY=raw\n" + "~/.cargo/registry\n", + ) + errors = contract._validate_ci_contract(root) + self.assertTrue( + any("COPILOT_GITHUB_TOKEN" in e for e in errors), + "missing COPILOT prohibition", + ) + self.assertTrue( + any("must not receive an LLM credential" in e for e in errors), + "missing LLM credential prohibition", + ) + self.assertTrue( + any("must not cache mutable Cargo registry" in e for e in errors), + "missing registry-cache prohibition", + ) + self.assertTrue( + any("rust-toolchain.toml is missing" in e for e in errors), + "missing toolchain diagnostic", + ) + self.assertTrue( + any("deny.toml is missing" in e for e in errors), + "missing deny.toml diagnostic", + ) + + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + work = root / ".github" / "workflows" + write(root, ".github/workflows/bad.yml", "uses: actions/checkout@v4\n") + write(root, "uses.txt", "uses: actions/checkout@v4\n") + write( + root, + ".github/workflows/leak.yml", + "COPILOT_GITHUB_TOKEN: nope\n", + ) + errors = contract._validate_action_pins(work) + self.assertTrue( + any("must use a full commit SHA" in e for e in errors), + "missing action-pin diagnostic", + ) + self.assertTrue( + any("COPILOT_GITHUB_TOKEN is prohibited" in e for e in errors), + "missing workflow COPILOT prohibition", + ) + + +if __name__ == "__main__": # pragma: no cover + unittest.main() \ No newline at end of file