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.
- sediment_derive/__init__.py +350 -0
- sediment_derive/abandonment.py +320 -0
- sediment_derive/attachment.py +319 -0
- sediment_derive/attribution.py +621 -0
- sediment_derive/attribution_share.py +560 -0
- sediment_derive/ci_resolution.py +411 -0
- sediment_derive/diff.py +333 -0
- sediment_derive/gc.py +245 -0
- sediment_derive/inference_call.py +130 -0
- sediment_derive/merge_retention.py +811 -0
- sediment_derive/mirror.py +863 -0
- sediment_derive/notes.py +184 -0
- sediment_derive/precision_harness.py +475 -0
- sediment_derive/precision_report.py +484 -0
- sediment_derive/provenance.py +36 -0
- sediment_derive/recovery.py +676 -0
- sediment_derive/repository_context.py +86 -0
- sediment_derive/repository_identity.py +619 -0
- sediment_derive/rollout.py +805 -0
- sediment_derive/scoring.py +34 -0
- sediment_derive/session_commit.py +125 -0
- sediment_derive/similarity.py +31 -0
- sediment_derive/split.py +70 -0
- sediment_derive/survival.py +326 -0
- sediment_derive/survival_scoring.py +119 -0
- sediment_derive-0.1.0.dist-info/METADATA +13 -0
- sediment_derive-0.1.0.dist-info/RECORD +29 -0
- sediment_derive-0.1.0.dist-info/WHEEL +4 -0
- sediment_derive-0.1.0.dist-info/licenses/LICENSE +661 -0
|
@@ -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
|