Skip to content

feat(api): record analysis-run lifecycle from LineageWeave and Naruon compatibility - #388

Closed
seonghobae wants to merge 1 commit into
feat/scientific-acceptance-http-lifecycle-gap-003afrom
feat/analysis-run-lifecycle-consumer-parity-gap-003a
Closed

feat(api): record analysis-run lifecycle from LineageWeave and Naruon compatibility#388
seonghobae wants to merge 1 commit into
feat/scientific-acceptance-http-lifecycle-gap-003afrom
feat/analysis-run-lifecycle-consumer-parity-gap-003a

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Consolidation decision — closed with retired lifecycle base

This PR is not merged. Exact head 2cf1441d0cd369c377274afb9a2e295f9d2df821 is 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.

… 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.
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3bb39aaf-31fa-42e7-9400-d87ef9cf85ba

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

Devin Review

Comment on lines +389 to +393
let _ = status_http_json(
&status,
&stored.request,
transition.scientific_acceptance_json.as_deref(),
)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 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.
Devin Review

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

Comment on lines +325 to +403
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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 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.

Devin Review

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

Comment on lines 277 to +283
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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Devin Review

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant