diff --git a/CHANGELOG.d/interpretation-run-stored-request-get.md b/CHANGELOG.d/interpretation-run-stored-request-get.md new file mode 100644 index 000000000..2b0c143a0 --- /dev/null +++ b/CHANGELOG.d/interpretation-run-stored-request-get.md @@ -0,0 +1 @@ +- `GET /v1/interpretation-runs/{idempotency_key}/request` returns the accepted contextual-orchestrator create request on `tepp-orchestrator-loopback` (ADR 0085). Metric-free; `scientific_authority` remains false. Does not infer causality. Naruon and LineageWeave refused. `NaruonLiveService` stays POST-only. Does not re-open cancel lineages. Not GAP-010 Figma/export, not persistence. diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index e82530848..0c0c2b30e 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -16,6 +16,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin | Interpretation-run CLI doctoring | [`docs/research/interpretation-run-cli.md`](docs/research/interpretation-run-cli.md) | | Interpretation-run collection GET doctoring | [`docs/research/interpretation-run-collection-http.md`](docs/research/interpretation-run-collection-http.md) | | Interpretation-run GET-by-id doctoring | [`docs/research/interpretation-run-retrieval-http.md`](docs/research/interpretation-run-retrieval-http.md) | +| Interpretation-run stored-request GET doctoring | [`docs/research/interpretation-run-stored-request-get.md`](docs/research/interpretation-run-stored-request-get.md) | | UML/runtime/scientific flows | [`docs/UML.md`](docs/UML.md) | | Logical/physical ERD | [`docs/ERD.md`](docs/ERD.md) | | Security policy | [`SECURITY.md`](SECURITY.md) | diff --git a/crates/orchestrator_live/src/interpretation_run_stored_request_http.rs b/crates/orchestrator_live/src/interpretation_run_stored_request_http.rs new file mode 100644 index 000000000..d9caa7006 --- /dev/null +++ b/crates/orchestrator_live/src/interpretation_run_stored_request_http.rs @@ -0,0 +1,284 @@ +//! Provider-owned interpretation-run stored-request GET contracts. +//! +//! GAP-003A unique slice: `GET /v1/interpretation-runs/{idempotency_key}/request` +//! returns the accepted metric-free `InterpretationRunRequest` on +//! `OrchestratorLiveService` / `tepp-orchestrator-loopback` so operators who +//! hold a retrieval identity do not replay POST. The stored request stays +//! `scientific_authority=false`. `tepp.scientific_acceptance.v1` never +//! appears. This module does not duplicate GET-by-id (#438), retrieval CLI +//! (#439), collection GET (#433), collection CLI (#436), create CLI (#425), +//! analysis-run stored-request GET (#377), cancel lineages (closed), Leiden, +//! or GAP-010 Figma/export. Persistence remains GAP-003B. Naruon and +//! `LineageWeave` are refused. `NaruonLiveService` stays POST-only. + +use crate::error::OrchestratorLiveError; +use crate::interpretation_run_cli::CONTEXTUAL_ORCHESTRATOR_CONSUMER_CODE; +use crate::interpretation_run_retrieval_http::INTERPRETATION_RUN_RETRIEVAL_ID_MAX_LEN; +use crate::request::{host_implies_table_access, require_nonempty, INTERPRETATION_RUN_PATH}; + +const FORBIDDEN_STORED_REQUEST_KEYS: [&str; 12] = [ + "rmse", + "rmse_standard_error", + "mean_bias", + "bias_standard_error", + "interval_coverage", + "se_gate_accepted", + "scientific_acceptance", + "causal_score", + "findings", + "evidence_text", + "report", + "event_label", +]; + +/// Typed GET exchange for interpretation-run stored-request retrieval. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct InterpretationRunStoredRequestHttpExchange { + /// HTTP method, always `GET`. + pub method: &'static str, + /// Absolute HTTPS target ending in `/v1/interpretation-runs/{key}/request`. + pub target_url: String, + /// Exact version, consumer, and content headers. No credentials. + pub headers: Vec<(String, String)>, + /// GET body, always empty. + pub body: String, +} + +/// Extract the opaque idempotency key from +/// `GET /v1/interpretation-runs/{key}/request`. +/// +/// # Errors +/// +/// Returns [`OrchestratorLiveError::InvalidWirePayload`] for collection, +/// GET-by-id, extra segments, a hostile encoding, empty identity, slash, or +/// NUL, and [`OrchestratorLiveError::LimitExceeded`] when oversized. +pub fn interpretation_run_stored_request_path_id( + path: &str, +) -> Result { + let remainder = path + .strip_prefix(INTERPRETATION_RUN_PATH) + .ok_or(OrchestratorLiveError::InvalidWirePayload)?; + let encoded = remainder + .strip_prefix('/') + .ok_or(OrchestratorLiveError::InvalidWirePayload)?; + let (encoded_id, rest) = encoded + .split_once('/') + .ok_or(OrchestratorLiveError::InvalidWirePayload)?; + if rest != "request" || encoded_id.is_empty() { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + let idempotency_key = decode_path_segment(encoded_id)?; + require_nonempty(&idempotency_key)?; + if idempotency_key.contains('/') || idempotency_key.contains('\0') { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + if idempotency_key.len() > INTERPRETATION_RUN_RETRIEVAL_ID_MAX_LEN { + return Err(OrchestratorLiveError::LimitExceeded); + } + Ok(idempotency_key) +} + +/// Whether `path` is the stored-request extra-segment resource. +#[must_use] +pub fn is_interpretation_run_stored_request_path(path: &str) -> bool { + interpretation_run_stored_request_path_id(path).is_ok() +} + +/// Build a credential-free contextual-orchestrator stored-request GET exchange. +/// +/// # Errors +/// +/// Returns a fail-closed origin or identity error. +pub fn contextual_orchestrator_interpretation_run_stored_request_exchange( + origin: &str, + idempotency_key: &str, +) -> Result { + require_nonempty(origin)?; + if !origin.starts_with("https://") || origin.ends_with('/') { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + let rest = origin + .strip_prefix("https://") + .ok_or(OrchestratorLiveError::InvalidWirePayload)?; + if rest.contains('@') || rest.contains('?') || rest.contains('#') || rest.contains('\\') { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + if host_implies_table_access(rest) { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + require_nonempty(idempotency_key)?; + if idempotency_key.contains('/') || idempotency_key.contains('\0') { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + if idempotency_key.len() > INTERPRETATION_RUN_RETRIEVAL_ID_MAX_LEN { + return Err(OrchestratorLiveError::LimitExceeded); + } + let encoded_id = encode_path_segment(idempotency_key); + Ok(InterpretationRunStoredRequestHttpExchange { + method: "GET", + target_url: format!("{origin}{INTERPRETATION_RUN_PATH}/{encoded_id}/request"), + headers: vec![ + ("content-type".into(), "application/json".into()), + ( + "tepp-consumer".into(), + CONTEXTUAL_ORCHESTRATOR_CONSUMER_CODE.into(), + ), + ("tepp-contract-version".into(), "1".into()), + ], + body: String::new(), + }) +} + +/// Refuse stored-request JSON that already carries scientific-metric keys. +/// +/// Empty payloads are admitted for the GET request body. +/// +/// # Errors +/// +/// Returns [`OrchestratorLiveError::InvalidWirePayload`] when a forbidden +/// metric, evidence, or causal-score key is present. +pub fn refuse_metrics_on_interpretation_run_stored_request_payload( + payload: &str, +) -> Result<(), OrchestratorLiveError> { + if payload.trim().is_empty() { + return Ok(()); + } + if payload.contains("tepp.scientific_acceptance.v1") { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + let value: serde_json::Value = + serde_json::from_str(payload).map_err(|_| OrchestratorLiveError::InvalidWirePayload)?; + refuse_metrics_on_json(&value) +} + +fn refuse_metrics_on_json(value: &serde_json::Value) -> Result<(), OrchestratorLiveError> { + match value { + serde_json::Value::Object(object) => { + if FORBIDDEN_STORED_REQUEST_KEYS + .iter() + .any(|key| object.contains_key(*key)) + { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + for nested in object.values() { + refuse_metrics_on_json(nested)?; + } + Ok(()) + } + serde_json::Value::Array(items) => { + for nested in items { + refuse_metrics_on_json(nested)?; + } + Ok(()) + } + _ => Ok(()), + } +} + +fn encode_path_segment(value: &str) -> String { + let mut out = String::with_capacity(value.len()); + for byte in value.bytes() { + match byte { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => { + out.push(byte as char); + } + _ => { + let hex = b"0123456789ABCDEF"; + out.push('%'); + out.push(hex[usize::from(byte >> 4)] as char); + out.push(hex[usize::from(byte & 0x0F)] as char); + } + } + } + out +} + +fn decode_path_segment(value: &str) -> Result { + let mut out = Vec::with_capacity(value.len()); + let bytes = value.as_bytes(); + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'%' => { + if index + 2 >= bytes.len() { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + let hi = from_hex(bytes[index + 1])?; + let lo = from_hex(bytes[index + 2])?; + out.push((hi << 4) | lo); + index += 3; + } + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => { + out.push(bytes[index]); + index += 1; + } + _ => return Err(OrchestratorLiveError::InvalidWirePayload), + } + } + let decoded = String::from_utf8(out).map_err(|_| OrchestratorLiveError::InvalidWirePayload)?; + if decoded.chars().any(char::is_control) { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + Ok(decoded) +} + +fn from_hex(byte: u8) -> Result { + match byte { + b'0'..=b'9' => Ok(byte - b'0'), + b'A'..=b'F' => Ok(byte - b'A' + 10), + b'a'..=b'f' => Ok(byte - b'a' + 10), + _ => Err(OrchestratorLiveError::InvalidWirePayload), + } +} + +#[cfg(test)] +mod tests { + use super::{ + contextual_orchestrator_interpretation_run_stored_request_exchange, + interpretation_run_stored_request_path_id, is_interpretation_run_stored_request_path, + }; + use crate::error::OrchestratorLiveError; + + #[test] + fn stored_request_exchange_is_metric_free_get_without_credentials() { + let exchange = contextual_orchestrator_interpretation_run_stored_request_exchange( + "https://tepp.example.test", + "idem-a", + ) + .expect("exchange"); + assert_eq!(exchange.method, "GET"); + assert!(exchange + .target_url + .ends_with("/v1/interpretation-runs/idem-a/request")); + assert!(exchange.body.is_empty()); + assert!(!exchange.headers.iter().any(|(name, _)| name + .eq_ignore_ascii_case("authorization") + || name.eq_ignore_ascii_case("idempotency-key"))); + assert!(is_interpretation_run_stored_request_path( + "/v1/interpretation-runs/idem-a/request" + )); + assert!(!is_interpretation_run_stored_request_path( + "/v1/interpretation-runs/idem-a" + )); + assert_eq!( + interpretation_run_stored_request_path_id("/v1/interpretation-runs/idem-a/request") + .expect("id"), + "idem-a" + ); + assert_eq!( + interpretation_run_stored_request_path_id("/v1/interpretation-runs/idem-a"), + Err(OrchestratorLiveError::InvalidWirePayload) + ); + assert_eq!( + interpretation_run_stored_request_path_id("/v1/interpretation-runs/idem-a/cancel"), + Err(OrchestratorLiveError::InvalidWirePayload) + ); + assert_eq!( + contextual_orchestrator_interpretation_run_stored_request_exchange( + "http://tepp.example.test", + "idem-a" + ), + Err(OrchestratorLiveError::InvalidWirePayload) + ); + } +} diff --git a/crates/orchestrator_live/src/lib.rs b/crates/orchestrator_live/src/lib.rs index 260f3c2db..b9aea0646 100644 --- a/crates/orchestrator_live/src/lib.rs +++ b/crates/orchestrator_live/src/lib.rs @@ -7,6 +7,8 @@ //! hypothetical and never scientific authority. Collection GET enumerates //! metric-free identities so operators do not guess idempotency keys. //! GET-by-id returns one of those identities without POST replay. +//! `GET /v1/interpretation-runs/{idempotency_key}/request` returns the stored +//! create request without POST replay. //! Table-access hosts, review/Copilot/GitHub credentials, and //! `COPILOT_GITHUB_TOKEN` fail closed. This crate does not implement TLS //! termination or call a model provider (ADR 0010; ADR 0011). The published @@ -19,6 +21,7 @@ mod http; mod interpretation_run_cli; mod interpretation_run_collection_http; mod interpretation_run_retrieval_http; +mod interpretation_run_stored_request_http; mod mode; mod request; mod service; @@ -89,6 +92,16 @@ pub use interpretation_run_retrieval_http::interpretation_run_retrieval_path_id; pub use interpretation_run_retrieval_http::InterpretationRunRetrievalHttpExchange; /// Maximum opaque idempotency-key length on interpretation-run GET-by-id. pub use interpretation_run_retrieval_http::INTERPRETATION_RUN_RETRIEVAL_ID_MAX_LEN; +/// Build a credential-free contextual-orchestrator stored-request GET exchange. +pub use interpretation_run_stored_request_http::contextual_orchestrator_interpretation_run_stored_request_exchange; +/// Extract the opaque idempotency key from a stored-request GET path. +pub use interpretation_run_stored_request_http::interpretation_run_stored_request_path_id; +/// Whether a path is the stored-request extra-segment resource. +pub use interpretation_run_stored_request_http::is_interpretation_run_stored_request_path; +/// Refuse metric keys on stored-request JSON. +pub use interpretation_run_stored_request_http::refuse_metrics_on_interpretation_run_stored_request_payload; +/// Typed GET exchange for interpretation-run stored-request retrieval. +pub use interpretation_run_stored_request_http::InterpretationRunStoredRequestHttpExchange; /// Closed ADR 0010 orchestration-mode vocabulary. pub use mode::OrchestrationMode; /// Accepted hypothetical interpretation-run response. diff --git a/crates/orchestrator_live/src/service.rs b/crates/orchestrator_live/src/service.rs index 969fa0349..430739e96 100644 --- a/crates/orchestrator_live/src/service.rs +++ b/crates/orchestrator_live/src/service.rs @@ -1,4 +1,6 @@ //! Loopback-only live HTTP/1.1 listener for interpretation POSTs (ADR 0010/0011). +//! `GET /v1/interpretation-runs/{idempotency_key}/request` returns the stored +//! create request without POST replay. use std::collections::HashMap; use std::net::{SocketAddr, TcpListener, TcpStream}; @@ -18,6 +20,10 @@ use crate::interpretation_run_collection_http::{ use crate::interpretation_run_retrieval_http::{ interpretation_run_retrieval_item_json, interpretation_run_retrieval_path_id, }; +use crate::interpretation_run_stored_request_http::{ + interpretation_run_stored_request_path_id, is_interpretation_run_stored_request_path, + refuse_metrics_on_interpretation_run_stored_request_payload, +}; use crate::request::{ to_json, InterpretationRunAccepted, InterpretationRunRequest, INTERPRETATION_RUN_PATH, }; @@ -30,6 +36,8 @@ use crate::request::{ /// `GET /v1/interpretation-runs` enumerates accepted hypothetical runs as /// metric-free identities. `GET /v1/interpretation-runs/{idempotency_key}` /// returns one of those identities without POST replay. +/// `GET /v1/interpretation-runs/{idempotency_key}/request` returns the stored +/// create request without POST replay. #[derive(Debug)] pub struct OrchestratorLiveService { listener: Option, @@ -180,6 +188,9 @@ impl OrchestratorLiveService { if is_interpretation_run_collection_path(path) { return self.list_interpretation_runs(path, &headers, body); } + if is_interpretation_run_stored_request_path(path) { + return self.get_interpretation_run_stored_request(path, &headers, body); + } return self.get_interpretation_run(path, &headers, body); } if method != "POST" || path != INTERPRETATION_RUN_PATH { @@ -261,6 +272,27 @@ impl OrchestratorLiveService { )) } + fn get_interpretation_run_stored_request( + &self, + path: &str, + headers: &HashMap, + body: &str, + ) -> Result { + let idempotency_key = interpretation_run_stored_request_path_id(path)?; + if !body.is_empty() { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + refuse_retrieval_get_headers(headers)?; + let stored = self + .accepted_runs + .get(&idempotency_key) + .map(|(request, _)| request) + .ok_or(OrchestratorLiveError::InvalidWirePayload)?; + let payload = stored.to_json()?; + refuse_metrics_on_interpretation_run_stored_request_payload(&payload)?; + Ok(OrchestratorLiveResponse::json(200, "OK", payload)) + } + fn accept_interpretation_run( &mut self, headers: &HashMap, diff --git a/crates/orchestrator_live/tests/interpretation_run_stored_request_http_contract.rs b/crates/orchestrator_live/tests/interpretation_run_stored_request_http_contract.rs new file mode 100644 index 000000000..cf87006e2 --- /dev/null +++ b/crates/orchestrator_live/tests/interpretation_run_stored_request_http_contract.rs @@ -0,0 +1,114 @@ +//! Contract tests for contextual-orchestrator interpretation-run stored-request GET. + +use std::io::{Read, Write}; + +use orchestrator_live::{ + contextual_orchestrator_interpretation_run_stored_request_exchange, + interpretation_run_stored_request_path_id, is_interpretation_run_stored_request_path, + InterpretationRunRequest, OrchestrationMode, OrchestratorLiveError, OrchestratorLiveService, + CONTEXTUAL_ORCHESTRATOR_CONSUMER_CODE, INTERPRETATION_RUN_CONTRACT_VERSION, + INTERPRETATION_RUN_PATH, +}; + +fn sample_request() -> InterpretationRunRequest { + InterpretationRunRequest::new( + INTERPRETATION_RUN_CONTRACT_VERSION, + "orch-live-idem-001", + "orch-tenant-workspace-demo", + "tepp-snapshot-demo-001", + "2026-08-01T00:00:00Z", + OrchestrationMode::Direct, + 2048, + vec!["span-001".into()], + false, + ) + .expect("sample") +} + +fn post_http(request: &InterpretationRunRequest) -> String { + let body = request.to_json().expect("json"); + format!( + "POST {INTERPRETATION_RUN_PATH} HTTP/1.1\r\nHost: 127.0.0.1\r\ncontent-type: application/json\r\ntepp-consumer: {CONTEXTUAL_ORCHESTRATOR_CONSUMER_CODE}\r\ntepp-contract-version: 1\r\nidempotency-key: {}\r\ncontent-length: {}\r\n\r\n{body}", + request.idempotency_key(), + body.len() + ) +} + +#[test] +fn stored_request_exchange_is_metric_free_get_without_credentials() { + let exchange = contextual_orchestrator_interpretation_run_stored_request_exchange( + "https://tepp.example.test", + "orch-live-idem-001", + ) + .expect("exchange"); + assert_eq!(exchange.method, "GET"); + assert!(exchange + .target_url + .ends_with("/v1/interpretation-runs/orch-live-idem-001/request")); + assert!(exchange.body.is_empty()); + assert_eq!( + interpretation_run_stored_request_path_id( + "/v1/interpretation-runs/orch-live-idem-001/request" + ) + .expect("id"), + "orch-live-idem-001" + ); + assert!(!is_interpretation_run_stored_request_path( + "/v1/interpretation-runs/orch-live-idem-001" + )); +} + +#[test] +fn live_get_returns_stored_request_without_scientific_authority() { + let request = sample_request(); + let mut service = OrchestratorLiveService::new(); + assert_eq!( + service.handle_http_request(&post_http(&request)).status_code, + 202 + ); + let got = service.handle_http_request( + "GET /v1/interpretation-runs/orch-live-idem-001/request HTTP/1.1\r\nHost: 127.0.0.1\r\ncontent-type: application/json\r\ntepp-consumer: contextual-orchestrator\r\ntepp-contract-version: 1\r\ncontent-length: 0\r\n\r\n", + ); + assert_eq!(got.status_code, 200, "{}", got.body); + let stored = InterpretationRunRequest::from_json(&got.body).expect("stored"); + assert_eq!(stored, request); + assert!(!got.body.contains("tepp.scientific_acceptance.v1")); + assert!(!got.body.contains("rmse")); + assert_eq!( + service + .handle_http_request( + "GET /v1/interpretation-runs/orch-live-idem-001/request HTTP/1.1\r\nHost: 127.0.0.1\r\ncontent-type: application/json\r\ntepp-consumer: naruon\r\ntepp-contract-version: 1\r\ncontent-length: 0\r\n\r\n" + ) + .status_code, + 400 + ); + assert_eq!( + interpretation_run_stored_request_path_id("/v1/interpretation-runs/idem-a/cancel"), + Err(OrchestratorLiveError::InvalidWirePayload) + ); +} + +#[test] +fn stored_request_serves_over_tcp() { + let request = sample_request(); + let mut service = OrchestratorLiveService::bind_loopback().expect("bind"); + assert_eq!( + service.handle_http_request(&post_http(&request)).status_code, + 202 + ); + let addr = service.local_addr().expect("addr"); + let handle = std::thread::spawn(move || { + drop(service.serve_one()); + }); + let http = "GET /v1/interpretation-runs/orch-live-idem-001/request HTTP/1.1\r\nHost: 127.0.0.1\r\ncontent-type: application/json\r\ntepp-consumer: contextual-orchestrator\r\ntepp-contract-version: 1\r\ncontent-length: 0\r\n\r\n"; + let mut stream = std::net::TcpStream::connect(addr).expect("connect"); + stream.write_all(http.as_bytes()).expect("write"); + stream.flush().expect("flush"); + let mut bytes = Vec::new(); + stream.read_to_end(&mut bytes).expect("read"); + let text = String::from_utf8(bytes).expect("utf8"); + assert!(text.contains("HTTP/1.1 200"), "{text}"); + assert!(text.contains("orch-live-idem-001"), "{text}"); + assert!(text.contains("\"scientific_authority\":false"), "{text}"); + handle.join().expect("join"); +} diff --git a/docs/API_CONTRACT.md b/docs/API_CONTRACT.md index 3698a7377..7d5705b9e 100644 --- a/docs/API_CONTRACT.md +++ b/docs/API_CONTRACT.md @@ -65,6 +65,7 @@ GET /v1/evidence-imports/{import_id} POST /v1/interpretation-runs GET /v1/interpretation-runs GET /v1/interpretation-runs/{idempotency_key} +GET /v1/interpretation-runs/{idempotency_key}/request POST /v1/analysis-runs POST /v1/temporal-context GET /v1/analysis-runs/{run_id} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 4f128ab0f..8b28b0cbb 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -109,6 +109,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | loopback contextual-orchestrator interpretation-run CLI | ADR 0064; ADR 0010/0011; API contract; RFC 9110 | `orchestrator_live` `tepp-interpretation-runs create` CLI against `tepp-orchestrator-loopback` (`POST /v1/interpretation-runs`); metric-free hypothetical JSON; `tepp.scientific_acceptance.v1` never appears; does not infer causality; naruon and LineageWeave refused | active-PR | | loopback contextual-orchestrator interpretation-run collection GET | ADR 0069; ADR 0010/0011; API contract; RFC 9110 | `orchestrator_live` `GET /v1/interpretation-runs` on `tepp-orchestrator-loopback`; metric-free hypothetical identities; empty body; no `idempotency-key`; `tepp.scientific_acceptance.v1` never appears; does not infer causality; naruon and LineageWeave refused | active-PR | | loopback contextual-orchestrator interpretation-run GET-by-id | ADR 0071; ADR 0069; ADR 0010/0011; API contract; RFC 9110 | `orchestrator_live` `GET /v1/interpretation-runs/{idempotency_key}` on `tepp-orchestrator-loopback`; metric-free hypothetical identity without POST replay; empty body; no pagination; `tepp.scientific_acceptance.v1` never appears; does not infer causality; naruon and LineageWeave refused | active-PR | +| loopback contextual-orchestrator interpretation-run stored-request GET | ADR 0085; ADR 0071; ADR 0010/0011; API contract; RFC 9110 | `orchestrator_live` `GET /v1/interpretation-runs/{idempotency_key}/request` on `tepp-orchestrator-loopback`; returns stored create request; `scientific_authority` remains false; `tepp.scientific_acceptance.v1` never appears; does not infer causality; naruon and LineageWeave refused | active-PR | | foundation validation / release-readiness ledger | ADR 0014; Test Strategy | PR #24 `docs/validation/temporal-event-foundation.md` on protected main | implemented-main | | scientific claim promotion separated from design/implementation/release | ADR 0014; ADR policy | `validation_core` exact-head promotion gates on this PR; documentation/CI/domain validation remain; full package/image release bundle remaining | partial | | CSAP/SOC 2/ISO/NIST assurance readiness | `docs/COMPLIANCE_READINESS.md`; research register | repository controls + future deployment evidence | accepted-target / deployment-owned | diff --git a/docs/adr/0085-interpretation-run-stored-request-get.md b/docs/adr/0085-interpretation-run-stored-request-get.md new file mode 100644 index 000000000..2cb36e82d --- /dev/null +++ b/docs/adr/0085-interpretation-run-stored-request-get.md @@ -0,0 +1,57 @@ +# ADR 0085 — Loopback interpretation-run stored-request GET + +**Decision status:** Accepted +**Implementation maturity:** active-PR +**Date:** 2026-09-01 +**Supersedes:** None; complements ADR 0071. Does not re-open cancel lineages. +Does not supersede ADR 0014. Unique versus protected main; 0026–0084 occupied. + +## Context + +ADR 0071 retrieves one accepted interpretation-run identity. Operators still +had no extra-segment GET for the stored create request. Analysis-run +stored-request GET (#377) is naruon-owned. Duplicating GET-by-id (#438), +retrieval CLI (#439), collection GET/CLI, create CLI, Leiden, or GAP-010 +would collide with live PRs. + +## Decision + +Publish `GET /v1/interpretation-runs/{idempotency_key}/request` on +`OrchestratorLiveService`. Extra-segment parse. Slash/NUL fail closed. Empty +body. `scientific_authority` remains false. `tepp.scientific_acceptance.v1` +never appears. Cancel extra-segment stays refused. + +## Alternatives considered + +1. Re-open cancel HTTP — rejected. +2. Return GET-by-id identity — rejected (ADR 0071). +3. Loopback stored-request GET — accepted. + +## Consequences + +HTTP 200 is not measurement evidence and is not an ADR 0014 claim. + +## Failure and recovery + +Non-orchestrator consumers, nonempty bodies, extra segments, slash/NUL, +missing keys, and metric keys fail closed. + +## Verification + +- `GET /v1/interpretation-runs/{idempotency_key}/request` of an accepted run + returns the stored create request without RMSE/`tepp.scientific_acceptance.v1`; +- naruon, LineageWeave, GET-by-id path, extra segments, slash/NUL, nonempty + body, and missing keys fail closed; +- Clippy `-D warnings`, `orchestrator_live` tests, rustdoc, and exact-head + review remain required. + +## Rollback and supersession + +Rollback removes the extra-segment GET; POST and GET-by-id remain valid. A +superseding ADR is required to persist the registry, bind a public address, +re-open cancel, emit scientific-acceptance, open naruon or LineageWeave, add +GET to `NaruonLiveService`, or treat retrieval success as an ADR 0014 claim. + +## Related authority + +ADR 0071, ADR 0069, ADR 0014, RFC 9110 (Fielding, Nottingham, & Reschke, 2022). diff --git a/docs/adr/README.md b/docs/adr/README.md index 4002ba826..519289f43 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -33,6 +33,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0064](0064-interpretation-run-cli.md) | Loopback `tepp-interpretation-runs create` is contextual-orchestrator POST /v1/interpretation-runs client | Accepted | active-PR | Complements ADR 0010/0011; does not supersede ADR 0014. Unique on protected main. Does not infer causality. | | [0069](0069-interpretation-run-collection-get.md) | Loopback `GET /v1/interpretation-runs` enumerates accepted hypothetical interpretation runs | Accepted | active-PR | Complements ADR 0010/0011/0064; does not supersede ADR 0014. Unique on this stack versus protected main (0026–0068 occupied). Does not infer causality. | | [0071](0071-interpretation-run-retrieval-get.md) | Loopback `GET /v1/interpretation-runs/{idempotency_key}` returns one accepted hypothetical identity | Accepted | active-PR | Complements ADR 0069; does not supersede ADR 0014. Unique on this interpretation stack versus protected main (0026–0070 occupied). Does not infer causality. | +| [0085](0085-interpretation-run-stored-request-get.md) | Loopback interpretation-run stored-request GET | Accepted | active-PR | Complements ADR 0071; `GET /v1/interpretation-runs/{idempotency_key}/request` returns the stored create request. Unique versus protected main (0026–0084 occupied). Does not re-open cancel lineages. | | [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. | diff --git a/docs/research/interpretation-run-stored-request-get.md b/docs/research/interpretation-run-stored-request-get.md new file mode 100644 index 000000000..60e43a30a --- /dev/null +++ b/docs/research/interpretation-run-stored-request-get.md @@ -0,0 +1,13 @@ +# Interpretation-run stored-request GET (doctoring) + +`GET /v1/interpretation-runs/{idempotency_key}/request` returns one accepted +contextual-orchestrator create request on `tepp-orchestrator-loopback`. HTTP +semantics follow RFC 9110 (Fielding, Nottingham, & Reschke, 2022). Fail-closed +unpublished consumers, extra segments, slash/NUL, credential flags, and +scientific-authority promotion are repository contract (ADR 0085; ADR 0014). + +`scientific_authority` remains false. `tepp.scientific_acceptance.v1` never +appears. HTTP 200 is not a scientific claim. + +Does not re-open cancel lineages, GAP-010 Figma/export, persistence, Leiden, +or an ADR 0014 claim-promotion package.