diff --git a/CHANGELOG.md b/CHANGELOG.md index 062a69412..e4a08dd60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ## [Unreleased] +- **Inferred-status analysis-run profile**: `analysis_engine` binds existing `inferred_status::EvidenceStatus`, `refuse_inferred_as_observed`, and `refuse_inferred_as_transition` to cutoff-safe `inferred_status_v1` (`tepp.inferred_status.v1`) with inference status `inferred_is_not_observed_and_not_transition`. `identity_recovery_rate` stays library-side. Not relation-absence, not episode-membership, not outcome-order, not membership-target, not location-membership, not membership-posterior ICC, not copied-text, not copy-identity, not citation-edge, not subevent containment, not GPU, not MCMC, and not topic birth/split/merge. + - `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. - `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 2, p. 12 `MANIFESTTRAITVAR`; §7.1, p. 19; p. 16 `MANIFESTTRAITVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-27T14:20Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised manifest-trait variance on current main after `0ce16e8` dropped the pre-consolidation code while research notes already named the map (register items 83–84). Table 2 names `MANIFESTTRAITVAR` `Ψ_τ` the additional time-invariant variance-covariance on the measurement level and sets it `NULL` when there is no manifest trait. Equation 5 writes `Γ ~ N(τ, Ψ)` and names that covariance the manifest traits. Section 7.1 names manifest traits stable individual differences in indicator levels, distinct from process-level `TRAITVAR` `φ_ξ`. Page 16 prints standardised matrices with the suffix `std` when appropriate. The printed example on p. 16 is `discreteDRIFTstd`, not `MANIFESTTRAITVARstd`. Footnote 4 standardises using only the relevant variance, not the total. The relevant variance for that named indicator-level correlation is `MANIFESTTRAITVAR`, not process-level `TRAITVAR` and not residual `MANIFESTVAR` `θ`. The 2017-era source forms `MANIFESTTRAITVARstd` only when `MANIFESTTRAITVAR != 0`, as `solve(sqrt(diag(MANIFESTTRAITVAR) + ridging)) %&% MANIFESTTRAITVAR` when `verbose = TRUE`. OpenMx `%&%` is `t(A) %*% B %*% A`. Unlike `TRAITVARstd`, that formation adds `diag(c(ridging), n.manifest)`. The default `ridging = FALSE` adds 0, not `0.0001`; that ridge is a numerical hack and is not this exact map. The scalar correlation is `ψ / ψ = 1` after strictly positive `MANIFESTTRAITVAR`. Form strictly positive `ψ` first, then `1 / √ψ`, then `(1 / √ψ) ψ (1 / √ψ)`. Unstandardised `MANIFESTTRAITVAR` is defined for a zero trait; standardised `MANIFESTTRAITVAR` is not. Zero `MANIFESTTRAITVAR` skips forming `MANIFESTTRAITVARstd` in the 2017-era source and fails closed here. Indicator-level trait variance is an event-time structural quantity, so a non-event clock fails closed. `MANIFESTTRAITVAR` does not require stable `a < 0`. Distinct positive `ψ` recover the same 1. `trait / trait = 1` is `TRAITVARstd` and recovers the same number and remains a distinct named quantity. `θ` is `MANIFESTVAR` and is measurement error, not this correlation. Meredith (1993) remains unread (web search 2026-08-27T14:20Z: Springer/Cambridge Core paywalled; Unpaywall historically `is_oa: false`; Springer `content/pdf` is an HTML stub). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread on the same terms (DOI `10.1007/bf02294457`). Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation. diff --git a/Cargo.lock b/Cargo.lock index 454a7d612..6cbef05d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,6 +73,7 @@ version = "0.2.0" dependencies = [ "corpus_split", "event_core", + "inferred_status", "membership_core", "relation_graph", "serde", diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 6fa4b9683..5ccbfee63 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -62,97 +62,12 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin | Provider-payload minimization doctoring | [`docs/research/provider-payload-minimization.md`](docs/research/provider-payload-minimization.md) | | Relation absence is not negative evidence | [`docs/research/relation-absence-not-negative.md`](docs/research/relation-absence-not-negative.md) | | Adaptive orchestration router doctoring | [`docs/research/adaptive-orchestration-router.md`](docs/research/adaptive-orchestration-router.md) | -| Mention-confidence Brier doctoring | [`docs/research/mention-confidence-brier.md`](docs/research/mention-confidence-brier.md) | -| Span-grounded mention extent doctoring | [`docs/research/span-grounded-mentions.md`](docs/research/span-grounded-mentions.md) | -| Span-grounded mention numeric provenance | [`docs/doctoring/span-grounded-mentions.md`](docs/doctoring/span-grounded-mentions.md) | -| Event-intelligence status-gate doctoring | [`docs/research/event-intelligence-status-gates.md`](docs/research/event-intelligence-status-gates.md) | | Stopword-deletion doctoring | [`docs/research/stopword-deletion.md`](docs/research/stopword-deletion.md) | | Topic log-ratio coordinate doctoring | [`docs/research/topic-logratio-coordinates.md`](docs/research/topic-logratio-coordinates.md) | | 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) | -| 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) | - -## Maturity vocabulary - -The canonical implementation-maturity vocabulary is defined in [`docs/adr/ADR_POLICY.md`](docs/adr/ADR_POLICY.md) and promotion evidence in [`docs/TRACEABILITY.md`](docs/TRACEABILITY.md). In particular, **an ADR with decision status `Accepted` is not automatically implemented or shipped.** - -- **implemented-main** — source is integrated on protected `main` and the relevant exact-current-head tests, scientific/recovery/validation evidence, security and supply-chain gates, and qualifying review required by live policy pass. -- **active-PR** — implementation exists only on an open PR and is not a protected-main claim. -- **partial** — an explicitly identified subset is implemented on protected main while the rest remains target work. -- **accepted-target** — accepted PRD/ADR architecture not yet integrated. -- **research-only** — evaluated research direction not accepted as production behavior. -- **out-of-scope** — explicitly outside TEPP ownership. -- **conceptual** — logical entity/service/model contract; not evidence of a migration or deployment. -- **deployment-owned** — evidence depends on a concrete deployed environment or organization and cannot be claimed by repository design alone. -- **external-assurance** — certification, attestation, legal opinion, or other independent assessment that TEPP cannot self-issue. - -## Documentation fitness - -The documentation graph is **design-sufficient** when a reviewer can reconstruct TEPP's product requirements, technical/scientific estimands, authority boundaries, temporal/event/membership semantics, data model, failure modes, security/privacy controls, validation strategy, API/integration contract, operability, research basis, ADR ownership/supersession, and release acceptance without chat history. - -It is **protected-main-sufficient** only after the canonical documents are integrated on protected `main`, remain semantically current with live code, and their required exact-head documentation/security/review gates pass. An active documentation PR can therefore be design-sufficient while the protected branch remains documentation-insufficient. - -At the time of this review, protected `main` at `7c29e7c971d7940e1fb3def1ed3aae2d1bc8ad4a` implements immutable evidence records/exact spans, the Rust workspace quality foundation, typed six-clock values/uncertain intervals (merged PR #8), Allen interval algebra and bounded path-consistency (merged PR #9), event ontology/membership, and PostgreSQL persistence through restore-integrity probes. Superseded PRs #5 and #6 remain historical lineage only, and drafts #93, #94, #97, #101, #102, #104, #108, #109, #111, and #112 are superseded non-landable lineage; unmerged or draft PRs are never implemented-main claims. The active-PR `prediction_contradiction` coverage gate requires observed Allen coverage (`during`, `starts`, `finishes`, or `equals`) before unmatched predicted mass may be authorized for promotion; `refuse_promotion` is that authority and not a contradiction-only filter — coverage may authorize promotion but does not convert a forecast into observed fact. Remaining TDT/CHRONOS intelligence, shared-latent topic estimation, GPU kernels, longitudinal ESEM/DSEM, visual analytics, production HTTP services, and deployment assurance stay accepted-target or deployment-owned unless a [`docs/TRACEABILITY.md`](docs/TRACEABILITY.md) row records a narrower implemented-main or partial subset. -# TEPP Documentation Map - -TEPP's approved PRD v0.4 and implementation plan are the primary product baseline. This index makes the technical, data, scientific, security/privacy, integration, quality, operating, and assurance contracts discoverable without duplicating that source material. - -| Area | Canonical document | -|---|---| -| Approved product requirements | [`docs/product/prd-v0.4-approved.md`](docs/product/prd-v0.4-approved.md) | -| Live product and technical gap baseline | [`docs/product-technical-gap-baseline.md`](docs/product-technical-gap-baseline.md) | -| Whole-conversation documentation fitness | [`docs/DOCUMENTATION_ASSESSMENT.md`](docs/DOCUMENTATION_ASSESSMENT.md) | -| Technical requirements | [`docs/TRD.md`](docs/TRD.md) | -| Architecture | [`ARCHITECTURE.md`](ARCHITECTURE.md) | -| Modular/API integration contract | [`docs/API_CONTRACT.md`](docs/API_CONTRACT.md) | -| naruon modular consumer contract | [`docs/connectors/naruon-artifact-consumer.md`](docs/connectors/naruon-artifact-consumer.md) | -| contextual-orchestrator interpretation port | [`docs/connectors/contextual-orchestrator-interpretation-port.md`](docs/connectors/contextual-orchestrator-interpretation-port.md) | -| UML/runtime/scientific flows | [`docs/UML.md`](docs/UML.md) | -| Logical/physical ERD | [`docs/ERD.md`](docs/ERD.md) | -| Security policy | [`SECURITY.md`](SECURITY.md) | -| Threat model | [`docs/THREAT_MODEL.md`](docs/THREAT_MODEL.md) | -| Privacy and data governance | [`docs/PRIVACY_DATA_GOVERNANCE.md`](docs/PRIVACY_DATA_GOVERNANCE.md) | -| Compliance/assurance readiness | [`docs/COMPLIANCE_READINESS.md`](docs/COMPLIANCE_READINESS.md) | -| LLM orchestration/test-time compute | [`docs/LLM_ORCHESTRATION.md`](docs/LLM_ORCHESTRATION.md) | -| Test/scientific validation strategy | [`docs/TEST_STRATEGY.md`](docs/TEST_STRATEGY.md) | -| Operability/recovery/release | [`docs/OPERABILITY.md`](docs/OPERABILITY.md) | -| Requirement/research/evidence traceability | [`docs/TRACEABILITY.md`](docs/TRACEABILITY.md) | -| Architecture decision index / ownership map | [`docs/adr/README.md`](docs/adr/README.md) | -| ADR status, maturity, and supersession policy | [`docs/adr/ADR_POLICY.md`](docs/adr/ADR_POLICY.md) | -| Delivery roadmap | [`docs/roadmaps/2026-08-05-tepp-delivery-roadmap.md`](docs/roadmaps/2026-08-05-tepp-delivery-roadmap.md) | -| Foundation implementation plan | [`docs/superpowers/plans/2026-08-05-temporal-event-foundation.md`](docs/superpowers/plans/2026-08-05-temporal-event-foundation.md) | -| Foundation validation ledger | [`docs/validation/temporal-event-foundation.md`](docs/validation/temporal-event-foundation.md) | -| Standards and APA 7 literature | [`docs/research/standards-and-literature.md`](docs/research/standards-and-literature.md) | -| Operational log / source-separation doctoring | [`docs/research/operational-log-source-separation.md`](docs/research/operational-log-source-separation.md) | -| Interval cutoff eligibility doctoring | [`docs/research/interval-cutoff-eligibility.md`](docs/research/interval-cutoff-eligibility.md) | -| Governance | [`GOVERNANCE.md`](GOVERNANCE.md) | -| Agent development rules | [`AGENTS.md`](AGENTS.md) | -| Agent context | [`CLAUDE.md`](CLAUDE.md) | -| Hourly NIM product-development operations | [`docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md`](docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md) | -| Actions workflow fleet audit | [`docs/operations/ACTIONS_WORKFLOW_FLEET.md`](docs/operations/ACTIONS_WORKFLOW_FLEET.md) | -| Actions fleet research doctoring | [`docs/research/actions-workflow-fleet.md`](docs/research/actions-workflow-fleet.md) | -| Mention-confidence Brier doctoring | [`docs/research/mention-confidence-brier.md`](docs/research/mention-confidence-brier.md) | -| Span-grounded mention extent doctoring | [`docs/research/span-grounded-mentions.md`](docs/research/span-grounded-mentions.md) | -| Span-grounded mention numeric provenance | [`docs/doctoring/span-grounded-mentions.md`](docs/doctoring/span-grounded-mentions.md) | -| Event-intelligence status-gate doctoring | [`docs/research/event-intelligence-status-gates.md`](docs/research/event-intelligence-status-gates.md) | -| VRAM budget / GPU fallback doctoring | [`docs/research/vram-budget-types.md`](docs/research/vram-budget-types.md) | -| Causal-identification gate doctoring | [`docs/research/causal-identification-gate.md`](docs/research/causal-identification-gate.md) | -| TDT story-segmentation `WindowDiff`/`Pk` doctoring | [`docs/research/tdt-story-segmentation.md`](docs/research/tdt-story-segmentation.md) | -| CHRONOS prediction-calibration doctoring | [`docs/research/chronos-prediction-calibration.md`](docs/research/chronos-prediction-calibration.md) | -| CHRONOS schema-slot calibration doctoring | [`docs/research/chronos-schema-slot-calibration.md`](docs/research/chronos-schema-slot-calibration.md) | -| Event-tracking calibration doctoring | [`docs/research/event-tracking-calibration.md`](docs/research/event-tracking-calibration.md) | -| Episode-membership identity doctoring | [`docs/research/episode-membership-identity.md`](docs/research/episode-membership-identity.md) | -| Entity/project target SQL doctoring | [`docs/research/entity-project-sql.md`](docs/research/entity-project-sql.md) | -| Scientific claim-promotion gate doctoring | [`docs/research/scientific-claim-promotion-gates.md`](docs/research/scientific-claim-promotion-gates.md) | -| Retention/deletion/legal-hold doctoring | [`docs/research/retention-deletion-legal-hold.md`](docs/research/retention-deletion-legal-hold.md) | -| Stopword-deletion doctoring | [`docs/research/stopword-deletion.md`](docs/research/stopword-deletion.md) | -| Provider-payload minimization doctoring | [`docs/research/provider-payload-minimization.md`](docs/research/provider-payload-minimization.md) | -| Relation absence is not negative evidence | [`docs/research/relation-absence-not-negative.md`](docs/research/relation-absence-not-negative.md) | -| Adaptive orchestration router doctoring | [`docs/research/adaptive-orchestration-router.md`](docs/research/adaptive-orchestration-router.md) | -| Hourly NIM OpenCode doctoring | [`docs/doctoring/hourly-nim-opencode-development.md`](docs/doctoring/hourly-nim-opencode-development.md) | +| Inferred-status analysis-run doctoring | [`docs/doctoring/inferred-status-analysis-run.md`](docs/doctoring/inferred-status-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..b6ed3bba3 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" } +inferred_status = { path = "../inferred_status", version = "0.2.0" } serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } diff --git a/crates/analysis_engine/src/inferred_status_artifact.rs b/crates/analysis_engine/src/inferred_status_artifact.rs new file mode 100644 index 000000000..489391121 --- /dev/null +++ b/crates/analysis_engine/src/inferred_status_artifact.rs @@ -0,0 +1,457 @@ +//! Digest-bound inferred-status refusals as an analysis-run profile. + +use inferred_status::{ + EvidenceStatus, InferredStatusError, refuse_inferred_as_observed, refuse_inferred_as_transition, +}; +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 inferred-status artifact. +pub const INFERRED_STATUS_ARTIFACT_SCHEMA_VERSION: &str = "tepp.inferred_status.v1"; +/// Model contract required by the inferred-status execution path. +pub const INFERRED_STATUS_MODEL_CONTRACT_VERSION: &str = "inferred_status_v1"; +/// Analysis-run output profile required for an inferred-status artifact. +pub const INFERRED_STATUS_OUTPUT_PROFILE: &str = "inferred_status_v1"; +/// Maximum canonical artifact JSON size. +pub const INFERRED_STATUS_ARTIFACT_BYTE_LIMIT: usize = 256 * 1024; +const INFERRED_STATUS_INFERENCE_STATUS: &str = "inferred_is_not_observed_and_not_transition"; + +/// One cutoff-admitted evidence row with closed observed/inferred status. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct InferredStatusEvidence { + evidence_id: String, + status: EvidenceStatus, + available_time: AvailableTime, +} + +impl InferredStatusEvidence { + /// Construct a bounded inferred-status evidence row. + /// + /// # Errors + /// + /// Returns [`AnalysisEngineError::InvalidEvidence`] when the evidence + /// identity is empty or oversized. + pub fn new( + evidence_id: impl Into, + status: EvidenceStatus, + available_time: AvailableTime, + ) -> Result { + let evidence_id = evidence_id.into(); + if !valid_identifier(&evidence_id) { + return Err(AnalysisEngineError::InvalidEvidence); + } + Ok(Self { + evidence_id, + status, + available_time, + }) + } + + /// Return the opaque evidence identity. + #[must_use] + pub fn evidence_id(&self) -> &str { + &self.evidence_id + } + + /// Return the closed observed/inferred status. + #[must_use] + pub const fn status(&self) -> EvidenceStatus { + self.status + } + + /// Return the availability time used for cutoff eligibility. + #[must_use] + pub const fn available_time(&self) -> AvailableTime { + self.available_time + } +} + +/// Completed, bounded inferred-status census for analysis-run clients. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct InferredStatusArtifact { + /// 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 rows. + pub knowledge_cutoff: String, + /// Number of evidence rows admitted at the cutoff. + pub evidence_count: u64, + /// Directly observed rows admitted at the cutoff. + pub observed_count: u64, + /// Inferred rows admitted at the cutoff. + pub inferred_count: u64, + /// Inferred rows refused as observed evidence. + pub refused_as_observed_count: u64, + /// Inferred rows refused as state transitions. + pub refused_as_transition_count: u64, + /// Fixed claim boundary for consumer copy. + pub inference_status: String, +} + +impl InferredStatusArtifact { + /// Parse and fully validate a bounded artifact JSON payload. + /// + /// # Errors + /// + /// Returns [`AnalysisEngineError::InvalidInferredStatusArtifact`] when the + /// schema, identifiers, counts, or claim boundary fail. + pub fn from_json(payload: &str) -> Result { + if payload.len() > INFERRED_STATUS_ARTIFACT_BYTE_LIMIT { + return Err(AnalysisEngineError::LimitExceeded); + } + let artifact: Self = serde_json::from_str(payload) + .map_err(|_| AnalysisEngineError::InvalidInferredStatusArtifact)?; + artifact.validate()?; + Ok(artifact) + } + + /// Serialize canonical validated artifact JSON. + /// + /// # Errors + /// + /// Returns a typed validation, serialization, or size failure. + pub fn to_json(&self) -> Result { + self.validate()?; + let payload = + serde_json::to_string(self).map_err(|_| AnalysisEngineError::SerializationFailure)?; + if payload.len() > INFERRED_STATUS_ARTIFACT_BYTE_LIMIT { + return Err(AnalysisEngineError::LimitExceeded); + } + Ok(payload) + } + + /// 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 status_sum = self.observed_count.checked_add(self.inferred_count); + if self.schema_version != INFERRED_STATUS_ARTIFACT_SCHEMA_VERSION + || !valid_identifier(&self.run_id) + || !valid_identifier(&self.snapshot_id) + || KnowledgeCutoff::parse_rfc3339(&self.knowledge_cutoff).is_err() + || self.evidence_count < 2 + || self.evidence_count > MAX_EVIDENCE_UNITS as u64 + || self.observed_count == 0 + || self.inferred_count == 0 + || status_sum != Some(self.evidence_count) + || self.refused_as_observed_count != self.inferred_count + || self.refused_as_transition_count != self.inferred_count + || self.inference_status != INFERRED_STATUS_INFERENCE_STATUS + { + return Err(AnalysisEngineError::InvalidInferredStatusArtifact); + } + Ok(()) + } +} + +/// One completed inferred-status artifact and its terminal result. +#[derive(Clone, Debug, PartialEq)] +pub struct InferredStatusExecution { + /// Digest-bound completed inferred-status census. + pub artifact: InferredStatusArtifact, + /// Terminal result carrying the artifact identity, digest, and schema. + pub terminal_result: AnalysisRunTerminalResult, +} + +/// Execute cutoff-safe inferred-status refusals as one analysis-run profile. +/// +/// The executor invokes [`refuse_inferred_as_observed`] and +/// [`refuse_inferred_as_transition`] already on protected main. Observed +/// statuses stay observed. Inferred statuses stay refusals, never observed +/// evidence and never transitions. It does not emit +/// `identity_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-class corpus, inferred treated as observed or transition, +/// duplicate evidence identity, oversized corpus, or invalid artifact +/// error. +pub fn execute_inferred_status_run( + request: &AnalysisRunRequest, + accepted: &AnalysisRunAccepted, + snapshot_id: &str, + knowledge_cutoff: KnowledgeCutoff, + evidence: &[InferredStatusEvidence], + 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); + } + if request.knowledge_cutoff != knowledge_cutoff.to_rfc3339() + || request.model_contract_version != INFERRED_STATUS_MODEL_CONTRACT_VERSION + || request.output_profile != INFERRED_STATUS_OUTPUT_PROFILE + { + return Err(AnalysisEngineError::InvalidEvidence); + } + if evidence.len() > MAX_EVIDENCE_UNITS { + return Err(AnalysisEngineError::LimitExceeded); + } + + let (observed_count, inferred_count, refused_as_observed_count, refused_as_transition_count) = + census_evidence(evidence, knowledge_cutoff)?; + let evidence_count = observed_count + .checked_add(inferred_count) + .ok_or(AnalysisEngineError::ArithmeticOverflow)?; + if evidence_count < 2 + || observed_count == 0 + || inferred_count == 0 + || refused_as_observed_count != inferred_count + || refused_as_transition_count != inferred_count + { + return Err(AnalysisEngineError::InvalidEvidence); + } + + let artifact = InferredStatusArtifact { + schema_version: INFERRED_STATUS_ARTIFACT_SCHEMA_VERSION.into(), + run_id: accepted.run_id.clone(), + snapshot_id: snapshot_id.to_owned(), + knowledge_cutoff: knowledge_cutoff.to_rfc3339(), + evidence_count, + observed_count, + inferred_count, + refused_as_observed_count, + refused_as_transition_count, + inference_status: INFERRED_STATUS_INFERENCE_STATUS.into(), + }; + let digest = artifact.sha256()?; + let summary = AnalysisResultSummary::new( + "inferred_status", + evidence_count, + 4, + INFERRED_STATUS_INFERENCE_STATUS, + )?; + let terminal_result = AnalysisRunTerminalResult::succeeded( + request, + accepted, + format!("inferred_status_artifact_{}", &digest[..16]), + digest, + INFERRED_STATUS_ARTIFACT_SCHEMA_VERSION, + completed_at, + summary, + )?; + Ok(InferredStatusExecution { + artifact, + terminal_result, + }) +} + +fn census_evidence( + evidence: &[InferredStatusEvidence], + knowledge_cutoff: KnowledgeCutoff, +) -> Result<(u64, u64, u64, u64), AnalysisEngineError> { + let mut seen = std::collections::BTreeSet::new(); + let mut observed_count = 0_u64; + let mut inferred_count = 0_u64; + let mut refused_as_observed_count = 0_u64; + let mut refused_as_transition_count = 0_u64; + for row in evidence { + if !seen.insert(row.evidence_id()) { + return Err(AnalysisEngineError::DuplicateEvidence); + } + if row.available_time().instant() > knowledge_cutoff.instant() { + continue; + } + match row.status() { + EvidenceStatus::Observed => { + refuse_inferred_as_observed(row.status()).map_err(map_inferred_status_error)?; + refuse_inferred_as_transition(row.status()).map_err(map_inferred_status_error)?; + observed_count = increment(observed_count)?; + } + EvidenceStatus::Inferred => { + match refuse_inferred_as_observed(row.status()) { + Err(InferredStatusError::InferredIsNotObserved) => { + refused_as_observed_count = increment(refused_as_observed_count)?; + } + Ok(()) | Err(_) => return Err(AnalysisEngineError::InvalidEvidence), + } + match refuse_inferred_as_transition(row.status()) { + Err(InferredStatusError::InferredIsNotTransition) => { + refused_as_transition_count = increment(refused_as_transition_count)?; + } + Ok(()) | Err(_) => return Err(AnalysisEngineError::InvalidEvidence), + } + inferred_count = increment(inferred_count)?; + } + } + } + Ok(( + observed_count, + inferred_count, + refused_as_observed_count, + refused_as_transition_count, + )) +} + +fn increment(count: u64) -> Result { + count + .checked_add(1) + .ok_or(AnalysisEngineError::ArithmeticOverflow) +} + +fn map_inferred_status_error(error: InferredStatusError) -> AnalysisEngineError { + match error { + InferredStatusError::InferredIsNotObserved + | InferredStatusError::InferredIsNotTransition + | InferredStatusError::InvalidStatusPayload + | _ => AnalysisEngineError::InvalidEvidence, + } +} + +#[cfg(test)] +mod tests { + use super::{ + INFERRED_STATUS_ARTIFACT_BYTE_LIMIT, INFERRED_STATUS_ARTIFACT_SCHEMA_VERSION, + INFERRED_STATUS_INFERENCE_STATUS, InferredStatusArtifact, + }; + use crate::{AnalysisEngineError, MAX_EVIDENCE_UNITS}; + + fn artifact() -> InferredStatusArtifact { + InferredStatusArtifact { + schema_version: INFERRED_STATUS_ARTIFACT_SCHEMA_VERSION.into(), + run_id: "run-1".into(), + snapshot_id: "snapshot-1".into(), + knowledge_cutoff: "2026-08-01T00:00:00Z".into(), + evidence_count: 2, + observed_count: 1, + inferred_count: 1, + refused_as_observed_count: 1, + refused_as_transition_count: 1, + inference_status: INFERRED_STATUS_INFERENCE_STATUS.into(), + } + } + + fn assert_invalid(artifact: &InferredStatusArtifact) { + assert_eq!( + artifact.to_json(), + Err(AnalysisEngineError::InvalidInferredStatusArtifact) + ); + } + + #[test] + fn artifact_round_trip_and_size_bounds_fail_closed() { + let artifact = artifact(); + let payload = artifact.to_json().expect("json"); + assert_eq!( + InferredStatusArtifact::from_json(&payload), + Ok(artifact.clone()) + ); + assert_eq!(artifact.sha256().expect("digest").len(), 64); + assert_eq!( + InferredStatusArtifact::from_json("{}"), + Err(AnalysisEngineError::InvalidInferredStatusArtifact) + ); + assert_eq!( + InferredStatusArtifact::from_json(&"x".repeat(INFERRED_STATUS_ARTIFACT_BYTE_LIMIT + 1)), + Err(AnalysisEngineError::LimitExceeded) + ); + } + + #[test] + fn artifact_claimed_count_above_execution_limit_fails_parse_and_serialize() { + let mut oversized = artifact(); + oversized.evidence_count = MAX_EVIDENCE_UNITS as u64 + 1; + oversized.observed_count = 1; + oversized.inferred_count = MAX_EVIDENCE_UNITS as u64; + oversized.refused_as_observed_count = MAX_EVIDENCE_UNITS as u64; + oversized.refused_as_transition_count = MAX_EVIDENCE_UNITS as u64; + + assert_eq!( + oversized.to_json(), + Err(AnalysisEngineError::InvalidInferredStatusArtifact) + ); + let unchecked_payload = serde_json::to_string(&oversized).expect("unchecked fixture json"); + assert_eq!( + InferredStatusArtifact::from_json(&unchecked_payload), + Err(AnalysisEngineError::InvalidInferredStatusArtifact) + ); + } + + #[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.evidence_count = 1; + value + }, + { + let mut value = artifact.clone(); + value.observed_count = 0; + value.evidence_count = 1; + value + }, + { + let mut value = artifact.clone(); + value.inferred_count = 0; + value.refused_as_observed_count = 0; + value.refused_as_transition_count = 0; + value.evidence_count = 1; + value + }, + { + let mut value = artifact.clone(); + value.refused_as_observed_count = 0; + value + }, + { + let mut value = artifact.clone(); + value.refused_as_transition_count = 0; + value + }, + { + let mut value = artifact.clone(); + value.inference_status.clear(); + value + }, + ]; + for invalid in invalid_artifacts { + assert_invalid(&invalid); + } + } +} diff --git a/crates/analysis_engine/src/lib.rs b/crates/analysis_engine/src/lib.rs index 72bd5854c..68a2c2434 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 inferred_status_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; +/// Inferred-status artifact and execution contracts from this engine. +pub use inferred_status_artifact::{ + INFERRED_STATUS_ARTIFACT_BYTE_LIMIT, INFERRED_STATUS_ARTIFACT_SCHEMA_VERSION, + INFERRED_STATUS_MODEL_CONTRACT_VERSION, INFERRED_STATUS_OUTPUT_PROFILE, InferredStatusArtifact, + InferredStatusEvidence, InferredStatusExecution, execute_inferred_status_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, + /// An inferred-status artifact violated its bounded schema or count invariants. + InvalidInferredStatusArtifact, } 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::InvalidInferredStatusArtifact => "invalid inferred-status artifact", }; formatter.write_str(message) } @@ -681,6 +691,10 @@ mod tests { AnalysisEngineError::InvalidTopicLineageArtifact, "invalid topic lineage artifact", ), + ( + AnalysisEngineError::InvalidInferredStatusArtifact, + "invalid inferred-status artifact", + ), ]; for (error, message) in messages { assert_eq!(error.to_string(), message); diff --git a/crates/analysis_engine/tests/inferred_status_execution_contract.rs b/crates/analysis_engine/tests/inferred_status_execution_contract.rs new file mode 100644 index 000000000..548c72e13 --- /dev/null +++ b/crates/analysis_engine/tests/inferred_status_execution_contract.rs @@ -0,0 +1,238 @@ +//! End-to-end contract for cutoff-safe inferred-status refusals. + +use analysis_engine::{ + AnalysisEngineError, INFERRED_STATUS_ARTIFACT_SCHEMA_VERSION, + INFERRED_STATUS_MODEL_CONTRACT_VERSION, INFERRED_STATUS_OUTPUT_PROFILE, InferredStatusEvidence, + MAX_EVIDENCE_UNITS, execute_inferred_status_run, +}; +use inferred_status::EvidenceStatus; +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: "inferred-status-idem".into(), + tenant_workspace_id: "tenant-workspace".into(), + snapshot_id: "snapshot-inferred-status".into(), + knowledge_cutoff: "2026-08-01T00:00:00Z".into(), + model_contract_version: INFERRED_STATUS_MODEL_CONTRACT_VERSION.into(), + output_profile: INFERRED_STATUS_OUTPUT_PROFILE.into(), + } +} + +fn accepted(request: &AnalysisRunRequest) -> AnalysisRunAccepted { + AnalysisRunAccepted::new("run-inferred-status", "accepted", &request.idempotency_key) + .expect("accepted") +} + +fn evidence(evidence_id: &str, status: EvidenceStatus, stamp: &str) -> InferredStatusEvidence { + InferredStatusEvidence::new(evidence_id, status, available(stamp)).expect("evidence") +} + +fn mixed_evidence() -> Vec { + vec![ + evidence( + "observed-a", + EvidenceStatus::Observed, + "2026-07-01T00:00:00Z", + ), + evidence( + "inferred-b", + EvidenceStatus::Inferred, + "2026-07-02T00:00:00Z", + ), + ] +} + +fn execute( + request: &AnalysisRunRequest, + evidence: &[InferredStatusEvidence], +) -> Result { + execute_inferred_status_run( + request, + &accepted(request), + "snapshot-inferred-status", + cutoff(), + evidence, + "2026-08-02T00:00:00Z", + ) +} + +#[test] +fn mixed_statuses_emit_digest_bound_refusals_without_recovery_metric() { + let request = request(); + let execution = execute(&request, &mixed_evidence()).expect("execution"); + assert_eq!( + execution.artifact.schema_version, + INFERRED_STATUS_ARTIFACT_SCHEMA_VERSION + ); + assert_eq!(execution.artifact.evidence_count, 2); + assert_eq!(execution.artifact.observed_count, 1); + assert_eq!(execution.artifact.inferred_count, 1); + assert_eq!(execution.artifact.refused_as_observed_count, 1); + assert_eq!(execution.artifact.refused_as_transition_count, 1); + assert_eq!( + execution.artifact.inference_status, + "inferred_is_not_observed_and_not_transition" + ); + let payload = execution.artifact.to_json().expect("json"); + assert!(!payload.contains("identity_recovery_rate")); + assert!(!payload.contains("scientific_acceptance")); + assert!(!payload.contains("unobserved")); + assert!(!payload.contains("no_relationship")); + 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(INFERRED_STATUS_ARTIFACT_SCHEMA_VERSION) + ); +} + +#[test] +fn future_available_evidence_is_excluded() { + let request = request(); + let mut with_future = mixed_evidence(); + with_future.push(evidence( + "future-c", + EvidenceStatus::Observed, + "2026-08-02T00:00:00Z", + )); + let execution = execute(&request, &with_future).expect("cutoff"); + assert_eq!(execution.artifact.evidence_count, 2); + assert_eq!(execution.artifact.observed_count, 1); +} + +#[test] +fn empty_or_single_class_and_duplicate_fail_closed() { + let request = request(); + let stamp = "2026-07-01T00:00:00Z"; + assert_eq!( + execute(&request, &[]), + Err(AnalysisEngineError::InvalidEvidence) + ); + let observed_only = vec![ + evidence("observed-a", EvidenceStatus::Observed, stamp), + evidence("observed-b", EvidenceStatus::Observed, stamp), + ]; + assert_eq!( + execute(&request, &observed_only), + Err(AnalysisEngineError::InvalidEvidence) + ); + let inferred_only = vec![ + evidence("inferred-a", EvidenceStatus::Inferred, stamp), + evidence("inferred-b", EvidenceStatus::Inferred, stamp), + ]; + assert_eq!( + execute(&request, &inferred_only), + Err(AnalysisEngineError::InvalidEvidence) + ); + let duplicates = vec![ + evidence("same", EvidenceStatus::Observed, stamp), + evidence("same", EvidenceStatus::Inferred, stamp), + ]; + assert_eq!( + execute(&request, &duplicates), + Err(AnalysisEngineError::DuplicateEvidence) + ); + assert_eq!( + InferredStatusEvidence::new("", EvidenceStatus::Observed, available(stamp)), + Err(AnalysisEngineError::InvalidEvidence) + ); +} + +#[test] +fn execution_refuses_snapshot_profile_cutoff_mismatch_and_oversize() { + let request = request(); + let rows = mixed_evidence(); + assert_eq!( + execute_inferred_status_run( + &request, + &accepted(&request), + "other-snapshot", + cutoff(), + &rows, + "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_inferred_status_run( + &mismatched, + &accepted(&mismatched), + "snapshot-inferred-status", + cutoff(), + &rows, + "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", + "citation_edge_v1", + "copied_text_v1", + "lineage_criterion_v1", + "composed_fitted_lineage_v1", + "case_deletion_refit_v1", + "topic_activity_v1", + "location_membership_v1", + "topic_context_posterior_v1", + "membership_posterior_icc_v1", + "membership_target_v1", + "outcome_order_v1", + "relation_absence_v1", + "episode_membership_v1", + ] { + let mut reused = request.clone(); + reused.output_profile = profile.into(); + assert_eq!( + execute_inferred_status_run( + &reused, + &accepted(&reused), + "snapshot-inferred-status", + cutoff(), + &rows, + "2026-08-02T00:00:00Z", + ), + Err(AnalysisEngineError::InvalidEvidence) + ); + } + let oversized: Vec = (0..=MAX_EVIDENCE_UNITS) + .map(|index| { + evidence( + &format!("evidence-{index}"), + EvidenceStatus::Observed, + "2026-07-01T00:00:00Z", + ) + }) + .collect(); + assert_eq!( + execute(&request, &oversized), + Err(AnalysisEngineError::LimitExceeded) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 2b783c2ab..14aca9d92 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 | +| inferred-status analysis-run profile | ADR 0003/0022/0073; inferred relations cannot be observed evidence or transitions | `analysis_engine` `inferred_status_v1` binds `refuse_inferred_as_observed` and `refuse_inferred_as_transition`; digest-bound refusals, not `identity_recovery_rate` inspect metric, not relation-absence, not episode-membership, not outcome-order, not membership-target, not location-membership, not subevent containment, 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/0073-inferred-status-analysis-run.md b/docs/adr/0073-inferred-status-analysis-run.md new file mode 100644 index 000000000..3630be117 --- /dev/null +++ b/docs/adr/0073-inferred-status-analysis-run.md @@ -0,0 +1,112 @@ +# ADR 0073 — Inferred-status refusals as an analysis-run output profile + +**Decision status:** Accepted +**Implementation maturity:** active-PR — composed on this branch; not implemented-main +**Date:** 2026-09-01 +**Supersedes:** None; complements ADR 0003 (inferred relations cannot be promoted to observed evidence or transitions) and ADR 0022 (cutoff-safe analysis-run execution). Does not reuse ADR 0072 (episode-membership), ADR 0071 (relation-absence), ADR 0070 (outcome-order), ADR 0069 (membership-target), ADR 0068 (topic-context posterior), ADR 0066 (location-membership), ADR 0065 (copied-text residue), ADR 0064 (provenance-is-not-transition / citation-edge), or ADR 0058 (copy-identity / template-copy). This is observed-versus-inferred promotion refusal, not unobserved-is-not-negative and not membership-window containment. +**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 an inferred relation as observed +evidence or as a state transition, via `inferred_status::EvidenceStatus`, +`refuse_inferred_as_observed`, and `refuse_inferred_as_transition`. +Operators still cannot request that census as a digest-bound analysis-run +output. + +Episode-membership (#461 / ADR 0072) binds membership-window containment. +Relation-absence (#460 / ADR 0071) binds `unobserved` as not-negative +with a three-status vocabulary that already treats `inferred` as +presence. Outcome-order (#458 / ADR 0070) binds IPO event-time order. +Membership-target (#434 / ADR 0069) binds `MembershipTargetKind`. +Location-membership (#430 / ADR 0066) binds geographic/market assignment. + +`identity_recovery_rate` stays library-side. This slice does not put a +`scientific_acceptance` metric on inspect payloads. `unobserved` and +`no_relationship` are not wire statuses here. + +GPU kernels, MCMC, and topic birth/split/merge remain later GAP-004 work +and are not this slice. + +## Decision + +Add the `inferred_status_v1` analysis-run output profile to +`analysis_engine`. The executor: + +- consumes already-validated `InferredStatusEvidence` rows with closed + `EvidenceStatus` values and availability time; +- requires the request snapshot and knowledge cutoff to match the offered + input construction; +- excludes rows whose availability is later than the knowledge cutoff; +- invokes `refuse_inferred_as_observed` and + `refuse_inferred_as_transition` without reimplementing the + `observed` / `inferred` vocabulary; +- requires a mixed census of at least one `observed` and one `inferred` + after cutoff exclusion; +- caps both executable evidence input and serialized/deserialized artifact + claims at `MAX_EVIDENCE_UNITS`, so a compact payload cannot claim a census + no valid run can produce; +- emits a canonical SHA-256-digested `tepp.inferred_status.v1` artifact + with per-status counts, matching inferred refusal counts, and + inference status `inferred_is_not_observed_and_not_transition`; +- does not emit `identity_recovery_rate`, invent MCMC, select GPU + backends, or emit topic birth/split/merge events. + +## Alternatives considered + +1. Duplicate episode-membership (#461 / ADR 0072) — rejected because that + profile binds membership-window containment, not inferred promotion. +2. Duplicate relation-absence (#460 / ADR 0071) — rejected because that + profile binds `unobserved` as not-negative and already treats + `inferred` as presence. +3. Duplicate outcome-order (#458 / ADR 0070) — rejected because that + profile binds IPO event-time order. +4. Duplicate membership-target (#434 / ADR 0069) — rejected because that + profile binds `MembershipTargetKind`. +5. Put `identity_recovery_rate` on the operator artifact — rejected + because inspect payloads stay metric-free and + `tepp.scientific_acceptance.v1` never appears. +6. Bind the existing inferred-status refusals to ADR 0022's + analysis-run profile — accepted. + +## Consequences + +Operators can request cutoff-safe inferred-status refusals as a +digest-bound terminal result. The artifact does not claim MCMC, GPU +parity, relation-absence, episode-membership, outcome-order, +membership-target, location-membership, membership-posterior ICC, +copied-text, copy-identity, citation-edge, subevent containment, +method-effect estimation, or topic birth/split/merge. Snapshot / +profile / cutoff mismatch, empty or single-class corpora, duplicate +evidence identities, and oversized corpora or artifact claims fail closed. + +## Verification + +The PR includes Rust unit and integration tests for mixed +`observed` / `inferred` corpora, cutoff exclusion, +empty/single-class/duplicate refusal, snapshot / profile / cutoff +mismatch, executable and compact-artifact oversize, and artifact tampering. Run: + +```text +cargo fmt --all -- --check +cargo test -p analysis_engine +cargo clippy -p analysis_engine --all-targets -- -D warnings +python3 scripts/validate_documentation.py +``` + +Then follow the repository-wide exact-head procedure in +[`AGENTS.md`](../../AGENTS.md#pull-request-and-autonomous-execution-loop): the +required hosted CI, documentation, security/supply-chain workflows and +qualifying review must apply to the PR's current exact SHA. Queued, stale or +predecessor-head results are not passing evidence. Because this profile is +`fold_into_landing_vehicle`, its final landing evidence belongs to the coherent +Validation / Analysis Run successor head rather than this micro-profile alone. + +## Rollback and supersession + +Rollback removes the `inferred_status_v1` profile. No persisted schema +migration is introduced. Supersede only with an ADR that keeps inferred +relations out of observed evidence and out of transitions, keeps this +distinct from relation-absence unobserved-is-not-negative, and keeps +`identity_recovery_rate` off inspect payloads. diff --git a/docs/adr/README.md b/docs/adr/README.md index 1254c8079..656a21928 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. | +| [0073](0073-inferred-status-analysis-run.md) | Inferred-status refusals as an analysis-run profile | Accepted | active-PR | Complements ADR 0003/0022; `EvidenceStatus` + `refuse_inferred_as_observed` / `refuse_inferred_as_transition`, not relation-absence, not episode-membership. | | [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. +- **inferred-status analysis-run profile:** ADR 0073. - **independent lineage criterion and posterior Project Journey:** ADR 0023. - **macOS-native Rust-owned MLX Metal execution:** ADR 0024. diff --git a/docs/doctoring/inferred-status-analysis-run.md b/docs/doctoring/inferred-status-analysis-run.md new file mode 100644 index 000000000..326b5713b --- /dev/null +++ b/docs/doctoring/inferred-status-analysis-run.md @@ -0,0 +1,19 @@ +# Inferred-status analysis-run composition + +**Active slice:** ADR 0073 / `inferred_status_v1` +**Protected-main status:** not implemented-main + +`inferred_status` already refuses to treat an inferred relation as +observed evidence or as a state transition. This slice binds +`EvidenceStatus`, `refuse_inferred_as_observed`, and +`refuse_inferred_as_transition` to a cutoff-safe analysis-run profile so +operators can request a digest-bound identity artifact. + +The artifact inference status is +`inferred_is_not_observed_and_not_transition`. +`identity_recovery_rate` stays library-side. `unobserved` and +`no_relationship` are not wire statuses here. This is not +relation-absence, not episode-membership, not outcome-order, not +membership-target, not location-membership, not membership-posterior +ICC, not copied-text, not copy-identity, not citation-edge, not +subevent containment, not GPU, not MCMC, and not topic birth/split/merge.