feat(api): record analysis-run lifecycle from LineageWeave and Naruon compatibility - #388
Conversation
… compatibility Add LineageWeave running/terminal exchange builders, NaruonLiveService lifecycle POST, and a tepp-loopback TCP proof stacked on the ADR 0028 lifecycle path. NaruonLiveService stays POST-only and Naruon-only.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| let _ = status_http_json( | ||
| &status, | ||
| &stored.request, | ||
| transition.scientific_acceptance_json.as_deref(), | ||
| )?; |
There was a problem hiding this comment.
🔴 Mismatched artifacts attach to runs
When status_http_json validates a succeeded transition, it never binds the artifact's run ID to the accepted run. Naruon can receive another run's artifact as its result.
Prompt for agents
Strengthen the shared scientific-acceptance validation used by NaruonLiveService and AnalysisRunLiveService. In crates/tepp_api/src/scientific_acceptance_http.rs, parse and require the artifact's run_id and compare it with the accepted/status/terminal run identity before storing or returning the artifact. Add lifecycle tests for both listeners showing that a missing or mismatched artifact run_id is rejected, while a matching run_id remains accepted.
Was this helpful? React with 👍 or 👎 to provide feedback.
| fn commit_lifecycle_transition( | ||
| &mut self, | ||
| path_run_id: &str, | ||
| headers: &HashMap<String, String>, | ||
| transition: &AnalysisRunLifecycleTransition, | ||
| ) -> Result<NaruonLiveResponse, ApiError> { | ||
| if transition.run_id != path_run_id { | ||
| return Err(ApiError::InvalidWirePayload); | ||
| } | ||
| let idempotency_key = header_value(headers, "idempotency-key")?; | ||
| if idempotency_key != transition.idempotency_key { | ||
| return Err(ApiError::InvalidWirePayload); | ||
| } | ||
| let replay_key = self | ||
| .runs_by_id | ||
| .get(path_run_id) | ||
| .cloned() | ||
| .ok_or(ApiError::InvalidWirePayload)?; | ||
| let stored = self | ||
| .accepted_runs | ||
| .get(&replay_key) | ||
| .ok_or(ApiError::InvalidWirePayload)?; | ||
| if stored.accepted.idempotency_key != idempotency_key { | ||
| return Err(ApiError::InvalidWirePayload); | ||
| } | ||
| let status = match transition.run_state { | ||
| AnalysisRunStatusState::Running => AnalysisRunStatus::running(&stored.accepted)?, | ||
| AnalysisRunStatusState::Succeeded | AnalysisRunStatusState::Failed => { | ||
| let result = transition | ||
| .terminal_result | ||
| .clone() | ||
| .ok_or(ApiError::InvalidWirePayload)?; | ||
| AnalysisRunStatus::terminal(&stored.request, &stored.accepted, result)? | ||
| } | ||
| AnalysisRunStatusState::Accepted => return Err(ApiError::InvalidWirePayload), | ||
| }; | ||
| if stored.status == status | ||
| && stored.scientific_acceptance_json == transition.scientific_acceptance_json | ||
| { | ||
| let response_body = status_http_json( | ||
| &stored.status, | ||
| &stored.request, | ||
| stored.scientific_acceptance_json.as_deref(), | ||
| )?; | ||
| return Ok(NaruonLiveResponse::json(200, "OK", response_body)); | ||
| } | ||
| match stored.status.run_state { | ||
| AnalysisRunStatusState::Accepted => {} | ||
| AnalysisRunStatusState::Running | ||
| if matches!( | ||
| transition.run_state, | ||
| AnalysisRunStatusState::Succeeded | AnalysisRunStatusState::Failed | ||
| ) => {} | ||
| AnalysisRunStatusState::Running | ||
| | AnalysisRunStatusState::Succeeded | ||
| | AnalysisRunStatusState::Failed => { | ||
| return Err(ApiError::InvalidWirePayload); | ||
| } | ||
| } | ||
| let stored = self | ||
| .accepted_runs | ||
| .get_mut(&replay_key) | ||
| .ok_or(ApiError::InvalidWirePayload)?; | ||
| require_status_binding(&stored.request, &stored.accepted, &status)?; | ||
| let _ = status_http_json( | ||
| &status, | ||
| &stored.request, | ||
| transition.scientific_acceptance_json.as_deref(), | ||
| )?; | ||
| stored.status = status; | ||
| stored | ||
| .scientific_acceptance_json | ||
| .clone_from(&transition.scientific_acceptance_json); | ||
| let response_body = status_http_json( | ||
| &stored.status, | ||
| &stored.request, | ||
| stored.scientific_acceptance_json.as_deref(), | ||
| )?; | ||
| Ok(NaruonLiveResponse::json(200, "OK", response_body)) |
There was a problem hiding this comment.
🔍 Lifecycle implementations can drift
commit_lifecycle_transition duplicates the shared listener's transition table, replay rules, validation, mutation, and serialization. Future lifecycle changes now require synchronized edits in both implementations.
Was this helpful? React with 👍 or 👎 to provide feedback.
| let accepted = | ||
| AnalysisRunAccepted::new(run_id, "accepted", request.idempotency_key.clone())?; | ||
| let body = accepted.to_json()?; | ||
| self.accepted_runs.insert(replay_key, (request, accepted)); | ||
| let status = AnalysisRunStatus::accepted(&accepted)?; | ||
| self.runs_by_id | ||
| .insert(accepted.run_id.clone(), replay_key.clone()); | ||
| self.accepted_runs.insert( |
There was a problem hiding this comment.
🔍 Collision guards remain inconsistent
The compatibility listener overwrites runs_by_id without the shared listener's collision check. Current private counters prevent normal collisions, but future restoration or persistence can invalidate that assumption.
(Refers to this code)
Was this helpful? React with 👍 or 👎 to provide feedback.
Consolidation decision — closed with retired lifecycle base
This PR is not merged. Exact head
2cf1441d0cd369c377274afb9a2e295f9d2df821is a consumer-parity lifecycle slice stacked on now-closed #360. Its generic exchange/idempotency/refusal work is useful, but the inherited terminal artifact contract is retired and this one-operation slice should not survive as independent architecture authority.Fold into the coherent Analysis Run application/adapter vehicle under #166/#435 and preserve: LineageWeave/Naruon consumer separation, accepted→running→terminal monotonicity, idempotent matching transitions, reverse/terminal mutation refusal, compatibility-listener restrictions, loopback TCP proof, and metric-free nonterminal receipts. Rebuild terminal evidence around the corrected Validation bounded context.
Branch/tests/reviews remain available as fold lineage.