Skip to content
Draft
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
497 changes: 468 additions & 29 deletions crates/tracedecay-application/src/feedback/concrete.rs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -954,7 +954,8 @@ async fn incomplete_publication_remains_readable_without_consuming_completed_ded
.expect("repeated incomplete publication");
assert_eq!(
store.record_publication(&context, &repeated).await,
FeedbackPublicationRecordState::Recorded
FeedbackPublicationRecordState::Reused(publication.clone()),
"a repeated incomplete cycle must return its actual committed identity"
);
assert_eq!(
store
Expand All @@ -963,6 +964,72 @@ async fn incomplete_publication_remains_readable_without_consuming_completed_ded
FeedbackCycleDedupeState::Unique,
"repeated incomplete publications must not consume completed dedupe"
);

// Run the real service again, not just the store port. A new local
// evaluation must reuse the committed finding identity and expose its
// read handles without converting incomplete remote coverage into Clean.
let mut repeated_request = cycle_request(digest('a'), observed_at);
repeated_request.input.request.cycle_id = repeated_input.request.cycle_id.clone();
let repeated_service = feedback_service(runtime.clone(), &repeated_request);
let mut repeated_providers = complete_advisory_providers();
repeated_providers[1].state = ProviderEvaluationStateV1::Unavailable;
let repeated_execution = repeated_service
.execute_with_advisory(
&context,
repeated_request,
FeedbackCycleAdvisoryV1 {
providers: repeated_providers,
findings: vec![github_finding()],
},
)
.await
.expect("replayed incomplete cycle");
assert_eq!(
repeated_execution.cycle.termination,
tracedecay_domain::feedback::FeedbackCycleTerminationV1::IncompleteCoverage
);
assert_eq!(repeated_execution.publication, Some(publication.clone()));
assert_eq!(
repeated_execution.cycle.result_id,
publication.result.result_id
);
assert_ne!(repeated_input.request.cycle_id, publication.result.cycle_id);
let replayed_canonical = compose_canonical_result(
&runtime,
repeated_execution,
tracedecay_domain::feedback::FeedbackDurabilityV1::Durable,
)
.expect("reused incomplete result handles");
assert_eq!(replayed_canonical.finding_handles.len(), 1);
let replayed_handles = replayed_canonical
.read_handles
.as_ref()
.expect("reused diagnostics/list handles");
let replayed_read = runtime
.owner()
.invoke(
FeedbackReadOperationV1::Diagnostics,
&replayed_handles.diagnostics_handle,
read_at,
)
.await
.expect("reused diagnostics handle");
let FeedbackReadInvocationResultV1::Diagnostics(replayed_read) = replayed_read else {
panic!("reused diagnostics result variant");
};
let ApplicationOutcome::Evidence(replayed_diagnostics) =
replayed_read.expect("reused diagnostics read").outcome
else {
panic!("reused diagnostics evidence");
};
assert_eq!(
replayed_diagnostics
.payload
.expect("reused diagnostics payload")
.cycle,
publication.result.clone()
);

