-
Notifications
You must be signed in to change notification settings - Fork 0
feat(api): list accepted temporal-context identities on loopback GET #449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| - `GET /v1/temporal-context` enumerates accepted LineageWeave temporal-context identities on `tepp-loopback` (ADR 0081). Metric-free `inference_status=temporal_association_only` rows. `tepp.scientific_acceptance.v1` never appears. Does not infer causality. Naruon refused. `NaruonLiveService` stays POST-only. Not temporal-context CLI, not project-history collection GET, not GAP-010 Figma/export, not persistence. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,8 +19,12 @@ use crate::naruon_http::NARUON_ANALYSIS_RUN_PATH; | |
| use crate::{ | ||
| AnalysisRunAccepted, AnalysisRunRequest, ApiError, DEFAULT_PROJECT_HISTORY_BYTE_LIMIT, | ||
| ErrorEnvelope, NARUON_LIVE_IO_TIMEOUT, NaruonLiveResponse, PROJECT_HISTORY_PATH, | ||
| ProjectHistoryProjection, ProjectHistoryRequest, TEMPORAL_CONTEXT_PATH, TemporalContextRequest, | ||
| build_temporal_context, project_history_projection, requests_are_idempotent_matches, | ||
| ProjectHistoryProjection, ProjectHistoryRequest, TEMPORAL_CONTEXT_COLLECTION_INFERENCE_STATUS, | ||
| TEMPORAL_CONTEXT_PATH, TemporalContextCollection, TemporalContextCollectionItem, | ||
| TemporalContextRequest, build_temporal_context, is_temporal_context_collection_path, | ||
| page_temporal_context_collection_items, parse_temporal_context_collection_page_cursor, | ||
| parse_temporal_context_collection_page_limit, project_history_projection, | ||
| requests_are_idempotent_matches, | ||
| }; | ||
|
|
||
| const MAX_LIVE_REQUEST_BODY_BYTES: usize = DEFAULT_PROJECT_HISTORY_BYTE_LIMIT; | ||
|
|
@@ -41,6 +45,7 @@ pub struct AnalysisRunLiveService { | |
| next_request_serial: u64, | ||
| accepted_runs: HashMap<String, (AnalysisRunRequest, AnalysisRunAccepted)>, | ||
| accepted_project_histories: HashMap<String, (ProjectHistoryRequest, ProjectHistoryProjection)>, | ||
| accepted_temporal_contexts: HashMap<String, TemporalContextCollectionItem>, | ||
| } | ||
|
|
||
| impl Default for AnalysisRunLiveService { | ||
|
|
@@ -60,6 +65,7 @@ impl AnalysisRunLiveService { | |
| next_request_serial: 1, | ||
| accepted_runs: HashMap::new(), | ||
| accepted_project_histories: HashMap::new(), | ||
| accepted_temporal_contexts: HashMap::new(), | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -143,33 +149,92 @@ impl AnalysisRunLiveService { | |
| let (header_block, body) = split_request_with_limit(request, MAX_LIVE_REQUEST_BODY_BYTES)?; | ||
| let mut lines = header_block.split("\r\n"); | ||
| let (method, path) = parse_request_line(lines.next().unwrap_or(""))?; | ||
| let headers = parse_headers(&mut lines)?; | ||
| if method == "GET" { | ||
| return self.list_temporal_contexts(path, &headers, body); | ||
| } | ||
| if method != "POST" | ||
| || (path != NARUON_ANALYSIS_RUN_PATH | ||
| && path != TEMPORAL_CONTEXT_PATH | ||
| && path != PROJECT_HISTORY_PATH) | ||
| { | ||
| return Err(ApiError::InvalidWirePayload); | ||
| } | ||
| let headers = parse_headers(&mut lines)?; | ||
| let consumer = require_headers( | ||
| &headers, | ||
| self.bound_addr, | ||
| path == NARUON_ANALYSIS_RUN_PATH || path == PROJECT_HISTORY_PATH, | ||
| )?; | ||
| if path == TEMPORAL_CONTEXT_PATH { | ||
| if consumer != LINEAGEWEAVE_CONSUMER_CODE { | ||
| return Err(ApiError::InvalidWirePayload); | ||
| } | ||
| let context_request = TemporalContextRequest::from_json(body)?; | ||
| let response = build_temporal_context(&context_request)?; | ||
| return Ok(json_response(200, "OK", response.to_json()?)); | ||
| return self.accept_temporal_context(consumer, &headers, body); | ||
| } | ||
| if path == PROJECT_HISTORY_PATH { | ||
| return self.accept_project_history(consumer, &headers, body); | ||
| } | ||
| self.accept_analysis_run(consumer, &headers, body) | ||
| } | ||
|
|
||
| fn accept_temporal_context( | ||
| &mut self, | ||
| consumer: &str, | ||
| headers: &HashMap<String, String>, | ||
| body: &str, | ||
| ) -> Result<NaruonLiveResponse, ApiError> { | ||
| if consumer != LINEAGEWEAVE_CONSUMER_CODE { | ||
| return Err(ApiError::InvalidWirePayload); | ||
| } | ||
| let context_request = TemporalContextRequest::from_json(body)?; | ||
| if let Some(idempotency_key) = headers.get("idempotency-key") { | ||
| let item = TemporalContextCollectionItem::new( | ||
| idempotency_key.clone(), | ||
| context_request.knowledge_cutoff.clone(), | ||
| TEMPORAL_CONTEXT_COLLECTION_INFERENCE_STATUS, | ||
| )?; | ||
| let replay_key = format!("{consumer}\u{1f}{idempotency_key}"); | ||
| if let Some(stored) = self.accepted_temporal_contexts.get(&replay_key) { | ||
| if stored.knowledge_cutoff != item.knowledge_cutoff { | ||
| return Err(ApiError::InvalidWirePayload); | ||
| } | ||
|
Comment on lines
+194
to
+197
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Reused keys return different results Reusing an idempotency key with the same cutoff but different events passes Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
| } else { | ||
| self.accepted_temporal_contexts.insert(replay_key, item); | ||
| } | ||
|
Comment on lines
+198
to
+200
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Failed posts remain listed as accepted When response serialization exceeds its limit, Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
| } | ||
| let response = build_temporal_context(&context_request)?; | ||
| Ok(json_response(200, "OK", response.to_json()?)) | ||
| } | ||
|
|
||
| fn list_temporal_contexts( | ||
| &self, | ||
| path: &str, | ||
| headers: &HashMap<String, String>, | ||
| body: &str, | ||
| ) -> Result<NaruonLiveResponse, ApiError> { | ||
| if !is_temporal_context_collection_path(path) { | ||
| return Err(ApiError::InvalidWirePayload); | ||
| } | ||
| if !body.is_empty() { | ||
| return Err(ApiError::InvalidWirePayload); | ||
| } | ||
| if headers.contains_key("idempotency-key") { | ||
| return Err(ApiError::InvalidWirePayload); | ||
| } | ||
| let consumer = require_headers(headers, self.bound_addr, false)?; | ||
| if consumer != LINEAGEWEAVE_CONSUMER_CODE { | ||
| return Err(ApiError::InvalidWirePayload); | ||
| } | ||
| let limit = parse_temporal_context_collection_page_limit( | ||
| headers.get("tepp-page-limit").map(String::as_str), | ||
| )?; | ||
| let cursor = parse_temporal_context_collection_page_cursor( | ||
| headers.get("tepp-page-cursor").map(String::as_str), | ||
| )?; | ||
| let items = self.accepted_temporal_contexts.values().cloned().collect(); | ||
| let (page, next_cursor) = | ||
| page_temporal_context_collection_items(items, cursor.as_deref(), limit); | ||
| let collection = TemporalContextCollection::new(page, next_cursor)?; | ||
| Ok(json_response(200, "OK", collection.to_json()?)) | ||
| } | ||
|
|
||
| fn accept_analysis_run( | ||
| &mut self, | ||
| consumer: &str, | ||
|
|
@@ -320,6 +385,7 @@ mod tests { | |
| DEFAULT_ANALYSIS_RUN_BYTE_LIMIT, ErrorEnvelope, LINEAGEWEAVE_CONSUMER_CODE, | ||
| NARUON_ANALYSIS_RUN_PATH, NARUON_CONSUMER_CODE, NARUON_LIVE_HEADER_BYTE_LIMIT, | ||
| NARUON_LIVE_HEADER_COUNT_LIMIT, NARUON_LIVE_IO_TIMEOUT, TEMPORAL_CONTEXT_PATH, | ||
| TemporalContextCollection, | ||
| }; | ||
|
|
||
| fn sample_run() -> AnalysisRunRequest { | ||
|
|
@@ -734,6 +800,69 @@ mod tests { | |
| assert_eq!(replay.body, accepted.body); | ||
| } | ||
|
|
||
| #[test] | ||
| fn temporal_context_collection_get_is_metric_free_and_fail_closed() { | ||
| let temporal_body = r#"{"contract_version":1,"consumer_code":"lineageweave","knowledge_cutoff":"2026-08-20T00:00:00Z","subject_post_id":null,"events":[{"event_id":"event-1","source_post_id":"post-1","event_type_code":"order_awarded","event_label":"Order awarded","event_time":"2026-08-01T09:00:00Z","available_time":"2026-08-01T10:00:00Z","project_reference":null,"actor_references":["actor-1"]}]}"#; | ||
| let mut service = AnalysisRunLiveService::new(); | ||
| let posted = format!( | ||
| "POST {TEMPORAL_CONTEXT_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: idem-a\r\ncontent-length: {}\r\n\r\n{temporal_body}", | ||
| temporal_body.len() | ||
| ); | ||
| assert_eq!(service.handle_http_request(&posted).status_code, 200); | ||
| let listed = service.handle_http_request( | ||
| &format!( | ||
| "GET {TEMPORAL_CONTEXT_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\ncontent-length: 0\r\n\r\n" | ||
| ), | ||
| ); | ||
| assert_eq!(listed.status_code, 200, "{}", listed.body); | ||
| assert!(!listed.body.contains("rmse")); | ||
| assert!(!listed.body.contains("event_label")); | ||
| assert!(!listed.body.contains("actor_references")); | ||
| assert!(!listed.body.contains("tepp.scientific_acceptance.v1")); | ||
| let page = TemporalContextCollection::from_json(&listed.body).expect("page"); | ||
| assert_eq!(page.contexts.len(), 1); | ||
| assert_eq!(page.contexts[0].idempotency_key, "idem-a"); | ||
| assert_eq!(page.contexts[0].inference_status, "temporal_association_only"); | ||
| assert_eq!( | ||
| service | ||
| .handle_http_request( | ||
| "GET /v1/analysis-runs HTTP/1.1\r\nHost: 127.0.0.1\r\ncontent-type: application/json\r\ntepp-consumer: lineageweave\r\ntepp-contract-version: 1\r\ncontent-length: 0\r\n\r\n" | ||
| ) | ||
| .status_code, | ||
| 400 | ||
| ); | ||
| assert_eq!( | ||
| service | ||
| .handle_http_request( | ||
| &format!( | ||
| "GET {TEMPORAL_CONTEXT_PATH} HTTP/1.1\r\nHost: 127.0.0.1\r\ncontent-type: application/json\r\ntepp-consumer: {NARUON_CONSUMER_CODE}\r\ntepp-contract-version: 1\r\ncontent-length: 0\r\n\r\n" | ||
| ) | ||
| ) | ||
| .status_code, | ||
| 400 | ||
| ); | ||
| assert_eq!( | ||
| service | ||
| .handle_http_request( | ||
| &format!( | ||
| "GET {TEMPORAL_CONTEXT_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: idem-a\r\ncontent-length: 0\r\n\r\n" | ||
| ) | ||
| ) | ||
| .status_code, | ||
| 400 | ||
| ); | ||
| assert_eq!( | ||
| service | ||
| .handle_http_request( | ||
| &format!( | ||
| "GET {TEMPORAL_CONTEXT_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\ncontent-length: 2\r\n\r\n{{}}" | ||
| ) | ||
| ) | ||
| .status_code, | ||
| 400 | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn parser_helpers_cover_framing_header_and_limit_edges() { | ||
| assert_eq!( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 Registry growth lacks a bound
Each distinct POST permanently grows
accepted_temporal_contexts. Add an entry limit or eviction policy before using this loopback service as a long-running process.Was this helpful? React with 👍 or 👎 to provide feedback.