Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.d/analysis-run-status-consumer-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `tepp_api` adds `lineageweave_analysis_run_status_exchange` and a `tepp-loopback` TCP GET-status proof (ADR 0028). Metric-free accepted/running GET is unchanged from ADR 0027. `NaruonLiveService` stays POST-only. Not lifecycle POST, not cancel, not an ADR 0014 claim.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang

## [Unreleased]

- `tepp_api` adds `lineageweave_analysis_run_status_exchange` and a `tepp-loopback` TCP GET-status proof (ADR 0028). Metric-free accepted/running GET is unchanged from ADR 0027. `NaruonLiveService` stays POST-only. Not lifecycle POST, not cancel, not an ADR 0014 claim.

- `tepp_api` loopback `AnalysisRunLiveService` now serves `GET /v1/analysis-runs/{run_id}` so accepted/running statuses stay metric-free and only a succeeded status with profile `scientific_acceptance_v1` may return `tepp.scientific_acceptance.v1`. Receipt RMSE/bias/coverage/SE-gate keys, a GET body, failed-plus-artifact emission, an all-zero digest, and digest mismatch fail closed. This is the GAP-003A HTTP status slice for issue #166; it does not duplicate the `analysis_engine` library bind (#356) or the terminal-result DTO wire (#358); persistence remains GAP-003B.

- `event_core` adds bounded Allen interval-consistency classification, atomic path-consistency closure, contradiction/resource refusals, and an explicit dependency-error fallback without claiming unrestricted global satisfiability.
Expand Down
2 changes: 2 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin
| Architecture | [`ARCHITECTURE.md`](ARCHITECTURE.md) |
| Modular/API integration contract | [`docs/API_CONTRACT.md`](docs/API_CONTRACT.md) |
| naruon modular consumer contract | [`docs/connectors/naruon-artifact-consumer.md`](docs/connectors/naruon-artifact-consumer.md) |
| Analysis-run status GET doctoring | [`docs/research/scientific-acceptance-http-status.md`](docs/research/scientific-acceptance-http-status.md) |
| Analysis-run status consumer-parity doctoring | [`docs/research/analysis-run-status-consumer-parity.md`](docs/research/analysis-run-status-consumer-parity.md) |
| contextual-orchestrator interpretation port | [`docs/connectors/contextual-orchestrator-interpretation-port.md`](docs/connectors/contextual-orchestrator-interpretation-port.md) |
| Orchestrator live HTTP doctoring | [`docs/research/orchestrator-live-http.md`](docs/research/orchestrator-live-http.md) |
| UML/runtime/scientific flows | [`docs/UML.md`](docs/UML.md) |
Expand Down
2 changes: 2 additions & 0 deletions crates/tepp_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ pub use lineageweave_http::LINEAGEWEAVE_CONSUMER_CODE;
pub use lineageweave_http::NARUON_CONSUMER_CODE;
/// Build a `LineageWeave` analysis-run exchange without provider credentials.
pub use lineageweave_http::lineageweave_analysis_run_exchange;
/// Build a `LineageWeave` analysis-run status GET without credentials.
pub use lineageweave_http::lineageweave_analysis_run_status_exchange;
/// Build a `LineageWeave` project-history exchange without provider credentials.
pub use lineageweave_http::lineageweave_project_history_exchange;
/// Build a credential-free `LineageWeave` temporal-context exchange.
Expand Down
62 changes: 60 additions & 2 deletions crates/tepp_api/src/lineageweave_http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::project_history::build_project_history_exchange;
use crate::{
AnalysisRunRequest, ApiError, NaruonHttpExchange, ProjectHistoryHttpExchange,
ProjectHistoryRequest, TEMPORAL_CONTEXT_CONTRACT_VERSION, TEMPORAL_CONTEXT_PATH,
TemporalContextRequest, naruon_analysis_run_exchange,
TemporalContextRequest, naruon_analysis_run_exchange, naruon_analysis_run_status_exchange,
};

