sediment-api 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_api/__init__.py +6 -0
- sediment_api/config.py +230 -0
- sediment_api/database.py +43 -0
- sediment_api/deps.py +191 -0
- sediment_api/main.py +212 -0
- sediment_api/mirror_gc.py +151 -0
- sediment_api/reports/__init__.py +9 -0
- sediment_api/reports/abandonment_report.py +123 -0
- sediment_api/reports/attribution_share_report.py +298 -0
- sediment_api/reports/dataset_diagnostics.py +311 -0
- sediment_api/reports/label_confidence_inspection.py +438 -0
- sediment_api/reports/lifecycle_report.py +73 -0
- sediment_api/reports/merge_retention_report.py +165 -0
- sediment_api/reports/model_report.py +1534 -0
- sediment_api/reports/precision_report.py +182 -0
- sediment_api/reports/recovery_yield_report.py +132 -0
- sediment_api/routers/__init__.py +3 -0
- sediment_api/routers/ci_vendor.py +149 -0
- sediment_api/routers/forge.py +452 -0
- sediment_api/routers/gateway.py +137 -0
- sediment_api/routers/otlp.py +120 -0
- sediment_api/routers/query.py +1010 -0
- sediment_api/routers/reports.py +116 -0
- sediment_api/routers/v1.py +170 -0
- sediment_api/services/__init__.py +2 -0
- sediment_api/services/operational_reports.py +391 -0
- sediment_api/worker.py +242 -0
- sediment_api/workers.py +382 -0
- sediment_api-0.1.0.dist-info/METADATA +20 -0
- sediment_api-0.1.0.dist-info/RECORD +32 -0
- sediment_api-0.1.0.dist-info/WHEEL +4 -0
- sediment_api-0.1.0.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""Mirror garbage collection operator CLI.
|
|
3
|
+
|
|
4
|
+
Thin stdout wrapper over ``sediment_derive.gc.gc_mirrors`` — the reusable
|
|
5
|
+
retention logic lives there; this script only reads args, supplies the one
|
|
6
|
+
legitimate wall-clock read (``now``), and prints. Dry-run by default: pass
|
|
7
|
+
``--apply`` to actually delete anything, since a removed mirror is not
|
|
8
|
+
undone by anything short of a fresh clone.
|
|
9
|
+
|
|
10
|
+
sediment mirror-gc --org acme-corp
|
|
11
|
+
sediment mirror-gc --org acme-corp --retention-days 30 --apply
|
|
12
|
+
sediment mirror-gc --org acme-corp --json
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
from dataclasses import asdict
|
|
22
|
+
from datetime import UTC, datetime
|
|
23
|
+
|
|
24
|
+
from sediment_core import normalize_org_id
|
|
25
|
+
from sediment_derive import MirrorManager
|
|
26
|
+
from sediment_derive.gc import MirrorGCPolicy, MirrorGCResult, gc_mirrors
|
|
27
|
+
|
|
28
|
+
from .database import add_database_url_argument, one_shot_fact_store
|
|
29
|
+
|
|
30
|
+
_DEFAULT_MIRROR_PATH = "./mirrors"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _result_dict(
|
|
34
|
+
result: MirrorGCResult, *, dry_run: bool, policy: MirrorGCPolicy
|
|
35
|
+
) -> dict[str, object]:
|
|
36
|
+
row = asdict(result)
|
|
37
|
+
row["action"] = result.action.value
|
|
38
|
+
if result.last_push_captured_at is not None:
|
|
39
|
+
row["last_push_captured_at"] = result.last_push_captured_at.isoformat()
|
|
40
|
+
# A dry run and an applied run both report action="removed" for an
|
|
41
|
+
# eligible mirror -- without these, a --json consumer reading that
|
|
42
|
+
# value from a dry run misreads it as actually deleted.
|
|
43
|
+
row["dry_run"] = dry_run
|
|
44
|
+
row["retention_days"] = policy.retention_days
|
|
45
|
+
row["policy_version"] = policy.policy_version
|
|
46
|
+
return row
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _print_table(results: list[MirrorGCResult], *, dry_run: bool) -> None:
|
|
50
|
+
if not results:
|
|
51
|
+
print("no mirrored repos found")
|
|
52
|
+
return
|
|
53
|
+
mode = "dry-run (pass --apply to delete)" if dry_run else "applied"
|
|
54
|
+
print(f"mode: {mode}")
|
|
55
|
+
for result in results:
|
|
56
|
+
last_push = (
|
|
57
|
+
result.last_push_captured_at.isoformat()
|
|
58
|
+
if result.last_push_captured_at is not None
|
|
59
|
+
else "never"
|
|
60
|
+
)
|
|
61
|
+
detail = f" ({result.skip_reason})" if result.skip_reason else ""
|
|
62
|
+
repository = result.repo if result.repo is not None else "name absent"
|
|
63
|
+
if result.repository_identity is not None:
|
|
64
|
+
identity = result.repository_identity
|
|
65
|
+
repository += (
|
|
66
|
+
f" [{identity.provider}:{identity.host}/{identity.repository_id}]"
|
|
67
|
+
)
|
|
68
|
+
print(f"{repository}\t{result.action.value}{detail}\tlast_push={last_push}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
72
|
+
"""The argv contract, extracted so the generated CLI reference can
|
|
73
|
+
walk it."""
|
|
74
|
+
parser = argparse.ArgumentParser(
|
|
75
|
+
prog="mirror_gc",
|
|
76
|
+
description="Reclaim bare mirrors for repos with no recent Push activity.",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--org",
|
|
80
|
+
default=os.environ.get("SEDIMENT_ORG_ID"),
|
|
81
|
+
required=not os.environ.get("SEDIMENT_ORG_ID"),
|
|
82
|
+
help="org id (default: $SEDIMENT_ORG_ID)",
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--retention-days",
|
|
86
|
+
type=int,
|
|
87
|
+
default=MirrorGCPolicy().retention_days,
|
|
88
|
+
help=f"days of no Push before a mirror is eligible for removal "
|
|
89
|
+
f"(default: {MirrorGCPolicy().retention_days})",
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--apply",
|
|
93
|
+
action="store_true",
|
|
94
|
+
help="actually delete eligible mirrors (default: dry-run, print only)",
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--json", action="store_true", help="emit JSON instead of a text report"
|
|
98
|
+
)
|
|
99
|
+
add_database_url_argument(parser)
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--mirror-path",
|
|
102
|
+
default=os.environ.get("SEDIMENT_MIRROR_PATH", _DEFAULT_MIRROR_PATH),
|
|
103
|
+
help="base dir for git mirrors (default: $SEDIMENT_MIRROR_PATH or "
|
|
104
|
+
f"{_DEFAULT_MIRROR_PATH!r})",
|
|
105
|
+
)
|
|
106
|
+
return parser
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main(argv: list[str] | None = None) -> int:
|
|
110
|
+
args = build_parser().parse_args(argv)
|
|
111
|
+
|
|
112
|
+
# Reject at the trust boundary, before FactStore or MirrorManager touch
|
|
113
|
+
# anything: a zero/negative retention flips the cutoff into the future
|
|
114
|
+
# and (with --apply) removes every mirror that has push history.
|
|
115
|
+
if args.retention_days < 1:
|
|
116
|
+
print(
|
|
117
|
+
f"error: --retention-days must be >= 1, got {args.retention_days}",
|
|
118
|
+
file=sys.stderr,
|
|
119
|
+
)
|
|
120
|
+
return 2
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
org_id = normalize_org_id(args.org)
|
|
124
|
+
except ValueError as exc:
|
|
125
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
126
|
+
return 2
|
|
127
|
+
|
|
128
|
+
policy = MirrorGCPolicy(retention_days=args.retention_days)
|
|
129
|
+
dry_run = not args.apply
|
|
130
|
+
with one_shot_fact_store(
|
|
131
|
+
args.database_url, operation="garbage-collect mirrors"
|
|
132
|
+
) as store:
|
|
133
|
+
results = gc_mirrors(
|
|
134
|
+
store,
|
|
135
|
+
MirrorManager(args.mirror_path),
|
|
136
|
+
org_id,
|
|
137
|
+
policy,
|
|
138
|
+
now=datetime.now(UTC),
|
|
139
|
+
dry_run=dry_run,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if args.json:
|
|
143
|
+
rows = [_result_dict(r, dry_run=dry_run, policy=policy) for r in results]
|
|
144
|
+
print(json.dumps(rows, indent=2))
|
|
145
|
+
else:
|
|
146
|
+
_print_table(results, dry_run=dry_run)
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""Read-only operator reports — the ``sediment report`` subcommands.
|
|
3
|
+
|
|
4
|
+
Each module keeps the shared script convention (AGENTS.md's scripts section):
|
|
5
|
+
``--org`` (defaulting from ``SEDIMENT_ORG_ID``), ``--database-url``/
|
|
6
|
+
``SEDIMENT_DATABASE_URL``, ``--json``, and empty result = "no data" + exit 0.
|
|
7
|
+
None of them import ``sediment_api.config`` — a report never needs API auth
|
|
8
|
+
config to read a store.
|
|
9
|
+
"""
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""Accepted Session status from captured Session-to-commit observations.
|
|
3
|
+
|
|
4
|
+
Missing observations remain unknown and count under session_commit_unobserved.
|
|
5
|
+
Legacy horizon and mirror options remain accepted for command compatibility.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from dataclasses import asdict
|
|
15
|
+
|
|
16
|
+
from sediment_core import normalize_org_id
|
|
17
|
+
from sediment_derive import AbandonmentPolicy, MirrorManager, derive_abandonment
|
|
18
|
+
|
|
19
|
+
from ..database import add_database_url_argument, one_shot_fact_store
|
|
20
|
+
|
|
21
|
+
_DEFAULT_MIRROR_PATH = "./mirrors"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _print_table(result, policy: AbandonmentPolicy) -> None:
|
|
25
|
+
print(f"grace_horizon_days: {policy.grace_horizon_days}")
|
|
26
|
+
print(f"as_of: {result.as_of.isoformat() if result.as_of else 'none'}")
|
|
27
|
+
print(f"abandoned_sessions: {len(result.sessions)}")
|
|
28
|
+
print(f"unshipped_accepts: {sum(s.accepted_decisions for s in result.sessions)}")
|
|
29
|
+
print(
|
|
30
|
+
f"observed_committed_sessions: {sum(item.status == 'committed' for item in result.outcomes)}"
|
|
31
|
+
)
|
|
32
|
+
print(
|
|
33
|
+
f"unknown_sessions: {sum(item.status == 'attribution_unavailable' for item in result.outcomes)}"
|
|
34
|
+
)
|
|
35
|
+
print("skipped:")
|
|
36
|
+
if not result.skipped:
|
|
37
|
+
print(" none")
|
|
38
|
+
else:
|
|
39
|
+
for reason, count in sorted(result.skipped.items()):
|
|
40
|
+
print(f" {reason}: {count}")
|
|
41
|
+
if result.sessions:
|
|
42
|
+
print("sessions:")
|
|
43
|
+
for session in result.sessions:
|
|
44
|
+
print(
|
|
45
|
+
f" {session.session_id} accepts={session.accepted_decisions}"
|
|
46
|
+
f" last_decision_at={session.last_decision_at.isoformat()}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
51
|
+
"""The argv contract for this report, extracted so the generated CLI
|
|
52
|
+
reference can walk it; ``main`` is unchanged."""
|
|
53
|
+
parser = argparse.ArgumentParser(
|
|
54
|
+
prog="abandonment_report", description=__doc__.splitlines()[0]
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--org",
|
|
58
|
+
default=os.environ.get("SEDIMENT_ORG_ID"),
|
|
59
|
+
required=not os.environ.get("SEDIMENT_ORG_ID"),
|
|
60
|
+
help="org id (default: $SEDIMENT_ORG_ID)",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--json", action="store_true", help="emit a JSON row instead of a table"
|
|
64
|
+
)
|
|
65
|
+
add_database_url_argument(parser)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--mirror-path",
|
|
68
|
+
default=os.environ.get("SEDIMENT_MIRROR_PATH", _DEFAULT_MIRROR_PATH),
|
|
69
|
+
help="base dir for git mirrors (default: $SEDIMENT_MIRROR_PATH or "
|
|
70
|
+
f"{_DEFAULT_MIRROR_PATH!r})",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--grace-horizon-days",
|
|
74
|
+
type=int,
|
|
75
|
+
default=AbandonmentPolicy().grace_horizon_days,
|
|
76
|
+
help="legacy horizon for policy compatibility; missing observations remain unknown "
|
|
77
|
+
f"(default: {AbandonmentPolicy().grace_horizon_days})",
|
|
78
|
+
)
|
|
79
|
+
return parser
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main(argv: list[str] | None = None) -> int:
|
|
83
|
+
args = build_parser().parse_args(argv)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
org_id = normalize_org_id(args.org)
|
|
87
|
+
policy = AbandonmentPolicy(grace_horizon_days=args.grace_horizon_days)
|
|
88
|
+
except ValueError as exc:
|
|
89
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
90
|
+
return 2
|
|
91
|
+
|
|
92
|
+
with one_shot_fact_store(
|
|
93
|
+
args.database_url, operation="generate abandonment report"
|
|
94
|
+
) as store:
|
|
95
|
+
result = derive_abandonment(
|
|
96
|
+
store, MirrorManager(args.mirror_path), org_id, policy
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if args.json:
|
|
100
|
+
print(
|
|
101
|
+
json.dumps(
|
|
102
|
+
{
|
|
103
|
+
"org_id": org_id,
|
|
104
|
+
"grace_horizon_days": policy.grace_horizon_days,
|
|
105
|
+
"policy_version": policy.policy_version,
|
|
106
|
+
"as_of": result.as_of.isoformat() if result.as_of else None,
|
|
107
|
+
"abandoned_sessions": [asdict(s) for s in result.sessions],
|
|
108
|
+
"accepted_session_outcomes": [
|
|
109
|
+
asdict(item) for item in result.outcomes
|
|
110
|
+
],
|
|
111
|
+
"skipped": dict(sorted(result.skipped.items())),
|
|
112
|
+
},
|
|
113
|
+
indent=2,
|
|
114
|
+
default=str,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
_print_table(result, policy)
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__": # pragma: no cover
|
|
123
|
+
sys.exit(main())
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""Attribution-share metric + decline-alert operator report.
|
|
3
|
+
|
|
4
|
+
Derives one org's per-repo notes-attribution share for the current
|
|
5
|
+
``--window-days`` window, derives the same metric again for the trailing
|
|
6
|
+
``--baseline-window-days`` window immediately preceding it, and reports any
|
|
7
|
+
``check_attribution_share_alerts`` verdict between them.
|
|
8
|
+
|
|
9
|
+
``--target-margin`` plans ``min_cases_for_decline_verdict`` via
|
|
10
|
+
``scripts/corpus_sizing.py``'s worst-case-p=0.5 planner
|
|
11
|
+
(``required_labelled_examples_for_proportion_margin``) -- the same derivation
|
|
12
|
+
``MIN_DRIFT_CASES`` uses for ``threshold_drift_report``, so the gate is a
|
|
13
|
+
computed corpus-size floor, never a hand-picked literal.
|
|
14
|
+
|
|
15
|
+
Like ``threshold_drift_report.py``, this script is deliberately
|
|
16
|
+
operator-facing only: every outcome -- an alert, or none -- exits 0. Only a
|
|
17
|
+
bad org id or an invalid ``--target-margin``/``--window-days`` exits 2. It is
|
|
18
|
+
not wired into any CI gate.
|
|
19
|
+
|
|
20
|
+
sediment report attribution-share --org acme-corp \
|
|
21
|
+
--target-margin 0.1
|
|
22
|
+
sediment report attribution-share --org acme-corp \
|
|
23
|
+
--target-margin 0.1 --json
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import sys
|
|
32
|
+
from dataclasses import asdict, replace
|
|
33
|
+
from datetime import UTC, datetime, timedelta
|
|
34
|
+
|
|
35
|
+
from sediment_core import FactStore, Push, normalize_org_id
|
|
36
|
+
from sediment_derive import (
|
|
37
|
+
AttributionShareAlert,
|
|
38
|
+
AttributionSharePolicy,
|
|
39
|
+
MirrorManager,
|
|
40
|
+
RepoAttributionShare,
|
|
41
|
+
check_attribution_share_alerts,
|
|
42
|
+
derive_attribution_share,
|
|
43
|
+
read_repository_context,
|
|
44
|
+
RepositoryContext,
|
|
45
|
+
RepositoryIdentity,
|
|
46
|
+
IdentifiedRepositoryKey,
|
|
47
|
+
LegacyRepositoryKey,
|
|
48
|
+
)
|
|
49
|
+
from sediment_derive.precision_harness import (
|
|
50
|
+
required_labelled_examples_for_proportion_margin,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
from ..database import add_database_url_argument, one_shot_fact_store
|
|
54
|
+
|
|
55
|
+
_DEFAULT_MIRROR_PATH = "./mirrors"
|
|
56
|
+
_WORST_CASE_ASSUMED_RATE = 0.5
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _baseline_rows(
|
|
60
|
+
store: FactStore,
|
|
61
|
+
mirrors: MirrorManager,
|
|
62
|
+
org_id: str,
|
|
63
|
+
policy: AttributionSharePolicy,
|
|
64
|
+
current: list[RepoAttributionShare],
|
|
65
|
+
*,
|
|
66
|
+
repository_context: RepositoryContext | None = None,
|
|
67
|
+
) -> list[RepoAttributionShare]:
|
|
68
|
+
"""The trailing baseline window per repo in ``current``: the
|
|
69
|
+
``baseline_window_days`` immediately preceding that repo's current
|
|
70
|
+
``window_start``. Bounds are enforced here (not left to the derive
|
|
71
|
+
function's latest-push anchor) so a cadence gap before the current
|
|
72
|
+
window cannot re-anchor the baseline at a stale pre-current push --
|
|
73
|
+
matching the contiguous-bounds discipline the model-report path
|
|
74
|
+
(``derive_model_report_attribution_share``) already follows. The
|
|
75
|
+
``_window`` reuse lives in ``derive_attribution_share`` itself; this is
|
|
76
|
+
only the bounded-selection + bounds-forcing wiring the interim CLI owns.
|
|
77
|
+
|
|
78
|
+
Both bounds stay data-driven (ADR 0001 -- never wall-clock):
|
|
79
|
+
``current.window_start`` is ``latest_push.captured_at - window_days``
|
|
80
|
+
(derived from push facts by ``derive_attribution_share``), and the
|
|
81
|
+
baseline lower bound is arithmetic on that value, never ``now()``."""
|
|
82
|
+
if repository_context is None:
|
|
83
|
+
with store.read_snapshot() as snapshot:
|
|
84
|
+
context = read_repository_context(snapshot, org_id, as_of=datetime.now(UTC))
|
|
85
|
+
return _baseline_rows(
|
|
86
|
+
snapshot, mirrors, org_id, policy, current, repository_context=context
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def row_key(row: RepoAttributionShare):
|
|
90
|
+
return (
|
|
91
|
+
IdentifiedRepositoryKey(row.org_id, row.repository_identity)
|
|
92
|
+
if row.repository_identity is not None
|
|
93
|
+
else LegacyRepositoryKey(row.org_id, row.repo)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
current_window_start = {row_key(row): row.window_start for row in current}
|
|
97
|
+
if not current_window_start:
|
|
98
|
+
return []
|
|
99
|
+
baseline_start = {
|
|
100
|
+
repo: ws - timedelta(days=policy.baseline_window_days)
|
|
101
|
+
for repo, ws in current_window_start.items()
|
|
102
|
+
}
|
|
103
|
+
older_pushes: list[Push] = [
|
|
104
|
+
push
|
|
105
|
+
for push in store.read_pushes(org_id, captured_through=repository_context.as_of)
|
|
106
|
+
if (key := repository_context.resolve_fact(push).key) in current_window_start
|
|
107
|
+
and baseline_start[key] <= push.captured_at < current_window_start[key]
|
|
108
|
+
]
|
|
109
|
+
if not older_pushes:
|
|
110
|
+
return []
|
|
111
|
+
baseline_policy = replace(policy, window_days=policy.baseline_window_days)
|
|
112
|
+
derived = derive_attribution_share(
|
|
113
|
+
store,
|
|
114
|
+
mirrors,
|
|
115
|
+
org_id,
|
|
116
|
+
baseline_policy,
|
|
117
|
+
pushes=older_pushes,
|
|
118
|
+
repository_context=repository_context,
|
|
119
|
+
as_of=repository_context.as_of,
|
|
120
|
+
)
|
|
121
|
+
return [
|
|
122
|
+
replace(
|
|
123
|
+
row,
|
|
124
|
+
window_start=baseline_start[row_key(row)],
|
|
125
|
+
window_end=current_window_start[row_key(row)],
|
|
126
|
+
)
|
|
127
|
+
for row in derived
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def attribution_share_row_payload(row: RepoAttributionShare) -> dict[str, object]:
|
|
132
|
+
payload = asdict(row)
|
|
133
|
+
payload["window_start"] = row.window_start.isoformat()
|
|
134
|
+
payload["window_end"] = row.window_end.isoformat()
|
|
135
|
+
return payload
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def attribution_share_alert_payload(
|
|
139
|
+
alert: AttributionShareAlert,
|
|
140
|
+
) -> dict[str, object]:
|
|
141
|
+
return {
|
|
142
|
+
"org_id": alert.org_id,
|
|
143
|
+
"repo": alert.repo,
|
|
144
|
+
"repository_identity": asdict(alert.repository_identity)
|
|
145
|
+
if alert.repository_identity
|
|
146
|
+
else None,
|
|
147
|
+
"kind": alert.kind.value,
|
|
148
|
+
"current": attribution_share_row_payload(alert.current),
|
|
149
|
+
"baseline": (
|
|
150
|
+
attribution_share_row_payload(alert.baseline)
|
|
151
|
+
if alert.baseline is not None
|
|
152
|
+
else None
|
|
153
|
+
),
|
|
154
|
+
"reason": alert.reason,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def format_repository_label(repo: str, identity: RepositoryIdentity | None) -> str:
|
|
159
|
+
"""Show the repository lifetime beside its representative display label."""
|
|
160
|
+
return (
|
|
161
|
+
repo
|
|
162
|
+
if identity is None
|
|
163
|
+
else f"{repo} [{identity.provider}/{identity.host}/{identity.repository_id}]"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _print_row(row: RepoAttributionShare) -> None:
|
|
168
|
+
label = format_repository_label(row.repo, row.repository_identity)
|
|
169
|
+
print(
|
|
170
|
+
f" {label}: git_notes_share={row.git_notes_share:.4f} "
|
|
171
|
+
f"ci=({row.git_notes_share_ci[0]:.4f}, {row.git_notes_share_ci[1]:.4f}) "
|
|
172
|
+
f"agent_plausible_commits={row.agent_plausible_commits} "
|
|
173
|
+
f"git_notes={row.git_notes_attributed} jaccard={row.jaccard_attributed} "
|
|
174
|
+
f"unattributed={row.unattributed}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
179
|
+
"""The argv contract for this report, extracted so the generated
|
|
180
|
+
CLI reference can walk it."""
|
|
181
|
+
parser = argparse.ArgumentParser(
|
|
182
|
+
prog="attribution_share_report",
|
|
183
|
+
description=(
|
|
184
|
+
"Report notes-attribution share per repo and flag a material "
|
|
185
|
+
"decline against a trailing baseline window."
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
parser.add_argument(
|
|
189
|
+
"--org",
|
|
190
|
+
default=os.environ.get("SEDIMENT_ORG_ID"),
|
|
191
|
+
required=not os.environ.get("SEDIMENT_ORG_ID"),
|
|
192
|
+
help="org id (default: $SEDIMENT_ORG_ID)",
|
|
193
|
+
)
|
|
194
|
+
parser.add_argument(
|
|
195
|
+
"--target-margin",
|
|
196
|
+
type=float,
|
|
197
|
+
required=True,
|
|
198
|
+
help=(
|
|
199
|
+
"target CI half-width in raw proportion points for the decline "
|
|
200
|
+
"verdict's min-cases gate, e.g. 0.1 for +/-10pp (worst-case "
|
|
201
|
+
"p=0.5 planner -- see scripts/corpus_sizing.py)"
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
parser.add_argument(
|
|
205
|
+
"--window-days",
|
|
206
|
+
type=int,
|
|
207
|
+
default=7,
|
|
208
|
+
help="current window width in days (default: 7)",
|
|
209
|
+
)
|
|
210
|
+
parser.add_argument(
|
|
211
|
+
"--baseline-window-days",
|
|
212
|
+
type=int,
|
|
213
|
+
default=28,
|
|
214
|
+
help="trailing baseline window width in days (default: 28)",
|
|
215
|
+
)
|
|
216
|
+
parser.add_argument(
|
|
217
|
+
"--json", action="store_true", help="emit JSON instead of a text report"
|
|
218
|
+
)
|
|
219
|
+
add_database_url_argument(parser)
|
|
220
|
+
parser.add_argument(
|
|
221
|
+
"--mirror-path",
|
|
222
|
+
default=os.environ.get("SEDIMENT_MIRROR_PATH", _DEFAULT_MIRROR_PATH),
|
|
223
|
+
help="base dir for git mirrors (default: $SEDIMENT_MIRROR_PATH or "
|
|
224
|
+
f"{_DEFAULT_MIRROR_PATH!r})",
|
|
225
|
+
)
|
|
226
|
+
return parser
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def main(argv: list[str] | None = None) -> int:
|
|
230
|
+
args = build_parser().parse_args(argv)
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
org_id = normalize_org_id(args.org)
|
|
234
|
+
min_cases = required_labelled_examples_for_proportion_margin(
|
|
235
|
+
_WORST_CASE_ASSUMED_RATE, args.target_margin
|
|
236
|
+
)
|
|
237
|
+
policy = AttributionSharePolicy(
|
|
238
|
+
window_days=args.window_days,
|
|
239
|
+
baseline_window_days=args.baseline_window_days,
|
|
240
|
+
min_cases_for_decline_verdict=min_cases,
|
|
241
|
+
)
|
|
242
|
+
except ValueError as exc:
|
|
243
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
244
|
+
return 2
|
|
245
|
+
|
|
246
|
+
with one_shot_fact_store(
|
|
247
|
+
args.database_url, operation="generate attribution-share report"
|
|
248
|
+
) as store:
|
|
249
|
+
mirrors = MirrorManager(args.mirror_path)
|
|
250
|
+
with store.read_snapshot() as snapshot:
|
|
251
|
+
context = read_repository_context(snapshot, org_id, as_of=datetime.now(UTC))
|
|
252
|
+
current = derive_attribution_share(
|
|
253
|
+
snapshot,
|
|
254
|
+
mirrors,
|
|
255
|
+
org_id,
|
|
256
|
+
policy,
|
|
257
|
+
repository_context=context,
|
|
258
|
+
as_of=context.as_of,
|
|
259
|
+
)
|
|
260
|
+
baseline = _baseline_rows(
|
|
261
|
+
snapshot, mirrors, org_id, policy, current, repository_context=context
|
|
262
|
+
)
|
|
263
|
+
alerts = check_attribution_share_alerts(current, baseline, policy)
|
|
264
|
+
|
|
265
|
+
if args.json:
|
|
266
|
+
print(
|
|
267
|
+
json.dumps(
|
|
268
|
+
{
|
|
269
|
+
"min_cases_for_decline_verdict": min_cases,
|
|
270
|
+
"current": [attribution_share_row_payload(row) for row in current],
|
|
271
|
+
"baseline": [
|
|
272
|
+
attribution_share_row_payload(row) for row in baseline
|
|
273
|
+
],
|
|
274
|
+
"alerts": [
|
|
275
|
+
attribution_share_alert_payload(alert) for alert in alerts
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
indent=2,
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
print(f"min_cases_for_decline_verdict: {min_cases}")
|
|
283
|
+
print("current:")
|
|
284
|
+
for row in current:
|
|
285
|
+
_print_row(row)
|
|
286
|
+
print("baseline:")
|
|
287
|
+
for row in baseline:
|
|
288
|
+
_print_row(row)
|
|
289
|
+
print("alerts:")
|
|
290
|
+
for alert in alerts:
|
|
291
|
+
print(
|
|
292
|
+
f" {format_repository_label(alert.repo, alert.repository_identity)}: {alert.kind.value} -- {alert.reason}"
|
|
293
|
+
)
|
|
294
|
+
return 0
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
if __name__ == "__main__":
|
|
298
|
+
raise SystemExit(main())
|