From bf45147ffc32f0024ff519c49311823fe664562c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 13:21:51 +0000 Subject: [PATCH] feat(analysis): send execute exchanges over tepp-loopback TCP Render typed naruon and LineageWeave scientific-acceptance execute exchanges onto the spawned tepp-loopback listener so POST /execute is published consumer HTTP without embedding the library. Public bind hosts and localhost fail closed. --- ARCHITECTURE.md | 4 +- ...cceptance-execute-exchange-loopback-tcp.md | 3 + CHANGELOG.md | 2 + DOCUMENTATION.md | 2 + crates/analysis_engine/src/lib.rs | 6 +- .../analysis_engine/src/loopback_execute.rs | 145 +++++++++- .../execute_exchange_loopback_tcp_contract.rs | 256 ++++++++++++++++++ docs/API_CONTRACT.md | 4 +- docs/TRACEABILITY.md | 1 + ...cceptance-execute-exchange-loopback-tcp.md | 74 +++++ docs/adr/README.md | 2 + docs/connectors/naruon-artifact-consumer.md | 1 + ...cceptance-execute-exchange-loopback-tcp.md | 30 ++ 13 files changed, 516 insertions(+), 14 deletions(-) create mode 100644 CHANGELOG.d/scientific-acceptance-execute-exchange-loopback-tcp.md create mode 100644 crates/analysis_engine/tests/execute_exchange_loopback_tcp_contract.rs create mode 100644 docs/adr/0037-scientific-acceptance-execute-exchange-loopback-tcp.md create mode 100644 docs/research/scientific-acceptance-execute-exchange-loopback-tcp.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 63baedf90..8a1a1ddc1 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, the published `tepp-loopback` binary that binds that wrapper, and typed naruon/`LineageWeave` execute exchanges | +| `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, typed naruon/`LineageWeave` execute exchanges, and HTTP/1.1 rendering of those exchanges onto the spawned `tepp-loopback` TCP listener | | `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, the published `tepp-loopback` binary that binds that wrapper, and typed naruon/`LineageWeave` execute exchanges | +| `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, typed naruon/`LineageWeave` execute exchanges, and HTTP/1.1 rendering of those exchanges onto the spawned `tepp-loopback` TCP listener | | `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-exchange-loopback-tcp.md b/CHANGELOG.d/scientific-acceptance-execute-exchange-loopback-tcp.md new file mode 100644 index 000000000..0f92bc552 --- /dev/null +++ b/CHANGELOG.d/scientific-acceptance-execute-exchange-loopback-tcp.md @@ -0,0 +1,3 @@ +### Added + +- `analysis_engine` GAP-003A execute-exchange loopback TCP slice (ADR 0037, active-PR, not implemented-main): typed naruon and `LineageWeave` execute exchanges render onto the spawned `tepp-loopback` TCP listener so a `scientific_acceptance_v1` run produces `tepp.scientific_acceptance.v1` without hand-rolled HTTP. Public bind hosts and `localhost` fail closed. Persistence remains GAP-003B. diff --git a/CHANGELOG.md b/CHANGELOG.md index 176201a7c..61c816b2c 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-exchange loopback TCP slice (ADR 0037, active-PR, not implemented-main): `loopback_http1_from_execute_exchange` renders typed naruon and `LineageWeave` execute exchanges onto the spawned `tepp-loopback` TCP listener so POST `/execute` is published consumer HTTP without embedding the library. Public bind hosts, `localhost`, and non-execute exchanges fail closed. This does not duplicate the execute consumer-exchange builders (#381), 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 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. diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index ad157ca20..8d3a3ecc7 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -62,6 +62,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin | 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) | +| Scientific-acceptance execute-exchange loopback TCP doctoring | [`docs/research/scientific-acceptance-execute-exchange-loopback-tcp.md`](docs/research/scientific-acceptance-execute-exchange-loopback-tcp.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) | @@ -155,6 +156,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin | 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) | +| Scientific-acceptance execute-exchange loopback TCP doctoring | [`docs/research/scientific-acceptance-execute-exchange-loopback-tcp.md`](docs/research/scientific-acceptance-execute-exchange-loopback-tcp.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 de23a49d3..70a4a8472 100644 --- a/crates/analysis_engine/src/lib.rs +++ b/crates/analysis_engine/src/lib.rs @@ -14,7 +14,8 @@ //! 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. Naruon and -//! `LineageWeave` mint that POST through typed execute exchanges in this crate. +//! `LineageWeave` mint that POST through typed execute exchanges in this crate +//! and render them onto the spawned `tepp-loopback` TCP listener. mod case_deletion_refit; mod lineage_criterion; @@ -60,7 +61,8 @@ pub use loopback_execute::{ ANALYSIS_RUN_EXECUTE_CONTRACT_VERSION, ANALYSIS_RUN_EXECUTE_PATH_SUFFIX, ScientificAcceptanceExecuteCorpus, ScientificAcceptanceExecuteEvidenceUnit, ScientificAcceptanceExecuteRequest, ScientificAcceptanceLoopbackService, - lineageweave_analysis_run_execute_exchange, naruon_analysis_run_execute_exchange, + lineageweave_analysis_run_execute_exchange, loopback_http1_from_execute_exchange, + loopback_http1_from_naruon_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 2ede7db60..cf2f7ee6a 100644 --- a/crates/analysis_engine/src/loopback_execute.rs +++ b/crates/analysis_engine/src/loopback_execute.rs @@ -7,23 +7,26 @@ //! corpus, recovery vectors, seed, and the pre-registered SE-gate multiplier. //! It must not carry `scientific_acceptance_json`. GET then returns the artifact //! without a caller-supplied terminal payload. Persistence remains GAP-003B. +//! Typed naruon/`LineageWeave` execute exchanges render onto the published +//! `tepp-loopback` TCP listener through [`loopback_http1_from_execute_exchange`]. use crate::{ - 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, + complete_validation_run, submit_validation_run, AnalysisCorpus, AnalysisEngineError, + AnalysisEvidenceUnit, RecoveryObservation, MAX_SE_GATE_K, SCIENTIFIC_ACCEPTANCE_OUTPUT_PROFILE, + SCIENTIFIC_ACCEPTANCE_SCHEMA_VERSION, VALIDATION_CPU_F64_MODEL, }; use serde::{Deserialize, Serialize}; +use std::fmt::Write as _; use std::net::SocketAddr; use temporal_core::{AvailableTime, EventTime}; use tepp_api::{ - 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, naruon_analysis_run_status_exchange, - parse_loopback_http_parts, + parse_loopback_http_parts, AnalysisResultSummary, AnalysisRunLiveService, AnalysisRunStatus, + AnalysisRunStatusState, AnalysisRunTerminalResult, ApiError, ErrorEnvelope, NaruonHttpExchange, + NaruonLiveResponse, ANALYSIS_RUN_ID_MAX_LEN, ANALYSIS_RUN_STATUS_PATH, + DEFAULT_ANALYSIS_RUN_BYTE_LIMIT, DEFAULT_PROJECT_HISTORY_BYTE_LIMIT, + LINEAGEWEAVE_CONSUMER_CODE, NARUON_CONSUMER_CODE, SCIENTIFIC_ACCEPTANCE_HTTP_PROFILE, + SCIENTIFIC_ACCEPTANCE_HTTP_SCHEMA, }; /// Result-metric keys that must not appear on an execute request object. @@ -453,6 +456,130 @@ pub fn lineageweave_analysis_run_execute_exchange( Ok(exchange) } +/// Render a typed naruon/`LineageWeave` exchange as HTTP/1.1 for a bound +/// loopback listener. +/// +/// The exchange keeps its HTTPS origin contract. Only the HTTP/1.1 `Host` +/// is the loopback bind address printed by `tepp-loopback`. Public or +/// non-loopback hosts fail closed before any socket is opened. +/// +/// # Errors +/// +/// Returns [`ApiError::AuthorizationDenied`] for a non-loopback host or a +/// credential-bearing header, [`ApiError::LimitExceeded`] when the body +/// exceeds [`DEFAULT_ANALYSIS_RUN_BYTE_LIMIT`], and +/// [`ApiError::InvalidWirePayload`] for an empty method, a non-`https` +/// target, or a missing path. +pub fn loopback_http1_from_naruon_exchange( + exchange: &NaruonHttpExchange, + loopback_host: &str, +) -> Result { + let host = require_loopback_host(loopback_host)?; + refuse_exchange_credential_headers(&exchange.headers)?; + if exchange.method.is_empty() { + return Err(ApiError::InvalidWirePayload); + } + let path = exchange_https_path(&exchange.target_url)?; + if exchange.body.len() > DEFAULT_ANALYSIS_RUN_BYTE_LIMIT { + return Err(ApiError::LimitExceeded); + } + let mut request = String::new(); + write!( + request, + "{} {path} HTTP/1.1\r\nHost: {host}\r\n", + exchange.method + ) + .map_err(|_| ApiError::InvalidWirePayload)?; + for (name, value) in &exchange.headers { + if name.eq_ignore_ascii_case("host") || name.eq_ignore_ascii_case("content-length") { + continue; + } + write!(request, "{name}: {value}\r\n").map_err(|_| ApiError::InvalidWirePayload)?; + } + write!( + request, + "content-length: {}\r\n\r\n{}", + exchange.body.len(), + exchange.body + ) + .map_err(|_| ApiError::InvalidWirePayload)?; + Ok(request) +} + +/// Render a typed execute exchange onto the published `tepp-loopback` listener. +/// +/// Requires POST `/execute` with a naruon or `LineageWeave` consumer identity. +/// Public bind hosts fail closed. +/// +/// # Errors +/// +/// Returns [`ApiError::InvalidWirePayload`] when the exchange is not a POST +/// `/execute` for naruon or `LineageWeave`. Other failures match +/// [`loopback_http1_from_naruon_exchange`]. +pub fn loopback_http1_from_execute_exchange( + exchange: &NaruonHttpExchange, + loopback_host: &str, +) -> Result { + require_execute_loopback_exchange(exchange)?; + loopback_http1_from_naruon_exchange(exchange, loopback_host) +} + +fn require_execute_loopback_exchange(exchange: &NaruonHttpExchange) -> Result<(), ApiError> { + if exchange.method != "POST" { + return Err(ApiError::InvalidWirePayload); + } + let path = exchange_https_path(&exchange.target_url)?; + if path.rsplit('/').next() != Some(ANALYSIS_RUN_EXECUTE_PATH_SUFFIX) + || !path.starts_with(ANALYSIS_RUN_STATUS_PATH) + { + return Err(ApiError::InvalidWirePayload); + } + 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 && consumer != LINEAGEWEAVE_CONSUMER_CODE { + return Err(ApiError::InvalidWirePayload); + } + Ok(()) +} + +fn require_loopback_host(host: &str) -> Result<&str, ApiError> { + let host = host.trim(); + let addr: SocketAddr = host.parse().map_err(|_| ApiError::InvalidWirePayload)?; + if addr.ip().is_loopback() { + Ok(host) + } else { + Err(ApiError::AuthorizationDenied) + } +} + +fn exchange_https_path(target_url: &str) -> Result<&str, ApiError> { + let rest = target_url + .strip_prefix("https://") + .ok_or(ApiError::InvalidWirePayload)?; + let path = rest + .find('/') + .map(|index| &rest[index..]) + .ok_or(ApiError::InvalidWirePayload)?; + if path.is_empty() { + return Err(ApiError::InvalidWirePayload); + } + Ok(path) +} + +fn refuse_exchange_credential_headers(headers: &[(String, String)]) -> Result<(), ApiError> { + for (name, _) in headers { + let lower = name.to_ascii_lowercase(); + if lower.contains("authorization") || lower.contains("token") || lower.contains("copilot") { + return Err(ApiError::AuthorizationDenied); + } + } + Ok(()) +} + fn parse_execute_body(body: &str) -> Result { if body.len() > DEFAULT_ANALYSIS_RUN_BYTE_LIMIT { return Err(ApiError::LimitExceeded); diff --git a/crates/analysis_engine/tests/execute_exchange_loopback_tcp_contract.rs b/crates/analysis_engine/tests/execute_exchange_loopback_tcp_contract.rs new file mode 100644 index 000000000..750f82fda --- /dev/null +++ b/crates/analysis_engine/tests/execute_exchange_loopback_tcp_contract.rs @@ -0,0 +1,256 @@ +//! GAP-003A typed execute exchanges over the spawned tepp-loopback TCP listener. + +use std::io::{BufRead, BufReader, Read, Write}; +use std::net::TcpStream; +use std::process::{Child, Command, Stdio}; +use std::time::Duration; + +use analysis_engine::{ + lineageweave_analysis_run_execute_exchange, loopback_http1_from_execute_exchange, + loopback_http1_from_naruon_exchange, naruon_analysis_run_execute_exchange, + ScientificAcceptanceExecuteRequest, ANALYSIS_RUN_EXECUTE_CONTRACT_VERSION, + SCIENTIFIC_ACCEPTANCE_OUTPUT_PROFILE, SCIENTIFIC_ACCEPTANCE_SCHEMA_VERSION, + VALIDATION_CPU_F64_MODEL, +}; +use tepp_api::{ + lineageweave_analysis_run_exchange, naruon_analysis_run_exchange, + naruon_analysis_run_status_exchange, AnalysisRunAccepted, AnalysisRunRequest, ApiError, + NaruonHttpExchange, ANALYSIS_RUN_CONTRACT_VERSION, LINEAGEWEAVE_CONSUMER_CODE, + NARUON_CONSUMER_CODE, SCIENTIFIC_ACCEPTANCE_HTTP_PROFILE, SCIENTIFIC_ACCEPTANCE_HTTP_SCHEMA, +}; + +const HTTPS_ORIGIN: &str = "https://tepp.example.com"; + +fn spawn_loopback(request_limit: &str) -> (Child, String) { + let mut child = Command::new(env!("CARGO_BIN_EXE_tepp-loopback")) + .args(["127.0.0.1:0", request_limit]) + .stdout(Stdio::piped()) + .spawn() + .expect("spawn loopback service"); + let mut address = String::new(); + BufReader::new(child.stdout.take().expect("stdout")) + .read_line(&mut address) + .expect("bound address"); + (child, address) +} + +fn exchange(address: &str, request: &str) -> String { + let mut stream = TcpStream::connect(address.trim()).expect("connect"); + stream + .set_read_timeout(Some(Duration::from_secs(5))) + .expect("read timeout"); + stream + .set_write_timeout(Some(Duration::from_secs(5))) + .expect("write timeout"); + stream.write_all(request.as_bytes()).expect("request"); + let mut response = String::new(); + stream.read_to_string(&mut response).expect("response"); + response +} + +fn request(profile: &str, model: &str, idempotency_key: &str) -> AnalysisRunRequest { + AnalysisRunRequest { + contract_version: ANALYSIS_RUN_CONTRACT_VERSION, + idempotency_key: idempotency_key.into(), + tenant_workspace_id: "tenant-workspace-execute-tcp".into(), + snapshot_id: "snapshot-execute-tcp".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) -> 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-31T13:00:00Z", + "study_label": "loopback-tcp-recovery", + "authored_by_llm": false, + "corpus": { + "snapshot_id": "snapshot-execute-tcp", + "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 response_body(response: &str) -> &str { + response.split("\r\n\r\n").nth(1).expect("http body") +} + +fn status_exchange_for_consumer( + run_id: &str, + idempotency_key: &str, + consumer: &str, +) -> NaruonHttpExchange { + let mut exchange = naruon_analysis_run_status_exchange(HTTPS_ORIGIN, run_id, idempotency_key) + .expect("status exchange"); + let header = exchange + .headers + .iter_mut() + .find(|(name, _)| name.eq_ignore_ascii_case("tepp-consumer")) + .expect("consumer header"); + consumer.clone_into(&mut header.1); + exchange +} + +fn accept_on_tcp(address: &str, create: &NaruonHttpExchange) -> AnalysisRunAccepted { + let request = + loopback_http1_from_naruon_exchange(create, address.trim()).expect("create http1"); + let accepted = exchange(address, &request); + assert!(accepted.starts_with("HTTP/1.1 202 Accepted"), "{accepted}"); + assert!(!accepted.contains("rmse")); + assert!(!accepted.contains("scientific_acceptance")); + AnalysisRunAccepted::from_json(response_body(&accepted)).expect("accepted") +} + +#[test] +fn execute_loopback_http1_refuses_public_bind_and_non_execute_exchanges() { + let execute = ScientificAcceptanceExecuteRequest::from_json(&execute_json( + "tepp-run-1", + "idem-naruon-execute-tcp", + )) + .expect("execute"); + let naruon = naruon_analysis_run_execute_exchange(HTTPS_ORIGIN, &execute).expect("naruon"); + assert_eq!( + loopback_http1_from_execute_exchange(&naruon, "8.8.8.8:80"), + Err(ApiError::AuthorizationDenied) + ); + assert_eq!( + loopback_http1_from_execute_exchange(&naruon, "localhost:18081"), + Err(ApiError::InvalidWirePayload) + ); + let status = + naruon_analysis_run_status_exchange(HTTPS_ORIGIN, "tepp-run-1", "idem-naruon-execute-tcp") + .expect("status"); + assert_eq!( + loopback_http1_from_execute_exchange(&status, "127.0.0.1:18081"), + Err(ApiError::InvalidWirePayload) + ); + let http1 = loopback_http1_from_execute_exchange(&naruon, "127.0.0.1:18081").expect("http1"); + assert!(http1.starts_with("POST /v1/analysis-runs/tepp-run-1/execute HTTP/1.1")); + assert!(http1.contains("Host: 127.0.0.1:18081")); + assert!(http1.contains("tepp-consumer: naruon")); + assert!(!http1.to_ascii_lowercase().contains("authorization")); + assert!(!http1.contains("scientific_acceptance_json")); +} + +#[test] +fn naruon_and_lineageweave_execute_exchanges_over_spawned_loopback_tcp() { + let (mut child, address) = spawn_loopback("6"); + let host = address.trim(); + + let naruon_run = request( + SCIENTIFIC_ACCEPTANCE_OUTPUT_PROFILE, + VALIDATION_CPU_F64_MODEL, + "idem-naruon-execute-tcp", + ); + let naruon_create = naruon_analysis_run_exchange(HTTPS_ORIGIN, &naruon_run).expect("create"); + let naruon_accepted = accept_on_tcp(&address, &naruon_create); + let naruon_execute = ScientificAcceptanceExecuteRequest::from_json(&execute_json( + &naruon_accepted.run_id, + naruon_run.idempotency_key.as_str(), + )) + .expect("naruon execute"); + let naruon_exchange = naruon_analysis_run_execute_exchange(HTTPS_ORIGIN, &naruon_execute) + .expect("naruon exchange"); + let naruon_http = + loopback_http1_from_execute_exchange(&naruon_exchange, host).expect("naruon http1"); + let naruon_response = exchange(&address, &naruon_http); + assert!( + naruon_response.starts_with("HTTP/1.1 200 OK"), + "{naruon_response}" + ); + assert!(naruon_response.contains(SCIENTIFIC_ACCEPTANCE_HTTP_SCHEMA)); + let naruon_get = loopback_http1_from_naruon_exchange( + &status_exchange_for_consumer( + &naruon_accepted.run_id, + naruon_run.idempotency_key.as_str(), + NARUON_CONSUMER_CODE, + ), + host, + ) + .expect("naruon get"); + let naruon_status = exchange(&address, &naruon_get); + assert!( + naruon_status.starts_with("HTTP/1.1 200 OK"), + "{naruon_status}" + ); + assert!(naruon_status.contains(SCIENTIFIC_ACCEPTANCE_SCHEMA_VERSION)); + assert!(naruon_status.contains(SCIENTIFIC_ACCEPTANCE_HTTP_PROFILE)); + assert!(naruon_status.contains("scientific_acceptance")); + + let lineage_run = request( + SCIENTIFIC_ACCEPTANCE_OUTPUT_PROFILE, + VALIDATION_CPU_F64_MODEL, + "idem-lineageweave-execute-tcp", + ); + let lineage_create = + lineageweave_analysis_run_exchange(HTTPS_ORIGIN, &lineage_run).expect("lineage create"); + let lineage_accepted = accept_on_tcp(&address, &lineage_create); + let lineage_execute = ScientificAcceptanceExecuteRequest::from_json(&execute_json( + &lineage_accepted.run_id, + lineage_run.idempotency_key.as_str(), + )) + .expect("lineage execute"); + let lineage_exchange = + lineageweave_analysis_run_execute_exchange(HTTPS_ORIGIN, &lineage_execute) + .expect("lineage exchange"); + let lineage_http = + loopback_http1_from_execute_exchange(&lineage_exchange, host).expect("lineage http1"); + assert!(lineage_http.contains("tepp-consumer: lineageweave")); + assert!(!lineage_http.contains("tepp-consumer: naruon")); + let lineage_response = exchange(&address, &lineage_http); + assert!( + lineage_response.starts_with("HTTP/1.1 200 OK"), + "{lineage_response}" + ); + let lineage_get = loopback_http1_from_naruon_exchange( + &status_exchange_for_consumer( + &lineage_accepted.run_id, + lineage_run.idempotency_key.as_str(), + LINEAGEWEAVE_CONSUMER_CODE, + ), + host, + ) + .expect("lineage get"); + let lineage_status = exchange(&address, &lineage_get); + assert!( + lineage_status.starts_with("HTTP/1.1 200 OK"), + "{lineage_status}" + ); + assert!(lineage_status.contains(SCIENTIFIC_ACCEPTANCE_SCHEMA_VERSION)); + assert!(lineage_status.contains(SCIENTIFIC_ACCEPTANCE_HTTP_PROFILE)); + assert!(lineage_status.contains("scientific_acceptance")); + assert!(lineage_status.contains("rmse")); + assert!(child.wait().expect("wait").success()); +} diff --git a/docs/API_CONTRACT.md b/docs/API_CONTRACT.md index 1b7a04e8d..faa872d05 100644 --- a/docs/API_CONTRACT.md +++ b/docs/API_CONTRACT.md @@ -116,7 +116,9 @@ binds that wrapper so operators reach `/execute` without embedding `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. +keys, and non-`https` origins. `loopback_http1_from_execute_exchange` renders +those exchanges onto the spawned `tepp-loopback` TCP listener; public bind +hosts and `localhost` fail closed. 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 807dbda9e..92e80d707 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -59,6 +59,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | 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 | +| scientific-acceptance execute exchange loopback TCP | ADR 0037; ADR 0034/0033/0032; API contract; National Academies (2019); Wasserstein & Lazar (2016); Wilson (1927) | `analysis_engine` `loopback_http1_from_execute_exchange` (this PR): typed naruon/`LineageWeave` execute exchanges render onto the spawned `tepp-loopback` TCP listener; public bind/`localhost`/non-execute exchanges fail closed; GET then returns `tepp.scientific_acceptance.v1`; 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/0037-scientific-acceptance-execute-exchange-loopback-tcp.md b/docs/adr/0037-scientific-acceptance-execute-exchange-loopback-tcp.md new file mode 100644 index 000000000..29ffee5d8 --- /dev/null +++ b/docs/adr/0037-scientific-acceptance-execute-exchange-loopback-tcp.md @@ -0,0 +1,74 @@ +# ADR 0037 — Scientific-acceptance execute exchange on loopback TCP + +**Decision status:** Accepted +**Implementation maturity:** active-PR +**Date:** 2026-08-31 +**Supersedes:** None; complements ADR 0034 (typed execute exchanges) and ADR 0033 (published binary). Does not reuse ADR 0035 or ADR 0036. Does not supersede ADR 0014 claim-promotion authority. + +## Context + +ADR 0034 mints typed naruon and `LineageWeave` execute exchanges. ADR 0033 publishes `tepp-loopback` so `/execute` is reachable on the packaged listener. The typed exchanges were still proven only through in-memory `handle_http_request`. The published binary test still hand-rolled HTTP. Operators therefore could not send the typed consumer contract over the spawned TCP listener without inventing HTTP/1.1. Public bind hosts must fail closed. `localhost` is not a loopback exception. Duplicating the execute consumer-exchange builders (#381), 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 HTTP/1.1 rendering of typed execute exchanges onto the spawned `tepp-loopback` TCP listener: + +- `loopback_http1_from_naruon_exchange` renders a typed HTTPS exchange onto a bound loopback `Host`. +- `loopback_http1_from_execute_exchange` requires POST `/execute` with a naruon or `LineageWeave` consumer identity, then renders that exchange. +- The exchange keeps its HTTPS origin contract. Only `Host` is the loopback bind address printed by `tepp-loopback`. +- Public bind hosts, unparseable hosts including `localhost`, credential headers, empty methods, non-`https` targets, and non-execute exchanges fail closed before any socket is opened. +- 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. +- Another execute consumer-exchange builder, published-binary move, engine-execute library, cancel consumer parity, loopback CLI, collection CLI, or retry HTTP. + +## Alternatives considered + +1. **Keep hand-rolled HTTP in the binary test** — rejected because GAP-003A is operator-visible and the typed exchange would remain an in-memory-only contract. +2. **Add the renderer to `tepp_api`** — rejected for this slice; execute body ownership stays in `analysis_engine` and the crate cycle remains forbidden. +3. **Treat `localhost` as loopback** — rejected; `localhost` is a name, not a loopback bind address. + +## Consequences + +- Naruon and `LineageWeave` can POST `/execute` to the spawned listener from the typed exchange without embedding the library or inventing HTTP/1.1. +- HTTP 200 on execute is not release evidence. + +## Failure and recovery + +Public bind hosts return authorization denied. `localhost`, missing paths, GET status exchanges, and non-execute verbs 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. +- The published listener remains loopback-only, size-bounded, and content-redacting. +- LLM-authored recovery cannot become scientific authority. + +## Compatibility and migration + +Create, GET, running, terminal, temporal-context, project-history, and typed execute builders are unchanged. Production adapters may replace loopback while preserving metric-free receipts and engine-produced scientific acceptance. + +## Verification + +Falsifiable evidence: + +- public bind hosts and `localhost` fail closed without opening a socket; +- GET status exchanges are refused by the execute renderer; +- naruon and `LineageWeave` typed execute exchanges over spawned `tepp-loopback` TCP then GET return `tepp.scientific_acceptance.v1`; +- Clippy `-D warnings`, `analysis_engine` and `tepp_api` tests, rustdoc, and exact-head review remain required. + +## Rollback and supersession + +Rollback removes the HTTP/1.1 renderer; the typed builders 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 0034 owns typed execute consumer exchanges. +- ADR 0033 owns the published `tepp-loopback` binary. +- ADR 0032 owns engine-on-loopback execute. +- 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 2c0a7ef30..070f2f868 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -36,6 +36,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [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. | +| [0037](0037-scientific-acceptance-execute-exchange-loopback-tcp.md) | Scientific-acceptance execute exchange on loopback TCP | Accepted | active-PR | Typed naruon/`LineageWeave` execute exchanges render onto the spawned `tepp-loopback` TCP listener. 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. | @@ -147,6 +148,7 @@ Use the narrowest owning ADR when decisions overlap: - **scientific-acceptance loopback engine execute:** ADR 0032. - **scientific-acceptance published loopback binary:** ADR 0033. - **scientific-acceptance execute consumer exchange:** ADR 0034. +- **scientific-acceptance execute exchange on loopback TCP:** ADR 0037. - **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 39769838b..f3179c8db 100644 --- a/docs/connectors/naruon-artifact-consumer.md +++ b/docs/connectors/naruon-artifact-consumer.md @@ -31,6 +31,7 @@ TEPP remains the scientific authority for estimation, recovery metrics, temporal | 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 | +| Typed execute exchange on spawned `tepp-loopback` TCP | `analysis_engine` `loopback_http1_from_execute_exchange` renders the typed POST onto the packaged loopback listener | 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-exchange-loopback-tcp.md b/docs/research/scientific-acceptance-execute-exchange-loopback-tcp.md new file mode 100644 index 000000000..3763f9b69 --- /dev/null +++ b/docs/research/scientific-acceptance-execute-exchange-loopback-tcp.md @@ -0,0 +1,30 @@ +# Scientific-acceptance execute exchange on loopback TCP + +## Scope + +This note doctors the GAP-003A typed execute-exchange loopback TCP slice: + +1. `loopback_http1_from_execute_exchange` renders a typed naruon/`LineageWeave` POST `/execute` onto the spawned `tepp-loopback` TCP listener; +2. the exchange keeps its HTTPS origin; only HTTP/1.1 `Host` is the loopback bind address; +3. public bind hosts, `localhost`, credential headers, and non-execute exchanges fail closed before any socket is opened; +4. POST create then the typed execute exchange over TCP then GET returns `tepp.scientific_acceptance.v1` for both consumers. + +Postgres persistence, restart/recovery, and Compose execution remain GAP-003B. This slice is not implemented-main. It does not duplicate the execute consumer-exchange builders (#381), 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 the published interface, not an ad-hoc consumer wire and not only an in-memory handler. Wasserstein and Lazar (2016) refuse to treat a passing threshold as automatic scientific authority, so the TCP path 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 renders the typed execute exchange onto the spawned `tepp-loopback` listener, refuses public bind and `localhost`, 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 + +- public bind hosts and `localhost` fail closed without opening a socket; +- GET status exchanges are refused by the execute renderer; +- naruon and `LineageWeave` typed execute exchanges over spawned `tepp-loopback` TCP then GET return `tepp.scientific_acceptance.v1`.