/// Stable consumer identity used by the Naruon adapter.
Expand Down Expand Up @@ -38,6 +38,30 @@ pub fn lineageweave_analysis_run_exchange(
Ok(exchange)
}

/// Build a `LineageWeave` → TEPP analysis-run status GET without credentials.
///
/// The function reuses TEPP's existing origin, path, and header validation,
/// then replaces only the published modular-consumer identity. Status remains
/// a metric-free read except on a succeeded scientific-acceptance profile.
///
/// # Errors
///
/// Returns the same fail-closed errors as [`naruon_analysis_run_status_exchange`].
pub fn lineageweave_analysis_run_status_exchange(
origin: &str,
run_id: &str,
idempotency_key: &str,
) -> Result<NaruonHttpExchange, ApiError> {
let mut exchange = naruon_analysis_run_status_exchange(origin, run_id, idempotency_key)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Empty status keys produce unusable requests

When the idempotency key is empty, lineageweave_analysis_run_status_exchange still returns a successful exchange. The listener rejects that generated request, so callers cannot poll the run.

Suggested change
let mut exchange = naruon_analysis_run_status_exchange(origin, run_id, idempotency_key)?;
crate::wire::require_nonempty(idempotency_key)?;
let mut exchange = naruon_analysis_run_status_exchange(origin, run_id, idempotency_key)?;
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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)
}

/// Build a credential-free `LineageWeave` temporal-context exchange.
///
/// # Errors
Expand Down Expand Up @@ -94,7 +118,7 @@ pub(crate) fn consumer_is_supported(consumer_code: &str) -> bool {
mod tests {
use super::{
LINEAGEWEAVE_CONSUMER_CODE, NARUON_CONSUMER_CODE, consumer_is_supported,
lineageweave_analysis_run_exchange,
lineageweave_analysis_run_exchange, lineageweave_analysis_run_status_exchange,
};
use crate::{ANALYSIS_RUN_CONTRACT_VERSION, AnalysisRunRequest, ApiError};

Expand Down Expand Up @@ -132,4 +156,38 @@ mod tests {
Err(ApiError::InvalidWirePayload)
);
}

#[test]
fn lineageweave_status_exchange_swaps_only_the_consumer_header() {
let exchange = lineageweave_analysis_run_status_exchange(
"https://tepp.example.test",
"tepp-run-1",
"idem-1",
)
.expect("exchange");
assert_eq!(exchange.method, "GET");
assert_eq!(
exchange.target_url,
"https://tepp.example.test/v1/analysis-runs/tepp-run-1"
);
assert!(exchange.body.is_empty());
assert!(
exchange
.headers
.contains(&("tepp-consumer".into(), LINEAGEWEAVE_CONSUMER_CODE.into()))
);
assert!(
!exchange
.headers
.contains(&("tepp-consumer".into(), NARUON_CONSUMER_CODE.into()))
);
assert_eq!(
lineageweave_analysis_run_status_exchange(
"http://tepp.example.test",
"tepp-run-1",
"k"
),
Err(ApiError::InvalidWirePayload)
);
}
}
42 changes: 42 additions & 0 deletions crates/tepp_api/tests/lineageweave_http_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use tepp_api::{
ANALYSIS_RUN_CONTRACT_VERSION, AnalysisRunAccepted, AnalysisRunLiveService, AnalysisRunRequest,
ApiError, LINEAGEWEAVE_CONSUMER_CODE, NARUON_ANALYSIS_RUN_PATH, NARUON_CONSUMER_CODE,
NARUON_LIVE_HEADER_BYTE_LIMIT, lineageweave_analysis_run_exchange,
lineageweave_analysis_run_status_exchange,
};

fn sample_run() -> AnalysisRunRequest {
Expand Down Expand Up @@ -68,6 +69,37 @@ fn lineageweave_exchange_uses_the_published_consumer_header_without_credentials(
}));
}