let complete_result =
tracedecay_domain::feedback::FeedbackCycleResultV1::new_with_advisory_provider_states(
&repeated_input.request,
Expand Down
48 changes: 44 additions & 4 deletions crates/tracedecay-contracts/src/feedback/ports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,44 @@ impl FeedbackPublicationV1 {
Ok(publication)
}

/// Two incomplete evaluations can share the same committed evidence even
/// when their cycle and result identities differ. No provider, authorization,
/// finding, runtime or policy evidence may be dropped to make that true.
pub fn same_incomplete_evidence(&self, current: &Self) -> bool {
let left = &self.result;
let right = &current.result;
self.dedupe_key == current.dedupe_key
&& self.runtime == current.runtime
&& self.authorized_scope == current.authorized_scope
&& self.authority.grant_id == current.authority.grant_id
&& self.authority.grant_revision == current.authority.grant_revision
&& self.authority.grant_digest == current.authority.grant_digest
&& self.authority.disclosure == current.authority.disclosure
&& self.authority.policy == current.authority.policy
&& self.input.request.trigger == current.input.request.trigger
&& self.input.request.budget == current.input.request.budget
&& self.input.target == current.input.target
&& self.input.actor == current.input.actor
&& left.termination == FeedbackCycleTerminationV1::IncompleteCoverage
&& right.termination == FeedbackCycleTerminationV1::IncompleteCoverage
&& left.scope == right.scope
&& left.content_identity == right.content_identity
&& left.durability == right.durability
&& left.policy_digest == right.policy_digest
&& left.configuration_digest == right.configuration_digest
&& left.provider_states == right.provider_states
&& left.advisory_provider_states == right.advisory_provider_states
&& left.baseline_states == right.baseline_states
&& left.impact == right.impact
&& left.impact_state == right.impact_state
&& left.affected_tests_state == right.affected_tests_state
&& left.findings == right.findings
&& left.total_findings == right.total_findings
&& left.returned_findings == right.returned_findings
&& left.omitted_findings == right.omitted_findings
&& left.advisory_only == right.advisory_only
}

pub fn validate(&self) -> Result<(), ApplicationContractError> {
self.input.validate()?;
self.input.saved()?;
Expand Down Expand Up @@ -302,12 +340,14 @@ impl FeedbackPublicationV1 {
}

/// Result of the daemon-serialized publication compare-and-insert.
/// `Duplicate` means the same result, or another complete result with the same
/// dedupe key, won the race. Incomplete publications never consume that key.
/// `Cancelled`, `TimedOut`, and `Unavailable` leave no row behind.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
/// `Duplicate` is reserved for a completed result or its exact replay.
/// `Reused` carries the actual committed incomplete result, preserving its
/// read handles without inserting a false identity or claiming remote success.
/// Cancellation, timeout and unavailable states do not write a publication.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FeedbackPublicationRecordState {
Recorded,
Reused(FeedbackPublicationV1),
Duplicate,
Cancelled,
TimedOut,
Expand Down
29 changes: 26 additions & 3 deletions crates/tracedecay-contracts/src/feedback/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,8 @@ pub struct FeedbackCycleExecutionResult {
pub dedupe_key: Option<FeedbackDedupeKeyV1>,
pub authority: Option<AuthorityReceipt>,
pub usage: FeedbackBudgetUsage,
/// Present only after the shared durable store atomically records this
/// exact inspectable publication. Duplicate, stale, cancelled, timed-out,
/// unavailable, and non-durable outcomes never expose a delivery handoff.
/// Present for the committed result, including an explicitly reused
/// incomplete publication. Never expose handles for an uncommitted identity.
pub publication: Option<FeedbackPublicationV1>,
}

Expand Down Expand Up @@ -1291,6 +1290,30 @@ where
publication: Some(publication),
..result
}),
FeedbackPublicationRecordState::Reused(existing) => {
if !existing.same_incomplete_evidence(&publication) {
return Err(ApplicationContractError::Inconsistent {
field: "feedback incomplete publication reuse",
});
}
existing.validate()?;
Ok(FeedbackCycleExecutionResult {
cycle: existing.result.clone(),
dedupe_key: Some(existing.dedupe_key.clone()),
authority: Some(existing.authority.clone()),
publication: Some(existing),
..result
})
}
// A bare Duplicate cannot represent incomplete evidence: that
// loses the committed reference and therefore all read handles.
FeedbackPublicationRecordState::Duplicate
if result.cycle.termination == FeedbackCycleTerminationV1::IncompleteCoverage =>
{
Err(ApplicationContractError::Inconsistent {
field: "feedback incomplete publication duplicate",
})
}
FeedbackPublicationRecordState::Duplicate => self.assemble(
request,
Some(dedupe_key),
Expand Down
107 changes: 107 additions & 0 deletions crates/tracedecay-contracts/tests/contracts_suite/feedback_cycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,43 @@ impl FeedbackCycleDedupePort for DedupeFixture {
}
}

