diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b495291a..30f239c3e 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] +- **Pareto candidate-`K` analysis-run profile**: cutoff-safe `pareto_candidate_k_v1` binds `select_candidate_k` and selected-`K` RMSE and refuses LLM-vote authority (`analysis_engine`). Not Schwarz fitted selection, not a Bayesian sampler, and not implemented-main. - 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..0949729f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,6 +74,7 @@ dependencies = [ "corpus_split", "event_core", "membership_core", + "model_selection", "relation_graph", "serde", "serde_json", diff --git a/crates/analysis_engine/Cargo.toml b/crates/analysis_engine/Cargo.toml index 7322212b2..c45e2eed7 100644 --- a/crates/analysis_engine/Cargo.toml +++ b/crates/analysis_engine/Cargo.toml @@ -15,6 +15,7 @@ publish = false [dependencies] event_core = { path = "../event_core", version = "0.2.0" } +model_selection = { path = "../model_selection", version = "0.2.0" } serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } diff --git a/crates/analysis_engine/src/lib.rs b/crates/analysis_engine/src/lib.rs index 72bd5854c..1629fff02 100644 --- a/crates/analysis_engine/src/lib.rs +++ b/crates/analysis_engine/src/lib.rs @@ -8,13 +8,17 @@ //! through [`tepp_api`]. It deliberately does not claim latent-variable or topic //! estimation authority; those estimators remain separate scientific crates. //! estimation authority; it invokes estimators through their scientific crate -//! contracts and preserves their artifact meaning. +//! contracts and preserves their artifact meaning. Pareto candidate-`K` +//! selection is invoked through [`model_selection`] and is not a Bayesian +//! sampler. mod case_deletion_refit; mod lineage_criterion; +mod pareto_candidate_k_artifact; mod topic_context_posterior; mod topic_lineage_artifact; +use model_selection::ModelSelectionError; use serde::Serialize; use sha2::{Digest, Sha256}; use std::collections::BTreeSet; @@ -46,6 +50,13 @@ pub use lineage_criterion::{ LineageCriterionFit, LineageCriterionFitError, LineageCriterionObservation, fit_lineage_criterion_posteriors, }; +/// Pareto candidate-`K` artifact and execution contracts from this engine. +pub use pareto_candidate_k_artifact::{ + PARETO_CANDIDATE_K_ARTIFACT_BYTE_LIMIT, PARETO_CANDIDATE_K_ARTIFACT_SCHEMA_VERSION, + PARETO_CANDIDATE_K_MODEL_CONTRACT_VERSION, PARETO_CANDIDATE_K_OUTPUT_PROFILE, + ParetoCandidateKArtifact, ParetoCandidateKExecution, ParetoCandidateKInput, + execute_pareto_candidate_k_run, +}; /// Bounded posterior topic-context producer contract and record types. pub use topic_context_posterior::{ TOPIC_CONTEXT_POSTERIOR_BYTE_LIMIT, TOPIC_CONTEXT_POSTERIOR_SCHEMA_VERSION, @@ -248,6 +259,10 @@ pub enum AnalysisEngineError { TopicMeasurement(TopicMeasurementError), /// A topic-lineage artifact violated its bounded schema or count invariants. InvalidTopicLineageArtifact, + /// A model-selection gate rejected the offered candidates or method. + ModelSelection(ModelSelectionError), + /// A Pareto candidate-`K` artifact violated its bounded schema or counts. + InvalidParetoCandidateKArtifact, } impl fmt::Display for AnalysisEngineError { @@ -262,6 +277,8 @@ 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::ModelSelection(error) => return error.fmt(formatter), + Self::InvalidParetoCandidateKArtifact => "invalid pareto candidate-k artifact", }; formatter.write_str(message) } @@ -281,6 +298,12 @@ impl From for AnalysisEngineError { } } +impl From for AnalysisEngineError { + fn from(error: ModelSelectionError) -> Self { + Self::ModelSelection(error) + } +} + /// Execute the cutoff-safe temporal evidence readiness analysis. /// /// Evidence whose `available_time` is later than the request cutoff is excluded @@ -413,7 +436,8 @@ mod tests { use super::{ ANALYSIS_ARTIFACT_SCHEMA_VERSION, ANALYSIS_STATISTIC_COUNT, AnalysisCorpus, AnalysisEngineError, AnalysisEvidenceUnit, MAX_ANALYSIS_IDENTIFIER_BYTES, - MAX_EVIDENCE_UNITS, TopicMeasurementError, add_membership_count, execute_analysis_run, + MAX_EVIDENCE_UNITS, ModelSelectionError, TopicMeasurementError, add_membership_count, + execute_analysis_run, }; use temporal_core::{AvailableTime, EventTime}; use tepp_api::{AnalysisRunAccepted, AnalysisRunRequest, AnalysisRunTerminalState, ApiError}; @@ -681,6 +705,14 @@ mod tests { AnalysisEngineError::InvalidTopicLineageArtifact, "invalid topic lineage artifact", ), + ( + AnalysisEngineError::InvalidParetoCandidateKArtifact, + "invalid pareto candidate-k artifact", + ), + ( + AnalysisEngineError::ModelSelection(ModelSelectionError::EmptyCandidateSet), + "empty model-selection candidate set", + ), ]; for (error, message) in messages { assert_eq!(error.to_string(), message); @@ -689,6 +721,12 @@ mod tests { assert_eq!(converted.to_string(), "invalid API wire payload"); let from_topic: AnalysisEngineError = TopicMeasurementError::DidNotConverge.into(); assert_eq!(from_topic.to_string(), "topic estimator did not converge"); + let from_selection: AnalysisEngineError = + ModelSelectionError::LlmVoteIsNotStatisticalAuthority.into(); + assert_eq!( + from_selection.to_string(), + "llm vote is not statistical authority" + ); assert_eq!( add_membership_count(u64::MAX, 1), Err(AnalysisEngineError::ArithmeticOverflow) diff --git a/crates/analysis_engine/src/pareto_candidate_k_artifact.rs b/crates/analysis_engine/src/pareto_candidate_k_artifact.rs new file mode 100644 index 000000000..8eca23bbb --- /dev/null +++ b/crates/analysis_engine/src/pareto_candidate_k_artifact.rs @@ -0,0 +1,576 @@ +//! Digest-bound Pareto candidate-`K` selection as an analysis-run profile. + +use model_selection::{ModelCandidate, select_candidate_k, selected_k_root_mean_square_error}; +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 Pareto candidate-`K` artifact. +pub const PARETO_CANDIDATE_K_ARTIFACT_SCHEMA_VERSION: &str = "tepp.pareto_candidate_k.v1"; +/// Model contract required by the Pareto candidate-`K` execution path. +pub const PARETO_CANDIDATE_K_MODEL_CONTRACT_VERSION: &str = "pareto_candidate_k_v1"; +/// Analysis-run output profile required for a Pareto candidate-`K` artifact. +pub const PARETO_CANDIDATE_K_OUTPUT_PROFILE: &str = "pareto_candidate_k_v1"; +/// Maximum canonical artifact JSON size accepted at the untrusted input boundary. +pub const PARETO_CANDIDATE_K_ARTIFACT_BYTE_LIMIT: usize = 256 * 1024; +const PARETO_CANDIDATE_K_INFERENCE_STATUS: &str = + "pareto_statistical_front_not_fitted_schwarz_sampler"; +// `select_candidate_k` performs an O(n^2) dominance scan. This application-path +// ceiling bounds one request to at most 65,536 ordered candidate comparisons; +// it is an operational resource contract, not a scientific claim about valid K. +const MAX_PARETO_CANDIDATES: usize = 256; + +/// Provenance-bound Pareto-front input over one historical evidence universe. +/// +/// `source_evidence_available_times` is the complete availability-time vector +/// for the evidence universe used to construct both the candidate diagnostics +/// and the selected-`K` replications. Construction fails closed when any source +/// evidence was unavailable at the bound knowledge cutoff. The engine therefore +/// never attempts to subtract future evidence from already-aggregated model +/// diagnostics. +#[derive(Clone, Debug, PartialEq)] +pub struct ParetoCandidateKInput { + snapshot_id: String, + knowledge_cutoff: KnowledgeCutoff, + source_evidence_available_times: Vec, + candidates: Vec, + selected_replications: Vec, + truth_k: u32, +} + +impl ParetoCandidateKInput { + /// Construct a Pareto-front selection payload with exact historical + /// provenance for the evidence used to create its diagnostics. + /// + /// # Errors + /// + /// Returns [`AnalysisEngineError::InvalidEvidence`] for invalid snapshot + /// identity, empty evidence provenance, or post-cutoff source evidence. + /// Returns [`AnalysisEngineError::LimitExceeded`] before selection when the + /// evidence, replication, or O(n²) candidate population exceeds its + /// application-path bound. + pub fn new( + snapshot_id: impl Into, + knowledge_cutoff: KnowledgeCutoff, + source_evidence_available_times: Vec, + candidates: Vec, + selected_replications: Vec, + truth_k: u32, + ) -> Result { + let value = Self { + snapshot_id: snapshot_id.into(), + knowledge_cutoff, + source_evidence_available_times, + candidates, + selected_replications, + truth_k, + }; + value.validate_provenance()?; + Ok(value) + } + + /// Return the immutable snapshot identity of the diagnostic evidence. + #[must_use] + pub fn snapshot_id(&self) -> &str { + &self.snapshot_id + } + + /// Return the knowledge cutoff used when constructing the diagnostics. + #[must_use] + pub const fn knowledge_cutoff(&self) -> KnowledgeCutoff { + self.knowledge_cutoff + } + + /// Return the number of source evidence units represented by the + /// candidate diagnostics and replication RMSE. + #[must_use] + pub fn evidence_count(&self) -> usize { + self.source_evidence_available_times.len() + } + + /// Borrow the offered candidates. + #[must_use] + pub fn candidates(&self) -> &[ModelCandidate] { + &self.candidates + } + + /// Borrow selected-`K` replications used for RMSE. + #[must_use] + pub fn selected_replications(&self) -> &[u32] { + &self.selected_replications + } + + /// Return the known-truth topic count. + #[must_use] + pub const fn truth_k(&self) -> u32 { + self.truth_k + } + + fn validate_provenance(&self) -> Result<(), AnalysisEngineError> { + if !valid_identifier(&self.snapshot_id) || self.source_evidence_available_times.is_empty() { + return Err(AnalysisEngineError::InvalidEvidence); + } + if self.source_evidence_available_times.len() > MAX_EVIDENCE_UNITS + || self.selected_replications.len() > MAX_EVIDENCE_UNITS + || self.candidates.len() > MAX_PARETO_CANDIDATES + { + return Err(AnalysisEngineError::LimitExceeded); + } + if self + .source_evidence_available_times + .iter() + .any(|available_time| available_time.instant() > self.knowledge_cutoff.instant()) + { + return Err(AnalysisEngineError::InvalidEvidence); + } + Ok(()) + } + + fn validate_against( + &self, + snapshot_id: &str, + knowledge_cutoff: KnowledgeCutoff, + ) -> Result<(), AnalysisEngineError> { + self.validate_provenance()?; + if self.snapshot_id != snapshot_id { + return Err(AnalysisEngineError::SnapshotMismatch); + } + if self.knowledge_cutoff.instant() != knowledge_cutoff.instant() { + return Err(AnalysisEngineError::InvalidEvidence); + } + Ok(()) + } +} + +/// Completed, bounded Pareto candidate-`K` selection for analysis-run clients. +/// +/// Fields are private so a validated completed artifact cannot be mutated into +/// an unchecked in-memory state after execution. Consumers read through the +/// accessors and obtain untrusted artifacts through [`Self::from_json`]. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ParetoCandidateKArtifact { + schema_version: String, + run_id: String, + snapshot_id: String, + knowledge_cutoff: String, + selected_k: u64, + candidate_count: u64, + statistical_count: u64, + truth_k: u64, + selected_k_rmse: f64, + inference_status: String, +} + +impl ParetoCandidateKArtifact { + /// Return the exact versioned schema identity. + #[must_use] + pub fn schema_version(&self) -> &str { + &self.schema_version + } + + /// Return the opaque accepted-run identity. + #[must_use] + pub fn run_id(&self) -> &str { + &self.run_id + } + + /// Return the immutable source snapshot identity. + #[must_use] + pub fn snapshot_id(&self) -> &str { + &self.snapshot_id + } + + /// Return the canonical historical evidence cutoff. + #[must_use] + pub fn knowledge_cutoff(&self) -> &str { + &self.knowledge_cutoff + } + + /// Return the statistically selected topic count `K`. + #[must_use] + pub const fn selected_k(&self) -> u64 { + self.selected_k + } + + /// Return the number of candidates offered to the Pareto gate. + #[must_use] + pub const fn candidate_count(&self) -> u64 { + self.candidate_count + } + + /// Return the number of statistically supported candidates. + #[must_use] + pub const fn statistical_count(&self) -> u64 { + self.statistical_count + } + + /// Return the known-truth topic count used for RMSE. + #[must_use] + pub const fn truth_k(&self) -> u64 { + self.truth_k + } + + /// Return the RMSE of selected-`K` replications against known truth. + #[must_use] + pub const fn selected_k_rmse(&self) -> f64 { + self.selected_k_rmse + } + + /// Return the fixed scientific claim boundary. + #[must_use] + pub fn inference_status(&self) -> &str { + &self.inference_status + } + + /// Parse and fully validate a bounded artifact JSON payload. + /// + /// # Errors + /// + /// Returns [`AnalysisEngineError::InvalidParetoCandidateKArtifact`] when the + /// schema, identifiers, counts, RMSE, or claim boundary fail, and + /// [`AnalysisEngineError::LimitExceeded`] when an untrusted payload exceeds + /// the 256 KiB admission limit. + pub fn from_json(payload: &str) -> Result { + if payload.len() > PARETO_CANDIDATE_K_ARTIFACT_BYTE_LIMIT { + return Err(AnalysisEngineError::LimitExceeded); + } + let artifact: Self = serde_json::from_str(payload) + .map_err(|_| AnalysisEngineError::InvalidParetoCandidateKArtifact)?; + artifact.validate()?; + Ok(artifact) + } + + /// Serialize canonical validated artifact JSON. + /// + /// Validation bounds both identifiers to 256 bytes and all remaining fields + /// to fixed strings or scalar wire values, so a valid canonical artifact is + /// structurally smaller than the separate 256 KiB untrusted-input limit. + /// + /// # Errors + /// + /// Returns [`AnalysisEngineError::SerializationFailure`] if serialization + /// unexpectedly fails. + 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> { + if self.schema_version != PARETO_CANDIDATE_K_ARTIFACT_SCHEMA_VERSION + || !valid_identifier(&self.run_id) + || !valid_identifier(&self.snapshot_id) + || KnowledgeCutoff::parse_rfc3339(&self.knowledge_cutoff).is_err() + || self.selected_k < 2 + || self.candidate_count == 0 + || self.candidate_count > MAX_PARETO_CANDIDATES as u64 + || self.statistical_count == 0 + || self.statistical_count > self.candidate_count + || self.truth_k < 2 + || !self.selected_k_rmse.is_finite() + || self.selected_k_rmse < 0.0 + || self.inference_status != PARETO_CANDIDATE_K_INFERENCE_STATUS + { + return Err(AnalysisEngineError::InvalidParetoCandidateKArtifact); + } + Ok(()) + } +} + +/// One completed Pareto candidate-`K` artifact and its terminal result. +#[derive(Clone, Debug, PartialEq)] +pub struct ParetoCandidateKExecution { + /// Digest-bound completed selection artifact. + pub artifact: ParetoCandidateKArtifact, + /// Terminal result carrying the artifact identity, digest, and schema. + pub terminal_result: AnalysisRunTerminalResult, +} + +/// Execute cutoff-safe Pareto candidate-`K` selection as one analysis-run profile. +/// +/// The executor invokes [`select_candidate_k`] and +/// [`selected_k_root_mean_square_error`] and does not reimplement Pareto +/// dominance or RMSE. Candidate diagnostics and RMSE replications are admitted +/// only when their construction provenance is bound to this snapshot and cutoff. +/// LLM votes cannot define the numerical optimum. This is not Schwarz fitted +/// selection, not a Bayesian sampler, and not GPU execution. +/// +/// # Errors +/// +/// Returns a request/receipt/snapshot/cutoff/profile error, provenance or +/// resource-bound failure, model-selection failure, or invalid artifact error. +pub fn execute_pareto_candidate_k_run( + request: &AnalysisRunRequest, + accepted: &AnalysisRunAccepted, + snapshot_id: &str, + knowledge_cutoff: KnowledgeCutoff, + input: &ParetoCandidateKInput, + 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 != PARETO_CANDIDATE_K_MODEL_CONTRACT_VERSION + || request.output_profile != PARETO_CANDIDATE_K_OUTPUT_PROFILE + { + return Err(AnalysisEngineError::InvalidEvidence); + } + input.validate_against(snapshot_id, knowledge_cutoff)?; + + let candidate_count = u64::try_from(input.candidates().len()) + .map_err(|_| AnalysisEngineError::ArithmeticOverflow)?; + let evidence_count = u64::try_from(input.evidence_count()) + .map_err(|_| AnalysisEngineError::ArithmeticOverflow)?; + let selected_k = u64::from(select_candidate_k(input.candidates())?); + let selected_k_rmse = + selected_k_root_mean_square_error(input.selected_replications(), input.truth_k())?; + let statistical_count = u64::try_from( + input + .candidates() + .iter() + .filter(|candidate| candidate.is_statistically_supported()) + .count(), + ) + .map_err(|_| AnalysisEngineError::ArithmeticOverflow)?; + let artifact = ParetoCandidateKArtifact { + schema_version: PARETO_CANDIDATE_K_ARTIFACT_SCHEMA_VERSION.into(), + run_id: accepted.run_id.clone(), + snapshot_id: snapshot_id.to_owned(), + knowledge_cutoff: knowledge_cutoff.to_rfc3339(), + selected_k, + candidate_count, + statistical_count, + truth_k: u64::from(input.truth_k()), + selected_k_rmse, + inference_status: PARETO_CANDIDATE_K_INFERENCE_STATUS.into(), + }; + let digest = artifact.sha256()?; + let summary = + AnalysisResultSummary::new("pareto_candidate_k", evidence_count, 2, "validated")?; + let terminal_result = AnalysisRunTerminalResult::succeeded( + request, + accepted, + format!("pareto_candidate_k_artifact_{}", &digest[..16]), + digest, + PARETO_CANDIDATE_K_ARTIFACT_SCHEMA_VERSION, + completed_at, + summary, + )?; + Ok(ParetoCandidateKExecution { + artifact, + terminal_result, + }) +} + +#[cfg(test)] +mod tests { + use super::{ + PARETO_CANDIDATE_K_ARTIFACT_BYTE_LIMIT, PARETO_CANDIDATE_K_ARTIFACT_SCHEMA_VERSION, + PARETO_CANDIDATE_K_INFERENCE_STATUS, ParetoCandidateKArtifact, ParetoCandidateKInput, + }; + use crate::AnalysisEngineError; + use model_selection::ModelCandidate; + use temporal_core::{AvailableTime, KnowledgeCutoff}; + + fn artifact() -> ParetoCandidateKArtifact { + ParetoCandidateKArtifact { + schema_version: PARETO_CANDIDATE_K_ARTIFACT_SCHEMA_VERSION.into(), + run_id: "run-1".into(), + snapshot_id: "snapshot-1".into(), + knowledge_cutoff: "2026-08-01T00:00:00Z".into(), + selected_k: 2, + candidate_count: 2, + statistical_count: 2, + truth_k: 2, + selected_k_rmse: 0.0, + inference_status: PARETO_CANDIDATE_K_INFERENCE_STATUS.into(), + } + } + + fn assert_invalid(artifact: &ParetoCandidateKArtifact) { + assert_eq!( + artifact.to_json(), + Err(AnalysisEngineError::InvalidParetoCandidateKArtifact) + ); + } + + fn cutoff() -> KnowledgeCutoff { + KnowledgeCutoff::parse_rfc3339("2026-08-01T00:00:00Z").expect("cutoff") + } + + fn available() -> AvailableTime { + AvailableTime::parse_rfc3339("2026-07-01T00:00:00Z").expect("available") + } + + #[test] + fn artifact_round_trip_and_size_bounds_fail_closed() { + let artifact = artifact(); + let payload = artifact.to_json().expect("json"); + assert_eq!( + ParetoCandidateKArtifact::from_json(&payload), + Ok(artifact.clone()) + ); + assert_eq!(artifact.sha256().expect("digest").len(), 64); + assert_eq!( + artifact.schema_version(), + PARETO_CANDIDATE_K_ARTIFACT_SCHEMA_VERSION + ); + assert_eq!(artifact.run_id(), "run-1"); + assert_eq!(artifact.snapshot_id(), "snapshot-1"); + assert_eq!(artifact.knowledge_cutoff(), "2026-08-01T00:00:00Z"); + assert_eq!(artifact.selected_k(), 2); + assert_eq!(artifact.candidate_count(), 2); + assert_eq!(artifact.statistical_count(), 2); + assert_eq!(artifact.truth_k(), 2); + assert_eq!(artifact.selected_k_rmse(), 0.0); + assert_eq!( + artifact.inference_status(), + PARETO_CANDIDATE_K_INFERENCE_STATUS + ); + assert_eq!( + ParetoCandidateKArtifact::from_json("{}"), + Err(AnalysisEngineError::InvalidParetoCandidateKArtifact) + ); + assert_eq!( + ParetoCandidateKArtifact::from_json( + &"x".repeat(PARETO_CANDIDATE_K_ARTIFACT_BYTE_LIMIT + 1) + ), + Err(AnalysisEngineError::LimitExceeded) + ); + } + + #[test] + fn maximal_valid_artifact_is_structurally_below_the_input_wire_limit() { + let mut maximal = artifact(); + maximal.run_id = "\\".repeat(256); + maximal.snapshot_id = "\\".repeat(256); + maximal.selected_k = u64::MAX; + maximal.candidate_count = MAX_PARETO_CANDIDATES as u64; + maximal.statistical_count = MAX_PARETO_CANDIDATES as u64; + maximal.truth_k = u64::MAX; + maximal.selected_k_rmse = f64::MAX; + + let payload = maximal.to_json().expect("maximal valid json"); + assert!(payload.len() < PARETO_CANDIDATE_K_ARTIFACT_BYTE_LIMIT); + assert_eq!(ParetoCandidateKArtifact::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.selected_k = 1; + value + }, + { + let mut value = artifact.clone(); + value.candidate_count = 0; + value + }, + { + let mut value = artifact.clone(); + value.candidate_count = 257; + value + }, + { + let mut value = artifact.clone(); + value.statistical_count = 0; + value + }, + { + let mut value = artifact.clone(); + value.statistical_count = 3; + value + }, + { + let mut value = artifact.clone(); + value.truth_k = 1; + value + }, + { + let mut value = artifact.clone(); + value.selected_k_rmse = f64::NAN; + value + }, + { + let mut value = artifact.clone(); + value.selected_k_rmse = -0.1; + value + }, + { + let mut value = artifact.clone(); + value.inference_status.clear(); + value + }, + ]; + for invalid in invalid_artifacts { + assert_invalid(&invalid); + } + } + + #[test] + fn input_accessors_expose_provenance_candidates_and_truth() { + let a = ModelCandidate::statistical(2, -30.0, 8.0).expect("a"); + let input = ParetoCandidateKInput::new( + "snapshot-1", + cutoff(), + vec![available()], + vec![a], + vec![2], + 2, + ) + .expect("input"); + assert_eq!(input.snapshot_id(), "snapshot-1"); + assert_eq!(input.knowledge_cutoff(), cutoff()); + assert_eq!(input.evidence_count(), 1); + assert_eq!(input.candidates(), &[a]); + assert_eq!(input.selected_replications(), &[2]); + assert_eq!(input.truth_k(), 2); + } +} diff --git a/crates/analysis_engine/tests/pareto_candidate_k_execution_contract.rs b/crates/analysis_engine/tests/pareto_candidate_k_execution_contract.rs new file mode 100644 index 000000000..6ae3d07d1 --- /dev/null +++ b/crates/analysis_engine/tests/pareto_candidate_k_execution_contract.rs @@ -0,0 +1,352 @@ +//! End-to-end contract for cutoff-safe Pareto candidate-`K` selection. + +use analysis_engine::{ + AnalysisEngineError, MAX_EVIDENCE_UNITS, PARETO_CANDIDATE_K_ARTIFACT_SCHEMA_VERSION, + PARETO_CANDIDATE_K_MODEL_CONTRACT_VERSION, PARETO_CANDIDATE_K_OUTPUT_PROFILE, + ParetoCandidateKInput, execute_pareto_candidate_k_run, +}; +use model_selection::{ModelCandidate, ModelSelectionError}; +use temporal_core::{AvailableTime, KnowledgeCutoff}; +use tepp_api::{AnalysisRunAccepted, AnalysisRunRequest, AnalysisRunTerminalState, ApiError}; + +fn cutoff() -> KnowledgeCutoff { + KnowledgeCutoff::parse_rfc3339("2026-02-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: "pareto-candidate-k-idem".into(), + tenant_workspace_id: "tenant-workspace".into(), + snapshot_id: "snapshot-pareto-candidate-k".into(), + knowledge_cutoff: "2026-02-01T00:00:00Z".into(), + model_contract_version: PARETO_CANDIDATE_K_MODEL_CONTRACT_VERSION.into(), + output_profile: PARETO_CANDIDATE_K_OUTPUT_PROFILE.into(), + } +} + +fn accepted(request: &AnalysisRunRequest) -> AnalysisRunAccepted { + AnalysisRunAccepted::new( + "run-pareto-candidate-k", + "accepted", + &request.idempotency_key, + ) + .expect("accepted") +} + +fn statistical_front() -> ParetoCandidateKInput { + ParetoCandidateKInput::new( + "snapshot-pareto-candidate-k", + cutoff(), + vec![ + available("2026-01-10T00:00:00Z"), + available("2026-01-11T00:00:00Z"), + available("2026-01-12T00:00:00Z"), + available("2026-01-13T00:00:00Z"), + available("2026-01-14T00:00:00Z"), + ], + vec![ + ModelCandidate::statistical(2, -30.0, 8.0).expect("k2"), + ModelCandidate::statistical(4, -30.0, 8.0).expect("k4"), + ModelCandidate::llm_vote_only(8).expect("llm"), + ], + vec![2, 2, 2], + 2, + ) + .expect("front") +} + +fn input( + candidates: Vec, + selected_replications: Vec, + truth_k: u32, +) -> ParetoCandidateKInput { + ParetoCandidateKInput::new( + "snapshot-pareto-candidate-k", + cutoff(), + vec![available("2026-01-10T00:00:00Z")], + candidates, + selected_replications, + truth_k, + ) + .expect("input") +} + +fn execute( + request: &AnalysisRunRequest, + input: &ParetoCandidateKInput, +) -> Result { + execute_pareto_candidate_k_run( + request, + &accepted(request), + "snapshot-pareto-candidate-k", + cutoff(), + input, + "2026-02-02T00:00:00Z", + ) +} + +#[test] +fn pareto_front_selects_smaller_k_and_reports_source_evidence_count() { + let request = request(); + let execution = execute(&request, &statistical_front()).expect("execution"); + assert_eq!( + execution.artifact.schema_version(), + PARETO_CANDIDATE_K_ARTIFACT_SCHEMA_VERSION + ); + assert_eq!(execution.artifact.selected_k(), 2); + assert_eq!(execution.artifact.candidate_count(), 3); + assert_eq!(execution.artifact.statistical_count(), 2); + assert_eq!(execution.artifact.truth_k(), 2); + assert!((execution.artifact.selected_k_rmse() - 0.0).abs() < f64::EPSILON); + assert_eq!( + execution.artifact.inference_status(), + "pareto_statistical_front_not_fitted_schwarz_sampler" + ); + assert_eq!( + execution.terminal_result.run_state, + AnalysisRunTerminalState::Succeeded + ); + let summary = execution.terminal_result.summary.as_ref().expect("summary"); + assert_eq!(summary.evidence_count, 5); + assert_ne!(summary.evidence_count, execution.artifact.candidate_count()); + assert_eq!(summary.validation_status, "validated"); + 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(PARETO_CANDIDATE_K_ARTIFACT_SCHEMA_VERSION) + ); +} + +#[test] +fn higher_likelihood_wins_and_llm_only_sets_fail_closed() { + let request = request(); + let higher = input( + vec![ + ModelCandidate::statistical(2, -30.0, 8.0).expect("k2"), + ModelCandidate::statistical(8, -20.0, 9.0).expect("k8"), + ], + vec![8], + 8, + ); + let execution = execute(&request, &higher).expect("likelihood"); + assert_eq!(execution.artifact.selected_k(), 8); + assert!((execution.artifact.selected_k_rmse() - 0.0).abs() < f64::EPSILON); + + let llm_only = input( + vec![ModelCandidate::llm_vote_only(3).expect("llm")], + vec![3], + 3, + ); + assert_eq!( + execute(&request, &llm_only), + Err(AnalysisEngineError::ModelSelection( + ModelSelectionError::LlmVoteIsNotStatisticalAuthority + )) + ); + let empty = input(Vec::new(), vec![2], 2); + assert_eq!( + execute(&request, &empty), + Err(AnalysisEngineError::ModelSelection( + ModelSelectionError::EmptyCandidateSet + )) + ); +} + +#[test] +fn mismatched_replications_record_positive_rmse() { + let request = request(); + let mismatched = input( + vec![ModelCandidate::statistical(2, -30.0, 8.0).expect("k2")], + vec![4, 4, 4], + 2, + ); + let execution = execute(&request, &mismatched).expect("rmse"); + assert_eq!(execution.artifact.selected_k(), 2); + assert!((execution.artifact.selected_k_rmse() - 2.0).abs() < f64::EPSILON); +} + +#[test] +fn provenance_constructor_checks_each_admission_boundary() { + let candidate = ModelCandidate::statistical(2, -30.0, 8.0).expect("candidate"); + let visible = available("2026-01-10T00:00:00Z"); + + assert_eq!( + ParetoCandidateKInput::new("", cutoff(), vec![visible], vec![candidate], vec![2], 2), + Err(AnalysisEngineError::InvalidEvidence) + ); + assert_eq!( + ParetoCandidateKInput::new( + "snapshot-pareto-candidate-k", + cutoff(), + Vec::new(), + vec![candidate], + vec![2], + 2, + ), + Err(AnalysisEngineError::InvalidEvidence) + ); + assert_eq!( + ParetoCandidateKInput::new( + "snapshot-pareto-candidate-k", + cutoff(), + vec![available("2026-02-01T00:00:01Z")], + vec![candidate], + vec![2], + 2, + ), + Err(AnalysisEngineError::InvalidEvidence) + ); + assert_eq!( + ParetoCandidateKInput::new( + "snapshot-pareto-candidate-k", + cutoff(), + vec![visible; MAX_EVIDENCE_UNITS + 1], + vec![candidate], + vec![2], + 2, + ), + Err(AnalysisEngineError::LimitExceeded) + ); + assert_eq!( + ParetoCandidateKInput::new( + "snapshot-pareto-candidate-k", + cutoff(), + vec![visible], + vec![candidate; 257], + vec![2], + 2, + ), + Err(AnalysisEngineError::LimitExceeded) + ); + assert_eq!( + ParetoCandidateKInput::new( + "snapshot-pareto-candidate-k", + cutoff(), + vec![visible], + vec![candidate], + vec![2; MAX_EVIDENCE_UNITS + 1], + 2, + ), + Err(AnalysisEngineError::LimitExceeded) + ); + + assert!( + ParetoCandidateKInput::new( + "snapshot-pareto-candidate-k", + cutoff(), + vec![visible; MAX_EVIDENCE_UNITS], + vec![candidate; 256], + vec![2; MAX_EVIDENCE_UNITS], + 2, + ) + .is_ok() + ); +} + +#[test] +fn equivalent_cutoff_instants_bind_and_input_provenance_is_rechecked() { + let mut equivalent = request(); + equivalent.knowledge_cutoff = "2026-01-31T19:00:00-05:00".into(); + assert!(execute(&equivalent, &statistical_front()).is_ok()); + + let wrong_snapshot = ParetoCandidateKInput::new( + "other-snapshot", + cutoff(), + vec![available("2026-01-10T00:00:00Z")], + vec![ModelCandidate::statistical(2, -30.0, 8.0).expect("candidate")], + vec![2], + 2, + ) + .expect("input"); + assert_eq!( + execute(&request(), &wrong_snapshot), + Err(AnalysisEngineError::SnapshotMismatch) + ); + + let earlier_cutoff = KnowledgeCutoff::parse_rfc3339("2026-01-31T23:59:59Z").expect("earlier"); + let wrong_cutoff = ParetoCandidateKInput::new( + "snapshot-pareto-candidate-k", + earlier_cutoff, + vec![available("2026-01-10T00:00:00Z")], + vec![ModelCandidate::statistical(2, -30.0, 8.0).expect("candidate")], + vec![2], + 2, + ) + .expect("input"); + assert_eq!( + execute(&request(), &wrong_cutoff), + Err(AnalysisEngineError::InvalidEvidence) + ); +} + +#[test] +fn execution_refuses_snapshot_profile_and_cutoff_mismatch() { + let request = request(); + assert_eq!( + execute_pareto_candidate_k_run( + &request, + &accepted(&request), + "other-snapshot", + cutoff(), + &statistical_front(), + "2026-02-02T00:00:00Z", + ), + Err(AnalysisEngineError::SnapshotMismatch) + ); + for invalid_request in [ + { + let mut value = request.clone(); + value.knowledge_cutoff = "2026-08-02T00:00:00Z".into(); + value + }, + { + let mut value = request.clone(); + value.model_contract_version = "other-model".into(); + value + }, + { + let mut value = request.clone(); + value.output_profile = "fitted_candidate_k_v1".into(); + value + }, + { + let mut value = request.clone(); + value.output_profile = "joint_posterior_draws_v1".into(); + value + }, + { + let mut value = request.clone(); + value.output_profile = "trsl_topic_lineage_v1".into(); + value + }, + ] { + assert_eq!( + execute(&invalid_request, &statistical_front()), + Err(AnalysisEngineError::InvalidEvidence) + ); + } +} + +#[test] +fn invalid_completed_at_fails_terminal_result_construction() { + let request = request(); + assert_eq!( + execute_pareto_candidate_k_run( + &request, + &accepted(&request), + "snapshot-pareto-candidate-k", + cutoff(), + &statistical_front(), + "not-a-timestamp", + ), + Err(AnalysisEngineError::Api(ApiError::InvalidWirePayload)) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 2b783c2ab..192119080 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -75,6 +75,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; `corpus_background` background-versus-unique-content identity on the active PR; estimator-side method model remains future | partial | | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; `prompt_source` prompt-versus-unique-content identity on the active PR; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates | ADR 0012; research | `model_selection` fits each candidate `K` with the CPU `f64` reference and scores the actual mixture likelihood plus Schwarz's (1978) `ℓ − (p ln N)/2` penalty before the Pareto gate; candidate blinding, blinded LLM review, GPU, and backend comparison remain accepted-target | active-PR | +| Pareto candidate-K analysis-run selection | ADR 0012/0022/0053 | `analysis_engine` `pareto_candidate_k_v1` binds `select_candidate_k` and selected-`K` RMSE; refuses LLM-vote authority; not Schwarz fitted selection, not joint Laplace draws, not a Bayesian sampler, and not implemented-main | active-PR | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | | posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `psychometric_core` construct/input gates, true-loading OLS recovery, posterior-draw point-estimate averaging, Rubin `T` on draw-level OLS loadings, CWC within/between OLS plus the contextual effect, event-time log-rate, constant- and time-varying-predictor discrete effects (Voelkle Eqs. 12 and 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; §7.2 level-change `CINT` is `κ = −a m x` (`a < 0`; not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`); §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, not the dissipating Dirac; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; extra `LAMBDA` is 0; `τ + λ μ_t` is not that observed mean; after-t0 extra-process `TDPREDEFFECT` uses `t − u` with `t0 < u < t` while `μ_t` uses `Δt`; that after-t0 observed mean is not the first-occasion extra-process observed mean; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` and is not `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v` and is not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`; stationary `T0VAR` is `trait + −q / (2 a) + (B / a)² v` (not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Eq. 5 of that constrained variance is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (JSS PDF re-opened 2026-08-22T03:20Z; form the stationary latent variance first, then `λ² p + θ + ψ`; `λ² p_0` is not that observed variance; `λ²(−q / (2 a)) + θ` is not that observed variance when `TRAITVAR` or `addedTIPREDVAR` is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`)); lagged stationary `T0VAR` is `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` (trait and `addedTIPREDVAR` do not decay; contemporaneous `T0VAR` is not that lagged map; decaying the constrained total as if it were all state is not that lagged map; Eq. 5 of that lagged covariance is `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ`; `Θ` does not enter; contemporaneous `Var(y_0)` is not that lagged observed covariance; the lagged latent covariance is not that observed covariance); later-occasion stationary `T0VAR` is `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` (trait and `addedTIPREDVAR` do not enter `Q_Δt`; under stationarity that composition equals contemporaneous `T0VAR`; evolving the constrained total as if it were all state is not that later map; the lagged covariance omits `Q_Δt`; `Q_Δt` is not that later map; Eq. 5 of that later-occasion variance is `λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ`; lagged observed covariance omits `Q_Δt` and `θ`; `MANIFESTVAR` is not `Var(y_t)`; the later-occasion latent variance is not `Var(y_t)`); predetermined later-occasion `T0VAR` is `trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v` (free `T0VAR` `p_0` is not that later map; setting `p_0 = −q / (2 a)` recovers the stationary later-occasion map; stationary later variance uses `−q / (2 a)` in place of `p_0` and is not that later map when `p_0` is free; evolving `trait + p_0 + (B / a)² v` as if it were all state is not that later map; Eq. 5 of that predetermined later-occasion variance is `λ²(trait + e^{2 a Δt} p_0 + Q_Δt + (B / a)² v) + θ + ψ`; `MANIFESTVAR` is not `Var(y_t)`; the predetermined later-occasion latent variance is not `Var(y_t)`; stationary later observed variance is not that observed variance when `p_0` is free); predetermined lagged `T0VAR` is `trait + e^{a Δt} p_0 + (B / a)² v` (free `T0VAR` `p_0` is not that lagged map; setting `p_0 = −q / (2 a)` recovers the stationary lagged map; stationary lagged covariance uses `−q / (2 a)` in place of `p_0` and is not that lagged map when `p_0` is free; evolving `trait + p_0 + (B / a)² v` as if it were all state is not that lagged map; later-occasion variance includes `Q_Δt` and is not that lagged map; Eq. 5 of that predetermined lagged covariance is `λ²(trait + e^{a Δt} p_0 + (B / a)² v) + ψ`; `MANIFESTVAR` does not enter; the predetermined lagged latent covariance is not that observed covariance; predetermined later observed variance includes `Q_Δt` and `θ` and is not that lagged observed covariance; stationary lagged observed covariance is not that observed covariance when `p_0` is free; the predetermined first-occasion variance of §4.3 predetermined `T0VAR` is `trait + p_0 + (B / a)² v`; free `p_0` is not that map; stationary first-occasion variance uses `−q / (2 a)` in place of `p_0` and is not that map when `p_0` is free; lagged covariance decays the state and is not that map; later-occasion variance includes `Q_Δt` and is not that map; Eq. 5 of that predetermined first-occasion variance is `λ²(trait + p_0 + (B / a)² v) + θ + ψ`; `MANIFESTVAR` is not that first-occasion observed variance; the predetermined first-occasion latent variance is not that observed variance; stationary first-occasion observed variance is not that observed variance when `p_0` is free; predetermined later observed variance includes `Q_Δt` and is not that first-occasion observed variance; later-start lagged covariance of predetermined `T0VAR` is `trait + e^{a s}(e^{2 a u} p_0 + Q_u) + (B / a)² v` (Driver et al., 2017, §4.3 `startoffset`; Eq. 4; JSS PDF re-opened 2026-08-23T10:27Z; first-occasion lagged omits `e^{a s} Q_u`; later-occasion variance does not lag; stationary lagged uses `−q / (2 a)`; decaying the later total is not that map; Eq. 5 of that later-start lagged covariance is `λ²` of it plus `ψ`; `Θ` does not enter; first-occasion lagged observed omits `e^{a s} Q_u`; later observed variance includes `Q_u` and `θ`; later-start later-occasion variance of predetermined `T0VAR` is `trait + e^{2 a s}(e^{2 a u} p_0 + Q_u) + Q_s + (B / a)² v` (Driver et al., 2017, §4.3 `startoffset`; Eq. 3–4 Chapman–Kolmogorov `Q_{u+s} = e^{2 a s} Q_u + Q_s`; JSS PDF re-opened 2026-08-23T11:05Z; later-occasion variance at `u` omits `Q_s`; later-start lagged covariance omits `Q_s`; stationary later uses `−q / (2 a)`; evolving the later total as if it were all state is not that map; ignoring `startoffset` omits `e^{2 a s} Q_u`; Eq. 5 of that later-start later-occasion variance is `λ²` of it plus `θ + ψ`; `MANIFESTVAR` is not that observed variance; p. 16 `discreteDRIFTstd` is `e^{a Δt}` after strictly positive `asymDIFFUSION` `-q / (2 a)` (footnote 4; unstandardised `e^{a Δt}` is defined for growing `a ≥ 0` and for zero diffusion and is not `discreteDRIFTstd`; the §7.1 trait-plus-state autocorrelation uses `TRAITVAR` and is not `discreteDRIFTstd`; p. 16 `discreteDIFFUSIONstd` is `Q_Δt / (−q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` (footnote 4; unstandardised `Q_Δt` is defined for growing `a ≥ 0` and for zero diffusion and is not `discreteDIFFUSIONstd`; the continuous standardisation `−2 a` is not `discreteDIFFUSIONstd`; `Q_Δt / (trait + p + added)` uses `TRAITVAR` and is not `discreteDIFFUSIONstd`; `TRAITVAR` is not the standardisation variance; p. 16 `DIFFUSIONstd` is `q / (−q / (2 a)) = −2 a` after strictly positive `asymDIFFUSION` `-q / (2 a)` (Driver et al., 2017, p. 16; Eq. 4; footnote 4; JSS PDF re-opened 2026-08-23T13:20Z; unstandardised `q` is defined for growing `a ≥ 0` and for zero diffusion and is not `DIFFUSIONstd`; the discrete standardisation `Q_Δt / (−q / (2 a))` depends on `Δt` and is not `DIFFUSIONstd`; `q / (trait + p + added)` uses `TRAITVAR` and is not `DIFFUSIONstd`; `TRAITVAR` is not the standardisation variance; p. 16 `DRIFTstd` is the continuous auto-effect after strictly positive `asymDIFFUSION` `-q / (2 a)` (Driver et al., 2017, p. 16; Eq. 1; footnote 4; JSS PDF re-opened 2026-08-23T13:28Z); unstandardised `a` is defined for growing `a ≥ 0` and for zero diffusion and is not `DRIFTstd`; the discrete standardisation `e^{a Δt}` depends on the event interval and is not `DRIFTstd`; `a p / (trait + p + added)` uses `TRAITVAR` and is not `DRIFTstd`; `TRAITVAR` is not the standardisation variance); p. 16 `asymTIPREDEFFECTstd` is `(-B / a) · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` and strictly positive predictor variance `v` (Driver et al., 2017, p. 16; §7.2; footnote 4; JSS PDF re-opened 2026-08-23T14:25Z; unstandardised `-B / a` is defined for a zero coefficient and for zero predictor variance and is not `asymTIPREDEFFECTstd`; the finite-interval standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the event interval and is not `asymTIPREDEFFECTstd`; `(-B / a) · √v / √(trait + p + added)` uses `TRAITVAR` and is not `asymTIPREDEFFECTstd`; `TRAITVAR` is not the standardisation variance); p. 16 `TIPREDEFFECTstd` is `B · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` `-q / (2 a)` and strictly positive predictor variance `v` (Driver et al., 2017, p. 16; §7.2; footnote 4; JSS PDF re-opened 2026-08-23T16:21Z; unstandardised `B` is defined for a zero coefficient and for zero predictor variance and is not `TIPREDEFFECTstd`; the asymptotic standardisation `(-B / a) · √v / √p` is the total change and is not `TIPREDEFFECTstd`; the finite-interval standardisation `A^{-1}[e^{A Δt} − I] B · √v / √p` depends on the event interval and is not `TIPREDEFFECTstd`; `B · √v / √(trait + p + added)` uses `TRAITVAR` and is not `TIPREDEFFECTstd`; `TRAITVAR` is not the standardisation variance); Table 3 `T0TIPREDEFFECTstd` is `t0_b · √v / √p_0` after strictly positive free `T0VAR` `p_0` and strictly positive predictor variance `v` (Driver et al., 2017, Table 3, p. 13; p. 16; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T17:20Z; the affected variance is free `T0VAR`, not `asymDIFFUSION`; unstandardised `t0_b` is defined for a zero coefficient and for zero predictor variance and is not `T0TIPREDEFFECTstd`; `TIPREDEFFECTstd` `B · √v / √(-q / (2 a))` is the continuous coefficient and is not `T0TIPREDEFFECTstd`; `asymTIPREDEFFECTstd` `(-B / a) · √v / √p` is the total change and is not `T0TIPREDEFFECTstd`; `t0_b · √v / √(trait + p_0 + added)` uses `TRAITVAR` and is not `T0TIPREDEFFECTstd`; `TRAITVAR` is not the standardisation variance); 2017-era `addedT0TIPREDVAR` is `t0_b² v` (Driver et al., 2017, Table 3, p. 13; p. 16; §7.2; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T18:20Z; `T0TIPREDEFFECT %*% TIPREDVAR %*% t(T0TIPREDEFFECT)` immediately after `T0TIPREDEFFECTstd`; form `t0_b` first, then square, then multiply by `v`; a zero coefficient or zero predictor variance is exactly zero; free `T0TIPREDEFFECT` does not require `a < 0`; `(B / a)² v` is `addedTIPREDVAR` and is not this first-occasion map; `t0_b · √v / √p_0` is `T0TIPREDEFFECTstd` and is not this variance; free `T0VAR` is not this extra TI variance; `TRAITVAR` is not this extra TI variance; Equation 5 of 2017-era `addedT0TIPREDVAR` is `λ² t0_b² v` (Driver et al., 2017, Eq. 5, p. 5; Table 3, p. 13; Table 2, p. 12; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-23T19:10Z; form `t0_b² v` first, then `(λ extra) λ` with `θ = 0`; a zero loading or zero extra is exactly zero; `t0_b² v` is the latent extra, not the observed extra; `λ² p_0 + θ` is first-occasion observed variance, not this extra; `λ² (B / a)² v` is Eq. 5 of `addedTIPREDVAR`, not this first-occasion observed extra; `MANIFESTVAR` `θ` is not this extra; Equation 5 of §7.2 `addedTIPREDVAR` is `λ² (B / a)² v`; form `(B / a)² v` first, then `(λ extra) λ` with `θ = 0`; a zero loading or zero extra is exactly zero; lasting asymptotic extra requires `a < 0`; `(B / a)² v` is the latent extra, not the observed extra; `λ² t0_b² v` is first-occasion extra observed TI variance, not this extra; `λ² p + θ` is stationary observed variance, not this extra; `MANIFESTVAR` `θ` is not this extra; p. 16 `TDPREDEFFECTstd` is `m · √v / √(-q / (2 a))` after strictly positive `asymDIFFUSION` and strictly positive time-dependent predictor variance; unstandardised `M` is not `TDPREDEFFECTstd`; `TIPREDEFFECTstd` is not `TDPREDEFFECTstd` even when `M = B`; intercept-style `A^{-1}[e^{A Δt} − I] M · √v / √p` is not `TDPREDEFFECTstd`; `m · √v / √(trait + p + added)` uses `TRAITVAR` and is not `TDPREDEFFECTstd`; Table 3 / p. 16 `T0TDPREDEFFECTstd` is `t0_m · √v / √p_0` after strictly positive free `T0VAR` and strictly positive TD predictor variance; unstandardised `t0_m` is not `T0TDPREDEFFECTstd`; `TDPREDEFFECTstd` uses `asymDIFFUSION` and is not `T0TDPREDEFFECTstd`; `T0TIPREDEFFECTstd` is not `T0TDPREDEFFECTstd` even when `t0_m = t0_b`; `t0_m · √v / √(trait + p_0 + added)` uses `TRAITVAR` and is not `T0TDPREDEFFECTstd`; free `T0VAR` does not require `a < 0`; p. 16 `T0VARstd` is `p_0 / p_0 = 1` after strictly positive free `T0VAR` (`solve(sqrt(diag(T0VAR))) %&% T0VAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; default ridge is 0); unstandardised `T0VAR` is not `T0VARstd`; `T0TDPREDEFFECTstd` is not `T0VARstd`; `addedT0TIPREDVAR` is not `T0VARstd`; p. 16 `TRAITVARstd` is `trait / trait = 1` after strictly positive `TRAITVAR` (`solve(sqrt(diag(TRAITVAR))) %&% TRAITVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; no ridge addend); unstandardised `TRAITVAR` is not `TRAITVARstd`; `T0VARstd` is not `TRAITVARstd` even when both equal 1; `addedT0TIPREDVAR` is not `TRAITVARstd`; p. 16 `MANIFESTTRAITVARstd` is `ψ / ψ = 1` after strictly positive `MANIFESTTRAITVAR` (`solve(sqrt(diag(MANIFESTTRAITVAR))) %&% MANIFESTTRAITVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0); unstandardised `MANIFESTTRAITVAR` is not `MANIFESTTRAITVARstd`; `TRAITVARstd` is not `MANIFESTTRAITVARstd` even when both equal 1; `MANIFESTVAR` is not `MANIFESTTRAITVARstd`; p. 16 `MANIFESTVARstd` is `θ / θ = 1` after strictly positive `MANIFESTVAR` (`solve(sqrt(diag(MANIFESTVAR))) %&% MANIFESTVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0; 2017-era `dimnames` assignment to `latentNames` is a source bug); unstandardised `MANIFESTVAR` is not `MANIFESTVARstd`; `MANIFESTTRAITVARstd` is not `MANIFESTVARstd` even when both equal 1; Equation 5 `Var(y)` is not `MANIFESTVARstd`; p. 16 `TIPREDVARstd` is `v / v = 1` after strictly positive `TIPREDVAR` (`solve(sqrt(diag(TIPREDVAR))) %&% TIPREDVAR`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0; `dimnames` are `TIpredNames`); unstandardised `TIPREDVAR` is not `TIPREDVARstd`; `MANIFESTVARstd` is not `TIPREDVARstd` even when both equal 1; §7.2 `addedTIPREDVAR` is not `TIPREDVARstd`; p. 16 `asymDIFFUSIONstd` is `p / p = 1` after strictly positive `asymDIFFUSION` (`solve(sqrt(diag(asymDIFFUSION))) %&% asymDIFFUSION`; OpenMx `%&%` is `t(A) %*% B %*% A`; 2017-era source adds ridging; default ridge is 0; `dimnames` are `latentNames`); unstandardised `asymDIFFUSION` is not `asymDIFFUSIONstd`; `TIPREDVARstd` is not `asymDIFFUSIONstd` even when both equal 1; `DIFFUSIONstd` `−2 a` is not `asymDIFFUSIONstd`; p. 16 `discreteCINTstd` is `A^{-1}[e^{A Δt} − I] κ / √p` after strictly positive `asymDIFFUSION`; unstandardised `discreteCINT` is not `discreteCINTstd`; `κ / √p` is not `discreteCINTstd`; `(-κ / a) / √p` is not `discreteCINTstd`; `asymCINTstd` is `(-κ / a) / √p` after strictly positive `asymDIFFUSION`; unstandardised `asymCINT` is not `asymCINTstd`; `κ / √p` is not `asymCINTstd`; `discreteCINTstd` is not `asymCINTstd`; `T0MEANSstd` is `μ_0 / √p_0` after strictly positive free `T0VAR`; unstandardised `T0MEANS` is not `T0MEANSstd`; `T0VARstd` is not `T0MEANSstd`; `μ_0 / √asymDIFFUSION` is not `T0MEANSstd`; `MANIFESTMEANSstd` is `τ / √θ` after strictly positive `MANIFESTVAR`; unstandardised `MANIFESTMEANS` is not `MANIFESTMEANSstd`; `MANIFESTVARstd` is not `MANIFESTMEANSstd`; `τ / √(λ² Var(η) + θ)` is not `MANIFESTMEANSstd`; p. 16 `CINTstd` is `κ / √p` after strictly positive `asymDIFFUSION`; unstandardised `CINT` is not `CINTstd`; `asymCINTstd` is not `CINTstd`; `discreteCINTstd` is not `CINTstd`; `κ / √(trait + p + added)` is not `CINTstd`;))))), irregular already-centered residual lag, and strong/strict-gated latent means on the stacked psychometric PR (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016, PMC5145197 opened 2026-08-19T22:15Z); full ESEM/DSEM remaining | partial | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | diff --git a/docs/adr/0053-pareto-candidate-k-analysis-run.md b/docs/adr/0053-pareto-candidate-k-analysis-run.md new file mode 100644 index 000000000..020971d7b --- /dev/null +++ b/docs/adr/0053-pareto-candidate-k-analysis-run.md @@ -0,0 +1,133 @@ +# ADR 0053 — Pareto candidate-`K` selection 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 0012 (candidate-`K` / Pareto gates) and ADR 0022 (cutoff-safe analysis-run execution). +**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 admits a unique `K` from a Pareto-filtered statistical +front inside `model_selection::select_candidate_k` and scores selected-`K` +RMSE against known truth. Operators still cannot request that gate as a +digest-bound analysis-run output. Schwarz fitted candidate-`K` selection is a +different profile. Joint Gauss-Newton Laplace draws are a different profile. +Topic activity/dormancy is a different profile. Full Bayesian sampling, GPU, +and topic birth/split/merge remain later GAP-004 work and are not this slice. + +The original branch treated already-aggregated candidate diagnostics as +"cutoff-safe" without carrying the evidence availability that produced them. +That permitted diagnostics derived from post-cutoff evidence to enter a +historical run. It also compared RFC 3339 cutoff text rather than temporal +instants, passed candidate count as terminal evidence count, and admitted an +unbounded candidate population into the quadratic Pareto-dominance scan. + +## Decision + +Add the `pareto_candidate_k_v1` analysis-run output profile to +`analysis_engine`. The executor: + +- consumes the existing `ModelCandidate` values and + `selected_k_root_mean_square_error` contract without reimplementing Pareto + dominance or RMSE; +- requires `ParetoCandidateKInput` to carry the immutable source snapshot, + typed `KnowledgeCutoff`, and the complete `AvailableTime` vector for the + evidence universe used to construct both the diagnostics and selected-`K` + replications; +- rejects construction if any source evidence became available after that + cutoff. The engine does not attempt to subtract future rows from diagnostics + that were already aggregated by another owner; +- binds request, executor and input cutoffs by `KnowledgeCutoff::instant()` so + equivalent legal RFC 3339 spellings represent one instant; +- bounds source evidence and selected replications by + `MAX_EVIDENCE_UNITS`, and bounds the quadratic Pareto candidate set to 256 + before `select_candidate_k` executes. The 256 ceiling is an application-path + CPU bound (at most 65,536 ordered candidate comparisons), not a scientific + assertion about admissible `K`; +- reports the source evidence-universe cardinality as + `AnalysisResultSummary.evidence_count`; candidate/statistical counts remain + artifact fields; +- keeps provider validation status (`validated`) separate from the artifact + inference claim `pareto_statistical_front_not_fitted_schwarz_sampler`; +- exposes completed artifact fields read-only through accessors. Untrusted + artifact construction continues through bounded `from_json` validation; +- refuses LLM-vote-only authority and empty candidate sets; and +- does not invent a Bayesian sampler, persist rows, select GPU backends, or + emit topic-lineage edges. + +The profile therefore proves admission and claim boundaries around an existing +statistical gate. It does not itself establish scientific recovery quality. +Issue #500 owns the missing profile-level recovery evidence: true-`K` recovery, +selected-`K` RMSE/bias with Monte Carlo uncertainty, convergence/failure +denominators, stability across the declared design, and leakage-safe temporal +evaluation where applicable. + +## Alternatives considered + +1. Trust the caller's statement that candidate diagnostics are cutoff-safe — + rejected because the original API carried no evidence with which to verify + that claim. +2. Drop post-cutoff rows after model diagnostics have already been aggregated — + rejected because candidate likelihood/complexity diagnostics cannot be + causally repaired by subtracting metadata after fitting. +3. Compare cutoff strings exactly — rejected because legal RFC 3339 strings can + encode the same instant with different offsets. +4. Leave candidate population unbounded — rejected because + `select_candidate_k` performs a quadratic dominance scan. +5. Bind another Schwarz `select_fitted_candidate_k` profile — rejected because + that bind is already a separate analysis-run profile. +6. Invent a Bayesian sampler or topic birth/split/merge engine — rejected + because those functions do not exist on protected main. + +## Consequences + +A successful artifact is bound to evidence that was already available at the +requested historical cutoff, and the terminal summary no longer mistakes +model candidates for source evidence. Future evidence fails at input +construction rather than contaminating a historical model-selection result. +Cross-snapshot or cutoff-rebound input fails closed at execution. + +The explicit 256-candidate ceiling constrains current O(n²) request cost. If a +buyer path requires a larger candidate universe, the owner must first replace +or profile the algorithm/representation and establish a new resource contract; +the ceiling must not be raised merely to pass a fixture. + +Scientific acceptance remains open under #500. This ADR is `Proposed` while the +implementation is confined to an unmerged Draft branch; it does not authorize +an `Accepted` or implemented-main claim. + +## Verification + +The PR includes contracts for: + +- equivalent RFC 3339 cutoff spellings; +- post-cutoff source-evidence refusal before Pareto selection; +- snapshot/cutoff provenance binding; +- the 256-candidate and `MAX_EVIDENCE_UNITS` replication bounds; +- source evidence count distinct from candidate count; +- provider-validation/domain-inference separation; +- LLM-vote non-authority and empty candidate sets; +- positive selected-`K` RMSE and artifact tampering; and +- immutable public artifact access through validated read-only accessors. + +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 +``` + +These contracts do not replace #500 scientific recovery evidence or required +hosted security, CodeQL, coverage and independent-review gates. + +## Rollback and supersession + +Rollback removes the `pareto_candidate_k_v1` profile. No persisted schema +migration is introduced. Supersede only with an ADR that keeps Pareto +statistical selection distinct from LLM votes, Schwarz fitted selection, +joint Laplace draws, and Bayesian sampling while preserving leakage-safe +provenance and an explicit resource envelope. diff --git a/docs/adr/README.md b/docs/adr/README.md index 1254c8079..294c25aae 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -28,6 +28,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0020](0020-span-grounded-semantic-units.md) | Span-grounded semantic units; language tags are not identity | Accepted | active-PR | First ADR 0004 production slice; concept alignment, invariance, and topic estimation are not claimed. | | [0021](0021-lineageweave-project-history-boundary.md) | LineageWeave project-history service boundary | Accepted | active-PR | Credential-free bounded project-history API preserves LineageWeave authorization ownership. | | [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. | +| [0053](0053-pareto-candidate-k-analysis-run.md) | Pareto candidate-`K` as an analysis-run profile | Accepted | active-PR | Complements ADR 0012/0022; Pareto statistical front, not Schwarz fitted selection, not joint Laplace draws, and not a Bayesian sampler. | | [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. | | [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. | @@ -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. +- **Pareto candidate-K analysis-run claim boundary:** ADR 0053. - **independent lineage criterion and posterior Project Journey:** ADR 0023. - **macOS-native Rust-owned MLX Metal execution:** ADR 0024. diff --git a/docs/doctoring/pareto-candidate-k-analysis-run.md b/docs/doctoring/pareto-candidate-k-analysis-run.md new file mode 100644 index 000000000..f46b7740b --- /dev/null +++ b/docs/doctoring/pareto-candidate-k-analysis-run.md @@ -0,0 +1,72 @@ +# Pareto candidate-`K` analysis-run composition + +**Active slice:** ADR 0053 / `pareto_candidate_k_v1` +**Protected-main status:** active PR only; not implemented-main + +`model_selection` owns Pareto selection and selected-`K` RMSE arithmetic. This +slice binds that existing gate to the Analysis Run boundary without copying the +numerical algorithm. + +The original profile was not actually leakage-safe: candidate diagnostics had +no source snapshot or availability provenance, request/executor cutoffs were +compared as RFC 3339 text, candidate count was reported as evidence count, and +the quadratic Pareto scan had no application-path population ceiling. + +Current branch repair lineage: + +- RED `34ad6d4072a5af5136f91471d7d08b9b28f38ef9` requires explicit source + snapshot/cutoff/availability provenance, rejection of post-cutoff evidence, + equivalent-instant cutoff binding, source evidence count distinct from model + candidate count, and pre-selection population limits; +- repair `c3173908a3ac90049886e0fd198564001404ace5` binds the diagnostics to the + complete source-evidence availability vector, validates the binding again at + execution, limits the O(n²) candidate scan to 256 candidates, bounds + replications by `MAX_EVIDENCE_UNITS`, compares cutoff instants, reports the + source-evidence denominator, and separates terminal `validated` status from + the domain inference claim; +- RED `1b1bc4b26a02fac85a859ebe6fce891a79291a23` requires external consumers to + use read-only artifact accessors rather than mutating public fields; +- repair `3cc15b76ab4f359850d3658349366d7cb2ef5af0` makes completed artifact fields + private while retaining validated serde parsing and explicit accessors; +- ADR repair `1cd3bdf49a02dec84e7d4986d627394b3e24732d` returns ADR 0053 from premature + `Accepted` authority to `Proposed` and records the temporal/resource/claim + boundaries; +- doctoring repair `4f4c84500a0676c9edd8a84f219585ed29086e88` aligns this profile description + with those boundaries; +- test-only `03a2a9bbe2225b6110d0f1f715332cb01493fbfd` exercises invalid/empty source + provenance, exact and +1 source-evidence/replication/candidate ceilings, and + input-cutoff revalidation; and +- repair `71086e9eacff6e1a4db025a73132d497902027ff` proves a maximal valid artifact + with worst-case JSON-escaped 256-byte identifiers remains below the 256 KiB + input wire limit, keeps the untrusted `from_json` limit, and removes only the + unreachable post-validation egress-size branch. + +Historical admission is intentionally conservative. The input represents the +complete evidence universe that produced its candidate diagnostics and +selected-`K` replications. If any contributing evidence has +`AvailableTime > KnowledgeCutoff`, construction fails closed. The engine does +not pretend it can subtract a future row from likelihood/complexity diagnostics +that were already fitted elsewhere. + +The 256-candidate ceiling is an operational bound on the current quadratic +selection path, not a scientific restriction on valid topic count. LLM votes +remain non-authoritative for the numerical optimum. + +Profile-level scientific acceptance is still open. Issue #500 requires +true-`K` recovery/selection frequency, selected-`K` RMSE and bias with Monte +Carlo uncertainty, explicit attempted/recovered/failed denominators, stability +across the declared design, and leakage-safe temporal evaluation where the +buyer path is longitudinal. Unit fixtures and deterministic RMSE examples do +not satisfy that evidence obligation. + +Shared `docs/TRACEABILITY.md`, `docs/product-technical-gap-baseline.md`, and the +ADR index are consolidation surfaces owned by the #435 documentation lane. A +PR body or comment is handoff evidence, not checked-in current-state authority. + +The profile stays Draft until its valid source/tests/ADR/doctoring delta is +inherited by the surviving Analysis Run vehicle, exact-head Rust/coverage/ +security/CodeQL/documentation checks are current, #500's scientific acceptance +boundary is respected, all valid review findings are resolved, and the live +ruleset's qualifying current-head approval is present. It is not Schwarz fitted +selection, not joint Gauss-Newton Laplace draws, not a Bayesian sampler, not GPU +execution, and not topic birth/split/merge.