#[test]
fn lineageweave_status_exchange_gets_the_published_consumer_without_credentials() {
let exchange = lineageweave_analysis_run_status_exchange(
"https://tepp.example.test",
"tepp-run-9",
"shared-idempotency-key",
)
.expect("lineageweave status");
assert_eq!(exchange.method, "GET");
assert_eq!(
exchange.target_url,
"https://tepp.example.test/v1/analysis-runs/tepp-run-9"
);
assert!(exchange.body.is_empty());
assert!(
exchange
.headers
.contains(&("tepp-consumer".into(), LINEAGEWEAVE_CONSUMER_CODE.into()))
);
assert!(exchange.headers.iter().all(|(name, _)| {
!matches!(
name.to_ascii_lowercase().as_str(),
"authorization" | "proxy-authorization" | "cookie" | "x-api-key"
)
}));
assert_eq!(
lineageweave_analysis_run_status_exchange("http://tepp.example.test", "tepp-run-9", "k"),
Err(ApiError::InvalidWirePayload)
);
}

#[test]
fn live_listener_accepts_lineageweave_and_isolates_consumer_idempotency() {
let loopback = AnalysisRunLiveService::bind_loopback().expect("loopback bind");
Expand Down Expand Up @@ -108,6 +140,16 @@ fn live_listener_accepts_lineageweave_and_isolates_consumer_idempotency() {
let conflict_response =
service.handle_http_request(&http_request(LINEAGEWEAVE_CONSUMER_CODE, &conflict));
assert_eq!(conflict_response.status_code, 400);

let status = service.handle_http_request(&format!(
"GET {NARUON_ANALYSIS_RUN_PATH}/{} HTTP/1.1\r\nHost: 127.0.0.1\r\ncontent-type: application/json\r\ntepp-consumer: {LINEAGEWEAVE_CONSUMER_CODE}\r\ntepp-contract-version: 1\r\nidempotency-key: {}\r\ncontent-length: 0\r\n\r\n",
lineageweave_accepted.run_id,
run.idempotency_key
));
assert_eq!(status.status_code, 200);
assert!(status.body.contains("\"accepted\""));
assert!(!status.body.contains("rmse"));
assert!(!status.body.contains("scientific_acceptance"));
}

#[test]
Expand Down
41 changes: 41 additions & 0 deletions crates/tepp_api/tests/loopback_binary_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,44 @@ fn binary_serves_one_bounded_temporal_context_request() {
assert!(response.contains("association_not_causal"));
assert!(child.wait().expect("wait").success());
}

#[test]
fn binary_reads_an_accepted_analysis_run_over_tcp() {
let mut child = Command::new(env!("CARGO_BIN_EXE_tepp-loopback"))
.args(["127.0.0.1:0", "2"])
.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");
let host = address.trim();
let body = r#"{"contract_version":1,"idempotency_key":"loopback-status-idem","tenant_workspace_id":"loopback-status-tenant","snapshot_id":"loopback-status-snapshot","knowledge_cutoff":"2026-08-01T00:00:00Z","model_contract_version":"tepp-analysis-run-v1","output_profile":"calibrated_event_measurement"}"#;
let create = format!(
"POST /v1/analysis-runs HTTP/1.1\r\nHost: {host}\r\ncontent-type: application/json\r\ntepp-consumer: lineageweave\r\ntepp-contract-version: 1\r\nidempotency-key: loopback-status-idem\r\ncontent-length: {}\r\n\r\n{body}",
body.len()
);
let mut stream = TcpStream::connect(host).expect("connect create");
stream.write_all(create.as_bytes()).expect("create");
let mut created = String::new();
stream.read_to_string(&mut created).expect("created");
assert!(created.starts_with("HTTP/1.1 202 Accepted"));
let json_start = created.find("{\"contract_version\"").expect("json");
let accepted: serde_json::Value =
serde_json::from_str(&created[json_start..]).expect("accepted json");
let run_id = accepted["run_id"].as_str().expect("run_id");
assert!(!created[json_start..].contains("rmse"));
let get = format!(
"GET /v1/analysis-runs/{run_id} HTTP/1.1\r\nHost: {host}\r\ncontent-type: application/json\r\ntepp-consumer: lineageweave\r\ntepp-contract-version: 1\r\nidempotency-key: loopback-status-idem\r\ncontent-length: 0\r\n\r\n"
);
let mut stream = TcpStream::connect(host).expect("connect status");
stream.write_all(get.as_bytes()).expect("status");
let mut status = String::new();
stream.read_to_string(&mut status).expect("status body");
assert!(status.starts_with("HTTP/1.1 200 OK"));
assert!(status.contains("\"run_state\":\"accepted\""));
assert!(!status.contains("rmse"));
assert!(!status.contains("scientific_acceptance"));
assert!(child.wait().expect("wait").success());
}
2 changes: 1 addition & 1 deletion docs/API_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

TEPP must work both as a standalone product and as a modular CWL component. Integrations with `naruon`, `contextual-orchestrator`, `.github`, or other repositories use explicit versioned API/artifact contracts. Cross-service direct table access is prohibited.

Current protected main exposes Rust library/domain contracts. The active stack adds a loopback HTTP/1.1 listener for naruon analysis-run, LineageWeave temporal-context, and export POSTs, including `POST /v1/project-histories` on the `AnalysisRunLiveService` contract boundary. `tepp-loopback` runs the shared consumer listener on `127.0.0.1:18081` by default; a caller may pass another loopback socket address and an optional maximum request count as its two arguments. The container is intended for a trusted same-host or shared-network-namespace sidecar, checks readiness through a synthetic bounded temporal-context request, and deliberately cannot bind a public or bridge address. It is not a production TLS/`$PORT` service. Endpoint examples below that are not covered by `NaruonLiveService` or `AnalysisRunLiveService` remain target interface shapes; export retrieval stays a target shape until an executable export route ships.
Current protected main exposes Rust library/domain contracts. The active stack adds a loopback HTTP/1.1 listener for naruon analysis-run, LineageWeave temporal-context, and export POSTs, including `POST /v1/project-histories` on the `AnalysisRunLiveService` contract boundary and `GET /v1/analysis-runs/{run_id}` for metric-free status. `lineageweave_analysis_run_status_exchange` is the published LineageWeave status builder. `NaruonLiveService` stays POST-only. `tepp-loopback` runs the shared consumer listener on `127.0.0.1:18081` by default; a caller may pass another loopback socket address and an optional maximum request count as its two arguments. The container is intended for a trusted same-host or shared-network-namespace sidecar, checks readiness through a synthetic bounded temporal-context request, and deliberately cannot bind a public or bridge address. It is not a production TLS/`$PORT` service. Endpoint examples below that are not covered by `NaruonLiveService` or `AnalysisRunLiveService` remain target interface shapes; export retrieval stays a target shape until an executable export route ships.

## 2. Contract families

Expand Down
1 change: 1 addition & 0 deletions docs/TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ The full APA 7th standards/literature register remains `docs/research/standards-
| known-truth temporal/event simulation manifests | PRD; TRD; Test Strategy | `tepp_simulation` on protected main; recovery metrics in `validation_core` | implemented-main |
| versioned service/API contracts and exports | PRD; API contract; ADR 0011/0013 | `tepp_api` analysis-run/export/JSON-LD/GraphML contracts on protected main (PR #21); request-bound terminal result active in PR #157; HTTP service remains accepted-target; the `orchestrator_live` loopback interpretation listener is on this PR | partial |
| loopback analysis-run scientific-acceptance GET | ADR 0027; API contract; RFC 9110; FIPS 180-4 | `tepp_api` `GET /v1/analysis-runs/{run_id}` on `AnalysisRunLiveService` (this PR): accepted/running stay metric-free; `tepp.scientific_acceptance.v1` only on succeeded `scientific_acceptance_v1`; not implemented-main | active-PR |
| analysis-run status consumer parity | ADR 0028; ADR 0027; RFC 9110 | `lineageweave_analysis_run_status_exchange` and `tepp-loopback` TCP GET proof; `NaruonLiveService` stays POST-only | 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 |
Expand Down
73 changes: 73 additions & 0 deletions docs/adr/0028-analysis-run-status-consumer-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# ADR 0028 — Analysis-run status consumer parity

**Decision status:** Accepted
**Implementation maturity:** active-PR
**Date:** 2026-08-31
**Supersedes:** None; complements ADR 0027 and ADR 0018. Does not supersede ADR 0014. ADR 0026 remains on other GAP-003A slices. This ADR number is unique on the GET-status lineage; other live PRs may reuse 0028 on unrelated stacks.

## Context

ADR 0027 added `GET /v1/analysis-runs/{run_id}` on `AnalysisRunLiveService` and a Naruon status-exchange builder. `LineageWeave` had a create-exchange builder but no status-exchange, so a published consumer would have to mint a Naruon-labelled GET. The packaged `tepp-loopback` binary had no TCP proof that status GET works on the shared listener.

Duplicating the GET listener, lifecycle POST, cancel HTTP, collection GET, retry, or engine-library slices would not close this consumer-parity gap. Opening GET on `NaruonLiveService` would violate that listener's POST-only Naruon compatibility contract.

## Decision

- `lineageweave_analysis_run_status_exchange` reuses the Naruon status builder and replaces only `tepp-consumer`.
- `AnalysisRunLiveService` remains the shared GET listener. LineageWeave consumers poll their own runs there; Naruon runs stay isolated.
- `NaruonLiveService` stays POST-only. It does not serve GET status.
- `tepp-loopback` proves create-then-GET over loopback TCP with the LineageWeave consumer.
- Accepted GET bodies stay metric-free. Scientific-acceptance attachment remains the ADR 0027 succeeded-profile gate.

## Non-goals

- A second GET listener, running/terminal POST, collection GET, retry, persistence, or production TLS.
- Opening `NaruonLiveService` to GET or to LineageWeave.
- An ADR 0014 scientific claim.

## Alternatives considered

1. **Leave status GET only on the Naruon builder** — rejected because a published LineageWeave consumer would have to mint a Naruon-labelled GET.
2. **Admit GET on `NaruonLiveService`** — rejected because that listener is Naruon-only POST (ADR 0011/0018).
3. **Mint a second status DTO** — rejected as a duplicate of ADR 0027.
4. **Consumer-parity status GET on the existing typed request** — accepted.

## Consequences

- Both published consumers can build a credential-free status GET exchange.
- Operators can observe LineageWeave status through `tepp-loopback` without a second HTTP stack.
- Naruon compatibility remains POST-only.

## Failure and recovery

Non-`https` origins, empty or oversized run identities, credential headers, and consumer/idempotency mismatch fail closed. The in-memory registry is not durable. HTTP `200` on an accepted GET is not an ADR 0014 claim.

## Security, privacy, scientific-integrity, and governance impact

- No credential headers cross the consumer boundary.
- GET remains loopback-only, size-bounded, and content-redacting.
- Metric-free accepted/running status is unchanged from ADR 0027.

## Compatibility and migration

The existing POST analysis-run, temporal-context, project-history, and GET status paths are unchanged. Production adapters may replace loopback while preserving the LineageWeave consumer header on status GET.

## Verification

Falsifiable evidence:

- LineageWeave status exchange sets only the published consumer header and uses GET;
- `tepp-loopback` create-then-GET over TCP returns `200` accepted without RMSE/scientific-acceptance keys;
- LineageWeave GET of its own run succeeds; Naruon GET of that run fails closed;
- Clippy `-D warnings`, `tepp_api` tests, rustdoc, and exact-head review remain required.

## Rollback and supersession

Rollback removes the LineageWeave status builder and the TCP GET proof; the ADR 0027 GET listener remains 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 0027 owns the loopback GET listener and Naruon status builder.
- ADR 0018 owns consumer-scoped ingress.
- ADR 0011 owns standalone/modular HTTP boundaries.
- ADR 0014 owns scientific claim promotion.
Loading
Loading