#[derive(Clone, Default)]
struct ReusingIncompleteAtRecordFixture {
committed: Rc<RefCell<Option<FeedbackPublicationV1>>>,
}

impl FeedbackCycleDedupePort for ReusingIncompleteAtRecordFixture {
fn lookup_completed<'a>(
&'a self,
_context: &'a RequestContext,
_key: &'a tracedecay_domain::feedback::FeedbackDedupeKeyV1,
) -> tracedecay_contracts::feedback::FeedbackPortFuture<'a, FeedbackCycleDedupeState> {
Box::pin(async { FeedbackCycleDedupeState::Unique })
}

fn record_publication<'a>(
&'a self,
_context: &'a RequestContext,
publication: &'a FeedbackPublicationV1,
) -> tracedecay_contracts::feedback::FeedbackPortFuture<'a, FeedbackPublicationRecordState>
{
let state = {
let mut committed = self.committed.borrow_mut();
match committed.as_ref() {
Some(previous) if previous.same_incomplete_evidence(publication) => {
FeedbackPublicationRecordState::Reused(previous.clone())
}
Some(_) => FeedbackPublicationRecordState::Unavailable,
None => {
*committed = Some(publication.clone());
FeedbackPublicationRecordState::Recorded
}
}
};
Box::pin(async move { state })
}
}

#[derive(Clone)]
struct RecordingDedupeFixture {
state: FeedbackCycleDedupeState,
Expand Down Expand Up @@ -1405,6 +1442,76 @@ fn dedupe_key_changes_when_authoritative_evidence_changes() {
assert_ne!(keys[0], keys[1]);
}

#[test]
fn repeated_incomplete_feedback_reuses_inspectable_committed_identity() {
let input = saved_input();
let provider = provider_identity(&input);
let dedupe = ReusingIncompleteAtRecordFixture::default();
let service = FeedbackCycleService::new(
runtime_port(&input),
DiagnosticsFixture {
calls: Rc::new(Cell::new(0)),
results: vec![complete_result(provider.clone(), Vec::new())],
},
ImpactFixture {
calls: Rc::new(Cell::new(0)),
outcome: FeedbackImpactPortOutcome::Unavailable,
},
dedupe.clone(),
ObservationFixture::default(),
RouteAuthorizationFixture::default(),
common::operation(),
);
let first = block_on(service.execute(
&common::context(&common::operation()),
execution_request(input.clone(), provider),
))
.unwrap();
assert_eq!(
first.cycle.termination,
FeedbackCycleTerminationV1::IncompleteCoverage
);
assert!(first.publication.is_some());

let mut repeated = input;
repeated.request.cycle_id = FeedbackCycleId::new("cycle.feedback.incomplete.repeated").unwrap();
let repeated_provider = provider_identity(&repeated);
let repeated_service = FeedbackCycleService::new(
runtime_port(&repeated),
DiagnosticsFixture {
calls: Rc::new(Cell::new(0)),
results: vec![complete_result(repeated_provider.clone(), Vec::new())],
},
ImpactFixture {
calls: Rc::new(Cell::new(0)),
outcome: FeedbackImpactPortOutcome::Unavailable,
},
dedupe,
ObservationFixture::default(),
RouteAuthorizationFixture::default(),
common::operation(),
);
let replayed = block_on(repeated_service.execute(
&common::context(&common::operation()),
execution_request(repeated.clone(), repeated_provider),
))
.unwrap();

assert_ne!(repeated.request.cycle_id, first.cycle.cycle_id);
assert_eq!(replayed.cycle, first.cycle);
assert_eq!(replayed.publication, first.publication);
assert_eq!(
replayed.cycle.termination,
FeedbackCycleTerminationV1::IncompleteCoverage,
"a durable incomplete result must not claim a clean pass"
);
assert_eq!(
replayed.cycle.impact_state,
Some(FeedbackImpactStateV1::Unavailable)
);
assert!(!replayed.cycle.provider_states.is_empty());
}

#[test]
fn unavailable_authoritative_baseline_cannot_produce_clean() {
let input = saved_input();
Expand Down
Loading
Loading