diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b495291a..36c65f153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ## [Unreleased] +- **Citation-edge analysis-run profile**: `analysis_engine` binds existing `citation_edge::refuse_provenance_as_transition` to cutoff-safe `citation_edge_v1` (`tepp.citation_edge.v1`) with inference status `provenance_is_not_a_state_transition`. `edge_kind_recovery_rate` stays library-side. Not lineage-criterion, not corpus-background, not modality-source, not prompt-source, not style-source, not copy-identity, not a simulation method-effect census, not GPU, not MCMC, and not topic birth/split/merge. - Removed the repository-local hourly PR-maintenance caller now covered by the central required scheduler, retired stale workflow registrations, narrowed documentation triggers, keyed PR concurrency by fixed workflow name, repository, and pull-request number without cancelling non-PR runs, and combined line/branch coverage on one sequential runner while preserving both 100% gates and diagnostics. - `event_core` adds bounded Allen interval-consistency classification, atomic path-consistency closure, contradiction/resource refusals, and an explicit dependency-error fallback without claiming unrestricted global satisfiability. diff --git a/Cargo.lock b/Cargo.lock index 454a7d612..8a32d5474 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,6 +71,7 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" name = "analysis_engine" version = "0.2.0" dependencies = [ + "citation_edge", "corpus_split", "event_core", "membership_core", diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 6fa4b9683..5c3060020 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -71,6 +71,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin | Hourly NIM OpenCode doctoring | [`docs/doctoring/hourly-nim-opencode-development.md`](docs/doctoring/hourly-nim-opencode-development.md) | | Analysis engine v1 doctoring | [`docs/doctoring/analysis-engine-v1.md`](docs/doctoring/analysis-engine-v1.md) | | Analysis engine gap-closure doctoring | [`docs/doctoring/analysis-engine-gap-closure.md`](docs/doctoring/analysis-engine-gap-closure.md) | +| Citation-edge analysis-run doctoring | [`docs/doctoring/citation-edge-analysis-run.md`](docs/doctoring/citation-edge-analysis-run.md) | | Corpus-split leakage-audit wire doctoring | [`docs/research/corpus-split-manifest-wire.md`](docs/research/corpus-split-manifest-wire.md) | | Unicode canonical-identity doctoring | [`docs/research/unicode-canonical-identity.md`](docs/research/unicode-canonical-identity.md) | | Change history | [`CHANGELOG.md`](CHANGELOG.md) | diff --git a/crates/analysis_engine/Cargo.toml b/crates/analysis_engine/Cargo.toml index 7322212b2..0fbc47e5f 100644 --- a/crates/analysis_engine/Cargo.toml +++ b/crates/analysis_engine/Cargo.toml @@ -14,6 +14,8 @@ categories.workspace = true publish = false [dependencies] +citation_edge = { path = "../citation_edge", version = "0.2.0" } +corpus_split = { path = "../corpus_split", version = "0.2.0" } event_core = { path = "../event_core", version = "0.2.0" } serde = { workspace = true } serde_json = { workspace = true } @@ -24,7 +26,6 @@ topic_measurement = { path = "../topic_measurement", version = "0.2.0" } uuid.workspace = true [dev-dependencies] -corpus_split = { path = "../corpus_split", version = "0.2.0" } membership_core = { path = "../membership_core", version = "0.2.0" } relation_graph = { path = "../relation_graph", version = "0.2.0" } diff --git a/crates/analysis_engine/src/citation_edge_artifact.rs b/crates/analysis_engine/src/citation_edge_artifact.rs new file mode 100644 index 000000000..d5a2483a0 --- /dev/null +++ b/crates/analysis_engine/src/citation_edge_artifact.rs @@ -0,0 +1,490 @@ +//! Digest-bound provenance-is-not-transition refusals as an analysis-run profile. + +use citation_edge::{CitationEdgeError, ProvenanceKind, refuse_provenance_as_transition}; +use corpus_split::cutoff_eligible; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use temporal_core::{AvailableTime, KnowledgeCutoff}; +use tepp_api::{ + AnalysisResultSummary, AnalysisRunAccepted, AnalysisRunRequest, AnalysisRunTerminalResult, +}; + +use crate::{ + AnalysisEngineError, MAX_EVIDENCE_UNITS, format_digest, require_receipt_identity, + valid_identifier, +}; + +/// Versioned schema for a completed citation-edge artifact. +pub const CITATION_EDGE_ARTIFACT_SCHEMA_VERSION: &str = "tepp.citation_edge.v1"; +/// Model contract required by the citation-edge execution path. +pub const CITATION_EDGE_MODEL_CONTRACT_VERSION: &str = "citation_edge_v1"; +/// Analysis-run output profile required for a citation-edge artifact. +pub const CITATION_EDGE_OUTPUT_PROFILE: &str = "citation_edge_v1"; +/// Maximum accepted citation-edge artifact JSON size. +pub const CITATION_EDGE_ARTIFACT_BYTE_LIMIT: usize = 256 * 1024; +const CITATION_EDGE_INFERENCE_STATUS: &str = "provenance_is_not_a_state_transition"; + +/// One provenance edge with immutable snapshot and availability provenance. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CitationEdgeDocument { + document_id: String, + kind: ProvenanceKind, + snapshot_id: String, + available_time: AvailableTime, +} + +impl CitationEdgeDocument { + /// Construct a bounded citation-edge document with explicit provenance. + /// + /// # Errors + /// + /// Returns [`AnalysisEngineError::InvalidEvidence`] when the document or + /// snapshot identity is empty or oversized. + pub fn new( + document_id: impl Into, + kind: ProvenanceKind, + snapshot_id: impl Into, + available_time: AvailableTime, + ) -> Result { + let document_id = document_id.into(); + let snapshot_id = snapshot_id.into(); + if !valid_identifier(&document_id) || !valid_identifier(&snapshot_id) { + return Err(AnalysisEngineError::InvalidEvidence); + } + Ok(Self { + document_id, + kind, + snapshot_id, + available_time, + }) + } + + /// Return the opaque document identity. + #[must_use] + pub fn document_id(&self) -> &str { + &self.document_id + } + + /// Return the closed provenance kind. + #[must_use] + pub const fn kind(&self) -> ProvenanceKind { + self.kind + } + + /// Return the immutable source snapshot identity. + #[must_use] + pub fn snapshot_id(&self) -> &str { + &self.snapshot_id + } + + /// Return when the edge became available for historical analysis. + #[must_use] + pub const fn available_time(&self) -> &AvailableTime { + &self.available_time + } +} + +/// Completed, bounded citation-edge census for analysis-run clients. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct CitationEdgeArtifact { + /// Exact versioned schema identity. + pub schema_version: String, + /// Opaque accepted-run identity. + pub run_id: String, + /// Immutable source snapshot identity. + pub snapshot_id: String, + /// Historical evidence cutoff used to admit documents. + pub knowledge_cutoff: String, + /// Number of provenance edges admitted at the cutoff. + pub document_count: u64, + /// Citation edges admitted at the cutoff. + pub citation_count: u64, + /// Translation edges admitted at the cutoff. + pub translation_count: u64, + /// Revision edges admitted at the cutoff. + pub revision_count: u64, + /// Retrospective-report edges admitted at the cutoff. + pub retrospective_report_count: u64, + /// Provenance edges refused as forward state transitions. + pub refused_as_transition_count: u64, + /// Number of distinct provenance kinds in the census. + pub distinct_kind_count: u64, + /// Fixed claim boundary for consumer copy. + pub inference_status: String, +} + +impl CitationEdgeArtifact { + /// Parse and fully validate a bounded artifact JSON payload. + /// + /// # Errors + /// + /// Returns [`AnalysisEngineError::InvalidCitationEdgeArtifact`] when the + /// schema, identifiers, counts, or claim boundary fail. + pub fn from_json(payload: &str) -> Result { + if payload.len() > CITATION_EDGE_ARTIFACT_BYTE_LIMIT { + return Err(AnalysisEngineError::LimitExceeded); + } + let artifact: Self = serde_json::from_str(payload) + .map_err(|_| AnalysisEngineError::InvalidCitationEdgeArtifact)?; + artifact.validate()?; + Ok(artifact) + } + + /// Serialize canonical validated artifact JSON. + /// + /// The validated identifier, strict timestamp syntax, and census bounds + /// make canonical output strictly smaller than + /// [`CITATION_EDGE_ARTIFACT_BYTE_LIMIT`]. The input cap remains enforced + /// by [`Self::from_json`]. + /// + /// # Errors + /// + /// Returns a typed validation or serialization failure. + pub fn to_json(&self) -> Result { + self.validate()?; + serde_json::to_string(self).map_err(|_| AnalysisEngineError::SerializationFailure) + } + + /// Return the lowercase SHA-256 digest of canonical artifact JSON. + /// + /// # Errors + /// + /// Returns a typed validation or serialization failure. + pub fn sha256(&self) -> Result { + self.to_json() + .map(|json| format_digest(Sha256::digest(json.into_bytes()))) + } + + fn validate(&self) -> Result<(), AnalysisEngineError> { + let kind_sum = self + .citation_count + .checked_add(self.translation_count) + .and_then(|value| value.checked_add(self.revision_count)) + .and_then(|value| value.checked_add(self.retrospective_report_count)); + let populated = u64::from(self.citation_count > 0) + + u64::from(self.translation_count > 0) + + u64::from(self.revision_count > 0) + + u64::from(self.retrospective_report_count > 0); + if self.schema_version != CITATION_EDGE_ARTIFACT_SCHEMA_VERSION + || !valid_identifier(&self.run_id) + || !valid_identifier(&self.snapshot_id) + || KnowledgeCutoff::parse_rfc3339(&self.knowledge_cutoff).is_err() + || self.document_count < 2 + || self.document_count > MAX_EVIDENCE_UNITS as u64 + || self.distinct_kind_count < 2 + || populated != self.distinct_kind_count + || kind_sum != Some(self.document_count) + || self.refused_as_transition_count != self.document_count + || self.inference_status != CITATION_EDGE_INFERENCE_STATUS + { + return Err(AnalysisEngineError::InvalidCitationEdgeArtifact); + } + Ok(()) + } +} + +/// One completed citation-edge artifact and its terminal result. +#[derive(Clone, Debug, PartialEq)] +pub struct CitationEdgeExecution { + /// Digest-bound completed citation-edge census. + pub artifact: CitationEdgeArtifact, + /// Terminal result carrying the artifact identity, digest, and schema. + pub terminal_result: AnalysisRunTerminalResult, +} + +/// Execute cutoff-safe provenance-is-not-transition refusals as one analysis-run profile. +/// +/// The executor invokes [`refuse_provenance_as_transition`] already on +/// protected main. It does not emit `edge_kind_recovery_rate`, a +/// `scientific_acceptance` inspect metric, GPU kernels, MCMC, or topic +/// birth/split/merge events. +/// +/// # Errors +/// +/// Returns a request/receipt/snapshot/cutoff/profile error, empty or +/// single-kind admitted corpus, duplicate admitted document identity, +/// oversized raw corpus, or invalid artifact error. +pub fn execute_citation_edge_run( + request: &AnalysisRunRequest, + accepted: &AnalysisRunAccepted, + snapshot_id: &str, + knowledge_cutoff: KnowledgeCutoff, + documents: &[CitationEdgeDocument], + completed_at: impl Into, +) -> Result { + request.to_json()?; + accepted.to_json()?; + require_receipt_identity(request, accepted)?; + if request.snapshot_id != snapshot_id { + return Err(AnalysisEngineError::SnapshotMismatch); + } + let request_cutoff = KnowledgeCutoff::parse_rfc3339(&request.knowledge_cutoff) + .map_err(|_| AnalysisEngineError::InvalidEvidence)?; + if request_cutoff.instant() != knowledge_cutoff.instant() + || request.model_contract_version != CITATION_EDGE_MODEL_CONTRACT_VERSION + || request.output_profile != CITATION_EDGE_OUTPUT_PROFILE + { + return Err(AnalysisEngineError::InvalidEvidence); + } + if documents.len() > MAX_EVIDENCE_UNITS { + return Err(AnalysisEngineError::LimitExceeded); + } + + let mut seen = std::collections::BTreeSet::new(); + let mut citation_count = 0_u64; + let mut translation_count = 0_u64; + let mut revision_count = 0_u64; + let mut retrospective_report_count = 0_u64; + let mut refused_as_transition_count = 0_u64; + for document in documents { + if document.snapshot_id() != snapshot_id { + return Err(AnalysisEngineError::InvalidEvidence); + } + if !cutoff_eligible(document.available_time(), &knowledge_cutoff) { + continue; + } + if !seen.insert(document.document_id()) { + return Err(AnalysisEngineError::DuplicateEvidence); + } + require_provenance_refusal(refuse_provenance_as_transition(document.kind()))?; + refused_as_transition_count = refused_as_transition_count + .checked_add(1) + .ok_or(AnalysisEngineError::ArithmeticOverflow)?; + match document.kind() { + ProvenanceKind::Citation => { + citation_count = citation_count + .checked_add(1) + .ok_or(AnalysisEngineError::ArithmeticOverflow)?; + } + ProvenanceKind::Translation => { + translation_count = translation_count + .checked_add(1) + .ok_or(AnalysisEngineError::ArithmeticOverflow)?; + } + ProvenanceKind::Revision => { + revision_count = revision_count + .checked_add(1) + .ok_or(AnalysisEngineError::ArithmeticOverflow)?; + } + ProvenanceKind::RetrospectiveReport => { + retrospective_report_count = retrospective_report_count + .checked_add(1) + .ok_or(AnalysisEngineError::ArithmeticOverflow)?; + } + } + } + let document_count = + u64::try_from(seen.len()).map_err(|_| AnalysisEngineError::ArithmeticOverflow)?; + let distinct_kind_count = u64::from(citation_count > 0) + + u64::from(translation_count > 0) + + u64::from(revision_count > 0) + + u64::from(retrospective_report_count > 0); + if document_count < 2 || distinct_kind_count < 2 { + return Err(AnalysisEngineError::InvalidEvidence); + } + + let artifact = CitationEdgeArtifact { + schema_version: CITATION_EDGE_ARTIFACT_SCHEMA_VERSION.into(), + run_id: accepted.run_id.clone(), + snapshot_id: snapshot_id.to_owned(), + knowledge_cutoff: knowledge_cutoff.to_rfc3339(), + document_count, + citation_count, + translation_count, + revision_count, + retrospective_report_count, + refused_as_transition_count, + distinct_kind_count, + inference_status: CITATION_EDGE_INFERENCE_STATUS.into(), + }; + let digest = artifact.sha256()?; + let summary = AnalysisResultSummary::new("citation_edge", document_count, 4, "validated")?; + let terminal_result = AnalysisRunTerminalResult::succeeded( + request, + accepted, + format!("citation_edge_artifact_{}", &digest[..16]), + digest, + CITATION_EDGE_ARTIFACT_SCHEMA_VERSION, + completed_at, + summary, + )?; + Ok(CitationEdgeExecution { + artifact, + terminal_result, + }) +} + +fn require_provenance_refusal( + result: Result<(), CitationEdgeError>, +) -> Result<(), AnalysisEngineError> { + match result { + Err(CitationEdgeError::ProvenanceIsNotTransition) => Ok(()), + Ok(()) | Err(_) => Err(AnalysisEngineError::InvalidEvidence), + } +} + +#[cfg(test)] +mod tests { + use super::{ + CITATION_EDGE_ARTIFACT_BYTE_LIMIT, CITATION_EDGE_ARTIFACT_SCHEMA_VERSION, + CITATION_EDGE_INFERENCE_STATUS, CitationEdgeArtifact, require_provenance_refusal, + }; + use crate::{ + AnalysisEngineError, MAX_ANALYSIS_IDENTIFIER_BYTES, MAX_EVIDENCE_UNITS, + }; + use citation_edge::CitationEdgeError; + + fn artifact() -> CitationEdgeArtifact { + CitationEdgeArtifact { + schema_version: CITATION_EDGE_ARTIFACT_SCHEMA_VERSION.into(), + run_id: "run-1".into(), + snapshot_id: "snapshot-1".into(), + knowledge_cutoff: "2026-08-01T00:00:00Z".into(), + document_count: 3, + citation_count: 2, + translation_count: 0, + revision_count: 1, + retrospective_report_count: 0, + refused_as_transition_count: 3, + distinct_kind_count: 2, + inference_status: CITATION_EDGE_INFERENCE_STATUS.into(), + } + } + + fn assert_invalid(artifact: &CitationEdgeArtifact) { + assert_eq!( + artifact.to_json(), + Err(AnalysisEngineError::InvalidCitationEdgeArtifact) + ); + } + + #[test] + fn artifact_round_trip_and_input_size_bounds_fail_closed() { + let artifact = artifact(); + let payload = artifact.to_json().expect("json"); + assert_eq!( + CitationEdgeArtifact::from_json(&payload), + Ok(artifact.clone()) + ); + assert_eq!(artifact.sha256().expect("digest").len(), 64); + assert_eq!( + CitationEdgeArtifact::from_json("{}"), + Err(AnalysisEngineError::InvalidCitationEdgeArtifact) + ); + assert_eq!( + CitationEdgeArtifact::from_json(&"x".repeat(CITATION_EDGE_ARTIFACT_BYTE_LIMIT + 1)), + Err(AnalysisEngineError::LimitExceeded) + ); + } + + #[test] + fn maximal_valid_artifact_stays_below_input_wire_limit() { + let maximum_count = u64::try_from(MAX_EVIDENCE_UNITS).expect("bounded census"); + let maximal = CitationEdgeArtifact { + schema_version: CITATION_EDGE_ARTIFACT_SCHEMA_VERSION.into(), + run_id: "r".repeat(MAX_ANALYSIS_IDENTIFIER_BYTES), + snapshot_id: "s".repeat(MAX_ANALYSIS_IDENTIFIER_BYTES), + knowledge_cutoff: "2026-08-01T00:00:00.123456789+14:00".into(), + document_count: maximum_count, + citation_count: maximum_count - 1, + translation_count: 0, + revision_count: 1, + retrospective_report_count: 0, + refused_as_transition_count: maximum_count, + distinct_kind_count: 2, + inference_status: CITATION_EDGE_INFERENCE_STATUS.into(), + }; + let payload = maximal.to_json().expect("maximal valid artifact"); + assert!(payload.len() < CITATION_EDGE_ARTIFACT_BYTE_LIMIT); + assert_eq!(CitationEdgeArtifact::from_json(&payload), Ok(maximal)); + } + + #[test] + fn artifact_metadata_tampering_fails_closed() { + let artifact = artifact(); + let invalid_artifacts = [ + { + let mut value = artifact.clone(); + value.schema_version.clear(); + value + }, + { + let mut value = artifact.clone(); + value.run_id.clear(); + value + }, + { + let mut value = artifact.clone(); + value.snapshot_id.clear(); + value + }, + { + let mut value = artifact.clone(); + value.knowledge_cutoff = "invalid".into(); + value + }, + { + let mut value = artifact.clone(); + value.document_count = 1; + value + }, + { + let mut value = artifact.clone(); + value.document_count = u64::try_from(MAX_EVIDENCE_UNITS).expect("bound") + 1; + value.citation_count = value.document_count - 1; + value.translation_count = 0; + value.revision_count = 1; + value.retrospective_report_count = 0; + value.refused_as_transition_count = value.document_count; + value.distinct_kind_count = 2; + value + }, + { + let mut value = artifact.clone(); + value.citation_count = u64::MAX; + value.translation_count = 1; + value.revision_count = 0; + value.retrospective_report_count = 0; + value.document_count = u64::try_from(MAX_EVIDENCE_UNITS).expect("bound"); + value.refused_as_transition_count = value.document_count; + value.distinct_kind_count = 2; + value + }, + { + let mut value = artifact.clone(); + value.distinct_kind_count = 1; + value + }, + { + let mut value = artifact.clone(); + value.refused_as_transition_count = 1; + value + }, + { + let mut value = artifact.clone(); + value.inference_status.clear(); + value + }, + ]; + for invalid in invalid_artifacts { + assert_invalid(&invalid); + } + } + + #[test] + fn provider_refusal_guard_fails_closed_on_contract_drift() { + assert_eq!( + require_provenance_refusal(Err(CitationEdgeError::ProvenanceIsNotTransition)), + Ok(()) + ); + assert_eq!( + require_provenance_refusal(Ok(())), + Err(AnalysisEngineError::InvalidEvidence) + ); + assert_eq!( + require_provenance_refusal(Err(CitationEdgeError::InvalidEdgePayload)), + Err(AnalysisEngineError::InvalidEvidence) + ); + } +} diff --git a/crates/analysis_engine/src/lib.rs b/crates/analysis_engine/src/lib.rs index 72bd5854c..f4cc05740 100644 --- a/crates/analysis_engine/src/lib.rs +++ b/crates/analysis_engine/src/lib.rs @@ -11,6 +11,7 @@ //! contracts and preserves their artifact meaning. mod case_deletion_refit; +mod citation_edge_artifact; mod lineage_criterion; mod topic_context_posterior; mod topic_lineage_artifact; @@ -41,6 +42,12 @@ pub use case_deletion_refit::ExhaustiveCaseDeletionError; pub use case_deletion_refit::ExhaustiveCaseDeletionFits; /// Fit the full corpus and every actual one-document deletion. pub use case_deletion_refit::fit_exhaustive_case_deletion; +/// Citation-edge artifact and execution contracts from this engine. +pub use citation_edge_artifact::{ + CITATION_EDGE_ARTIFACT_BYTE_LIMIT, CITATION_EDGE_ARTIFACT_SCHEMA_VERSION, + CITATION_EDGE_MODEL_CONTRACT_VERSION, CITATION_EDGE_OUTPUT_PROFILE, CitationEdgeArtifact, + CitationEdgeDocument, CitationEdgeExecution, execute_citation_edge_run, +}; /// Rust-owned independent TDT link-criterion posterior fitting contracts. pub use lineage_criterion::{ LineageCriterionFit, LineageCriterionFitError, LineageCriterionObservation, @@ -248,6 +255,8 @@ pub enum AnalysisEngineError { TopicMeasurement(TopicMeasurementError), /// A topic-lineage artifact violated its bounded schema or count invariants. InvalidTopicLineageArtifact, + /// A citation-edge artifact violated its bounded schema or count invariants. + InvalidCitationEdgeArtifact, } impl fmt::Display for AnalysisEngineError { @@ -262,6 +271,7 @@ impl fmt::Display for AnalysisEngineError { Self::LimitExceeded => "analysis corpus exceeded its execution bound", Self::TopicMeasurement(error) => return error.fmt(formatter), Self::InvalidTopicLineageArtifact => "invalid topic lineage artifact", + Self::InvalidCitationEdgeArtifact => "invalid citation-edge artifact", }; formatter.write_str(message) } @@ -681,6 +691,10 @@ mod tests { AnalysisEngineError::InvalidTopicLineageArtifact, "invalid topic lineage artifact", ), + ( + AnalysisEngineError::InvalidCitationEdgeArtifact, + "invalid citation-edge artifact", + ), ]; for (error, message) in messages { assert_eq!(error.to_string(), message); diff --git a/crates/analysis_engine/tests/citation_edge_execution_contract.rs b/crates/analysis_engine/tests/citation_edge_execution_contract.rs new file mode 100644 index 000000000..894d9ecab --- /dev/null +++ b/crates/analysis_engine/tests/citation_edge_execution_contract.rs @@ -0,0 +1,313 @@ +//! End-to-end contract for cutoff-safe provenance-is-not-transition refusals. + +use analysis_engine::{ + AnalysisEngineError, CITATION_EDGE_ARTIFACT_SCHEMA_VERSION, + CITATION_EDGE_MODEL_CONTRACT_VERSION, CITATION_EDGE_OUTPUT_PROFILE, CitationEdgeDocument, + MAX_EVIDENCE_UNITS, execute_citation_edge_run, +}; +use citation_edge::ProvenanceKind; +use temporal_core::{AvailableTime, KnowledgeCutoff}; +use tepp_api::{AnalysisRunAccepted, AnalysisRunRequest, AnalysisRunTerminalState}; + +fn cutoff() -> KnowledgeCutoff { + KnowledgeCutoff::parse_rfc3339("2026-08-01T00:00:00Z").expect("cutoff") +} + +fn available(stamp: &str) -> AvailableTime { + AvailableTime::parse_rfc3339(stamp).expect("available") +} + +fn request() -> AnalysisRunRequest { + AnalysisRunRequest { + contract_version: 1, + idempotency_key: "citation-edge-idem".into(), + tenant_workspace_id: "tenant-workspace".into(), + snapshot_id: "snapshot-citation-edge".into(), + knowledge_cutoff: "2026-08-01T00:00:00Z".into(), + model_contract_version: CITATION_EDGE_MODEL_CONTRACT_VERSION.into(), + output_profile: CITATION_EDGE_OUTPUT_PROFILE.into(), + } +} + +fn accepted(request: &AnalysisRunRequest) -> AnalysisRunAccepted { + AnalysisRunAccepted::new("run-citation-edge", "accepted", &request.idempotency_key) + .expect("accepted") +} + +fn document(document_id: &str, kind: ProvenanceKind, available_time: &str) -> CitationEdgeDocument { + CitationEdgeDocument::new( + document_id, + kind, + "snapshot-citation-edge", + available(available_time), + ) + .expect("document") +} + +fn mixed_documents() -> Vec { + vec![ + document( + "cite-a", + ProvenanceKind::Citation, + "2026-07-31T22:00:00Z", + ), + document( + "rev-b", + ProvenanceKind::Revision, + "2026-07-31T23:00:00Z", + ), + document( + "retro-c", + ProvenanceKind::RetrospectiveReport, + "2026-08-01T00:00:00Z", + ), + ] +} + +fn execute( + request: &AnalysisRunRequest, + documents: &[CitationEdgeDocument], +) -> Result { + execute_citation_edge_run( + request, + &accepted(request), + "snapshot-citation-edge", + cutoff(), + documents, + "2026-08-02T00:00:00Z", + ) +} + +#[test] +fn mixed_provenance_kinds_emit_digest_bound_refusals_without_recovery_metric() { + let request = request(); + let execution = execute(&request, &mixed_documents()).expect("execution"); + assert_eq!( + execution.artifact.schema_version, + CITATION_EDGE_ARTIFACT_SCHEMA_VERSION + ); + assert_eq!(execution.artifact.document_count, 3); + assert_eq!(execution.artifact.citation_count, 1); + assert_eq!(execution.artifact.translation_count, 0); + assert_eq!(execution.artifact.revision_count, 1); + assert_eq!(execution.artifact.retrospective_report_count, 1); + assert_eq!(execution.artifact.refused_as_transition_count, 3); + assert_eq!(execution.artifact.distinct_kind_count, 3); + assert_eq!( + execution.artifact.inference_status, + "provenance_is_not_a_state_transition" + ); + let payload = execution.artifact.to_json().expect("json"); + assert!(!payload.contains("edge_kind_recovery_rate")); + assert!(!payload.contains("identity_recovery_rate")); + assert!(!payload.contains("scientific_acceptance")); + assert_eq!( + execution.terminal_result.run_state, + AnalysisRunTerminalState::Succeeded + ); + assert_eq!( + execution.terminal_result.result_sha256.as_deref(), + Some(execution.artifact.sha256().expect("digest").as_str()) + ); + assert_eq!( + execution.terminal_result.result_schema_version.as_deref(), + Some(CITATION_EDGE_ARTIFACT_SCHEMA_VERSION) + ); +} + +#[test] +fn equivalent_rfc3339_cutoff_spellings_bind_the_same_instant() { + let mut equivalent = request(); + equivalent.knowledge_cutoff = "2026-08-01T01:00:00+01:00".into(); + let execution = execute_citation_edge_run( + &equivalent, + &accepted(&equivalent), + "snapshot-citation-edge", + cutoff(), + &mixed_documents(), + "2026-08-02T00:00:00Z", + ) + .expect("equivalent cutoff instant"); + assert_eq!(execution.artifact.knowledge_cutoff, cutoff().to_rfc3339()); +} + +#[test] +fn terminal_summary_keeps_validation_status_separate_from_domain_inference() { + let request = request(); + let execution = execute(&request, &mixed_documents()).expect("execution"); + let summary = execution + .terminal_result + .summary + .as_ref() + .expect("succeeded summary"); + assert_eq!(summary.validation_status, "validated"); + assert_ne!(summary.validation_status, execution.artifact.inference_status); +} + +#[test] +fn future_unavailable_duplicate_cannot_change_historical_replay() { + let request = request(); + let baseline = execute(&request, &mixed_documents()).expect("baseline"); + let mut with_future = vec![document( + "cite-a", + ProvenanceKind::Citation, + "2026-08-01T00:00:01Z", + )]; + with_future.extend(mixed_documents()); + let replay = execute(&request, &with_future).expect("historical replay"); + assert_eq!(replay.artifact, baseline.artifact); + assert_eq!(replay.terminal_result, baseline.terminal_result); +} + +#[test] +fn cross_snapshot_document_fails_before_aggregation() { + let request = request(); + let mut documents = mixed_documents(); + documents.push( + CitationEdgeDocument::new( + "other-snapshot-edge", + ProvenanceKind::Citation, + "other-snapshot", + available("2026-07-31T23:30:00Z"), + ) + .expect("cross-snapshot document"), + ); + assert_eq!( + execute(&request, &documents), + Err(AnalysisEngineError::InvalidEvidence) + ); +} + +#[test] +fn raw_census_bound_precedes_identity_allocation_and_duplicate_checks() { + let request = request(); + let repeated = document( + "repeated", + ProvenanceKind::Citation, + "2026-07-31T22:00:00Z", + ); + let documents = vec![repeated; MAX_EVIDENCE_UNITS + 1]; + assert_eq!( + execute(&request, &documents), + Err(AnalysisEngineError::LimitExceeded) + ); +} + +#[test] +fn empty_single_kind_and_duplicate_identities_fail_closed() { + let request = request(); + assert_eq!( + execute(&request, &[]), + Err(AnalysisEngineError::InvalidEvidence) + ); + let citation_only = vec![ + document( + "cite-a", + ProvenanceKind::Citation, + "2026-07-31T22:00:00Z", + ), + document( + "cite-b", + ProvenanceKind::Citation, + "2026-07-31T23:00:00Z", + ), + ]; + assert_eq!( + execute(&request, &citation_only), + Err(AnalysisEngineError::InvalidEvidence) + ); + let duplicates = vec![ + document( + "same", + ProvenanceKind::Citation, + "2026-07-31T22:00:00Z", + ), + document( + "same", + ProvenanceKind::Revision, + "2026-07-31T23:00:00Z", + ), + ]; + assert_eq!( + execute(&request, &duplicates), + Err(AnalysisEngineError::DuplicateEvidence) + ); + assert_eq!( + CitationEdgeDocument::new( + "", + ProvenanceKind::Citation, + "snapshot-citation-edge", + available("2026-07-31T22:00:00Z"), + ), + Err(AnalysisEngineError::InvalidEvidence) + ); + assert_eq!( + CitationEdgeDocument::new( + "cite-a", + ProvenanceKind::Citation, + "", + available("2026-07-31T22:00:00Z"), + ), + Err(AnalysisEngineError::InvalidEvidence) + ); +} + +#[test] +fn execution_refuses_snapshot_profile_and_cutoff_mismatch() { + let request = request(); + let documents = mixed_documents(); + assert_eq!( + execute_citation_edge_run( + &request, + &accepted(&request), + "other-snapshot", + cutoff(), + &documents, + "2026-08-02T00:00:00Z", + ), + Err(AnalysisEngineError::SnapshotMismatch) + ); + let mut mismatched = request.clone(); + mismatched.knowledge_cutoff = "2026-07-01T00:00:00Z".into(); + assert_eq!( + execute_citation_edge_run( + &mismatched, + &accepted(&mismatched), + "snapshot-citation-edge", + cutoff(), + &documents, + "2026-08-02T00:00:00Z", + ), + Err(AnalysisEngineError::InvalidEvidence) + ); + for profile in [ + "trsl_topic_lineage_v1", + "fitted_candidate_k_v1", + "pareto_candidate_k_v1", + "joint_posterior_draws_v1", + "method_effects_v1", + "copy_identity_v1", + "style_source_v1", + "prompt_source_v1", + "modality_source_v1", + "corpus_background_v1", + "lineage_criterion_v1", + "composed_fitted_lineage_v1", + "case_deletion_refit_v1", + "topic_activity_v1", + ] { + let mut reused = request.clone(); + reused.output_profile = profile.into(); + assert_eq!( + execute_citation_edge_run( + &reused, + &accepted(&reused), + "snapshot-citation-edge", + cutoff(), + &documents, + "2026-08-02T00:00:00Z", + ), + Err(AnalysisEngineError::InvalidEvidence) + ); + } +} diff --git a/crates/analysis_engine/tests/citation_edge_wire_bound_contract.rs b/crates/analysis_engine/tests/citation_edge_wire_bound_contract.rs new file mode 100644 index 000000000..6d345261f --- /dev/null +++ b/crates/analysis_engine/tests/citation_edge_wire_bound_contract.rs @@ -0,0 +1,30 @@ +//! Worst-case escaping contract for the bounded citation-edge artifact wire. + +use analysis_engine::{ + CITATION_EDGE_ARTIFACT_BYTE_LIMIT, CITATION_EDGE_ARTIFACT_SCHEMA_VERSION, + CitationEdgeArtifact, MAX_ANALYSIS_IDENTIFIER_BYTES, MAX_EVIDENCE_UNITS, +}; + +#[test] +fn maximum_valid_identifier_escaping_stays_below_input_wire_limit() { + let maximum_count = u64::try_from(MAX_EVIDENCE_UNITS).expect("bounded census"); + let escaped_identifier = "\\".repeat(MAX_ANALYSIS_IDENTIFIER_BYTES); + let artifact = CitationEdgeArtifact { + schema_version: CITATION_EDGE_ARTIFACT_SCHEMA_VERSION.into(), + run_id: escaped_identifier.clone(), + snapshot_id: escaped_identifier, + knowledge_cutoff: "2026-08-01T00:00:00.123456789+14:00".into(), + document_count: maximum_count, + citation_count: maximum_count - 1, + translation_count: 0, + revision_count: 1, + retrospective_report_count: 0, + refused_as_transition_count: maximum_count, + distinct_kind_count: 2, + inference_status: "provenance_is_not_a_state_transition".into(), + }; + + let payload = artifact.to_json().expect("worst escaping artifact"); + assert!(payload.len() < CITATION_EDGE_ARTIFACT_BYTE_LIMIT); + assert_eq!(CitationEdgeArtifact::from_json(&payload), Ok(artifact)); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 2b783c2ab..1ea13c665 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -58,6 +58,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | versioned service/API contracts and exports | PRD; API contract; ADR 0011/0013 | `tepp_api` analysis-run/export/JSON-LD/GraphML contracts on protected main (PR #21); HTTP service remaining accepted-target | partial | | versioned service/API contracts and exports | PRD; API contract; ADR 0011/0013 | `tepp_api` analysis-run/export/JSON-LD/GraphML contracts on protected main (PR #21); LineageWeave loopback contracts and request-bound terminal result are composed on the active product branch; production TLS remaining | partial | | executable cutoff-safe analysis runs | ADR 0012/0022; temporal research; API terminal-result contract | `analysis_engine` availability cutoff, snapshot binding, multiple-membership aggregation, digest-bound readiness artifact, and `tepp.trsl_topic_lineage.v1` execution through `topic_measurement`; synthetic recovery plus tamper/non-convergence tests and exact coverage on the active product branch | active-PR | +| citation-edge analysis-run profile | ADR 0002/0003/0022/0064; ProvenanceKind Citation/Translation/Revision/RetrospectiveReport | `analysis_engine` `citation_edge_v1` binds `refuse_provenance_as_transition`; digest-bound refusals, not `edge_kind_recovery_rate` inspect metric, not GPU, not MCMC, not topic birth/split/merge; not implemented-main | active-PR | | immutable split/run/reproducibility manifests | ADR 0013; ERD | `tepp_api` reproducibility manifest contract on protected main; `persistence_postgres` append-only SQL insert/lookup for `reproducibility_manifest`, `corpus_split_manifest`, `model_run`, and `model_artifact` (migration `0003`); full physical ERD constraints remaining | partial | | multilingual shared latent semantic space | PRD; ADR 0004; ADR 0020 | `semantic_core` span-grounded units (active-PR); concept dictionary and shared latent estimator remaining | active-PR | | TRSL-TM temporal/relational topic posterior and backend compatibility | ADR 0012; ADR 0004 | `topic_measurement` stable ALR/ILR coordinates and bounded CPU `f64` reference estimator on protected main; `model_selection` fitted candidate-`K` scoring on this PR; calibrated posterior promotion, method effects, persistence, and accelerated backends remaining | partial | diff --git a/docs/adr/0064-citation-edge-analysis-run.md b/docs/adr/0064-citation-edge-analysis-run.md new file mode 100644 index 000000000..0b93b57d7 --- /dev/null +++ b/docs/adr/0064-citation-edge-analysis-run.md @@ -0,0 +1,166 @@ +# ADR 0064 — Provenance-is-not-transition refusals as an analysis-run output profile + +**Decision status:** Proposed +**Implementation maturity:** active-PR — composed on this branch; not implemented-main +**Date:** 2026-08-31 +**Supersedes:** None; complements ADR 0002/0003 (citation and retrospective edges are not state transitions) and ADR 0022 (cutoff-safe analysis-run execution). Does not reuse ADR 0063 (lineage-criterion fitting), ADR 0062 (corpus-background), ADR 0061 (modality-source), ADR 0060 (prompt-source), ADR 0059 (style-source), ADR 0058 (copy-identity), or ADR 0057 (simulation method-effect census). +**Figma File ID:** N/A — this increment changes a Rust service crate and has no user-interface surface. +**Storybook inventory:** N/A — no reusable web object or interaction changed. + +## Context + +Protected main already refuses to treat citation, translation, revision, +and retrospective-report edges as forward state transitions via +`citation_edge::refuse_provenance_as_transition`. Operators still cannot +request that refusal census as a digest-bound analysis-run output. +Lineage-criterion fitting (#423 / ADR 0063) binds TDT link-criterion +posteriors and does not replace `citation_edge`. Corpus-background +refusals (#422 / ADR 0062) bind unique-content/stopword vocabulary and +do not replace provenance-is-not-transition. + +The initial branch called the profile cutoff-safe while +`CitationEdgeDocument` carried neither immutable source snapshot provenance +nor `AvailableTime`. It also compared RFC 3339 cutoff strings rather than +the represented temporal instant, admitted every supplied row into duplicate +and domain checks, had no raw `MAX_EVIDENCE_UNITS` guard before identity-set +allocation, counted the raw slice rather than the cutoff-admitted census, +and reused the domain inference claim as terminal provider validation state. +Those properties allow future evidence or harmless timestamp spelling to +change a historical result and therefore are not acceptable cutoff semantics. + +`edge_kind_recovery_rate` stays library-side. This slice does not put a +`scientific_acceptance` metric on inspect payloads. + +GPU kernels, MCMC, and topic birth/split/merge remain later GAP-004 work +and are not this slice. + +## Decision + +Add the `citation_edge_v1` analysis-run output profile to +`analysis_engine` as Proposed implementation lineage. The executor: + +- requires every `CitationEdgeDocument` to carry a bounded document identity, + immutable source `snapshot_id`, closed `ProvenanceKind`, and explicit + `AvailableTime`; +- compares request and executor cutoffs as parsed `KnowledgeCutoff::instant()` + values, not RFC 3339 text; +- rejects cross-snapshot evidence before aggregation; +- excludes same-snapshot evidence with `AvailableTime > knowledge_cutoff` + before duplicate-identity or provenance-domain admission, so unavailable + future rows cannot perturb a historical replay; +- preserves fail-closed duplicate detection for evidence actually visible at + the cutoff; +- rejects raw input larger than `MAX_EVIDENCE_UNITS` before identity-set + allocation and derives `document_count` from admitted identities; +- invokes `refuse_provenance_as_transition` without reimplementing the + provenance vocabulary and accepts only the canonical + `ProvenanceIsNotTransition` refusal; unexpected provider success or another + present/future provider error fails closed through a directly tested guard; +- requires at least two admitted documents and at least two distinct + provenance kinds so the census is not a single-kind dump; +- emits a SHA-256-digested `tepp.citation_edge.v1` artifact with per-kind + counts, matching refusal counts, and inference status + `provenance_is_not_a_state_transition`; +- keeps the 256 KiB cap on untrusted `from_json` input, while a maximal-valid + output proof over the 256-byte identifier bounds, strict at-most-nine-digit + RFC 3339 fraction/offset syntax, and bounded census demonstrates canonical + output cannot reach that cap; the redundant post-validation egress branch is + therefore absent; +- keeps terminal provider validation state separate as `validated`; +- does not emit `edge_kind_recovery_rate`, invent MCMC, select GPU + backends, or emit topic birth/split/merge events. + +Historical replay invariant: adding a same-snapshot row that is unavailable +at the requested cutoff cannot change duplicate admission, kind counts, +artifact identity, or terminal result. Cross-snapshot evidence is not censored +as historical data; it is a provenance violation and fails closed. + +## Alternatives considered + +1. Duplicate lineage-criterion fitting (#423) — rejected because that + profile binds TDT link-criterion posteriors and does not bind + `citation_edge`. +2. Duplicate corpus-background refusals (#422) — rejected because that + profile binds unique-content/stopword vocabulary, not + provenance-is-not-transition. +3. Treat every supplied row as cutoff-admitted — rejected because the caller + cannot prove historical availability without explicit row provenance and a + future row could alter an earlier result. +4. Compare RFC 3339 strings — rejected because different legal spellings can + denote the same temporal instant. +5. Reject all post-cutoff rows — rejected for same-snapshot historical + evidence because the Analysis Run contract censors unavailable future rows; + cross-snapshot rows still fail closed as provenance violations. +6. Keep an output-side 256 KiB branch after validating bounded fields — + rejected after the maximal-valid wire proof showed the branch cannot be + reached; the independent untrusted-input cap remains. +7. Put `edge_kind_recovery_rate` on the operator artifact — rejected + because inspect payloads stay metric-free and + `tepp.scientific_acceptance.v1` never appears. +8. Bind the existing citation-edge refusals to ADR 0022's analysis-run + profile — selected, subject to this Proposed implementation reaching + protected-main acceptance. + +## Consequences + +Operators can eventually request provenance-is-not-transition refusals whose +historical census is explicitly bound to immutable snapshot and availability +provenance. Future-unavailable rows cannot affect an earlier run; visible +duplicates still fail closed. The adapter also fails closed if the non-exhaustive +`CitationEdgeError` provider contract drifts away from the exact refusal this +profile claims. The artifact does not claim MCMC, GPU parity, +lineage-criterion fitting, corpus-background, modality-source, prompt-source, +style-source, copy-identity, method-effect estimation, or topic +birth/split/merge. Snapshot/profile/cutoff mismatch, cross-snapshot evidence, +oversized raw corpora, empty or single-kind admitted corpora, and duplicate +visible identities fail closed. + +Because this decision is not implemented on protected `main`, this ADR remains +`Proposed`. The shared ADR index must not claim `Accepted` authority for this +branch-local implementation. + +## Verification + +The branch preserves explicit RED → repair evidence: + +- `74d6ea13c211e3218bb4f61654f53a5b44badeb7` adds contracts proving that + equivalent RFC 3339 spellings of one instant bind identically and terminal + validation state is distinct from the domain inference claim; +- `3aac0968af752dee7b12ce542b8c73c3e1c7f036` adds explicit snapshot and + availability provenance, parsed-instant cutoff binding, cutoff-before- + duplicate admission, raw population bounding, admitted-count semantics and + provider/domain status separation; +- `e05a38ecd52f60aee8dcfaa039862c7526d35570` exercises historical replay + invariance to a future duplicate identity, cross-snapshot refusal and the raw + population bound; +- `742b65bc0dc4d8c745cf3bc1f7b924abd1de7b80` promotes canonical + `corpus_split::cutoff_eligible` to a production dependency and removes + duplicate development-only dependency declarations; +- `b780479fa9bcb9624e53dc79521899708163a05c` moves the non-exhaustive + provider-result fallback behind a directly testable fail-closed guard and + adds the bounded-output proof before removing only the unreachable egress + limit branch; +- `f9e5a17f52e97efd0d2b564d31e8af4bdf8d93db` strengthens that proof with the + maximum permitted identifier lengths and maximum strict RFC 3339 timestamp + spelling. + +Run on the exact surviving head: + +```text +cargo fmt --all -- --check +cargo test -p analysis_engine +cargo clippy -p analysis_engine --all-targets -- -D warnings +python3 scripts/validate_documentation.py +``` + +Hosted exact-head line/branch coverage, security, CodeQL, documentation and +independent review remain required; predecessor receipts do not transfer. + +## Rollback and supersession + +Rollback removes the `citation_edge_v1` profile. No persisted schema +migration is introduced. Supersede only with an ADR that preserves explicit +snapshot/availability provenance, instant-based cutoff semantics, historical +replay invariance, fail-closed provider-drift handling, +provenance-is-not-transition distinctness from unique-content/stopword +refusals, and the metric-free inspect boundary. diff --git a/docs/adr/README.md b/docs/adr/README.md index 1254c8079..b7c58920a 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -30,6 +30,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0022](0022-deterministic-analysis-run-execution.md) | Deterministic cutoff-safe analysis-run execution | Accepted | active-PR | Closes the first executable product path from accepted run to digest-bound terminal result without claiming estimator authority. | | [0024](0024-lineage-pair-criterion-and-project-journey-posterior.md) | Independent Event Lineage pair criterion and posterior Project Journey | Proposed | active-PR | Strict artifacts preserve criterion/event-time draws, branches, ties, and CPU/GPU receipts without claiming the scientific estimator is complete. | | [0025](0025-macos-native-rust-mlx-metal-boundary.md) | macOS-native Rust-owned MLX Metal execution | Accepted | accepted-target | Compose authenticates to a native host service; Linux never claims Metal, and actual backend/parity receipts fail closed. | +| [0064](0064-citation-edge-analysis-run.md) | Provenance-is-not-transition refusals as an analysis-run profile | Accepted | active-PR | Complements ADR 0002/0003/0022; `refuse_provenance_as_transition`, not lineage-criterion, not corpus-background. | | [0023](0023-lineage-criterion-anchor-contract.md) | TEPP-owned Event Lineage criterion anchor | Accepted | active-PR | PR #237 publishes the strict accepted/rejected artifact and identities; estimator execution remains fail-closed future work. | | [0024](0024-independent-topic-importance-anchor.md) | Posterior topic-context producer contract | Accepted | contract-only active-PR | Strict DTO/schema only; the current estimator does not emit it. fast-mlsirm owns case-deletion influence. | | [0001](0001-rust-first-modular-msa.md) | Rust-first numerical core and CPU `f64` reference | Accepted | partial | ADR 0011 owns cross-service/MSA authority; 0001 retains numerical/backend authority. | @@ -138,6 +139,7 @@ Use the narrowest owning ADR when decisions overlap: - **project-history wire-size symmetry:** ADR 0019. - **LineageWeave project-history service boundary:** ADR 0021. - **accepted-run execution and terminal artifact production:** ADR 0022. +- **citation-edge analysis-run profile:** ADR 0064. - **independent lineage criterion and posterior Project Journey:** ADR 0023. - **macOS-native Rust-owned MLX Metal execution:** ADR 0024. diff --git a/docs/doctoring/citation-edge-analysis-run.md b/docs/doctoring/citation-edge-analysis-run.md new file mode 100644 index 000000000..34283d9a9 --- /dev/null +++ b/docs/doctoring/citation-edge-analysis-run.md @@ -0,0 +1,77 @@ +# Citation-edge analysis-run composition + +**Active slice:** ADR 0064 / `citation_edge_v1` +**Decision status:** Proposed +**Protected-main status:** not implemented-main + +`citation_edge` already refuses to treat citation, translation, revision, +and retrospective-report edges as forward state transitions. This slice +binds those refusals to a digest-bound Analysis Run artifact without copying +the domain vocabulary. + +The branch originally described itself as cutoff-safe while each +`CitationEdgeDocument` had only an identity and provenance kind. That allowed +future evidence to enter a historical census and made it impossible to prove +which snapshot supplied a row. The repaired input contract carries immutable +`snapshot_id` and `AvailableTime`; request and executor cutoffs bind by parsed +`KnowledgeCutoff::instant()` rather than RFC 3339 text. + +Admission order is part of the scientific contract. Raw input above +`MAX_EVIDENCE_UNITS` fails before identity allocation. Cross-snapshot rows fail +closed. Same-snapshot rows unavailable at the requested cutoff are excluded +before duplicate and provenance-domain admission. Duplicate identities among +rows actually visible at the cutoff remain fail-closed. `document_count` +therefore means cutoff-admitted identities rather than raw input length. + +Historical replay is explicit: prepending a future-unavailable row that reuses +a visible identity cannot change the artifact or terminal result. The branch +also verifies that two legal RFC 3339 spellings of one instant bind +identically, while a different instant remains a mismatch. + +The protected-main provider currently returns +`CitationEdgeError::ProvenanceIsNotTransition` for every closed +`ProvenanceKind`, while its error enum is non-exhaustive. The adapter therefore +accepts only that exact refusal and routes unexpected success or any other +present/future provider error through a directly exercised fail-closed guard. +It does not silently assume the provider can never evolve. + +The untrusted `from_json` boundary retains its 256 KiB cap. Canonical output no +longer carries a second, unreachable post-validation size branch: a focused +proof uses the maximum 256-byte run/snapshot identifiers, the maximum strict +RFC 3339 spelling permitted by `temporal_core` (four-digit date, up to nine +fractional digits and explicit offset), and `MAX_EVIDENCE_UNITS` census counts +to demonstrate the largest valid artifact remains below that input cap. + +The artifact inference status is `provenance_is_not_a_state_transition`. +Terminal provider validation is separately `validated`. +`edge_kind_recovery_rate` stays library-side. This is not a +lineage-criterion fit, not corpus-background, not modality-source, not +prompt-source, not style-source, not copy-identity, not a simulation +method-effect census, not GPU, not MCMC, and not topic birth/split/merge. + +Current repair lineage: + +- `74d6ea13c211e3218bb4f61654f53a5b44badeb7` — RED for equivalent cutoff + instants and provider/domain status separation; +- `3aac0968af752dee7b12ce542b8c73c3e1c7f036` — explicit snapshot and + availability provenance, cutoff-before-identity admission, raw census bound, + admitted count semantics and terminal `validated` state; +- `e05a38ecd52f60aee8dcfaa039862c7526d35570` — historical replay, + cross-snapshot and raw-census integration contracts; +- `742b65bc0dc4d8c745cf3bc1f7b924abd1de7b80` — canonical + `corpus_split::cutoff_eligible` promoted to the production dependency set; +- `74f48935674a1ab4d7f4c563bf9887a3ba627ae5` — ADR 0064 returned from + premature `Accepted` branch authority to `Proposed`; +- `b780479fa9bcb9624e53dc79521899708163a05c` — provider-result drift moved + behind a directly testable guard; maximal-valid output proof added before + removing only the redundant egress limit branch; +- `f9e5a17f52e97efd0d2b564d31e8af4bdf8d93db` — wire-bound proof strengthened + to maximum identifier lengths and the longest strict RFC 3339 timestamp + form; +- `ccf8c4ed05ee975f6256c495fca9a9a88dcc1b15` — ADR currentized with those + bounded/fail-closed decisions. + +No predecessor workflow receipt is evidence for a later head. The eventual +surviving Analysis Run vehicle must reacquire exact-head line/branch coverage, +documentation/security/CodeQL gates and qualifying independent review after +conflict-resolving consolidation.