diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ffe514db6..a745dc1e9 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -43,6 +43,11 @@ flowchart LR Every boundary must be independently usable and expose versioned contracts for integration with organization repositories, `naruon`, and `contextual-orchestrator`. +The `analysis_engine` vertical slice is intentionally separate from `tepp_api`: +the API owns wire contracts while the engine owns deterministic execution. It +does not replace the future topic or psychometric estimators and does not read +another service's application tables. + ## Implemented foundation topology Task 1 materializes the first storage-independent workspace boundaries. The @@ -60,7 +65,8 @@ boundaries above remain the target modular MSA architecture. | `corpus_split` | cutoff-safe, relation-aware partitioning | | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | -| `tepp_api` | versioned DTO, schema, and export contracts | +| `tepp_api` | versioned DTO, schema, terminal-result, and export contracts | +| `analysis_engine` | bounded cutoff-safe temporal evidence readiness execution and digest-bound terminal artifacts | No crate exposes placeholder production behavior in Task 1. This prevents an empty façade from becoming a de facto public API before its invariants and tests diff --git a/CHANGELOG.md b/CHANGELOG.md index d4f0c7c05..4f480291d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- Registered the analysis-engine gap-closure doctoring in the canonical documentation map so its product and scientific traceability record is discoverable. +- Authored Rust coverage classification now ignores standalone structural closing parentheses, preventing formatting-only LCOV rows from appearing as uncovered production behavior. +- Stacked `analysis_engine` vertical slice (ADR 0017): bounded Rust execution from an accepted analysis run to a cutoff-safe, multiple-membership-aware, SHA-256-digest-bound terminal artifact or redacted no-eligible-evidence result. This is active-PR evidence and does not claim psychometric estimator authority. - Coverage classification preserves the final expression line of multiline Rust `match` guards while respecting preceding-arm boundaries, keeping the 100% authored-line gate conservative. - `tepp_api` fail-closed analysis-result boundaries: status constructors reject terminal envelopes that cannot fit the default 64 KiB status limit, and diff --git a/Cargo.lock b/Cargo.lock index fb502b9cd..6643af564 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,17 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "analysis_engine" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "sha2", + "temporal_core", + "tepp_api", +] + [[package]] name = "atoi" version = "2.0.0" diff --git a/Cargo.toml b/Cargo.toml index 925659406..51543a95d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/analysis_engine", ] default-members = [ "crates/evidence_core", @@ -23,6 +24,7 @@ default-members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/analysis_engine", ] [workspace.package] diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 3f0949473..d5bb55030 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -37,6 +37,8 @@ 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) | | 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) | +| 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) | | Change history | [`CHANGELOG.md`](CHANGELOG.md) | ## Maturity vocabulary diff --git a/README.md b/README.md index ae74015d3..f28ddad27 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,9 @@ implemented in Rust. ## Current implementation state -This branch establishes the Task 1 Rust workspace and quality-gate foundation. -The ten bounded crates compile independently but intentionally expose no -placeholder production APIs. Domain behavior begins in Task 2 with immutable -evidence identifiers and source records. +This branch establishes the Rust workspace and quality-gate foundation. The +eleven bounded crates compile independently; domain behavior includes immutable +evidence records and the active stacked cutoff-safe analysis execution slice. ```text crates/evidence_core @@ -22,6 +21,7 @@ crates/corpus_split crates/tepp_simulation crates/validation_core crates/tepp_api +crates/analysis_engine ``` ## Local verification @@ -56,3 +56,7 @@ this skeleton-only slice; it must never conceal uncovered production behavior. No release, production-readiness, GPU, database, or statistical-recovery claim is made by this foundation slice. + +The active stacked analysis-engine slice adds a bounded executable readiness path +from an accepted run to a digest-bound terminal artifact. It is not yet +implemented-main and does not replace scientific estimator contracts. diff --git a/crates/analysis_engine/Cargo.toml b/crates/analysis_engine/Cargo.toml new file mode 100644 index 000000000..9cd55140d --- /dev/null +++ b/crates/analysis_engine/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "analysis_engine" +description = "Deterministic cutoff-safe temporal evidence readiness execution." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true +publish = false + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +tepp_api = { path = "../tepp_api", version = "0.1.0" } +temporal_core = { path = "../temporal_core", version = "0.1.0" } + +[lints] +workspace = true diff --git a/crates/analysis_engine/src/lib.rs b/crates/analysis_engine/src/lib.rs new file mode 100644 index 000000000..22e574f85 --- /dev/null +++ b/crates/analysis_engine/src/lib.rs @@ -0,0 +1,695 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +//! Deterministic, cutoff-safe execution for the first TEPP analysis vertical slice. +//! +//! The engine consumes identity-free evidence metadata, excludes evidence that +//! was unavailable at the requested knowledge cutoff, counts multiple-membership +//! assignments without collapsing them, and emits a digest-bound terminal result +//! through [`tepp_api`]. It deliberately does not claim latent-variable or topic +//! estimation authority; those estimators remain separate scientific crates. + +use serde::Serialize; +use sha2::{Digest, Sha256}; +use std::collections::BTreeSet; +use std::fmt; +use std::fmt::Write as _; +use temporal_core::{AvailableTime, EventTime, KnowledgeCutoff}; +use tepp_api::{ + AnalysisResultSummary, AnalysisRunAccepted, AnalysisRunRequest, AnalysisRunTerminalResult, + ApiError, +}; + +/// Versioned artifact schema emitted by this engine. +pub const ANALYSIS_ARTIFACT_SCHEMA_VERSION: &str = "tepp.temporal_evidence_readiness.v1"; +/// Number of deterministic statistics represented in the artifact summary. +pub const ANALYSIS_STATISTIC_COUNT: u64 = 4; +/// Maximum number of evidence units accepted by one in-memory execution. +pub const MAX_EVIDENCE_UNITS: usize = 100_000; +/// Maximum UTF-8 byte length of one snapshot or opaque evidence identifier. +pub const MAX_ANALYSIS_IDENTIFIER_BYTES: usize = 256; + +/// A bounded identity-free evidence unit offered to one analysis run. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AnalysisEvidenceUnit { + evidence_id: String, + event_time: EventTime, + available_time: AvailableTime, + membership_count: u32, +} + +impl AnalysisEvidenceUnit { + /// Construct an evidence unit with explicit event, availability, and + /// multiple-membership metadata. + /// + /// # Errors + /// + /// Returns [`AnalysisEngineError::InvalidEvidence`] when the identity is + /// empty or no membership assignment is supplied. + pub fn new( + evidence_id: impl Into, + event_time: EventTime, + available_time: AvailableTime, + membership_count: u32, + ) -> Result { + let evidence_id = evidence_id.into(); + if !valid_identifier(&evidence_id) || membership_count == 0 { + return Err(AnalysisEngineError::InvalidEvidence); + } + Ok(Self { + evidence_id, + event_time, + available_time, + membership_count, + }) + } + + /// Return the opaque evidence identity. + #[must_use] + pub fn evidence_id(&self) -> &str { + &self.evidence_id + } + + /// Return the event-valid time. + #[must_use] + pub const fn event_time(&self) -> EventTime { + self.event_time + } + + /// Return the evidence availability time. + #[must_use] + pub const fn available_time(&self) -> AvailableTime { + self.available_time + } + + /// Return the number of simultaneous membership assignments. + #[must_use] + pub const fn membership_count(&self) -> u32 { + self.membership_count + } +} + +/// A bounded snapshot of evidence metadata for one analysis run. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AnalysisCorpus { + snapshot_id: String, + evidence_units: Vec, +} + +impl AnalysisCorpus { + /// Construct a snapshot-owned corpus. + /// + /// # Errors + /// + /// Returns [`AnalysisEngineError::InvalidEvidence`] for an empty snapshot + /// identity or [`AnalysisEngineError::LimitExceeded`] for an oversized + /// in-memory corpus. + pub fn new( + snapshot_id: impl Into, + evidence_units: Vec, + ) -> Result { + let snapshot_id = snapshot_id.into(); + if !valid_identifier(&snapshot_id) { + return Err(AnalysisEngineError::InvalidEvidence); + } + if evidence_units.len() > MAX_EVIDENCE_UNITS { + return Err(AnalysisEngineError::LimitExceeded); + } + Ok(Self { + snapshot_id, + evidence_units, + }) + } + + /// Return the immutable snapshot identity. + #[must_use] + pub fn snapshot_id(&self) -> &str { + &self.snapshot_id + } + + /// Return the evidence units in source order. + #[must_use] + pub fn evidence_units(&self) -> &[AnalysisEvidenceUnit] { + &self.evidence_units + } +} + +/// Digest-bound, identity-free output artifact for one successful execution. +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct AnalysisArtifact { + /// Versioned artifact schema. + pub schema_version: String, + /// Opaque accepted-run identity. + pub run_id: String, + /// Immutable source snapshot identity. + pub snapshot_id: String, + /// Historical cutoff applied to availability. + pub knowledge_cutoff: String, + /// Number of evidence units available by the cutoff. + pub eligible_evidence_count: u64, + /// Sum of preserved multiple-membership assignments. + pub eligible_membership_count: u64, + /// Earliest event-valid time among eligible evidence. + pub earliest_event_time: String, + /// Latest event-valid time among eligible evidence. + pub latest_event_time: String, +} + +impl AnalysisArtifact { + /// Serialize the canonical artifact bytes used for digesting. + /// + /// # Errors + /// + /// Returns [`AnalysisEngineError::SerializationFailure`] if serialization + /// unexpectedly fails. + pub fn to_json(&self) -> Result { + serde_json::to_string(self).map_err(|_| AnalysisEngineError::SerializationFailure) + } + + /// Return the lowercase SHA-256 digest of the canonical artifact JSON. + /// + /// # Errors + /// + /// Returns [`AnalysisEngineError::SerializationFailure`] if serialization + /// unexpectedly fails. + pub fn sha256(&self) -> Result { + self.to_json() + .map(|json| format_digest(Sha256::digest(json.into_bytes()))) + } +} + +/// One complete execution response, including the internal artifact and the +/// request-bound terminal wire result. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AnalysisExecution { + /// Digest-bound artifact, present only when the terminal result succeeded. + pub artifact: Option, + /// Request-bound terminal result returned to the service boundary. + pub terminal_result: AnalysisRunTerminalResult, +} + +/// Fail-closed errors from the deterministic analysis vertical slice. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum AnalysisEngineError { + /// Request or accepted receipt failed its API contract. + Api(ApiError), + /// Evidence metadata was empty or structurally invalid. + InvalidEvidence, + /// Two evidence units reused one opaque identity. + DuplicateEvidence, + /// Corpus snapshot identity differed from the request snapshot. + SnapshotMismatch, + /// A bounded integer aggregation overflowed. + ArithmeticOverflow, + /// A serialized artifact could not be produced. + SerializationFailure, + /// The in-memory corpus exceeded the execution bound. + LimitExceeded, +} + +impl fmt::Display for AnalysisEngineError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::Api(error) => return error.fmt(formatter), + Self::InvalidEvidence => "invalid analysis evidence", + Self::DuplicateEvidence => "duplicate analysis evidence identity", + Self::SnapshotMismatch => "analysis snapshot identity mismatch", + Self::ArithmeticOverflow => "analysis evidence count overflow", + Self::SerializationFailure => "analysis artifact serialization failed", + Self::LimitExceeded => "analysis corpus exceeded its execution bound", + }; + formatter.write_str(message) + } +} + +impl std::error::Error for AnalysisEngineError {} + +impl From for AnalysisEngineError { + fn from(error: ApiError) -> Self { + Self::Api(error) + } +} + +/// Execute the cutoff-safe temporal evidence readiness analysis. +/// +/// Evidence whose `available_time` is later than the request cutoff is excluded +/// before aggregation. Event time remains a separate clock, and all membership +/// assignments are summed rather than collapsed to one group. Successful output +/// contains only bounded counts and temporal extrema; source text, credentials, +/// and direct identities never enter the artifact. +/// +/// # Errors +/// +/// Returns a fail-closed error for invalid contracts, snapshot mismatch, +/// duplicate evidence identities, or invalid arithmetic/serialization state. +pub fn execute_analysis_run( + request: &AnalysisRunRequest, + accepted: &AnalysisRunAccepted, + corpus: &AnalysisCorpus, + completed_at: impl Into, +) -> Result { + request.to_json()?; + accepted.to_json()?; + require_receipt_identity(request, accepted)?; + if request.snapshot_id != corpus.snapshot_id { + return Err(AnalysisEngineError::SnapshotMismatch); + } + let cutoff = KnowledgeCutoff::parse_rfc3339(&request.knowledge_cutoff) + .map_err(|_| AnalysisEngineError::Api(ApiError::InvalidWirePayload))?; + let mut identities = BTreeSet::new(); + let mut eligible = Vec::new(); + for unit in &corpus.evidence_units { + if !identities.insert(unit.evidence_id.clone()) { + return Err(AnalysisEngineError::DuplicateEvidence); + } + if unit.available_time.instant() <= cutoff.instant() { + eligible.push(unit); + } + } + + let completed_at = completed_at.into(); + if eligible.is_empty() { + let terminal_result = AnalysisRunTerminalResult::failed( + request, + accepted, + completed_at, + "no_eligible_evidence", + )?; + return Ok(AnalysisExecution { + artifact: None, + terminal_result, + }); + } + + // The corpus bound makes this conversion and sum strictly smaller than + // `u64::MAX`: 100,000 * u32::MAX is below the 64-bit range. + let eligible_evidence_count = eligible.len() as u64; + let eligible_membership_count = eligible + .iter() + .fold(0_u64, |sum, unit| sum + u64::from(unit.membership_count)); + let (earliest, latest) = eligible.iter().fold( + (eligible[0].event_time, eligible[0].event_time), + |(earliest, latest), unit| (earliest.min(unit.event_time), latest.max(unit.event_time)), + ); + let artifact = AnalysisArtifact { + schema_version: ANALYSIS_ARTIFACT_SCHEMA_VERSION.to_owned(), + run_id: accepted.run_id.clone(), + snapshot_id: request.snapshot_id.clone(), + knowledge_cutoff: cutoff.to_rfc3339(), + eligible_evidence_count, + eligible_membership_count, + earliest_event_time: earliest.to_rfc3339(), + latest_event_time: latest.to_rfc3339(), + }; + artifact.sha256().and_then(move |digest| { + let artifact_id = format!("analysis_artifact_{}", &digest[..16]); + let summary = AnalysisResultSummary { + analysis_family: "temporal_evidence_readiness".to_owned(), + evidence_count: eligible_evidence_count, + statistic_count: ANALYSIS_STATISTIC_COUNT, + validation_status: "validated".to_owned(), + }; + let terminal_result = AnalysisRunTerminalResult::succeeded( + request, + accepted, + artifact_id, + digest, + ANALYSIS_ARTIFACT_SCHEMA_VERSION, + completed_at, + summary, + ) + .map_err(AnalysisEngineError::from)?; + Ok(AnalysisExecution { + artifact: Some(artifact), + terminal_result, + }) + }) +} + +/// Require the accepted receipt to carry the request's idempotency identity. +fn require_receipt_identity( + request: &AnalysisRunRequest, + accepted: &AnalysisRunAccepted, +) -> Result<(), AnalysisEngineError> { + if request.idempotency_key != accepted.idempotency_key { + return Err(AnalysisEngineError::Api(ApiError::InvalidWirePayload)); + } + Ok(()) +} + +fn format_digest(digest: impl AsRef<[u8]>) -> String { + let mut output = String::with_capacity(digest.as_ref().len() * 2); + for byte in digest.as_ref() { + let _ = write!(output, "{byte:02x}"); + } + output +} + +fn valid_identifier(value: &str) -> bool { + !value.trim().is_empty() + && value.len() <= MAX_ANALYSIS_IDENTIFIER_BYTES + && !value.chars().any(char::is_control) +} + +#[cfg(test)] +mod tests { + use super::{ + ANALYSIS_ARTIFACT_SCHEMA_VERSION, ANALYSIS_STATISTIC_COUNT, AnalysisCorpus, + AnalysisEngineError, AnalysisEvidenceUnit, MAX_ANALYSIS_IDENTIFIER_BYTES, + MAX_EVIDENCE_UNITS, execute_analysis_run, + }; + use temporal_core::{AvailableTime, EventTime}; + use tepp_api::{AnalysisRunAccepted, AnalysisRunRequest, AnalysisRunTerminalState, ApiError}; + + fn request() -> AnalysisRunRequest { + AnalysisRunRequest { + contract_version: 1, + idempotency_key: "idem-analysis-1".into(), + tenant_workspace_id: "tenant-workspace-1".into(), + snapshot_id: "snapshot-1".into(), + knowledge_cutoff: "2026-08-01T00:00:00Z".into(), + model_contract_version: "temporal-evidence-v1".into(), + output_profile: "validation-report".into(), + } + } + + fn accepted() -> AnalysisRunAccepted { + AnalysisRunAccepted::new("run-1", "accepted", "idem-analysis-1").expect("accepted") + } + + fn unit(id: &str, event: &str, available: &str, memberships: u32) -> AnalysisEvidenceUnit { + AnalysisEvidenceUnit::new( + id, + EventTime::parse_rfc3339(event).expect("event"), + AvailableTime::parse_rfc3339(available).expect("available"), + memberships, + ) + .expect("unit") + } + + #[test] + fn successful_run_is_cutoff_safe_and_preserves_multiple_memberships() { + let corpus = AnalysisCorpus::new( + "snapshot-1", + vec![ + unit( + "evidence-1", + "2026-07-01T00:00:00Z", + "2026-07-15T00:00:00Z", + 2, + ), + unit( + "evidence-2", + "2026-07-20T00:00:00Z", + "2026-08-01T00:00:00Z", + 3, + ), + unit( + "late-evidence", + "2026-07-25T00:00:00Z", + "2026-08-02T00:00:00Z", + 9, + ), + ], + ) + .expect("corpus"); + let execution = + execute_analysis_run(&request(), &accepted(), &corpus, "2026-08-03T00:00:00Z") + .expect("execution"); + let artifact = execution.artifact.expect("artifact"); + assert_eq!(artifact.schema_version, ANALYSIS_ARTIFACT_SCHEMA_VERSION); + assert_eq!(artifact.eligible_evidence_count, 2); + assert_eq!(artifact.eligible_membership_count, 5); + assert_eq!(artifact.earliest_event_time, "2026-07-01T00:00:00Z"); + assert_eq!(artifact.latest_event_time, "2026-07-20T00:00:00Z"); + assert_eq!( + execution.terminal_result.run_state, + AnalysisRunTerminalState::Succeeded + ); + let summary = execution.terminal_result.summary.as_ref().expect("summary"); + assert_eq!(summary.evidence_count, 2); + assert_eq!(summary.statistic_count, ANALYSIS_STATISTIC_COUNT); + assert_eq!(summary.validation_status, "validated"); + assert!(execution.terminal_result.result_sha256.is_some()); + assert!(execution.terminal_result.to_json().is_ok()); + } + + #[test] + fn no_eligible_evidence_returns_a_redacted_failure_result() { + let corpus = AnalysisCorpus::new( + "snapshot-1", + vec![unit( + "late", + "2026-07-25T00:00:00Z", + "2026-08-02T00:00:00Z", + 1, + )], + ) + .expect("corpus"); + let execution = + execute_analysis_run(&request(), &accepted(), &corpus, "2026-08-03T00:00:00Z") + .expect("failure result"); + assert!(execution.artifact.is_none()); + assert_eq!( + execution.terminal_result.run_state, + AnalysisRunTerminalState::Failed + ); + assert_eq!( + execution.terminal_result.failure_code.as_deref(), + Some("no_eligible_evidence") + ); + assert!(execution.terminal_result.summary.is_none()); + } + + #[test] + fn trust_boundary_and_shape_errors_fail_closed() { + assert_eq!( + AnalysisEvidenceUnit::new( + "", + EventTime::parse_rfc3339("2026-07-01T00:00:00Z").expect("event"), + AvailableTime::parse_rfc3339("2026-07-01T00:00:00Z").expect("available"), + 1, + ), + Err(AnalysisEngineError::InvalidEvidence) + ); + assert_eq!( + AnalysisCorpus::new("", Vec::new()), + Err(AnalysisEngineError::InvalidEvidence) + ); + assert_eq!( + AnalysisCorpus::new("\n", Vec::new()), + Err(AnalysisEngineError::InvalidEvidence) + ); + assert_eq!( + AnalysisCorpus::new("s".repeat(MAX_ANALYSIS_IDENTIFIER_BYTES + 1), Vec::new()), + Err(AnalysisEngineError::InvalidEvidence) + ); + assert_eq!( + AnalysisEvidenceUnit::new( + "e", + EventTime::parse_rfc3339("2026-07-01T00:00:00Z").expect("event"), + AvailableTime::parse_rfc3339("2026-07-01T00:00:00Z").expect("available"), + 0, + ), + Err(AnalysisEngineError::InvalidEvidence) + ); + assert_eq!( + AnalysisEvidenceUnit::new( + "e".repeat(MAX_ANALYSIS_IDENTIFIER_BYTES + 1), + EventTime::parse_rfc3339("2026-07-01T00:00:00Z").expect("event"), + AvailableTime::parse_rfc3339("2026-07-01T00:00:00Z").expect("available"), + 1, + ), + Err(AnalysisEngineError::InvalidEvidence) + ); + let corpus = AnalysisCorpus::new( + "snapshot-2", + vec![unit( + "evidence-1", + "2026-07-01T00:00:00Z", + "2026-07-01T00:00:00Z", + 1, + )], + ) + .expect("corpus"); + assert_eq!( + execute_analysis_run(&request(), &accepted(), &corpus, "2026-08-03T00:00:00Z"), + Err(AnalysisEngineError::SnapshotMismatch) + ); + let mismatched_receipt = + AnalysisRunAccepted::new("run-1", "accepted", "other-idempotency").expect("receipt"); + let matching_corpus = AnalysisCorpus::new( + "snapshot-1", + vec![unit( + "evidence-1", + "2026-07-01T00:00:00Z", + "2026-07-01T00:00:00Z", + 1, + )], + ) + .expect("corpus"); + assert_eq!( + execute_analysis_run( + &request(), + &mismatched_receipt, + &matching_corpus, + "2026-08-03T00:00:00Z" + ), + Err(AnalysisEngineError::Api(ApiError::InvalidWirePayload)) + ); + let duplicate = AnalysisCorpus::new( + "snapshot-1", + vec![ + unit("same", "2026-07-01T00:00:00Z", "2026-07-01T00:00:00Z", 1), + unit("same", "2026-07-02T00:00:00Z", "2026-07-02T00:00:00Z", 1), + ], + ) + .expect("corpus"); + assert_eq!( + execute_analysis_run(&request(), &accepted(), &duplicate, "2026-08-03T00:00:00Z"), + Err(AnalysisEngineError::DuplicateEvidence) + ); + assert_eq!( + AnalysisEngineError::Api(ApiError::LimitExceeded).to_string(), + "API request exceeded configured limits" + ); + assert_eq!( + AnalysisEngineError::SerializationFailure.to_string(), + "analysis artifact serialization failed" + ); + } + + #[test] + fn public_accessors_limits_and_error_messages_are_executable() { + let evidence = unit( + "evidence-accessor", + "2026-07-01T00:00:00Z", + "2026-07-01T00:00:00Z", + 4, + ); + assert_eq!(evidence.evidence_id(), "evidence-accessor"); + assert_eq!( + evidence.event_time(), + EventTime::parse_rfc3339("2026-07-01T00:00:00Z").expect("event") + ); + assert_eq!( + evidence.available_time(), + AvailableTime::parse_rfc3339("2026-07-01T00:00:00Z").expect("available") + ); + assert_eq!(evidence.membership_count(), 4); + let corpus = + AnalysisCorpus::new("snapshot-accessor", vec![evidence.clone()]).expect("corpus"); + assert_eq!(corpus.snapshot_id(), "snapshot-accessor"); + assert_eq!(corpus.evidence_units(), &[evidence]); + + let oversized = AnalysisCorpus::new( + "snapshot-limit", + vec![ + unit("bounded", "2026-07-01T00:00:00Z", "2026-07-01T00:00:00Z", 1,); + MAX_EVIDENCE_UNITS + 1 + ], + ); + assert_eq!(oversized, Err(AnalysisEngineError::LimitExceeded)); + + let messages = [ + ( + AnalysisEngineError::InvalidEvidence, + "invalid analysis evidence", + ), + ( + AnalysisEngineError::DuplicateEvidence, + "duplicate analysis evidence identity", + ), + ( + AnalysisEngineError::SnapshotMismatch, + "analysis snapshot identity mismatch", + ), + ( + AnalysisEngineError::ArithmeticOverflow, + "analysis evidence count overflow", + ), + ( + AnalysisEngineError::SerializationFailure, + "analysis artifact serialization failed", + ), + ( + AnalysisEngineError::LimitExceeded, + "analysis corpus exceeded its execution bound", + ), + ]; + for (error, message) in messages { + assert_eq!(error.to_string(), message); + } + let converted: AnalysisEngineError = ApiError::InvalidWirePayload.into(); + assert_eq!(converted.to_string(), "invalid API wire payload"); + } + + #[test] + fn malformed_request_receipt_cutoff_and_completion_fail_closed() { + let corpus = AnalysisCorpus::new( + "snapshot-1", + vec![unit( + "evidence-1", + "2026-07-01T00:00:00Z", + "2026-07-01T00:00:00Z", + 1, + )], + ) + .expect("corpus"); + + let mut invalid_request = request(); + invalid_request.idempotency_key.clear(); + assert_eq!( + execute_analysis_run( + &invalid_request, + &accepted(), + &corpus, + "2026-08-03T00:00:00Z" + ), + Err(AnalysisEngineError::Api(ApiError::InvalidWirePayload)) + ); + + let mut invalid_accepted = accepted(); + invalid_accepted.run_id.clear(); + assert_eq!( + execute_analysis_run( + &request(), + &invalid_accepted, + &corpus, + "2026-08-03T00:00:00Z" + ), + Err(AnalysisEngineError::Api(ApiError::InvalidWirePayload)) + ); + + let mut invalid_cutoff = request(); + invalid_cutoff.knowledge_cutoff = "not-a-time".into(); + assert_eq!( + execute_analysis_run( + &invalid_cutoff, + &accepted(), + &corpus, + "2026-08-03T00:00:00Z" + ), + Err(AnalysisEngineError::Api(ApiError::InvalidWirePayload)) + ); + + assert_eq!( + execute_analysis_run(&request(), &accepted(), &corpus, "not-a-time"), + Err(AnalysisEngineError::Api(ApiError::InvalidWirePayload)) + ); + + let no_evidence = AnalysisCorpus::new( + "snapshot-1", + vec![unit( + "late", + "2026-07-01T00:00:00Z", + "2026-08-02T00:00:00Z", + 1, + )], + ) + .expect("corpus"); + assert_eq!( + execute_analysis_run(&request(), &accepted(), &no_evidence, "not-a-time"), + Err(AnalysisEngineError::Api(ApiError::InvalidWirePayload)) + ); + } +} diff --git a/crates/analysis_engine/tests/crate_contract.rs b/crates/analysis_engine/tests/crate_contract.rs new file mode 100644 index 000000000..c401c2877 --- /dev/null +++ b/crates/analysis_engine/tests/crate_contract.rs @@ -0,0 +1,6 @@ +//! Package identity contract for the analysis engine. + +#[test] +fn package_identity_is_stable() { + assert_eq!(env!("CARGO_PKG_NAME"), "analysis_engine"); +} diff --git a/crates/analysis_engine/tests/end_to_end_contract.rs b/crates/analysis_engine/tests/end_to_end_contract.rs new file mode 100644 index 000000000..829e56f5d --- /dev/null +++ b/crates/analysis_engine/tests/end_to_end_contract.rs @@ -0,0 +1,107 @@ +//! Realistic cutoff-safe end-to-end analysis execution. + +use analysis_engine::{ + AnalysisCorpus, AnalysisEngineError, AnalysisEvidenceUnit, execute_analysis_run, +}; +use temporal_core::{AvailableTime, EventTime}; +use tepp_api::{AnalysisRunAccepted, AnalysisRunRequest, AnalysisRunTerminalState}; + +fn evidence(id: &str, available: &str, memberships: u32) -> AnalysisEvidenceUnit { + AnalysisEvidenceUnit::new( + id, + EventTime::parse_rfc3339("2026-07-10T12:00:00Z").expect("event time"), + AvailableTime::parse_rfc3339(available).expect("available time"), + memberships, + ) + .expect("evidence") +} + +#[test] +fn production_shape_run_excludes_future_available_evidence() { + let request = AnalysisRunRequest { + contract_version: 1, + idempotency_key: "customer-run-2026-08-01".into(), + tenant_workspace_id: "workspace-opaque-1".into(), + snapshot_id: "snapshot-customer-2026-08-01".into(), + knowledge_cutoff: "2026-08-01T00:00:00Z".into(), + model_contract_version: "temporal-evidence-v1".into(), + output_profile: "validation-report".into(), + }; + let accepted = + AnalysisRunAccepted::new("run-customer-1", "accepted", "customer-run-2026-08-01") + .expect("accepted"); + let corpus = AnalysisCorpus::new( + "snapshot-customer-2026-08-01", + vec![ + evidence("invoice-renewal", "2026-07-31T23:59:59Z", 2), + evidence("later-correction", "2026-08-01T00:00:01Z", 4), + ], + ) + .expect("snapshot"); + let execution = execute_analysis_run(&request, &accepted, &corpus, "2026-08-01T00:01:00Z") + .expect("execute"); + assert_eq!( + execution.terminal_result.run_state, + AnalysisRunTerminalState::Succeeded + ); + assert_eq!( + execution + .artifact + .expect("artifact") + .eligible_evidence_count, + 1 + ); +} + +#[test] +fn snapshot_identity_is_not_inferred_from_customer_payload() { + let request = AnalysisRunRequest { + contract_version: 1, + idempotency_key: "run".into(), + tenant_workspace_id: "workspace".into(), + snapshot_id: "request-snapshot".into(), + knowledge_cutoff: "2026-08-01T00:00:00Z".into(), + model_contract_version: "model-v1".into(), + output_profile: "report".into(), + }; + let accepted = AnalysisRunAccepted::new("run", "accepted", "run").expect("accepted"); + let corpus = AnalysisCorpus::new( + "other-snapshot", + vec![evidence("evidence", "2026-07-01T00:00:00Z", 1)], + ) + .expect("snapshot"); + assert_eq!( + execute_analysis_run(&request, &accepted, &corpus, "2026-08-01T00:01:00Z"), + Err(AnalysisEngineError::SnapshotMismatch) + ); +} + +#[test] +fn mismatched_receipt_identity_is_rejected_before_corpus_scan() { + let request = AnalysisRunRequest { + contract_version: 1, + idempotency_key: "request-idempotency".into(), + tenant_workspace_id: "workspace".into(), + snapshot_id: "snapshot".into(), + knowledge_cutoff: "2026-08-01T00:00:00Z".into(), + model_contract_version: "model-v1".into(), + output_profile: "report".into(), + }; + let accepted = + AnalysisRunAccepted::new("run", "accepted", "receipt-idempotency").expect("accepted"); + let corpus = AnalysisCorpus::new( + "snapshot", + vec![ + evidence("duplicate-evidence", "2026-07-01T00:00:00Z", 1), + evidence("duplicate-evidence", "2026-07-02T00:00:00Z", 1), + ], + ) + .expect("snapshot"); + + assert_eq!( + execute_analysis_run(&request, &accepted, &corpus, "2026-08-01T00:01:00Z"), + Err(AnalysisEngineError::Api( + tepp_api::ApiError::InvalidWirePayload + )) + ); +} diff --git a/crates/tepp_api/src/naruon_http.rs b/crates/tepp_api/src/naruon_http.rs index b0dec3244..a066d532a 100644 --- a/crates/tepp_api/src/naruon_http.rs +++ b/crates/tepp_api/src/naruon_http.rs @@ -357,6 +357,12 @@ mod tests { "x-anthropic-key", "x-bytez-api-key", "x-openrouter-api-key", + "x-api_key", + "x-secret", + "x-credential", + "x-openai", + "x-bytez", + "x-openrouter", "x-provider-api_key", "x-provider-secret", "x-provider-credential", diff --git a/docs/API_CONTRACT.md b/docs/API_CONTRACT.md index 0fdc07543..074da319e 100644 --- a/docs/API_CONTRACT.md +++ b/docs/API_CONTRACT.md @@ -1,7 +1,7 @@ # TEPP API and Modular Integration Contract **Status:** Accepted target contract; exact endpoints are introduced only with executable services. -**Last reviewed:** 2026-08-16 +**Last reviewed:** 2026-08-21 ## 1. Authority boundary @@ -21,6 +21,7 @@ Current protected main exposes Rust library/domain contracts. The active PR adds | LLM interpretation provider port | `tepp_api` orchestration router + future HTTP gateway | contextual-orchestrator | partial | | model/artifact/export API | `tepp_api` export envelopes + future HTTP service | standalone UI/CWL consumers | partial | | analysis-run request/accepted/status/terminal-result contracts | `tepp_api` v1 wire DTOs | naruon, orchestrator, UI | active-PR #157 | +| cutoff-safe analysis-run readiness execution | `analysis_engine` bounded Rust crate | `tepp_api`, future HTTP/service adapters | active-PR #178 stacked on #157 | ## 3. Versioning @@ -58,6 +59,13 @@ snapshot, cutoff, model, profile, and idempotency bindings before treating the run as measurement evidence. The Rust DTO is available before the future HTTP service is deployed. +The stacked `analysis_engine` slice provides the first executable service-side +path behind these DTOs. It consumes a bounded identity-free snapshot, excludes +evidence unavailable at the historical cutoff, preserves multiple-membership +counts, and emits a digest-bound terminal result or a redacted failure. It is +not a substitute for the approved topic or psychometric estimators and remains +active-PR evidence until its exact-head checks and protected merge pass. + ## 5. Analysis request authority An analysis request cannot supply arbitrary facts that bypass validated domain state. The service resolves and validates: diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 8101fa64c..f73788b20 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -19,7 +19,8 @@ The full APA 7th standards/literature register remains `docs/research/standards- | recovery metrics (RMSE, bias, coverage, graph, temporal order, Monte Carlo SE gates) | PRD; Test Strategy; ADR 0007/0014 | `validation_core` on protected main (PR #19); SE-aware Monte Carlo gates included | implemented-main | | PostgreSQL bitemporal/lineage persistence | ADR 0013; Architecture/ERD | `persistence_postgres` migration contracts, in-memory adapters, live SQL session/document SQL port, tenant RLS (`0002` + session GUC/role helpers), `DATABASE_URL` SQLx gate, optional `live-sqlx` `PgPool` driver, exact-head live PostgreSQL CI with isolation proof, append-only immutability triggers (`0004`), temporal interval ordering CHECKs (`0005`), typed membership assignment (`0006` implemented-main), event-relation/mention/instance SQL (#37–#39 implemented-main), source-artifact SQL (#40 implemented-main), audit-event SQL (#41 implemented-main), concurrent document-write stress (#43 implemented-main), backup/restore integrity revalidation (active PR); remaining physical ERD constraints | partial | | known-truth temporal/event simulation manifests | PRD; TRD; Test Strategy | `tepp_simulation` on protected main; recovery metrics in `validation_core` | implemented-main | -| 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); request-bound terminal result and typed status/read contract active in PR #157; 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); request-bound terminal result active in PR #157; HTTP service remains accepted-target | partial | +| executable cutoff-safe analysis-run readiness | ADR 0017; temporal research; API terminal-result contract | stacked `analysis_engine` PR on #157: availability cutoff, snapshot binding, multiple-membership aggregation, digest-bound artifact, realistic end-to-end tests | 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 | future semantic/concept/topic crates | accepted-target | | TRSL-TM temporal/relational topic posterior and backend compatibility | ADR 0012; ADR 0004 | future `topic_measurement` | accepted-target | diff --git a/docs/adr/0017-deterministic-analysis-run-execution.md b/docs/adr/0017-deterministic-analysis-run-execution.md new file mode 100644 index 000000000..567395e8d --- /dev/null +++ b/docs/adr/0017-deterministic-analysis-run-execution.md @@ -0,0 +1,79 @@ +# ADR 0017 — Deterministic cutoff-safe analysis-run execution + +**Decision status:** Accepted +**Implementation maturity:** active-PR — stacked on PR #157; not implemented-main +**Date:** 2026-08-21 +**Supersedes:** None; complements ADR 0002, ADR 0003, ADR 0011, ADR 0013, and the terminal-result contract introduced by PR #157. +**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 + +TEPP already accepts an analysis request and can describe a completed result, +but a buyer needs a demonstrable path between those contracts. Without one +bounded execution slice, an accepted run is only a receipt and consumers cannot +verify cutoff safety, multiple-membership preservation, or artifact identity. + +## Decision + +Add the standalone `analysis_engine` Rust crate as the first executable vertical +slice. It consumes a request, an accepted receipt, and a bounded identity-free +evidence snapshot. It: + +- excludes evidence whose `available_time` is later than the request's + `knowledge_cutoff`; +- preserves multiple-membership assignments by summing their counts rather than + reducing an evidence unit to one group; +- binds the result to the accepted run and source snapshot; +- verifies request/receipt idempotency identity before scanning the corpus; +- emits a canonical SHA-256-digested `AnalysisArtifact` and the versioned + `AnalysisRunTerminalResult` from `tepp_api`; +- returns a content-redacted failed terminal result when no evidence is + eligible; and +- remains a readiness/counting slice, not latent-variable, topic, or + psychometric estimator authority. + +The engine is deterministic, synchronous, bounded to `100_000` evidence units, +and CPU-only. Scientific estimators and their Rust CPU `f64`/GPU parity +contracts remain separate boundaries under ADR 0001 and ADR 0006. + +## Alternatives considered + +1. Keep the API as contracts only — rejected because an accepted run would not + produce a buyer-verifiable terminal outcome. +2. Put execution into `tepp_api` — rejected because transport contracts and + scientific execution would become one service boundary. +3. Add a bounded standalone engine behind the existing contracts — accepted + because it is independently testable and composable without shared tables. + +## Consequences + +Consumers can run a reproducible readiness check while seeing only opaque +identifiers, bounded counts, temporal extrema, and a digest. The engine does +not expose source text or identity mappings and does not claim a psychometric +measurement. The initial linear scan is intentionally simple; a production +large-corpus adapter must stream snapshots and preserve the same artifact +semantics before raising the bound. + +## Verification + +The stacked PR includes Rust unit and integration tests for cutoff exclusion, +multiple-membership summation, snapshot binding, duplicate identities, empty +eligibility, receipt validation, and package identity. Run: + +```text +cargo fmt --all -- --check +cargo test -p analysis_engine +cargo clippy -p analysis_engine --all-targets -- -D warnings +``` + +The supporting research and APA 7th citations are recorded in +`docs/doctoring/analysis-engine-v1.md` and the standards register. + +## Rollback and supersession + +Rollback removes the `analysis_engine` workspace member and stops publishing +the readiness artifact while preserving the request and terminal-result DTOs. +No persisted schema migration is introduced. Supersession requires a new ADR +if execution changes cutoff semantics, artifact authority, privacy fields, or +scientific estimands. diff --git a/docs/adr/README.md b/docs/adr/README.md index 258eb7f31..c1e54fddc 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -22,6 +22,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0014](0014-scientific-claim-promotion-and-release-evidence.md) | Scientific claim promotion and release evidence authority | Accepted | partial | Separates design, implementation, scientific/product claim, and release authority; repository SBOM/provenance generator implemented, full release bundle remaining. | | [0015](0015-autonomous-development-review-and-merge-authority.md) | Autonomous development, review, and merge authority separation | Accepted | active-PR | Separates model proposal, deterministic verification, publication, independent review, and merge/release authority. | | [0016](0016-tdt-chronos-event-intelligence-boundary.md) | TDT, CHRONOS, and Event Ontology intelligence boundary | Accepted | accepted-target | Separates observed evidence, detection/tracking, prediction/schema inference, temporal consistency, and promoted transition authority. | +| [0017](0017-deterministic-analysis-run-execution.md) | Deterministic cutoff-safe analysis-run execution | Accepted | active-PR | Stacked on PR #157; closes the first executable buyer path from accepted run to digest-bound terminal result without claiming estimator authority. | ## Decision ownership summary @@ -43,6 +44,7 @@ Use the narrowest owning ADR when decisions overlap: - **claim maturity / release evidence:** ADR 0014; - **autonomous development/review/merge authority:** ADR 0015; - **TDT/CHRONOS event intelligence:** ADR 0016. +- **accepted-run execution and terminal artifact production:** ADR 0017. ## Change and supersession rule diff --git a/docs/doctoring/analysis-engine-gap-closure.md b/docs/doctoring/analysis-engine-gap-closure.md new file mode 100644 index 000000000..c9fef79dc --- /dev/null +++ b/docs/doctoring/analysis-engine-gap-closure.md @@ -0,0 +1,46 @@ +# Analysis Engine v1 — Buyer Gap Closure + +**Review date:** 2026-08-21 +**Active slice:** PR #157 terminal-result contract → stacked analysis execution +engine for issue #166 + +The organization-wide buyer-gap register is maintained by a separate landing +vehicle. This document records only the analysis-engine slice so it can land +without competing with that register or requiring another PR to be present. + +## Buyer-visible gap + +An accepted analysis run previously had a durable receipt and a terminal-result +DTO, but no executable path that applied a historical availability cutoff and +returned a verifiable artifact. A buyer could submit work but could not yet +demonstrate that the result was complete, cutoff-safe, multiple-membership aware, +and unchanged after transport. + +## Bounded closure + +The stacked `analysis_engine` crate closes the first vertical slice with a +standalone Rust API. It accepts a bounded identity-free evidence snapshot, +rejects snapshot mismatches and duplicate opaque IDs, excludes future-available +evidence, preserves membership counts, and emits a digest-bound terminal result +or a redacted no-eligible-evidence failure. + +This is readiness evidence, not a psychometric estimate. Latent-variable, +multilingual, GPU, and HTTP service gaps remain separately governed by their +own ADRs and must not be implied by this slice. + +## Next leverage-ranked gaps + +1. Add a streaming snapshot adapter with the same digest and cutoff semantics. +2. Bind the engine to a versioned standalone HTTP port without cross-service + table access. +3. Add known-truth estimator execution with RMSE, bias, interval coverage, + multilevel/multiple-membership recovery, and CPU/GPU parity. +4. Add buyer-facing visual analytics only after the interaction contract is + stable; then create a Figma file and Storybook inventory and record its real + File ID in a new UI ADR. + +## Evidence boundary + +The current implementation is active-PR evidence only. Exact-head checks, +independent review, protected merge, release evidence, and deployment controls +are required before a capability is promoted to implemented-main. diff --git a/docs/doctoring/analysis-engine-v1.md b/docs/doctoring/analysis-engine-v1.md new file mode 100644 index 000000000..f3a9f26a9 --- /dev/null +++ b/docs/doctoring/analysis-engine-v1.md @@ -0,0 +1,52 @@ +# Analysis Engine v1 — Evidence Doctoring + +## Claim boundary + +The stacked PR proves one deterministic temporal-evidence-readiness execution +slice. It does not prove production psychometric estimation, topic validity, +GPU performance, HTTP deployment, certification, or customer-wide scale. + +## Decision-to-evidence mapping + +| Contract | Implementation evidence | Customer action enabled | +|---|---|---| +| Historical cutoff safety | `available_time <= knowledge_cutoff` filter in `analysis_engine` | Re-run a historical snapshot without future-availability leakage | +| Multiple membership | `membership_count` is summed for every eligible unit | Inspect inclusive counts without atomistic single-group collapse | +| Terminal completion | `AnalysisRunTerminalResult` is built from the accepted request and receipt | Poll one stable terminal contract instead of treating acceptance as completion | +| Artifact integrity | Canonical JSON and SHA-256 digest | Verify that a downloaded result matches the published artifact identity | +| Privacy boundary | Artifact contains opaque IDs, counts, and times only | Keep identity mapping in the authorized source boundary | + +## Scientific and standards basis + +The implementation preserves TEPP's distinct event and availability clocks and +does not infer an event time from availability time. The API payload is explicit +JSON, and the artifact digest is an integrity check rather than proof of origin +or scientific truth. These interpretations follow the existing temporal, +interchange, and hashing register entries (Bray, 2017; International +Organization for Standardization, 2012; National Institute of Standards and +Technology, 2015). + +## Verification record + +The local preflight for this slice passed with Rust 1.97.1: + +- `cargo fmt --all -- --check`; +- `cargo test -p analysis_engine` — 5 unit tests, 1 crate-contract test, 2 + end-to-end tests, and doctest collection; +- `cargo clippy -p analysis_engine --all-targets -- -D warnings`. + +The protected-hosted exact-head checks and qualifying independent reviews are +still pending. This document must not be used as implemented-main or release +evidence before that merge. + +## APA 7th references + +Bray, T. (Ed.). (2017). *The JavaScript Object Notation (JSON) data interchange +format* (RFC 8259). RFC Editor. https://doi.org/10.17487/RFC8259 + +International Organization for Standardization. (2012). *Language resource +management—Semantic annotation framework (SemAF)—Part 1: Time and events +(SemAF-Time, ISO-TimeML)* (ISO Standard No. 24617-1:2012). + +National Institute of Standards and Technology. (2015). *Secure Hash Standard +(SHS)* (FIPS PUB 180-4). https://doi.org/10.6028/NIST.FIPS.180-4 diff --git a/scripts/check_coverage.py b/scripts/check_coverage.py index 77de890e0..30f07bdf5 100644 --- a/scripts/check_coverage.py +++ b/scripts/check_coverage.py @@ -82,7 +82,7 @@ def is_executable_source_line( return False if text.startswith("#[") or text.startswith("#!["): return False - if text in {"{", "}", "},", ");", "];", "();", "};"}: + if text in {"{", "}", "},", ")", ");", "];", "();", "};"}: return False if text.startswith("use ") or text.startswith("pub use "): return False diff --git a/scripts/check_workspace_contract.py b/scripts/check_workspace_contract.py index c7b1ecf58..425233491 100644 --- a/scripts/check_workspace_contract.py +++ b/scripts/check_workspace_contract.py @@ -23,6 +23,7 @@ "tepp_simulation", "validation_core", "tepp_api", + "analysis_engine", ) REQUIRED_CI_SNIPPETS: tuple[str, ...] = ( diff --git a/tests/quality/test_check_coverage.py b/tests/quality/test_check_coverage.py index 4175f40f6..4a95e8858 100644 --- a/tests/quality/test_check_coverage.py +++ b/tests/quality/test_check_coverage.py @@ -321,9 +321,10 @@ def test_executable_source_line_filters_noise_records(self) -> None: " }", # 55 "}", # 56 " executable_statement();", # 57 executable - "pub(crate) fn crate_visible() {", # 58 visibility-qualified fn - "State::Accepted => {", # 59 match-arm structure - "State::Guarded(value) if valid(value) => {", # 60 guarded arm is executable + ")", # 58 standalone structural close + "pub(crate) fn crate_visible() {", # 59 visibility-qualified fn + "State::Accepted => {", # 60 match-arm structure + "State::Guarded(value) if valid(value) => {", # 61 guarded arm is executable ] source.write_text("\n".join(source_lines) + "\n", encoding="utf-8") path = str(source) @@ -338,7 +339,7 @@ def test_executable_source_line_filters_noise_records(self) -> None: coverage_contract.is_executable_source_line(path, len(source_lines) + 5) ) - expected_executable = {13, 40, 44, 57, 60} + expected_executable = {13, 40, 44, 57, 61} for line_number in range(1, len(source_lines) + 1): is_exec = coverage_contract.is_executable_source_line(path, line_number) if line_number in expected_executable: diff --git a/tests/quality/test_check_docstrings.py b/tests/quality/test_check_docstrings.py index 2c11f7a53..b99537c52 100644 --- a/tests/quality/test_check_docstrings.py +++ b/tests/quality/test_check_docstrings.py @@ -24,7 +24,7 @@ def test_live_repository_is_documented(self) -> None: sources = docstrings.rust_sources(REPOSITORY_ROOT) crate_roots = sorted(REPOSITORY_ROOT.glob("crates/*/src/lib.rs")) - self.assertEqual(len(crate_roots), 10) + self.assertEqual(len(crate_roots), 11) self.assertTrue(set(crate_roots).issubset(sources)) self.assertGreaterEqual(len(sources), len(crate_roots)) self.assertEqual(docstrings.validate_repository(REPOSITORY_ROOT), [])