From b9aff1e0e8e717c008313084d8e1b0dcec2a2daf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 13:08:48 +0000 Subject: [PATCH] feat(analysis): mint execute exchanges for naruon and LineageWeave Typed credential-free POST /v1/analysis-runs/{run_id}/execute so consumers obtain tepp.scientific_acceptance.v1 without hand-rolled HTTP. ADR 0034. --- ARCHITECTURE.md | 4 +- ...ic-acceptance-execute-consumer-exchange.md | 3 + CHANGELOG.md | 2 + DOCUMENTATION.md | 2 + crates/analysis_engine/src/lib.rs | 7 +- .../analysis_engine/src/loopback_execute.rs | 216 ++++++++++++--- .../execute_consumer_exchange_contract.rs | 261 ++++++++++++++++++ docs/API_CONTRACT.md | 5 +- docs/TRACEABILITY.md | 1 + ...ic-acceptance-execute-consumer-exchange.md | 107 +++++++ docs/adr/README.md | 2 + docs/connectors/naruon-artifact-consumer.md | 1 + ...ic-acceptance-execute-consumer-exchange.md | 31 +++ 13 files changed, 592 insertions(+), 50 deletions(-) create mode 100644 CHANGELOG.d/scientific-acceptance-execute-consumer-exchange.md create mode 100644 crates/analysis_engine/tests/execute_consumer_exchange_contract.rs create mode 100644 docs/adr/0034-scientific-acceptance-execute-consumer-exchange.md create mode 100644 docs/research/scientific-acceptance-execute-consumer-exchange.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ac511e679..63baedf90 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -70,7 +70,7 @@ boundaries above remain the target modular MSA architecture. | `tepp_simulation` | known-truth temporal/event data generation | | `validation_core` | RMSE, bias, coverage, graph, Monte Carlo, and exact-head claim-promotion metrics | | `tepp_api` | versioned DTO, schema, terminal-result, and export contracts | -| `analysis_engine` | bounded cutoff-safe temporal evidence readiness execution, digest-bound terminal artifacts, GAP-003A scientific-acceptance validation runs (`tepp.scientific_acceptance.v1`; not implemented-main), loopback `POST /v1/analysis-runs/{run_id}/execute` that produces that artifact without a caller-supplied payload, and the published `tepp-loopback` binary that binds that wrapper | +| `analysis_engine` | bounded cutoff-safe temporal evidence readiness execution, digest-bound terminal artifacts, GAP-003A scientific-acceptance validation runs (`tepp.scientific_acceptance.v1`; not implemented-main), loopback `POST /v1/analysis-runs/{run_id}/execute` that produces that artifact without a caller-supplied payload, the published `tepp-loopback` binary that binds that wrapper, and typed naruon/`LineageWeave` execute exchanges | | `episode_membership` | event-time episode membership containment gate | | `prompt_source` | prompt boilerplate is not unique latent content and not stopword deletion | | `corpus_background` | corpus-background wording is not unique latent content and not stopword deletion | @@ -113,7 +113,7 @@ boundaries above remain the target modular MSA architecture. | `episode_membership` | episode membership cannot escape the episode event-time interval | | `membership_target` | language, episode, template, department, and opportunity-pool targets cannot collapse into entity or project | | `topic_measurement` | logistic-normal ALR/ILR coordinates and the CPU `f64` TRSL-TM reference estimator | -| `analysis_engine` | bounded cutoff-safe temporal evidence readiness execution, digest-bound terminal artifacts, GAP-003A scientific-acceptance validation runs (`tepp.scientific_acceptance.v1`; not implemented-main), loopback `POST /v1/analysis-runs/{run_id}/execute` that produces that artifact without a caller-supplied payload, and the published `tepp-loopback` binary that binds that wrapper | +| `analysis_engine` | bounded cutoff-safe temporal evidence readiness execution, digest-bound terminal artifacts, GAP-003A scientific-acceptance validation runs (`tepp.scientific_acceptance.v1`; not implemented-main), loopback `POST /v1/analysis-runs/{run_id}/execute` that produces that artifact without a caller-supplied payload, the published `tepp-loopback` binary that binds that wrapper, and typed naruon/`LineageWeave` execute exchanges | | `psychometric_core` | posterior-aware structural input gates, CWC within/between OLS plus the contextual effect, event-time log-rate, unequal-interval discrete-lag remapping, constant-predictor discrete effect, time-varying-predictor discrete effect (Eq. 14), exact scalar discrete process noise (Driver et al., 2017, Eq. 3), lagged latent covariance and unconditional latent variance (Driver et al., 2017, Eq. 3–4), stationary within-subject variance (Driver et al., 2017, Eq. 4 as `Δt → ∞`; `asymDIFFUSION`), trait-plus-state variance (Driver et al., 2017, §4.3 `TRAITVAR`; not process noise), observed-indicator variance and lagged observed covariance (Driver et al., 2017, Eq. 5; Table 2 `MANIFESTVAR` is `Θ`, not `Var(y)`; `MANIFESTTRAITVAR` is not `MANIFESTVAR`; `Θ` does not enter lagged observed covariance; observed-indicator mean is `τ + λ μ`; `MANIFESTMEANS` is not `E(y)`; `CINT` is not `MANIFESTMEANS`; discrete latent mean is `exp(a Δt) μ_0 + (exp(a Δt) − 1)/a κ`; `T0MEANS` is not `μ_t`; `CINT` is not the discrete increment; evolved observed mean is `τ + λ μ_t`; `τ + λ μ_0` is not `E(y_t)`; contemporaneous `TDPREDEFFECT` impulse is `m x`, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that contemporaneous impulse is `τ + λ(μ_t + m x)`, and `τ + λ μ_t` is not that observed mean; time-independent `TIPREDEFFECT` increment is `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, not `M x`, not Voelkle Eq. 14, and not the coefficient `B`; Eq. 5 of that increment is `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; `τ + λ(μ_t + e^{a(t−u)} m x)` is not that observed mean when `u ≠ t`; within-interval `TDPREDEFFECT` carry is `e^{A(t−u)} M x` for `t0 < u < t`, not the contemporaneous Dirac, not `CINT`, not `TIPREDEFFECT`, and not Voelkle Eq. 14; Eq. 5 of that carry is `τ + λ(μ_t + e^{a(t−u)} m x)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that carried observed mean when `u ≠ t`; first-occasion `T0TIPREDEFFECT` shift is `t0_b z` and Eq. 3 first-summand carry is `e^{A Δt} t0_b z` (`T0TIPREDEFFECT` is not `TIPREDEFFECT` `B`; `t0_b z` is not `A^{-1}[e^{A Δt} − I] B z`; `e^{A Δt} t0_b z` is not `t0_b z`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_b z)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean), first-occasion `T0TDPREDEFFECT` shift is `t0_m x0` and Eq. 3 first-summand carry is `e^{A Δt} t0_m x0` (`T0TDPREDEFFECT` is not `TDPREDEFFECT` `M`; `t0_m x0` is not `M x`; `e^{A Δt} t0_m x0` is not `t0_m x0`; `e^{A Δt} t0_m x0` is not `e^{A(t−u)} M x` for `t0 < u < t`; `t0_m x0` is not `t0_b z`; an impulse at `u ≤ t0` that used `M` is already in `η(t0)` as `TDPREDEFFECT`, not as `T0TDPREDEFFECT`; Eq. 5 of that carry is `τ + λ(μ_t + e^{a Δt} t0_m x0)`, and `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + A^{-1}[e^{A Δt} − I] B z)` is not that observed mean; `τ + λ(μ_t + e^{a Δt} t0_b z)` is not that observed mean; §7.2 level-change `CINT` is `κ = −a m x` with `a < 0` so `−κ / a = m x` (`−a m x` is not the dissipating Dirac, not a free `CINT`, not `TIPREDEFFECT`, and not the extra near-zero-drift latent process also named in §7.2; Eq. 3 of that setting is `(1 − e^{a Δt}) m x`, which is not `m x`, not `κ`, and not `TIPREDEFFECT`; §7.2 extra-process contribution is `a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)` (`ε = a` is `a_{ηξ} x Δt e^{a Δt}`; identification `TDPREDEFFECT` on the extra process is 1; printed extra `DRIFT` is `−0.000001`; not `κ = −a m x`, not `(1 − e^{a Δt}) m x`, and not the dissipating Dirac `m x`; `ε ≥ 0` fails closed; Eq. 5 of that contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε Δt} − e^{a Δt}) / (ε − a)`; the extra process has `LAMBDA` 0 and is not an observed indicator; `τ + λ μ_t` is not that observed mean; `τ + λ(μ_t + m x)` is not that observed mean; the contribution is not `E(y_t)`; the evolved-plus-contribution latent mean is not `E(y_t)`; after-t0 extra-process `TDPREDEFFECT` is `a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)` for `t0 < u < t` while `μ_t` uses `Δt`; Eq. 5 of that after-t0 contribution is `τ + λ(μ_t + a_{ηξ} x (e^{ε(t−u)} − e^{a(t−u)}) / (ε − a)`; the first-occasion extra-process observed mean is not that observed mean when `u ≠ t0`; `e^{a(t−u)} m x` is a Dirac on the original process, not this `DRIFT` drive; §7.2 `asymTIPREDEFFECT` is `-B z / a` for `a < 0` (`-B z / a` is not the coefficient `B`, not `A^{-1}[e^{A Δt} − I] B z`, not `CINT`, and not `M x`; §7.2 `addedTIPREDVAR` is `(B / a)² v`, not `TRAITVAR`, not `asymDIFFUSION`, and not `-B z / a`; Table 2 `asymCINT` is `-κ / a` for `a < 0` and is not `κ`, not `A^{-1}[e^{A Δt} − I] κ`, not `T0MEANS`, and not `-B z / a`; p. 16 stationary `T0MEANS` is `-κ / a + −B z / a` and is not free `T0MEANS`, not `asymCINT` alone, not `asymTIPREDEFFECT` alone, and not the finite-interval discrete latent mean; Eq. 5 of that constrained mean is `τ + λ(−κ / a + −B z / a)`; `τ + λ μ_0` is not that observed mean; `τ + λ(−κ / a)` is not that observed mean when `B z ≠ 0`; `τ + λ μ_t` is not that observed mean; `MANIFESTMEANS` is not `E(y_0)`; the constrained latent mean is not `E(y_0)`; stationary `T0VAR` is `trait + −q / (2 a) + (B / a)² v` (not free `T0VAR`, not `asymDIFFUSION` alone, not `TRAITVAR` alone, not `addedTIPREDVAR` alone, and not the finite-interval discrete latent variance. Eq. 5 of that constrained variance is `λ²(trait + −q / (2 a) + (B / a)² v) + θ + ψ` (JSS PDF re-opened 2026-08-22T03:20Z; form the stationary latent variance first, then `λ² p + θ + ψ`; `λ² p_0` is not that observed variance; `λ²(−q / (2 a)) + θ` is not that observed variance when `TRAITVAR` or `addedTIPREDVAR` is nonzero; `MANIFESTVAR` is not `Var(y_0)`; the constrained latent variance is not `Var(y_0)`); lagged stationary `T0VAR` is `trait + e^{a Δt}(−q / (2 a)) + (B / a)² v` (trait and `addedTIPREDVAR` do not decay; contemporaneous `T0VAR` is not that lagged map; decaying the constrained total as if it were all state is not that lagged map; Eq. 5 of that lagged covariance is `λ²(trait + e^{a Δt}(−q / (2 a)) + (B / a)² v) + ψ`; `Θ` does not enter; contemporaneous `Var(y_0)` is not that lagged observed covariance; the lagged latent covariance is not that observed covariance); later-occasion stationary `T0VAR` is `trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v` (trait and `addedTIPREDVAR` do not enter `Q_Δt`; under stationarity that composition equals contemporaneous `T0VAR`; evolving the constrained total as if it were all state is not that later map; the lagged covariance omits `Q_Δt`; `Q_Δt` is not that later map; Eq. 5 of that later-occasion variance is `λ²(trait + e^{2 a Δt}(−q / (2 a)) + Q_Δt + (B / a)² v) + θ + ψ`; lagged observed covariance omits `Q_Δt` and `θ`; `MANIFESTVAR` is not `Var(y_t)`; the later-occasion latent variance is not `Var(y_t)`))), irregular already-centered residual lag, Rubin `T` on OLS loadings, and strong-gated latent means (two-observation residual variance is identically `0` and caps at strong/scalar; Putnick & Bornstein, 2016) | | `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | | `tepp_api` | versioned DTO, schema, and export contracts | diff --git a/CHANGELOG.d/scientific-acceptance-execute-consumer-exchange.md b/CHANGELOG.d/scientific-acceptance-execute-consumer-exchange.md new file mode 100644 index 000000000..427ddb75c --- /dev/null +++ b/CHANGELOG.d/scientific-acceptance-execute-consumer-exchange.md @@ -0,0 +1,3 @@ +### Added + +- `analysis_engine` GAP-003A execute consumer-exchange slice (ADR 0034, active-PR, not implemented-main): naruon and `LineageWeave` mint credential-free `POST /v1/analysis-runs/{run_id}/execute` through typed exchanges so a `scientific_acceptance_v1` run produces `tepp.scientific_acceptance.v1` without hand-rolled HTTP. Persistence remains GAP-003B. diff --git a/CHANGELOG.md b/CHANGELOG.md index 819c64439..176201a7c 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] +- `analysis_engine` GAP-003A execute consumer-exchange slice (ADR 0034, active-PR, not implemented-main): `naruon_analysis_run_execute_exchange` and `lineageweave_analysis_run_execute_exchange` mint credential-free HTTPS `POST /v1/analysis-runs/{run_id}/execute` so naruon and `LineageWeave` obtain `tepp.scientific_acceptance.v1` without hand-rolled HTTP or a caller-supplied artifact. The typed body refuses `scientific_acceptance_json`, receipt metric keys, and LLM-authored recovery. Non-`https` origins fail closed. This does not duplicate the published binary (#375), engine-execute library (#370), cancel consumer parity (#373), loopback CLI (#362), collection CLI (#371), retry (#369), GET (#359), lifecycle POST (#360), cancel HTTP (#361), collection GET (#368), DTO (#358), or engine library (#356); persistence remains GAP-003B. + - `analysis_engine` GAP-003A published-binary slice (ADR 0033, active-PR, not implemented-main): the `tepp-loopback` binary now binds `ScientificAcceptanceLoopbackService` so `POST /v1/analysis-runs/{run_id}/execute` is reachable on the packaged loopback listener without embedding the library. CLI arguments, default bind `127.0.0.1:18081`, and temporal-context health checks are unchanged. `tepp_api` no longer ships that binary (crate cycle). This does not duplicate the engine-execute library (#370), loopback CLI (#362), collection CLI (#371), GET (#359), lifecycle POST (#360), cancel HTTP (#361), collection GET (#368), retry HTTP (#369), DTO (#358), or engine library (#356); persistence remains GAP-003B. - `analysis_engine` GAP-003A engine-on-loopback slice (ADR 0032, active-PR, not implemented-main): `ScientificAcceptanceLoopbackService` serves `POST /v1/analysis-runs/{run_id}/execute` so an accepted `scientific_acceptance_v1` loopback run produces `tepp.scientific_acceptance.v1` without a caller-supplied artifact. The execute body carries corpus, recovery, seed, and pre-registered SE-gate `k` and refuses `scientific_acceptance_json` plus receipt metric keys. Wrong profile, LLM recovery, unknown run, consumer mismatch, already-terminal status, and digest mismatch fail closed. This does not duplicate the engine library (#356), terminal-result DTO (#358), GET (#359), lifecycle POST (#360), cancel HTTP (#361), loopback CLI (#362), or collection GET (#368); persistence remains GAP-003B. diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index c39d5fd64..ad157ca20 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -61,6 +61,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin | Validation-run scientific-acceptance doctoring | [`docs/research/validation-run-scientific-acceptance.md`](docs/research/validation-run-scientific-acceptance.md) | | Scientific-acceptance loopback engine doctoring | [`docs/research/scientific-acceptance-loopback-engine.md`](docs/research/scientific-acceptance-loopback-engine.md) | | Scientific-acceptance published loopback binary doctoring | [`docs/research/scientific-acceptance-loopback-binary.md`](docs/research/scientific-acceptance-loopback-binary.md) | +| Scientific-acceptance execute consumer-exchange doctoring | [`docs/research/scientific-acceptance-execute-consumer-exchange.md`](docs/research/scientific-acceptance-execute-consumer-exchange.md) | | Retention/deletion/legal-hold doctoring | [`docs/research/retention-deletion-legal-hold.md`](docs/research/retention-deletion-legal-hold.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) | @@ -153,6 +154,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin | Validation-run scientific-acceptance doctoring | [`docs/research/validation-run-scientific-acceptance.md`](docs/research/validation-run-scientific-acceptance.md) | | Scientific-acceptance loopback engine doctoring | [`docs/research/scientific-acceptance-loopback-engine.md`](docs/research/scientific-acceptance-loopback-engine.md) | | Scientific-acceptance published loopback binary doctoring | [`docs/research/scientific-acceptance-loopback-binary.md`](docs/research/scientific-acceptance-loopback-binary.md) | +| Scientific-acceptance execute consumer-exchange doctoring | [`docs/research/scientific-acceptance-execute-consumer-exchange.md`](docs/research/scientific-acceptance-execute-consumer-exchange.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) | diff --git a/crates/analysis_engine/src/lib.rs b/crates/analysis_engine/src/lib.rs index 55321ca0e..de23a49d3 100644 --- a/crates/analysis_engine/src/lib.rs +++ b/crates/analysis_engine/src/lib.rs @@ -13,7 +13,8 @@ //! accepted analysis run so GET can return `tepp.scientific_acceptance.v1` //! without a caller-supplied artifact. The published `tepp-loopback` binary //! binds that wrapper so `POST /v1/analysis-runs/{run_id}/execute` is reachable -//! on the loopback listener without embedding this crate. +//! on the loopback listener without embedding this crate. Naruon and +//! `LineageWeave` mint that POST through typed execute exchanges in this crate. mod case_deletion_refit; mod lineage_criterion; @@ -57,7 +58,9 @@ pub use lineage_criterion::{ /// Loopback engine-execute contracts for scientific acceptance. pub use loopback_execute::{ ANALYSIS_RUN_EXECUTE_CONTRACT_VERSION, ANALYSIS_RUN_EXECUTE_PATH_SUFFIX, - ScientificAcceptanceLoopbackService, + ScientificAcceptanceExecuteCorpus, ScientificAcceptanceExecuteEvidenceUnit, + ScientificAcceptanceExecuteRequest, ScientificAcceptanceLoopbackService, + lineageweave_analysis_run_execute_exchange, naruon_analysis_run_execute_exchange, }; /// Bounded posterior topic-context producer contract and record types. pub use topic_context_posterior::{ diff --git a/crates/analysis_engine/src/loopback_execute.rs b/crates/analysis_engine/src/loopback_execute.rs index 8e8101e5f..2ede7db60 100644 --- a/crates/analysis_engine/src/loopback_execute.rs +++ b/crates/analysis_engine/src/loopback_execute.rs @@ -9,19 +9,21 @@ //! without a caller-supplied terminal payload. Persistence remains GAP-003B. use crate::{ - AnalysisCorpus, AnalysisEngineError, AnalysisEvidenceUnit, RecoveryObservation, + AnalysisCorpus, AnalysisEngineError, AnalysisEvidenceUnit, MAX_SE_GATE_K, RecoveryObservation, SCIENTIFIC_ACCEPTANCE_OUTPUT_PROFILE, SCIENTIFIC_ACCEPTANCE_SCHEMA_VERSION, VALIDATION_CPU_F64_MODEL, complete_validation_run, submit_validation_run, }; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use temporal_core::{AvailableTime, EventTime}; use tepp_api::{ - ANALYSIS_RUN_STATUS_PATH, AnalysisResultSummary, AnalysisRunLiveService, AnalysisRunStatus, - AnalysisRunStatusState, AnalysisRunTerminalResult, ApiError, DEFAULT_ANALYSIS_RUN_BYTE_LIMIT, - DEFAULT_PROJECT_HISTORY_BYTE_LIMIT, ErrorEnvelope, NaruonLiveResponse, + ANALYSIS_RUN_ID_MAX_LEN, ANALYSIS_RUN_STATUS_PATH, AnalysisResultSummary, + AnalysisRunLiveService, AnalysisRunStatus, AnalysisRunStatusState, AnalysisRunTerminalResult, + ApiError, DEFAULT_ANALYSIS_RUN_BYTE_LIMIT, DEFAULT_PROJECT_HISTORY_BYTE_LIMIT, ErrorEnvelope, + LINEAGEWEAVE_CONSUMER_CODE, NARUON_CONSUMER_CODE, NaruonHttpExchange, NaruonLiveResponse, SCIENTIFIC_ACCEPTANCE_HTTP_PROFILE, SCIENTIFIC_ACCEPTANCE_HTTP_SCHEMA, - analysis_run_execute_path_run_id, parse_loopback_http_parts, + analysis_run_execute_path_run_id, naruon_analysis_run_status_exchange, + parse_loopback_http_parts, }; /// Result-metric keys that must not appear on an execute request object. @@ -262,44 +264,118 @@ impl ScientificAcceptanceLoopbackService { } } -/// Supported execute-body contract version. -#[derive(Clone, Debug, Deserialize, PartialEq)] +/// Execute-body contract naruon and `LineageWeave` may POST to `/execute`. +/// +/// The body carries corpus, recovery, seed, and the pre-registered SE-gate +/// multiplier. It must not carry `scientific_acceptance_json` or receipt +/// metric keys. LLM-authored recovery is refused. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[serde(deny_unknown_fields)] -struct ScientificAcceptanceExecuteRequest { - contract_version: u16, - run_id: String, - idempotency_key: String, - seed: u64, - se_gate_k: f64, - completed_at: String, - study_label: String, - authored_by_llm: bool, - corpus: ExecuteCorpus, - truth: Vec, - recovered: Vec, - interval_lower: Vec, - interval_upper: Vec, - truth_times: Vec, - recovered_times: Vec, +pub struct ScientificAcceptanceExecuteRequest { + /// Semantic contract version for this payload family. + pub contract_version: u16, + /// Opaque server-assigned run identity. + pub run_id: String, + /// Exact request idempotency key. + pub idempotency_key: String, + /// Deterministic engine seed bound into the validation run. + pub seed: u64, + /// Pre-registered SE-gate multiplier. Not a receipt metric. + pub se_gate_k: f64, + /// RFC 3339 completion timestamp recorded on the terminal status. + pub completed_at: String, + /// Study label stamped onto the recovery observation. + pub study_label: String, + /// LLM-authored recovery is refused and must be `false`. + pub authored_by_llm: bool, + /// Cutoff-eligible evidence corpus. + pub corpus: ScientificAcceptanceExecuteCorpus, + /// Known-truth recovery vector. + pub truth: Vec, + /// Recovered vector stamped to the same run. + pub recovered: Vec, + /// Interval lower bounds. + pub interval_lower: Vec, + /// Interval upper bounds. + pub interval_upper: Vec, + /// Known-truth event times. + pub truth_times: Vec, + /// Recovered event times. + pub recovered_times: Vec, } -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +/// Evidence corpus carried on an execute request. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] -struct ExecuteCorpus { - snapshot_id: String, - evidence_units: Vec, +pub struct ScientificAcceptanceExecuteCorpus { + /// Snapshot identity that must match the accepted run. + pub snapshot_id: String, + /// Evidence units offered to the cutoff-safe engine. + pub evidence_units: Vec, } -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +/// One evidence unit on an execute corpus. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] -struct ExecuteEvidenceUnit { - evidence_id: String, - event_time: String, - available_time: String, - membership_count: u32, +pub struct ScientificAcceptanceExecuteEvidenceUnit { + /// Opaque evidence identity. + pub evidence_id: String, + /// RFC 3339 event time. + pub event_time: String, + /// RFC 3339 availability time. + pub available_time: String, + /// Multiple-membership count preserved by the engine. + pub membership_count: u32, +} + +impl ScientificAcceptanceExecuteRequest { + /// Parse and validate an execute body with the default byte limit. + /// + /// # Errors + /// + /// Returns wire, version, limit, metric-key, LLM, or field-validation errors. + pub fn from_json(payload: &str) -> Result { + parse_execute_body(payload) + } + + /// Serialize this execute body after complete validation. + /// + /// # Errors + /// + /// Returns validation, metric-key, or serialization errors. + pub fn to_json(&self) -> Result { + self.validate()?; + let payload = serde_json::to_string(self).map_err(|_| ApiError::InvalidWirePayload)?; + if payload.len() > DEFAULT_ANALYSIS_RUN_BYTE_LIMIT { + return Err(ApiError::LimitExceeded); + } + refuse_result_metrics_on_execute(&payload)?; + Ok(payload) + } + + fn validate(&self) -> Result<(), ApiError> { + if self.contract_version != ANALYSIS_RUN_EXECUTE_CONTRACT_VERSION { + return Err(ApiError::UnsupportedContractVersion); + } + if self.run_id.is_empty() + || self.idempotency_key.is_empty() + || self.completed_at.is_empty() + || self.study_label.is_empty() + || self.corpus.snapshot_id.is_empty() + { + return Err(ApiError::InvalidWirePayload); + } + if self.authored_by_llm || !self.se_gate_k.is_finite() { + return Err(ApiError::InvalidWirePayload); + } + if self.se_gate_k < 0.0 || self.se_gate_k > MAX_SE_GATE_K { + return Err(ApiError::InvalidWirePayload); + } + Ok(()) + } } -impl ExecuteCorpus { +impl ScientificAcceptanceExecuteCorpus { fn to_corpus(&self) -> Result { let mut units = Vec::with_capacity(self.evidence_units.len()); for unit in &self.evidence_units { @@ -318,6 +394,65 @@ impl ExecuteCorpus { } } +/// Build a naruon → TEPP scientific-acceptance execute exchange. +/// +/// The builder reuses the published status-path origin and run-identity +/// gates, then POSTs `/execute` with a metric-free engine body. It does not +/// inject credentials. +/// +/// # Errors +/// +/// Returns [`ApiError::InvalidWirePayload`] for a non-`https` origin, a +/// table-access URL, an invalid execute body, or LLM recovery, and +/// [`ApiError::LimitExceeded`] when the run identity exceeds +/// [`ANALYSIS_RUN_ID_MAX_LEN`]. +pub fn naruon_analysis_run_execute_exchange( + origin: &str, + execute: &ScientificAcceptanceExecuteRequest, +) -> Result { + execute.validate()?; + if execute.run_id.len() > ANALYSIS_RUN_ID_MAX_LEN { + return Err(ApiError::LimitExceeded); + } + let mut exchange = + naruon_analysis_run_status_exchange(origin, &execute.run_id, &execute.idempotency_key)?; + let consumer = exchange + .headers + .iter() + .find(|(name, _)| name.eq_ignore_ascii_case("tepp-consumer")) + .map(|(_, value)| value.as_str()) + .ok_or(ApiError::InvalidWirePayload)?; + if consumer != NARUON_CONSUMER_CODE { + return Err(ApiError::InvalidWirePayload); + } + exchange.method = "POST"; + exchange.target_url = format!("{}/{ANALYSIS_RUN_EXECUTE_PATH_SUFFIX}", exchange.target_url); + exchange.body = execute.to_json()?; + Ok(exchange) +} + +/// Build a `LineageWeave` → TEPP scientific-acceptance execute exchange. +/// +/// Reuses the naruon execute builder and replaces only the published +/// modular-consumer identity. +/// +/// # Errors +/// +/// Returns the same fail-closed errors as [`naruon_analysis_run_execute_exchange`]. +pub fn lineageweave_analysis_run_execute_exchange( + origin: &str, + execute: &ScientificAcceptanceExecuteRequest, +) -> Result { + let mut exchange = naruon_analysis_run_execute_exchange(origin, execute)?; + let consumer_header = exchange + .headers + .iter_mut() + .find(|(name, _)| name.eq_ignore_ascii_case("tepp-consumer")) + .ok_or(ApiError::InvalidWirePayload)?; + LINEAGEWEAVE_CONSUMER_CODE.clone_into(&mut consumer_header.1); + Ok(exchange) +} + fn parse_execute_body(body: &str) -> Result { if body.len() > DEFAULT_ANALYSIS_RUN_BYTE_LIMIT { return Err(ApiError::LimitExceeded); @@ -325,16 +460,7 @@ fn parse_execute_body(body: &str) -> Result AnalysisRunRequest { + AnalysisRunRequest { + contract_version: ANALYSIS_RUN_CONTRACT_VERSION, + idempotency_key: idempotency_key.into(), + tenant_workspace_id: "tenant-workspace-execute".into(), + snapshot_id: "snapshot-execute".into(), + knowledge_cutoff: "2026-08-01T00:00:00Z".into(), + model_contract_version: model.into(), + output_profile: profile.into(), + } +} + +fn execute_json(run_id: &str, idempotency_key: &str, authored_by_llm: bool) -> String { + serde_json::json!({ + "contract_version": ANALYSIS_RUN_EXECUTE_CONTRACT_VERSION, + "run_id": run_id, + "idempotency_key": idempotency_key, + "seed": 42, + "se_gate_k": 3.0, + "completed_at": "2026-08-31T10:00:00Z", + "study_label": "loopback-recovery", + "authored_by_llm": authored_by_llm, + "corpus": { + "snapshot_id": "snapshot-execute", + "evidence_units": [ + { + "evidence_id": "evidence-1", + "event_time": "2026-07-01T00:00:00Z", + "available_time": "2026-07-10T00:00:00Z", + "membership_count": 1 + }, + { + "evidence_id": "evidence-2", + "event_time": "2026-07-01T00:00:00Z", + "available_time": "2026-07-20T00:00:00Z", + "membership_count": 1 + }, + { + "evidence_id": "future", + "event_time": "2026-07-01T00:00:00Z", + "available_time": "2026-08-02T00:00:00Z", + "membership_count": 1 + } + ] + }, + "truth": [0.70, 0.55, 0.40, -0.20, 0.85], + "recovered": [0.70, 0.55, 0.40, -0.20, 0.85], + "interval_lower": [0.50, 0.35, 0.20, -0.40, 0.65], + "interval_upper": [0.90, 0.75, 0.60, 0.00, 1.00], + "truth_times": [1.0, 2.0, 3.0, 4.0, 5.0], + "recovered_times": [1.1, 1.9, 3.2, 3.8, 5.1] + }) + .to_string() +} + +fn http_from_exchange(exchange: &NaruonHttpExchange) -> String { + let without_scheme = exchange + .target_url + .split_once("://") + .map(|(_, rest)| rest) + .expect("scheme"); + let path = without_scheme + .find('/') + .map(|index| &without_scheme[index..]) + .expect("path"); + let mut request = format!("{} {path} HTTP/1.1\r\nHost: 127.0.0.1\r\n", exchange.method); + for (name, value) in &exchange.headers { + write!(request, "{name}: {value}\r\n").expect("header"); + } + write!( + request, + "content-length: {}\r\n\r\n{}", + exchange.body.len(), + exchange.body + ) + .expect("len"); + request +} + +fn http_get(path: &str, consumer: &str, idempotency_key: &str) -> String { + format!( + "GET {path} HTTP/1.1\r\nHost: 127.0.0.1\r\ncontent-type: application/json\r\ntepp-consumer: {consumer}\r\ntepp-contract-version: 1\r\nidempotency-key: {idempotency_key}\r\ncontent-length: 0\r\n\r\n" + ) +} + +fn accept_from_exchange( + service: &mut ScientificAcceptanceLoopbackService, + exchange: &NaruonHttpExchange, +) -> AnalysisRunAccepted { + let accepted = service.handle_http_request(&http_from_exchange(exchange)); + assert_eq!(accepted.status_code, 202, "{}", accepted.body); + assert!(!accepted.body.contains("rmse")); + AnalysisRunAccepted::from_json(&accepted.body).expect("accepted") +} + +#[test] +fn naruon_execute_exchange_is_typed_https_post_without_artifact_or_credentials() { + let execute = ScientificAcceptanceExecuteRequest::from_json(&execute_json( + "tepp-run-1", + "idem-naruon-execute", + false, + )) + .expect("execute"); + let exchange = naruon_analysis_run_execute_exchange("https://tepp.example.com", &execute) + .expect("exchange"); + assert_eq!(exchange.method, "POST"); + assert!( + exchange + .target_url + .ends_with("/v1/analysis-runs/tepp-run-1/execute") + ); + assert!( + exchange + .headers + .contains(&("tepp-consumer".into(), NARUON_CONSUMER_CODE.into())) + ); + assert!(!exchange.body.contains("scientific_acceptance_json")); + assert!(!exchange.body.contains("rmse")); + assert!(!exchange.headers.iter().any(|(name, _)| { + name.to_ascii_lowercase().contains("authorization") + || name.to_ascii_lowercase().contains("token") + || name.to_ascii_lowercase().contains("copilot") + })); + assert_eq!( + naruon_analysis_run_execute_exchange("http://tepp.example.com", &execute), + Err(ApiError::InvalidWirePayload) + ); +} + +#[test] +fn lineageweave_execute_exchange_replaces_only_the_consumer_identity() { + let execute = ScientificAcceptanceExecuteRequest::from_json(&execute_json( + "tepp-run-1", + "idem-lineageweave-execute", + false, + )) + .expect("execute"); + let exchange = lineageweave_analysis_run_execute_exchange("https://tepp.example.com", &execute) + .expect("exchange"); + assert_eq!(exchange.method, "POST"); + assert!( + exchange + .target_url + .ends_with("/v1/analysis-runs/tepp-run-1/execute") + ); + assert!( + exchange + .headers + .contains(&("tepp-consumer".into(), LINEAGEWEAVE_CONSUMER_CODE.into())) + ); + assert!( + !exchange + .headers + .iter() + .any(|(_, value)| value == NARUON_CONSUMER_CODE) + ); +} + +#[test] +fn execute_exchange_refuses_llm_metrics_and_unknown_artifact_fields() { + assert!( + ScientificAcceptanceExecuteRequest::from_json(&execute_json( + "tepp-run-1", + "idem-naruon-execute", + true, + )) + .is_err() + ); + let with_rmse = execute_json("tepp-run-1", "idem-naruon-execute", false).replacen( + '{', + r#"{"rmse":0.1,"#, + 1, + ); + assert!(ScientificAcceptanceExecuteRequest::from_json(&with_rmse).is_err()); + let with_artifact = execute_json("tepp-run-1", "idem-naruon-execute", false).replacen( + '{', + r#"{"scientific_acceptance_json":"{}","#, + 1, + ); + assert!(ScientificAcceptanceExecuteRequest::from_json(&with_artifact).is_err()); +} + +#[test] +fn naruon_and_lineageweave_execute_exchanges_produce_scientific_acceptance() { + let mut service = ScientificAcceptanceLoopbackService::new(); + let naruon_run = request( + SCIENTIFIC_ACCEPTANCE_OUTPUT_PROFILE, + VALIDATION_CPU_F64_MODEL, + "idem-naruon-execute", + ); + let naruon_create = + naruon_analysis_run_exchange("https://tepp.example.com", &naruon_run).expect("create"); + let naruon_accepted = accept_from_exchange(&mut service, &naruon_create); + let naruon_execute = ScientificAcceptanceExecuteRequest::from_json(&execute_json( + &naruon_accepted.run_id, + naruon_run.idempotency_key.as_str(), + false, + )) + .expect("execute"); + let naruon_exchange = + naruon_analysis_run_execute_exchange("https://tepp.example.com", &naruon_execute) + .expect("naruon execute"); + let naruon_response = service.handle_http_request(&http_from_exchange(&naruon_exchange)); + assert_eq!(naruon_response.status_code, 200, "{}", naruon_response.body); + assert!( + naruon_response + .body + .contains(SCIENTIFIC_ACCEPTANCE_HTTP_SCHEMA) + ); + + let lineage_run = request( + SCIENTIFIC_ACCEPTANCE_OUTPUT_PROFILE, + VALIDATION_CPU_F64_MODEL, + "idem-lineageweave-execute", + ); + let lineage_create = + lineageweave_analysis_run_exchange("https://tepp.example.com", &lineage_run) + .expect("lineage create"); + let lineage_accepted = accept_from_exchange(&mut service, &lineage_create); + let lineage_execute = ScientificAcceptanceExecuteRequest::from_json(&execute_json( + &lineage_accepted.run_id, + lineage_run.idempotency_key.as_str(), + false, + )) + .expect("lineage execute"); + let lineage_exchange = + lineageweave_analysis_run_execute_exchange("https://tepp.example.com", &lineage_execute) + .expect("lineage execute exchange"); + let lineage_response = service.handle_http_request(&http_from_exchange(&lineage_exchange)); + assert_eq!( + lineage_response.status_code, 200, + "{}", + lineage_response.body + ); + let get = service.handle_http_request(&http_get( + &format!("{NARUON_ANALYSIS_RUN_PATH}/{}", lineage_accepted.run_id), + LINEAGEWEAVE_CONSUMER_CODE, + lineage_run.idempotency_key.as_str(), + )); + assert_eq!(get.status_code, 200, "{}", get.body); + assert!(get.body.contains(SCIENTIFIC_ACCEPTANCE_SCHEMA_VERSION)); + assert!(get.body.contains(SCIENTIFIC_ACCEPTANCE_HTTP_PROFILE)); + assert!(get.body.contains("scientific_acceptance")); +} diff --git a/docs/API_CONTRACT.md b/docs/API_CONTRACT.md index 1761c1e5f..1b7a04e8d 100644 --- a/docs/API_CONTRACT.md +++ b/docs/API_CONTRACT.md @@ -113,7 +113,10 @@ loopback path that produces `tepp.scientific_acceptance.v1` for a body carries corpus, recovery, seed, and the pre-registered SE-gate multiplier and refuses `scientific_acceptance_json`. The published `tepp-loopback` binary binds that wrapper so operators reach `/execute` without embedding -`analysis_engine`. Production TLS remains a later adapter. +`analysis_engine`. `naruon_analysis_run_execute_exchange` and +`lineageweave_analysis_run_execute_exchange` are the typed credential-free +consumer contracts for that POST; they refuse LLM recovery, receipt metric +keys, and non-`https` origins. Production TLS remains a later adapter. The stacked `analysis_engine` slice provides the first executable service-side path behind these DTOs. It consumes a bounded identity-free snapshot, excludes diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index deb35fa61..807dbda9e 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -58,6 +58,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | loopback analysis-run scientific-acceptance lifecycle POST | ADR 0028; API contract; RFC 9110; FIPS 180-4 | `tepp_api` `POST /v1/analysis-runs/{run_id}/running` and `/terminal` on `AnalysisRunLiveService` (this PR): production status-update path; accepted/running stay metric-free; `tepp.scientific_acceptance.v1` only after succeeded `scientific_acceptance_v1`; not implemented-main | active-PR | | loopback scientific-acceptance engine execute | ADR 0032; ADR 0026/0027/0028; API contract; National Academies (2019); Wasserstein & Lazar (2016); Wilson (1927) | `analysis_engine` `POST /v1/analysis-runs/{run_id}/execute` on `ScientificAcceptanceLoopbackService` (this PR): engine produces `tepp.scientific_acceptance.v1` without caller-supplied artifact; metric keys/LLM/wrong-profile/unknown-run/consumer-mismatch/already-terminal fail closed; not implemented-main; Postgres persistence remains GAP-003B | active-PR | | published scientific-acceptance loopback binary | ADR 0033; ADR 0032; API contract; National Academies (2019); Wasserstein & Lazar (2016); Wilson (1927) | `analysis_engine` `tepp-loopback` binary (this PR): packaged listener binds `ScientificAcceptanceLoopbackService` so POST create/execute then GET returns `tepp.scientific_acceptance.v1` without embedding the library; temporal-context health remains; `tepp_api` no longer ships the binary; not implemented-main; Postgres persistence remains GAP-003B | active-PR | +| scientific-acceptance execute consumer exchange | ADR 0034; ADR 0032/0033; API contract; National Academies (2019); Wasserstein & Lazar (2016); Wilson (1927) | `analysis_engine` `naruon_analysis_run_execute_exchange` / `lineageweave_analysis_run_execute_exchange` (this PR): typed credential-free HTTPS POST `/execute`; LLM/metric keys/unknown artifact fields/`http://` fail closed; not implemented-main; Postgres persistence remains GAP-003B | active-PR | | executable cutoff-safe analysis-run readiness | ADR 0021; 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 | | delayed-reporting cutoff eligibility in truth corpora | ADR 0002; research | `tepp_simulation` eligible-at-cutoff filter on the active PR | active-PR | | 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 | diff --git a/docs/adr/0034-scientific-acceptance-execute-consumer-exchange.md b/docs/adr/0034-scientific-acceptance-execute-consumer-exchange.md new file mode 100644 index 000000000..9a986d49e --- /dev/null +++ b/docs/adr/0034-scientific-acceptance-execute-consumer-exchange.md @@ -0,0 +1,107 @@ +# ADR 0034 — Scientific-acceptance execute consumer exchange + +**Decision status:** Accepted +**Implementation maturity:** active-PR +**Date:** 2026-08-31 +**Supersedes:** None; complements ADR 0032 (engine execute) and ADR 0033 (published binary). Does not reuse ADR 0030 or ADR 0031. Does not supersede ADR 0014 claim-promotion authority. + +## Context + +ADR 0032 owns `POST /v1/analysis-runs/{run_id}/execute` on +`ScientificAcceptanceLoopbackService`. ADR 0033 publishes that wrapper as +`tepp-loopback`. Naruon and `LineageWeave` still have no typed execute +exchange: they would have to hand-roll HTTPS, path encoding, consumer +headers, and the metric-free execute body. Cancel consumer parity (#373) is a +different stack and a different verb. `tepp_api` cannot depend on +`analysis_engine` (crate cycle), so the execute body and exchange builders +belong in `analysis_engine`. Duplicating the published binary (#375), +engine-execute library (#370), cancel consumer parity (#373), loopback CLI +(#362), collection CLI (#371), retry (#369), GET, lifecycle POST, cancel +HTTP, collection GET, DTO, or engine-library slices would collide with live +PRs. + +## Decision + +`analysis_engine` owns typed execute consumer exchanges: + +- `ScientificAcceptanceExecuteRequest` is the deny-unknown-fields execute + body. It carries corpus, recovery, seed, and pre-registered SE-gate `k`. + It refuses `scientific_acceptance_json`, receipt metric keys, and + LLM-authored recovery. +- `naruon_analysis_run_execute_exchange` builds a credential-free HTTPS + `POST /v1/analysis-runs/{run_id}/execute`. +- `lineageweave_analysis_run_execute_exchange` reuses that builder and + replaces only the published `tepp-consumer` identity. +- Non-`https` origins, table-access hosts, empty or oversized run + identities, unsupported contract versions, and hostile bodies fail closed. +- Persistence remains GAP-003B. + +## Non-goals + +- Production TLS, public bind, or durable status storage. +- Leiden community detection, Driver p.16 std-family restoration, or Figma/export work. +- Promoting an ADR 0014 scientific claim from HTTP success. +- Cancel consumer parity, collection GET, loopback CLI, collection CLI, retry HTTP, or another published-binary move. + +## Alternatives considered + +1. **Keep hand-rolled execute HTTP in each consumer** — rejected because + GAP-003A is operator-visible and create/GET/lifecycle already have typed + exchanges. +2. **Add the builders to `tepp_api`** — rejected as a crate cycle once the + body is the engine execute contract. +3. **Reuse the cancel consumer-parity PR** — rejected; cancel is a different + verb on a different stack. + +## Consequences + +- Naruon and `LineageWeave` can mint `/execute` without embedding the + library or inventing routes. +- HTTP 200 on execute is not release evidence. + +## Failure and recovery + +Non-`https` origins, table-access hosts, LLM recovery, metric keys, unknown +artifact fields, empty identities, and oversized run identifiers return a +fail-closed API error before any socket is opened. The in-memory registry is +not durable. + +## Security, privacy, scientific-integrity, and governance impact + +- No credential headers cross the consumer boundary. +- Execute remains loopback-served, size-bounded, and content-redacting. +- LLM-authored recovery cannot become scientific authority. + +## Compatibility and migration + +Create, GET, running, terminal, temporal-context, and project-history +exchanges are unchanged. Production adapters may replace loopback while +preserving metric-free receipts and engine-produced scientific acceptance. + +## Verification + +Falsifiable evidence: + +- naruon execute exchange is HTTPS POST `/execute` without credentials or + `scientific_acceptance_json`; +- LineageWeave execute exchange changes only `tepp-consumer`; +- LLM recovery, metric keys, unknown artifact fields, and `http://` origins + fail closed; +- POST create then the typed execute exchange then GET returns + `tepp.scientific_acceptance.v1` for both consumers; +- Clippy `-D warnings`, `analysis_engine` and `tepp_api` tests, rustdoc, and + exact-head review remain required. + +## Rollback and supersession + +Rollback removes the execute exchange builders; the engine wrapper and +published binary remain valid. A superseding ADR is required to persist +status, bind a public address, or treat HTTP success as an ADR 0014 claim. + +## Related authority + +- ADR 0032 owns engine-on-loopback execute. +- ADR 0033 owns the published `tepp-loopback` binary. +- ADR 0018 owns consumer-scoped ingress and metric-free `202 Accepted`. +- ADR 0011 owns standalone/modular HTTP boundaries. +- ADR 0014 owns scientific claim promotion. diff --git a/docs/adr/README.md b/docs/adr/README.md index b786a4afd..2c0a7ef30 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -35,6 +35,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0028](0028-scientific-acceptance-http-lifecycle.md) | Scientific-acceptance loopback HTTP lifecycle POST | Accepted | active-PR | POST `/running` and `/terminal` are the production status-update path; GET remains ADR 0027. Persistence remains GAP-003B. | | [0032](0032-scientific-acceptance-loopback-engine.md) | Scientific-acceptance loopback engine execute | Accepted | active-PR | POST `/execute` runs `analysis_engine` on the loopback lifecycle so GET returns `tepp.scientific_acceptance.v1` without a caller-supplied artifact. Persistence remains GAP-003B. | | [0033](0033-scientific-acceptance-loopback-binary.md) | Scientific-acceptance published loopback binary | Accepted | active-PR | `tepp-loopback` moves to `analysis_engine` and binds `ScientificAcceptanceLoopbackService` so `/execute` is reachable on the packaged listener. Persistence remains GAP-003B. | +| [0034](0034-scientific-acceptance-execute-consumer-exchange.md) | Scientific-acceptance execute consumer exchange | Accepted | active-PR | Naruon and `LineageWeave` mint credential-free POST `/execute` through typed exchanges in `analysis_engine`. Persistence remains GAP-003B. | | [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. | @@ -145,6 +146,7 @@ Use the narrowest owning ADR when decisions overlap: - **accepted-run execution and terminal artifact production:** ADR 0022. - **scientific-acceptance loopback engine execute:** ADR 0032. - **scientific-acceptance published loopback binary:** ADR 0033. +- **scientific-acceptance execute consumer exchange:** ADR 0034. - **independent lineage criterion and posterior Project Journey:** ADR 0023. - **macOS-native Rust-owned MLX Metal execution:** ADR 0024. diff --git a/docs/connectors/naruon-artifact-consumer.md b/docs/connectors/naruon-artifact-consumer.md index 632084828..39769838b 100644 --- a/docs/connectors/naruon-artifact-consumer.md +++ b/docs/connectors/naruon-artifact-consumer.md @@ -30,6 +30,7 @@ TEPP remains the scientific authority for estimation, recovery metrics, temporal | Live loopback POST | `tepp_api` `NaruonLiveService` → `POST /v1/analysis-runs` and `/v1/exports` | naruon → TEPP | | Live loopback scientific-acceptance execute | `analysis_engine` `ScientificAcceptanceLoopbackService` → `POST /v1/analysis-runs/{run_id}/execute` | naruon → TEPP | | Published `tepp-loopback` binary | `analysis_engine` `tepp-loopback` binds `ScientificAcceptanceLoopbackService` so `/execute` is reachable without embedding the library | naruon → TEPP | +| Typed scientific-acceptance execute exchange | `analysis_engine` `naruon_analysis_run_execute_exchange` → `POST /v1/analysis-runs/{run_id}/execute` | naruon → TEPP | Committed examples live under `examples/`. Schemas for analysis-run requests and corpus-split manifests live under `schemas/`. diff --git a/docs/research/scientific-acceptance-execute-consumer-exchange.md b/docs/research/scientific-acceptance-execute-consumer-exchange.md new file mode 100644 index 000000000..73642dcfc --- /dev/null +++ b/docs/research/scientific-acceptance-execute-consumer-exchange.md @@ -0,0 +1,31 @@ +# Scientific-acceptance execute consumer exchange + +## Scope + +This note doctors the GAP-003A naruon/`LineageWeave` execute consumer-exchange slice: + +1. `ScientificAcceptanceExecuteRequest` is the typed execute body naruon and `LineageWeave` may POST; +2. `naruon_analysis_run_execute_exchange` and `lineageweave_analysis_run_execute_exchange` mint credential-free HTTPS `POST /v1/analysis-runs/{run_id}/execute`; +3. the body carries corpus, recovery, seed, and pre-registered SE-gate `k` and must not carry `scientific_acceptance_json`; +4. LLM-authored recovery, receipt metric keys, unknown artifact fields, and non-`https` origins fail closed. + +Postgres persistence, restart/recovery, and Compose execution remain GAP-003B. This slice is not implemented-main. It does not duplicate the published binary (#375), engine-execute library (#370), cancel consumer parity (#373), loopback CLI (#362), collection CLI (#371), retry (#369), GET (#359), lifecycle POST (#360), cancel HTTP (#361), collection GET (#368), DTO (#358), or engine library (#356). + +## Authoritative sources + +National Academies of Sciences, Engineering, and Medicine. (2019). *Reproducibility and replicability in science*. The National Academies Press. https://doi.org/10.17226/25303 + +Wasserstein, R. L., & Lazar, N. A. (2016). The ASA statement on *p*-values: Context, process, and purpose. *The American Statistician, 70*(2), 129–133. https://doi.org/10.1080/00031305.2016.1154108 + +Wilson, E. B. (1927). Probable inference, the law of succession, and statistical inference. *Journal of the American Statistical Association, 22*(158), 209–212. https://doi.org/10.1080/01621459.1927.10502953 + +## Application + +The National Academies (2019) require that a computational procedure be invoked through a published interface, not an ad-hoc consumer wire. Wasserstein and Lazar (2016) refuse to treat a passing threshold as automatic scientific authority, so the typed exchange produces the same `tepp.scientific_acceptance.v1` evidence as the library bind and never treats HTTP `200` as ADR 0014 promotion. Wilson (1927) supplies the coverage interval already implemented in `validation_core`. TEPP therefore gives naruon and `LineageWeave` a credential-free execute exchange, refuses caller-supplied artifacts and LLM-authored recovery, and reports RMSE, bias, coverage, temporal order, and the SE-aware gate only after engine completion (National Academies of Sciences, Engineering, and Medicine, 2019; Wasserstein & Lazar, 2016; Wilson, 1927). Meredith (1993) remains unread (Unpaywall/OpenAlex 2026-08-31T13:00Z: `is_oa: false`, 0 locations). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread on the same terms (DOI `10.1007/bf02294457`). + +## Verification + +- naruon execute exchange is HTTPS POST `/execute` without credentials or `scientific_acceptance_json`; +- LineageWeave execute exchange changes only `tepp-consumer`; +- LLM recovery, metric keys, unknown artifact fields, and `http://` origins fail closed; +- POST create then the typed execute exchange then GET returns `tepp.scientific_acceptance.v1` for both consumers.