sediment-derive 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,350 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """Sediment derive: the raw git substrate (mirror, diff, similarity) and the
3
+ pure derivations (attribution) computed over it."""
4
+
5
+ from .abandonment import (
6
+ ACCEPTED_SESSION_STATUSES,
7
+ AcceptedSessionOutcome,
8
+ AcceptedSessionStatus,
9
+ AbandonmentPolicy,
10
+ AbandonmentResult,
11
+ SessionAbandonment,
12
+ derive_abandonment,
13
+ )
14
+ from .attachment import (
15
+ CIOutcomeIndexResult,
16
+ DECISION_ATTACHMENT_SKIP_REASONS,
17
+ DecisionAttachmentSkipReason,
18
+ DecisionAttachmentResult,
19
+ drop_subsumed_codex_decisions,
20
+ index_ci_outcomes_by_repo_commit,
21
+ index_ci_outcomes_by_commit_key_result,
22
+ join_decisions_by_call_id,
23
+ join_decisions_by_call_id_result,
24
+ )
25
+ from .attribution_share import (
26
+ AttributionShareAlert,
27
+ AttributionShareAlertKind,
28
+ AttributionSharePolicy,
29
+ AttributionShareResult,
30
+ RepoAttributionShare,
31
+ check_attribution_share_alerts,
32
+ derive_attribution_share,
33
+ derive_attribution_share_result,
34
+ )
35
+ from .attribution import (
36
+ ATTRIBUTION_IMPLEMENTATION_VERSION,
37
+ Attribution,
38
+ AttributionPolicy,
39
+ AttributionResult,
40
+ AttributionSource,
41
+ SimilarityPolicy,
42
+ derive_attribution_result,
43
+ derive_attributions,
44
+ )
45
+ from .ci_resolution import (
46
+ CI_RESOLUTION_SKIP_REASONS,
47
+ CIResolution,
48
+ CIResolutionPolicy,
49
+ CIResolutionResult,
50
+ CIResolutionSkipReason,
51
+ CIWorkflowResolution,
52
+ derive_ci_resolution_result,
53
+ derive_ci_resolutions,
54
+ )
55
+ from .diff import (
56
+ DIFF_SKIP_REASONS,
57
+ DiffSkipReason,
58
+ DiffParseResult,
59
+ DiffSection,
60
+ FileDiff,
61
+ is_code_file,
62
+ parse_diff_sections,
63
+ parse_unified_diff,
64
+ )
65
+ from .gc import GCAction, MirrorGCPolicy, MirrorGCResult, gc_mirrors
66
+ from .inference_call import (
67
+ InferenceCall,
68
+ inference_fact_id,
69
+ inference_gateway_provider,
70
+ inference_input_messages,
71
+ inference_model,
72
+ inference_observed_at,
73
+ inference_prompt,
74
+ inference_prompt_key,
75
+ inference_tool_calls,
76
+ inference_user_id,
77
+ model_call_ids,
78
+ render_scoring_text,
79
+ )
80
+ from .mirror import (
81
+ FETCH_REFSPECS,
82
+ FileRead,
83
+ FileReadStatus,
84
+ MirrorError,
85
+ MirrorManager,
86
+ MirrorPolicy,
87
+ MirrorPolicyError,
88
+ RepoMirror,
89
+ )
90
+ from .merge_retention import (
91
+ MERGE_RETENTION_IMPLEMENTATION_VERSION,
92
+ MERGE_MEMBERSHIP_REASONS,
93
+ MERGE_MEMBERSHIP_STATUSES,
94
+ MERGE_RETENTION_SKIP_REASONS,
95
+ MergeRetention,
96
+ MergeMembershipOutcome,
97
+ MergeMembershipStatus,
98
+ MergeRetentionPolicy,
99
+ MergeRetentionResult,
100
+ derive_merge_retention_result,
101
+ derive_merge_retentions,
102
+ )
103
+ from .precision_harness import (
104
+ MIN_DRIFT_CASES,
105
+ LabeledCase,
106
+ PrecisionRecallResult,
107
+ ThresholdDriftReport,
108
+ evaluate_scorer,
109
+ load_labelled_cases,
110
+ sweep_thresholds,
111
+ threshold_drift_report,
112
+ )
113
+ from .precision_report import (
114
+ AttributionPrecisionReport,
115
+ AttributionSourcePrecisionReport,
116
+ GroundTruthRow,
117
+ evaluate_attribution_precision,
118
+ generate_precision_report_for_org,
119
+ load_ground_truth_manifest,
120
+ )
121
+ from .recovery import (
122
+ RECOVERY_IMPLEMENTATION_VERSION,
123
+ RECOVERY_SKIP_REASONS,
124
+ RecoveryPolicy,
125
+ RecoveryResult,
126
+ RecoverySample,
127
+ RecoveryAttributionEvidence,
128
+ RecoverySkipReason,
129
+ derive_recovery_pairs,
130
+ derive_recovery_result,
131
+ )
132
+ from .provenance import Provenance
133
+ from .repository_context import read_repository_context
134
+ from .repository_identity import (
135
+ REPOSITORY_IDENTITY_POLICY_VERSION,
136
+ REPOSITORY_IDENTITY_SKIP_REASONS,
137
+ CommitKey,
138
+ IdentifiedRepositoryKey,
139
+ LegacyRepositoryKey,
140
+ RepositoryContext,
141
+ RepositoryIdentity,
142
+ RepositoryIdentitySkipReason,
143
+ RepositoryKey,
144
+ RepositoryResolution,
145
+ build_repository_context,
146
+ commit_sort_key,
147
+ repository_identity_evidence_of,
148
+ repository_identity_of,
149
+ repository_sort_key,
150
+ )
151
+ from .rollout import (
152
+ ROLLOUT_FRAGMENT_REASONS,
153
+ ROLLOUT_IMPLEMENTATION_VERSION,
154
+ RolloutFragmentReason,
155
+ CommitRef,
156
+ Rollout,
157
+ RolloutPolicy,
158
+ RolloutResult,
159
+ Turn,
160
+ derive_rollout_result,
161
+ derive_rollouts,
162
+ project_session_turns,
163
+ )
164
+ from .scoring import JaccardScorer, Scorer
165
+ from .session_commit import SessionCommitBindingResult, bind_session_commit_keys_result
166
+ from .similarity import jaccard_tokens, tokenize
167
+ from .survival import (
168
+ FATE_SKIP_REASONS,
169
+ EditFate,
170
+ Fate,
171
+ FatePolicy,
172
+ FateResult,
173
+ FateSkipReason,
174
+ attach_edit_retention,
175
+ derive_fate_result,
176
+ derive_fates,
177
+ external_lines_after,
178
+ )
179
+ from .split import (
180
+ EVAL,
181
+ TRAIN,
182
+ Split,
183
+ is_eval,
184
+ session_eval_score,
185
+ split_of,
186
+ )
187
+ from .survival_scoring import four_gram_containment, four_gram_survival
188
+
189
+ # The notes wire-contract models (notes.py) are deliberately not re-exported:
190
+ # they are internal to the derivation, not package API.
191
+ __all__ = [
192
+ "CIOutcomeIndexResult",
193
+ "SessionCommitBindingResult",
194
+ "bind_session_commit_keys_result",
195
+ "index_ci_outcomes_by_commit_key_result",
196
+ "REPOSITORY_IDENTITY_POLICY_VERSION",
197
+ "REPOSITORY_IDENTITY_SKIP_REASONS",
198
+ "CommitKey",
199
+ "IdentifiedRepositoryKey",
200
+ "LegacyRepositoryKey",
201
+ "RepositoryContext",
202
+ "RepositoryIdentity",
203
+ "RepositoryIdentitySkipReason",
204
+ "RepositoryKey",
205
+ "RepositoryResolution",
206
+ "build_repository_context",
207
+ "commit_sort_key",
208
+ "repository_identity_evidence_of",
209
+ "repository_identity_of",
210
+ "read_repository_context",
211
+ "AttributionShareResult",
212
+ "derive_attribution_share_result",
213
+ "repository_sort_key",
214
+ "ACCEPTED_SESSION_STATUSES",
215
+ "AcceptedSessionOutcome",
216
+ "AcceptedSessionStatus",
217
+ "derive_abandonment",
218
+ "SessionAbandonment",
219
+ "AbandonmentResult",
220
+ "AbandonmentPolicy",
221
+ "EVAL",
222
+ "FETCH_REFSPECS",
223
+ "FileRead",
224
+ "FileReadStatus",
225
+ "TRAIN",
226
+ "AttributionShareAlert",
227
+ "AttributionShareAlertKind",
228
+ "AttributionSharePolicy",
229
+ "AttributionSource",
230
+ "Attribution",
231
+ "AttributionPolicy",
232
+ "AttributionPrecisionReport",
233
+ "AttributionResult",
234
+ "AttributionSourcePrecisionReport",
235
+ "CI_RESOLUTION_SKIP_REASONS",
236
+ "CIResolution",
237
+ "CIResolutionPolicy",
238
+ "CIResolutionResult",
239
+ "CIResolutionSkipReason",
240
+ "CIWorkflowResolution",
241
+ "DECISION_ATTACHMENT_SKIP_REASONS",
242
+ "DecisionAttachmentSkipReason",
243
+ "DecisionAttachmentResult",
244
+ "CommitRef",
245
+ "ATTRIBUTION_IMPLEMENTATION_VERSION",
246
+ "MERGE_RETENTION_IMPLEMENTATION_VERSION",
247
+ "DIFF_SKIP_REASONS",
248
+ "DiffSkipReason",
249
+ "DiffParseResult",
250
+ "DiffSection",
251
+ "parse_diff_sections",
252
+ "FileDiff",
253
+ "GCAction",
254
+ "GroundTruthRow",
255
+ "JaccardScorer",
256
+ "LabeledCase",
257
+ "MIN_DRIFT_CASES",
258
+ "InferenceCall",
259
+ "MirrorError",
260
+ "MirrorGCPolicy",
261
+ "MirrorGCResult",
262
+ "MirrorManager",
263
+ "MirrorPolicy",
264
+ "MirrorPolicyError",
265
+ "MERGE_MEMBERSHIP_REASONS",
266
+ "MERGE_MEMBERSHIP_STATUSES",
267
+ "MERGE_RETENTION_SKIP_REASONS",
268
+ "MergeRetention",
269
+ "MergeMembershipOutcome",
270
+ "MergeMembershipStatus",
271
+ "MergeRetentionPolicy",
272
+ "MergeRetentionResult",
273
+ "PrecisionRecallResult",
274
+ "Provenance",
275
+ "RecoveryPolicy",
276
+ "RecoveryResult",
277
+ "RecoverySample",
278
+ "RecoveryAttributionEvidence",
279
+ "RecoverySkipReason",
280
+ "RECOVERY_IMPLEMENTATION_VERSION",
281
+ "RECOVERY_SKIP_REASONS",
282
+ "RepoAttributionShare",
283
+ "RepoMirror",
284
+ "Rollout",
285
+ "RolloutPolicy",
286
+ "RolloutResult",
287
+ "RolloutFragmentReason",
288
+ "ROLLOUT_FRAGMENT_REASONS",
289
+ "ROLLOUT_IMPLEMENTATION_VERSION",
290
+ "Scorer",
291
+ "SimilarityPolicy",
292
+ "Split",
293
+ "ThresholdDriftReport",
294
+ "Turn",
295
+ "EditFate",
296
+ "Fate",
297
+ "FatePolicy",
298
+ "FateResult",
299
+ "FateSkipReason",
300
+ "FATE_SKIP_REASONS",
301
+ "attach_edit_retention",
302
+ "external_lines_after",
303
+ "check_attribution_share_alerts",
304
+ "derive_attribution_share",
305
+ "derive_attributions",
306
+ "derive_attribution_result",
307
+ "derive_ci_resolution_result",
308
+ "derive_ci_resolutions",
309
+ "derive_fate_result",
310
+ "derive_fates",
311
+ "derive_merge_retention_result",
312
+ "derive_merge_retentions",
313
+ "derive_recovery_pairs",
314
+ "derive_recovery_result",
315
+ "derive_rollout_result",
316
+ "derive_rollouts",
317
+ "project_session_turns",
318
+ "drop_subsumed_codex_decisions",
319
+ "evaluate_attribution_precision",
320
+ "evaluate_scorer",
321
+ "four_gram_containment",
322
+ "four_gram_survival",
323
+ "gc_mirrors",
324
+ "generate_precision_report_for_org",
325
+ "index_ci_outcomes_by_repo_commit",
326
+ "inference_fact_id",
327
+ "inference_gateway_provider",
328
+ "inference_input_messages",
329
+ "inference_model",
330
+ "inference_observed_at",
331
+ "inference_prompt",
332
+ "inference_prompt_key",
333
+ "inference_tool_calls",
334
+ "inference_user_id",
335
+ "is_code_file",
336
+ "is_eval",
337
+ "jaccard_tokens",
338
+ "join_decisions_by_call_id",
339
+ "join_decisions_by_call_id_result",
340
+ "load_ground_truth_manifest",
341
+ "load_labelled_cases",
342
+ "model_call_ids",
343
+ "parse_unified_diff",
344
+ "render_scoring_text",
345
+ "session_eval_score",
346
+ "split_of",
347
+ "sweep_thresholds",
348
+ "threshold_drift_report",
349
+ "tokenize",
350
+ ]
@@ -0,0 +1,320 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """Observation-backed accepted Session status; absence cannot prove abandonment.
3
+
4
+ ADR 0014 retains the public result shapes for historical callers. A captured
5
+ SessionCommitObservation proves committed status. Missing observation evidence
6
+ remains attribution_unavailable, irrespective of live notes or similarity.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from collections import Counter, defaultdict
13
+ from collections.abc import Iterable, Mapping
14
+ from dataclasses import dataclass, field
15
+ from datetime import UTC, datetime
16
+ from typing import Literal
17
+
18
+ from sediment_core import (
19
+ DeveloperDecision,
20
+ FactStore,
21
+ NonEmptyId,
22
+ OrgId,
23
+ Push,
24
+ SessionCommitObservation,
25
+ )
26
+
27
+ from .attribution import Attribution, AttributionPolicy
28
+ from .gc import MirrorGCPolicy
29
+ from .inference_call import InferenceCall, inference_observed_at
30
+ from .mirror import MirrorManager
31
+ from .provenance import Provenance
32
+ from .rollout import CommitRef
33
+ from .session_commit import SESSION_COMMIT_UNOBSERVED, bind_session_commit_keys_result
34
+ from .repository_context import read_repository_context
35
+ from .repository_identity import RepositoryContext, repository_identity_evidence_of
36
+
37
+ logger = logging.getLogger("sediment.derive.abandonment")
38
+
39
+ # Emitted skips are no_accepted_decision, reached_a_commit, and
40
+ # SESSION_COMMIT_UNOBSERVED. The grace/Attribution constants and legacy Session
41
+ # shapes remain compatibility exports; absent observations never prove abandonment.
42
+ SKIP_NO_ACCEPTED_DECISION = "no_accepted_decision"
43
+ SKIP_REACHED_A_COMMIT = "reached_a_commit"
44
+ SKIP_WITHIN_GRACE_HORIZON = "within_grace_horizon"
45
+ SKIP_ATTRIBUTION_UNAVAILABLE = "attribution_unavailable"
46
+
47
+ AcceptedSessionStatus = Literal[
48
+ "committed",
49
+ "abandoned",
50
+ "in_flight",
51
+ "attribution_unavailable",
52
+ ]
53
+ ACCEPTED_SESSION_STATUSES: tuple[AcceptedSessionStatus, ...] = (
54
+ "committed",
55
+ "abandoned",
56
+ "in_flight",
57
+ "attribution_unavailable",
58
+ )
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class AbandonmentPolicy:
63
+ """Versioned factual Session status with legacy policy parsing compatibility.
64
+
65
+ The attribution, mirror, and horizon fields remain accepted for historical
66
+ policy documents. They cannot supply missing observation evidence.
67
+ """
68
+
69
+ attribution: AttributionPolicy = field(default_factory=AttributionPolicy)
70
+ mirror_gc: MirrorGCPolicy = field(default_factory=MirrorGCPolicy)
71
+ grace_horizon_days: int = 14
72
+ policy_version: str = "6"
73
+
74
+ def __post_init__(self) -> None:
75
+ if self.grace_horizon_days < 1:
76
+ raise ValueError(
77
+ "AbandonmentPolicy.grace_horizon_days must be >= 1 "
78
+ f"(got {self.grace_horizon_days}); a zero horizon calls every "
79
+ "in-flight session abandoned."
80
+ )
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class SessionAbandonment:
85
+ """One session whose accepted work reached no commit — derived, never
86
+ persisted (ADR 0001)."""
87
+
88
+ org_id: str
89
+ session_id: str
90
+ accepted_decisions: int # how many accepts went unshipped
91
+ explicit_accepted_decisions: int # how many were human gestures
92
+ last_decision_at: datetime # the anchor the horizon is measured from
93
+ as_of: datetime # the fact set's newest timestamp — see the module docstring
94
+ provenance: Provenance
95
+
96
+
97
+ @dataclass(frozen=True)
98
+ class AcceptedSessionOutcome:
99
+ """One accepted Session's terminal abandonment classification."""
100
+
101
+ org_id: OrgId
102
+ session_id: NonEmptyId
103
+ accepted_decisions: int
104
+ explicit_accepted_decisions: int
105
+ last_decision_at: datetime
106
+ as_of: datetime
107
+ status: AcceptedSessionStatus
108
+ provenance: Provenance
109
+
110
+
111
+ @dataclass
112
+ class AbandonmentResult:
113
+ """Accepted Session outcomes plus compatible abandonment views."""
114
+
115
+ sessions: list[SessionAbandonment] = field(default_factory=list)
116
+ skipped: Counter[str] = field(default_factory=Counter)
117
+ as_of: datetime | None = None
118
+ provenance: Provenance | None = None
119
+ outcomes: list[AcceptedSessionOutcome] = field(default_factory=list)
120
+
121
+
122
+ def _as_of(
123
+ decisions: list[DeveloperDecision],
124
+ pushes: list[Push],
125
+ completions: list[InferenceCall],
126
+ ) -> datetime | None:
127
+ """The newest timestamp anywhere in the input facts.
128
+
129
+ A function of the fact set alone — never ``datetime.now()``, which would
130
+ make the same facts yield different verdicts on different days and break
131
+ recomputability over all history (ADR 0001, non-negotiable rule 2).
132
+ Both ``occurred_at`` and ``captured_at`` count: a fact captured late is
133
+ still evidence that time has passed.
134
+ """
135
+ stamps: list[datetime] = []
136
+ for d in decisions:
137
+ stamps.append(d.occurred_at)
138
+ stamps.append(d.captured_at)
139
+ stamps.extend(p.captured_at for p in pushes)
140
+ stamps.extend(inference_observed_at(call) for call in completions)
141
+ return max(stamp.astimezone(UTC) for stamp in stamps) if stamps else None
142
+
143
+
144
+ def derive_abandonment(
145
+ store: FactStore,
146
+ mirrors: MirrorManager,
147
+ org_id: str,
148
+ policy: AbandonmentPolicy | None = None,
149
+ *,
150
+ attributions: Iterable[Attribution] | None = None,
151
+ decisions: Iterable[DeveloperDecision] | None = None,
152
+ pushes: Iterable[Push] | None = None,
153
+ completions: Iterable[InferenceCall] | None = None,
154
+ as_of: datetime | None = None,
155
+ session_commits: Mapping[str, Iterable[CommitRef]] | None = None,
156
+ policy_digest: str | None = None,
157
+ session_commit_observations: Iterable[SessionCommitObservation] | None = None,
158
+ repository_context: RepositoryContext | None = None,
159
+ ) -> AbandonmentResult:
160
+ """Classify accepted Sessions using captured observations at the boundary.
161
+
162
+ Supplied Fact collections are authoritative, including empty collections.
163
+ Legacy ``attributions`` and ``session_commits`` arguments remain accepted but
164
+ cannot establish factual identity without source observations (ADR 0014).
165
+ """
166
+ with store.read_snapshot() as snapshot:
167
+ return _derive_abandonment(
168
+ snapshot,
169
+ mirrors,
170
+ org_id,
171
+ policy,
172
+ attributions=attributions,
173
+ decisions=decisions,
174
+ pushes=pushes,
175
+ completions=completions,
176
+ as_of=as_of,
177
+ session_commits=session_commits,
178
+ policy_digest=policy_digest,
179
+ session_commit_observations=session_commit_observations,
180
+ repository_context=repository_context,
181
+ )
182
+
183
+
184
+ def _derive_abandonment(
185
+ store: FactStore,
186
+ mirrors: MirrorManager,
187
+ org_id: str,
188
+ policy: AbandonmentPolicy | None = None,
189
+ *,
190
+ attributions: Iterable[Attribution] | None = None,
191
+ decisions: Iterable[DeveloperDecision] | None = None,
192
+ pushes: Iterable[Push] | None = None,
193
+ completions: Iterable[InferenceCall] | None = None,
194
+ as_of: datetime | None = None,
195
+ session_commits: Mapping[str, Iterable[CommitRef]] | None = None,
196
+ policy_digest: str | None = None,
197
+ session_commit_observations: Iterable[SessionCommitObservation] | None = None,
198
+ repository_context: RepositoryContext | None = None,
199
+ ) -> AbandonmentResult:
200
+ """Derive abandonment inside the caller's fact snapshot."""
201
+
202
+ if as_of is not None and as_of.utcoffset() is None:
203
+ raise ValueError("as_of must be an aware datetime")
204
+ if repository_context is not None:
205
+ if repository_context.org_id != org_id or (
206
+ as_of is not None and repository_context.as_of != as_of.astimezone(UTC)
207
+ ):
208
+ raise ValueError("repository context must match organization and boundary")
209
+ as_of = repository_context.as_of
210
+ policy = policy or AbandonmentPolicy()
211
+ provenance = Provenance(
212
+ policy_version=policy.policy_version,
213
+ quarantine_revision=store.quarantine_revision(org_id),
214
+ policy_digest=policy_digest,
215
+ )
216
+ result = AbandonmentResult(provenance=provenance)
217
+
218
+ decision_facts = (
219
+ store.read_decision_projections(org_id)
220
+ if decisions is None
221
+ else list(decisions)
222
+ )
223
+ result.as_of = as_of
224
+ if not decision_facts:
225
+ return result
226
+ push_facts = store.read_pushes(org_id) if pushes is None else list(pushes)
227
+ completion_facts = (
228
+ store.read_inference_call_summaries(org_id)
229
+ if completions is None
230
+ else list(completions)
231
+ )
232
+ observation_facts = list(
233
+ store.read_session_commit_observations(org_id, as_of=as_of)
234
+ if session_commit_observations is None
235
+ else session_commit_observations
236
+ )
237
+ preloaded_without_context = (
238
+ session_commit_observations is not None and repository_context is None
239
+ )
240
+ context = repository_context or read_repository_context(
241
+ store,
242
+ org_id,
243
+ as_of=as_of,
244
+ supplemental_legacy_evidence=(
245
+ repository_identity_evidence_of(item)
246
+ for item in observation_facts
247
+ if preloaded_without_context and item.repository_id is None
248
+ ),
249
+ )
250
+ effective_as_of = as_of or max(
251
+ context.as_of, _as_of(decision_facts, push_facts, completion_facts)
252
+ )
253
+ if context.as_of != effective_as_of:
254
+ context = read_repository_context(
255
+ store,
256
+ org_id,
257
+ as_of=effective_as_of,
258
+ supplemental_legacy_evidence=(
259
+ repository_identity_evidence_of(item)
260
+ for item in observation_facts
261
+ if preloaded_without_context and item.repository_id is None
262
+ ),
263
+ )
264
+ result.as_of = effective_as_of
265
+ if preloaded_without_context:
266
+ declined = [
267
+ item
268
+ for item in observation_facts
269
+ if item.repository_id is not None
270
+ and item.org_id == org_id
271
+ and item.captured_at.astimezone(UTC) <= effective_as_of
272
+ ]
273
+ if declined:
274
+ result.skipped["repository_identity_unresolved"] += len(declined)
275
+ logger.warning(
276
+ "abandonment_repository_declined reason=repository_identity_unresolved count=%d",
277
+ len(declined),
278
+ )
279
+ observation_facts = [
280
+ item for item in observation_facts if item.repository_id is None
281
+ ]
282
+ binding_result = bind_session_commit_keys_result(
283
+ observation_facts, org_id, as_of=effective_as_of, repository_context=context
284
+ )
285
+ result.skipped.update(binding_result.skipped)
286
+ observed_sessions = {session_id for _, session_id in binding_result.bindings}
287
+ decs_by_session: dict[str, list[DeveloperDecision]] = defaultdict(list)
288
+ for decision in decision_facts:
289
+ if decision.org_id == org_id and decision.captured_at.astimezone(
290
+ UTC
291
+ ) <= effective_as_of.astimezone(UTC):
292
+ decs_by_session[decision.session_id].append(decision)
293
+ for session_id, decs in sorted(decs_by_session.items()):
294
+ accepted = [item for item in decs if item.accepted]
295
+ if not accepted:
296
+ result.skipped[SKIP_NO_ACCEPTED_DECISION] += 1
297
+ continue
298
+ observed = session_id in observed_sessions
299
+ reason = SKIP_REACHED_A_COMMIT if observed else SESSION_COMMIT_UNOBSERVED
300
+ result.skipped[reason] += 1
301
+ result.outcomes.append(
302
+ AcceptedSessionOutcome(
303
+ org_id=org_id,
304
+ session_id=session_id,
305
+ accepted_decisions=len(accepted),
306
+ explicit_accepted_decisions=sum(item.explicit for item in accepted),
307
+ last_decision_at=max(item.occurred_at.astimezone(UTC) for item in decs),
308
+ as_of=effective_as_of,
309
+ status="committed" if observed else "attribution_unavailable",
310
+ provenance=provenance,
311
+ )
312
+ )
313
+ logger.info(
314
+ "abandonment_derived org_id=%s abandoned=%d skipped=%s as_of=%s",
315
+ org_id,
316
+ len(result.sessions),
317
+ dict(sorted(result.skipped.items())),
318
+ effective_as_of.isoformat(),
319
+ )
320
+ return result