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,438 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""Label-confidence inspection — a stratified sample of attributed_completions with
|
|
3
|
+
``resolve_confidence``'s factor breakdown, so an operator can eyeball what
|
|
4
|
+
the label-confidence policy actually produced without writing Python. NVIDIA's
|
|
5
|
+
agentic-RL guidance treats "run the reward function against 50-100 outputs
|
|
6
|
+
and manually inspect the scores" as mandatory pre-training hygiene; this is
|
|
7
|
+
that hygiene, wired to ``sediment_export.generate_label_confidence_inspection``.
|
|
8
|
+
|
|
9
|
+
Prints an aligned table on stdout by default, or ``--json`` for machines.
|
|
10
|
+
An empty result (no data for the org) is a valid answer — it prints
|
|
11
|
+
"no data" and exits 0, not an error, same convention as
|
|
12
|
+
``model_report.py``.
|
|
13
|
+
|
|
14
|
+
sediment report label-confidence-inspection --org acme-corp
|
|
15
|
+
sediment report label-confidence-inspection --org acme-corp --n 100
|
|
16
|
+
sediment report label-confidence-inspection --org acme-corp --latency-buckets 4
|
|
17
|
+
sediment report label-confidence-inspection --org acme-corp --json
|
|
18
|
+
sediment report label-confidence-inspection --org acme-corp --sensitivity
|
|
19
|
+
sediment report label-confidence-inspection --org acme-corp --sensitivity \\
|
|
20
|
+
--knob implicit_accept_multiplier --values 1.0,1.1,1.5
|
|
21
|
+
|
|
22
|
+
Every row carries a ``human_judgment`` field that is always empty/null — a
|
|
23
|
+
column for a human to fill in later against real data. This tool never
|
|
24
|
+
generates a judgment and labels it as if it were the human's (see
|
|
25
|
+
``sediment_export.label_confidence_inspection``'s module docstring).
|
|
26
|
+
|
|
27
|
+
Storage is read the same way ``model_report.py`` reads it —
|
|
28
|
+
``--database-url``/``SEDIMENT_DATABASE_URL`` and ``--mirror-path``/
|
|
29
|
+
``SEDIMENT_MIRROR_PATH`` — and, like that script, this one does NOT import
|
|
30
|
+
``sediment_api.config.settings`` (which requires ``SEDIMENT_ORG_ID`` and,
|
|
31
|
+
outside dev mode, real auth secrets just to construct): report any org a
|
|
32
|
+
deployment holds via ``--org``, without touching API auth config.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import argparse
|
|
38
|
+
import json
|
|
39
|
+
import os
|
|
40
|
+
import sys
|
|
41
|
+
from dataclasses import asdict
|
|
42
|
+
|
|
43
|
+
from sediment_core import normalize_org_id
|
|
44
|
+
from sediment_derive import MirrorManager
|
|
45
|
+
from sediment_export import (
|
|
46
|
+
DEFAULT_SWEEP_GRIDS,
|
|
47
|
+
LABEL_CONFIDENCE_POLICY_KNOBS,
|
|
48
|
+
DecisionLatencyReport,
|
|
49
|
+
InspectionRow,
|
|
50
|
+
LabelConfidenceSensitivityRow,
|
|
51
|
+
SFTPolicy,
|
|
52
|
+
generate_decision_latency_report,
|
|
53
|
+
generate_label_confidence_inspection,
|
|
54
|
+
generate_label_confidence_sensitivity,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
from ..database import add_database_url_argument, one_shot_fact_store
|
|
58
|
+
|
|
59
|
+
_DEFAULT_MIRROR_PATH = "./mirrors"
|
|
60
|
+
_DEFAULT_N = 50
|
|
61
|
+
_DEFAULT_MIN_CONFIDENCE = 0.6
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _decisions_str(row: InspectionRow) -> str:
|
|
65
|
+
if not row.decisions:
|
|
66
|
+
return "-"
|
|
67
|
+
return "; ".join(
|
|
68
|
+
f"{d.agent_harness}/{'explicit' if d.explicit else 'implicit'}/"
|
|
69
|
+
f"{'accept' if d.accepted else 'reject'}"
|
|
70
|
+
for d in row.decisions
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _ci_str(row: InspectionRow) -> str:
|
|
75
|
+
if not row.ci_outcomes:
|
|
76
|
+
return "-"
|
|
77
|
+
return "; ".join(o.result for o in row.ci_outcomes)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _confidence_str(row: InspectionRow) -> str:
|
|
81
|
+
if row.confidence is None:
|
|
82
|
+
return "None"
|
|
83
|
+
b = row.confidence
|
|
84
|
+
return (
|
|
85
|
+
f"{b.final:.3f} "
|
|
86
|
+
f"(decision={b.decision_factor:.3f} x ci={b.ci_factor:.3f} "
|
|
87
|
+
f"x reliability={b.ci_reliability:.3f} "
|
|
88
|
+
f"x sim={b.similarity_discount:.3f})"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _print_table(rows: list[InspectionRow]) -> None:
|
|
93
|
+
if not rows:
|
|
94
|
+
print("no data")
|
|
95
|
+
return
|
|
96
|
+
for row in rows:
|
|
97
|
+
print(
|
|
98
|
+
f"--- recipe {row.recipe_id} v{row.recipe_version} "
|
|
99
|
+
f"eligibility_source={row.eligibility_source or 'ineligible'}"
|
|
100
|
+
)
|
|
101
|
+
if row.attribution_source == "abandonment":
|
|
102
|
+
print(f" abandonment {row.inference_call_id}")
|
|
103
|
+
else:
|
|
104
|
+
assert row.repo is not None
|
|
105
|
+
assert row.commit_sha is not None
|
|
106
|
+
assert row.file_path is not None
|
|
107
|
+
print(f"--- {row.repo}@{row.commit_sha[:12]} {row.file_path}")
|
|
108
|
+
print(f" inference_call_id: {row.inference_call_id}")
|
|
109
|
+
print(f" snippet: {row.completion_snippet!r}")
|
|
110
|
+
print(f" decisions: {_decisions_str(row)}")
|
|
111
|
+
print(f" ci_outcomes: {_ci_str(row)}")
|
|
112
|
+
if row.attribution_source == "abandonment":
|
|
113
|
+
print(" evidence: abandonment")
|
|
114
|
+
else:
|
|
115
|
+
assert row.similarity_score is not None
|
|
116
|
+
print(
|
|
117
|
+
f" attribution: {row.similarity_score:.3f} "
|
|
118
|
+
f"({row.attribution_source})"
|
|
119
|
+
)
|
|
120
|
+
print(f" decision_branch: {row.decision_branch} ci_bucket: {row.ci_bucket}")
|
|
121
|
+
print(f" confidence: {_confidence_str(row)}")
|
|
122
|
+
print(
|
|
123
|
+
" policy_v3: "
|
|
124
|
+
f"{_float_str(row.policy_v3_confidence)} "
|
|
125
|
+
f"(delta={_delta_str(row.confidence_delta_from_policy_v3)})"
|
|
126
|
+
)
|
|
127
|
+
if row.ci_resolution is not None:
|
|
128
|
+
print(
|
|
129
|
+
" ci_resolution: "
|
|
130
|
+
f"verdict={row.ci_resolution.verdict} "
|
|
131
|
+
f"reliability={_float_str(row.ci_resolution.reliability)} "
|
|
132
|
+
f"suspected_flake={row.ci_resolution.suspected_flake}"
|
|
133
|
+
)
|
|
134
|
+
if row.ci_resolution_skips:
|
|
135
|
+
print(f" ci_skipped: {row.ci_resolution_skips}")
|
|
136
|
+
print(f" human_judgment: {row.human_judgment!r} (fill in by hand)")
|
|
137
|
+
print(f"{len(rows)} row(s)")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _float_str(value: float | None) -> str:
|
|
141
|
+
if value is None:
|
|
142
|
+
return "-"
|
|
143
|
+
return f"{value:.3f}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _delta_str(value: float | int | None) -> str:
|
|
147
|
+
if value is None:
|
|
148
|
+
return "-"
|
|
149
|
+
if isinstance(value, int):
|
|
150
|
+
return f"{value:+d}"
|
|
151
|
+
return f"{value:+.3f}"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _parse_values(raw: str) -> tuple[float, ...]:
|
|
155
|
+
values: list[float] = []
|
|
156
|
+
for part in raw.split(","):
|
|
157
|
+
token = part.strip()
|
|
158
|
+
if not token:
|
|
159
|
+
raise ValueError("--values must be a comma-separated list of floats")
|
|
160
|
+
values.append(float(token))
|
|
161
|
+
return tuple(values)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _print_sensitivity_table(rows: list[LabelConfidenceSensitivityRow]) -> None:
|
|
165
|
+
if not rows:
|
|
166
|
+
print("no data")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
current_stratum: tuple[str, int, str, str] | None = None
|
|
170
|
+
for row in rows:
|
|
171
|
+
stratum = (
|
|
172
|
+
row.recipe_id,
|
|
173
|
+
row.recipe_version,
|
|
174
|
+
row.eligibility_source or "ineligible",
|
|
175
|
+
row.knob,
|
|
176
|
+
)
|
|
177
|
+
if stratum != current_stratum:
|
|
178
|
+
current_stratum = stratum
|
|
179
|
+
print(
|
|
180
|
+
f"--- {row.recipe_id} v{row.recipe_version} "
|
|
181
|
+
f"eligibility_source={row.eligibility_source} {row.knob} "
|
|
182
|
+
f"affected_attributed_completions: {row.affected_attributed_completions}"
|
|
183
|
+
)
|
|
184
|
+
print(
|
|
185
|
+
" value sft_count delta sft_mean sft_median "
|
|
186
|
+
"all_mean all_median v3_all_mean delta_v3"
|
|
187
|
+
)
|
|
188
|
+
print(
|
|
189
|
+
f" {row.value:>5.3f} "
|
|
190
|
+
f"{row.sft_eligible_count:>9d} "
|
|
191
|
+
f"{_delta_str(row.sft_eligible_delta):>5} "
|
|
192
|
+
f"{_float_str(row.sft_confidences.mean):>8} "
|
|
193
|
+
f"{_float_str(row.sft_confidences.median):>10} "
|
|
194
|
+
f"{_float_str(row.all_confidences.mean):>8} "
|
|
195
|
+
f"{_float_str(row.all_confidences.median):>10} "
|
|
196
|
+
f"{_float_str(row.policy_v3_all_confidences.mean):>11} "
|
|
197
|
+
f"{_delta_str(row.all_mean_delta_from_policy_v3):>8}"
|
|
198
|
+
)
|
|
199
|
+
print(f"{len(rows)} sweep row(s)")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _print_latency_report(report: DecisionLatencyReport) -> None:
|
|
203
|
+
if not report.buckets:
|
|
204
|
+
print("no data")
|
|
205
|
+
if any(report.skipped_decisions.values()):
|
|
206
|
+
print(f"skipped: {report.skipped_decisions}")
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
print(
|
|
210
|
+
f"latency_buckets: requested={report.buckets_requested} "
|
|
211
|
+
f"returned={report.buckets_returned}"
|
|
212
|
+
)
|
|
213
|
+
print(
|
|
214
|
+
f"decisions: included={report.included_decisions} "
|
|
215
|
+
f"total={report.total_decisions}"
|
|
216
|
+
)
|
|
217
|
+
print(f"skipped: {report.skipped_decisions}")
|
|
218
|
+
if report.accept_rate_delta is not None:
|
|
219
|
+
print(
|
|
220
|
+
f"accept_rate_delta_slowest_minus_fastest: {report.accept_rate_delta:.3f}"
|
|
221
|
+
)
|
|
222
|
+
if report.mean_confidence_delta is not None:
|
|
223
|
+
print(
|
|
224
|
+
"mean_confidence_delta_slowest_minus_fastest: "
|
|
225
|
+
f"{report.mean_confidence_delta:.3f}"
|
|
226
|
+
)
|
|
227
|
+
for bucket in report.buckets:
|
|
228
|
+
confidence = (
|
|
229
|
+
"None"
|
|
230
|
+
if bucket.mean_confidence is None
|
|
231
|
+
else f"{bucket.mean_confidence:.3f}"
|
|
232
|
+
)
|
|
233
|
+
print(
|
|
234
|
+
f"--- {bucket.bucket} "
|
|
235
|
+
f"{bucket.min_latency_ms:.0f}-{bucket.max_latency_ms:.0f} ms"
|
|
236
|
+
)
|
|
237
|
+
print(
|
|
238
|
+
f" decisions: {bucket.decisions} "
|
|
239
|
+
f"(accepts={bucket.accepts} rejects={bucket.rejects})"
|
|
240
|
+
)
|
|
241
|
+
print(f" accept_rate: {bucket.accept_rate:.3f}")
|
|
242
|
+
print(
|
|
243
|
+
f" mean_latency_ms: {bucket.mean_latency_ms:.1f} "
|
|
244
|
+
f"mean_confidence: {confidence} "
|
|
245
|
+
f"(n={bucket.confidence_count})"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
250
|
+
"""The argv contract for this report, extracted so the generated
|
|
251
|
+
CLI reference can walk it."""
|
|
252
|
+
parser = argparse.ArgumentParser(
|
|
253
|
+
prog="label_confidence_inspection", description=__doc__.splitlines()[0]
|
|
254
|
+
)
|
|
255
|
+
parser.add_argument(
|
|
256
|
+
"--org",
|
|
257
|
+
default=os.environ.get("SEDIMENT_ORG_ID"),
|
|
258
|
+
required=not os.environ.get("SEDIMENT_ORG_ID"),
|
|
259
|
+
help="org id (default: $SEDIMENT_ORG_ID)",
|
|
260
|
+
)
|
|
261
|
+
parser.add_argument(
|
|
262
|
+
"--n",
|
|
263
|
+
type=int,
|
|
264
|
+
default=None,
|
|
265
|
+
help=f"sample size, stratified across the confidence ladder's real "
|
|
266
|
+
f"branches (default: {_DEFAULT_N})",
|
|
267
|
+
)
|
|
268
|
+
parser.add_argument(
|
|
269
|
+
"--json", action="store_true", help="emit JSON rows instead of a table"
|
|
270
|
+
)
|
|
271
|
+
parser.add_argument(
|
|
272
|
+
"--recipe",
|
|
273
|
+
choices=("sft_curated", "sft_verified"),
|
|
274
|
+
default=None,
|
|
275
|
+
help="SFT evidence recipe (default: sft_curated)",
|
|
276
|
+
)
|
|
277
|
+
parser.add_argument(
|
|
278
|
+
"--sensitivity",
|
|
279
|
+
action="store_true",
|
|
280
|
+
help="sweep LabelConfidencePolicy knobs over all assembled attributed_completions",
|
|
281
|
+
)
|
|
282
|
+
parser.add_argument(
|
|
283
|
+
"--knob",
|
|
284
|
+
action="append",
|
|
285
|
+
choices=LABEL_CONFIDENCE_POLICY_KNOBS,
|
|
286
|
+
help="LabelConfidencePolicy knob to sweep in sensitivity mode; may repeat",
|
|
287
|
+
)
|
|
288
|
+
parser.add_argument(
|
|
289
|
+
"--values",
|
|
290
|
+
help="comma-separated grid for a single --knob in sensitivity mode",
|
|
291
|
+
)
|
|
292
|
+
parser.add_argument(
|
|
293
|
+
"--min-confidence",
|
|
294
|
+
type=float,
|
|
295
|
+
default=None,
|
|
296
|
+
help=f"SFT confidence floor for sensitivity mode "
|
|
297
|
+
f"(default: {_DEFAULT_MIN_CONFIDENCE})",
|
|
298
|
+
)
|
|
299
|
+
parser.add_argument(
|
|
300
|
+
"--latency-buckets",
|
|
301
|
+
type=int,
|
|
302
|
+
help="emit the decision-latency diagnostic using this many quantile "
|
|
303
|
+
"buckets instead of the label-confidence-inspection sample",
|
|
304
|
+
)
|
|
305
|
+
add_database_url_argument(parser)
|
|
306
|
+
parser.add_argument(
|
|
307
|
+
"--mirror-path",
|
|
308
|
+
default=os.environ.get("SEDIMENT_MIRROR_PATH", _DEFAULT_MIRROR_PATH),
|
|
309
|
+
help="base dir for git mirrors (default: $SEDIMENT_MIRROR_PATH or "
|
|
310
|
+
f"{_DEFAULT_MIRROR_PATH!r})",
|
|
311
|
+
)
|
|
312
|
+
return parser
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def main(argv: list[str] | None = None) -> int:
|
|
316
|
+
args = build_parser().parse_args(argv)
|
|
317
|
+
|
|
318
|
+
if args.sensitivity and args.latency_buckets is not None:
|
|
319
|
+
print(
|
|
320
|
+
"error: use --sensitivity or --latency-buckets, not both",
|
|
321
|
+
file=sys.stderr,
|
|
322
|
+
)
|
|
323
|
+
return 2
|
|
324
|
+
if args.knob and not args.sensitivity:
|
|
325
|
+
print("error: --knob requires --sensitivity", file=sys.stderr)
|
|
326
|
+
return 2
|
|
327
|
+
if args.min_confidence is not None and not args.sensitivity:
|
|
328
|
+
print("error: --min-confidence requires --sensitivity", file=sys.stderr)
|
|
329
|
+
return 2
|
|
330
|
+
if args.n is not None and args.sensitivity:
|
|
331
|
+
print("error: --n does not apply to --sensitivity", file=sys.stderr)
|
|
332
|
+
return 2
|
|
333
|
+
if args.n is not None and args.latency_buckets is not None:
|
|
334
|
+
print("error: --n does not apply to --latency-buckets", file=sys.stderr)
|
|
335
|
+
return 2
|
|
336
|
+
if args.recipe is not None and args.latency_buckets is not None:
|
|
337
|
+
print("error: --recipe does not apply to --latency-buckets", file=sys.stderr)
|
|
338
|
+
return 2
|
|
339
|
+
n = args.n if args.n is not None else _DEFAULT_N
|
|
340
|
+
recipe = args.recipe if args.recipe is not None else "sft_curated"
|
|
341
|
+
min_confidence = (
|
|
342
|
+
args.min_confidence
|
|
343
|
+
if args.min_confidence is not None
|
|
344
|
+
else _DEFAULT_MIN_CONFIDENCE
|
|
345
|
+
)
|
|
346
|
+
if n <= 0:
|
|
347
|
+
print("error: --n must be a positive integer", file=sys.stderr)
|
|
348
|
+
return 2
|
|
349
|
+
if args.values and not args.sensitivity:
|
|
350
|
+
print("error: --values requires --sensitivity", file=sys.stderr)
|
|
351
|
+
return 2
|
|
352
|
+
if args.values and (not args.knob or len(args.knob) != 1):
|
|
353
|
+
print("error: --values requires exactly one --knob", file=sys.stderr)
|
|
354
|
+
return 2
|
|
355
|
+
if not 0.0 <= min_confidence <= 1.0:
|
|
356
|
+
print("error: --min-confidence must be in [0.0, 1.0]", file=sys.stderr)
|
|
357
|
+
return 2
|
|
358
|
+
if args.latency_buckets is not None and args.latency_buckets <= 0:
|
|
359
|
+
print("error: --latency-buckets must be a positive integer", file=sys.stderr)
|
|
360
|
+
return 2
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
org_id = normalize_org_id(args.org)
|
|
364
|
+
except ValueError as exc:
|
|
365
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
366
|
+
return 2
|
|
367
|
+
|
|
368
|
+
with one_shot_fact_store(
|
|
369
|
+
args.database_url, operation="inspect label confidence"
|
|
370
|
+
) as store:
|
|
371
|
+
mirrors = MirrorManager(args.mirror_path)
|
|
372
|
+
sft_policy = SFTPolicy(
|
|
373
|
+
recipe_id=recipe,
|
|
374
|
+
min_confidence=min_confidence,
|
|
375
|
+
)
|
|
376
|
+
if args.sensitivity:
|
|
377
|
+
if args.knob:
|
|
378
|
+
try:
|
|
379
|
+
values = _parse_values(args.values) if args.values else None
|
|
380
|
+
except ValueError as exc:
|
|
381
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
382
|
+
return 2
|
|
383
|
+
grids = {
|
|
384
|
+
knob: values if values is not None else DEFAULT_SWEEP_GRIDS[knob]
|
|
385
|
+
for knob in args.knob
|
|
386
|
+
}
|
|
387
|
+
else:
|
|
388
|
+
grids = DEFAULT_SWEEP_GRIDS
|
|
389
|
+
rows = generate_label_confidence_sensitivity(
|
|
390
|
+
store,
|
|
391
|
+
mirrors,
|
|
392
|
+
org_id,
|
|
393
|
+
sft_min_confidence=min_confidence,
|
|
394
|
+
sft_policy=sft_policy,
|
|
395
|
+
grids=grids,
|
|
396
|
+
)
|
|
397
|
+
latency_report = None
|
|
398
|
+
elif args.latency_buckets is not None:
|
|
399
|
+
latency_report = generate_decision_latency_report(
|
|
400
|
+
store,
|
|
401
|
+
mirrors,
|
|
402
|
+
org_id,
|
|
403
|
+
latency_buckets=args.latency_buckets,
|
|
404
|
+
)
|
|
405
|
+
rows = None
|
|
406
|
+
else:
|
|
407
|
+
rows = generate_label_confidence_inspection(
|
|
408
|
+
store,
|
|
409
|
+
mirrors,
|
|
410
|
+
org_id,
|
|
411
|
+
n=n,
|
|
412
|
+
sft_policy=sft_policy,
|
|
413
|
+
)
|
|
414
|
+
latency_report = None
|
|
415
|
+
|
|
416
|
+
if args.json:
|
|
417
|
+
if latency_report is not None:
|
|
418
|
+
print(json.dumps(asdict(latency_report), indent=2))
|
|
419
|
+
else:
|
|
420
|
+
print(json.dumps([asdict(r) for r in rows or []], indent=2))
|
|
421
|
+
else:
|
|
422
|
+
if latency_report is not None:
|
|
423
|
+
print(f"org: {org_id} latency buckets: {args.latency_buckets}")
|
|
424
|
+
_print_latency_report(latency_report)
|
|
425
|
+
elif args.sensitivity:
|
|
426
|
+
print(
|
|
427
|
+
f"org: {org_id} sensitivity sweep "
|
|
428
|
+
f"sft min_confidence: {min_confidence:.3f}"
|
|
429
|
+
)
|
|
430
|
+
_print_sensitivity_table(rows)
|
|
431
|
+
else:
|
|
432
|
+
print(f"org: {org_id} sample size: {n}")
|
|
433
|
+
_print_table(rows or [])
|
|
434
|
+
return 0
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
if __name__ == "__main__":
|
|
438
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""JSON-only accepted-work lifecycle report."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from sediment_core import normalize_org_id
|
|
11
|
+
from sediment_derive import MirrorManager
|
|
12
|
+
|
|
13
|
+
from ..database import add_database_url_argument, one_shot_fact_store
|
|
14
|
+
from ..services.operational_reports import (
|
|
15
|
+
LifecycleReportRequest,
|
|
16
|
+
generate_lifecycle_report,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
_DEFAULT_MIRROR_PATH = "./mirrors"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
23
|
+
parser = argparse.ArgumentParser(
|
|
24
|
+
prog="lifecycle_report",
|
|
25
|
+
description="Report accepted-work progression and operational evidence.",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--org",
|
|
29
|
+
default=os.environ.get("SEDIMENT_ORG_ID"),
|
|
30
|
+
required=not os.environ.get("SEDIMENT_ORG_ID"),
|
|
31
|
+
help="org id (default: $SEDIMENT_ORG_ID)",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--json",
|
|
35
|
+
action="store_true",
|
|
36
|
+
required=True,
|
|
37
|
+
help="emit the canonical JSON artifact (required)",
|
|
38
|
+
)
|
|
39
|
+
add_database_url_argument(parser)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--mirror-path",
|
|
42
|
+
default=os.environ.get("SEDIMENT_MIRROR_PATH", _DEFAULT_MIRROR_PATH),
|
|
43
|
+
help="base dir for git mirrors (default: $SEDIMENT_MIRROR_PATH or "
|
|
44
|
+
f"{_DEFAULT_MIRROR_PATH!r})",
|
|
45
|
+
)
|
|
46
|
+
return parser
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main(argv: list[str] | None = None) -> int:
|
|
50
|
+
args = build_parser().parse_args(argv)
|
|
51
|
+
try:
|
|
52
|
+
org_id = normalize_org_id(args.org)
|
|
53
|
+
except ValueError as exc:
|
|
54
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
55
|
+
return 2
|
|
56
|
+
try:
|
|
57
|
+
with one_shot_fact_store(
|
|
58
|
+
args.database_url, operation="generate accepted-work lifecycle report"
|
|
59
|
+
) as store:
|
|
60
|
+
envelope = generate_lifecycle_report(
|
|
61
|
+
store,
|
|
62
|
+
MirrorManager(args.mirror_path),
|
|
63
|
+
LifecycleReportRequest(org_id),
|
|
64
|
+
)
|
|
65
|
+
except Exception as exc:
|
|
66
|
+
print(f"error: couldn't generate lifecycle report: {exc}", file=sys.stderr)
|
|
67
|
+
return 1
|
|
68
|
+
print(envelope.to_json())
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""Pull request merge-retention diagnostic report.
|
|
3
|
+
|
|
4
|
+
sediment report merge-retention --org acme-corp
|
|
5
|
+
sediment report merge-retention --org acme-corp --json
|
|
6
|
+
sediment report merge-retention --org acme-corp --rows-out review.jsonl
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from dataclasses import asdict
|
|
16
|
+
|
|
17
|
+
from sediment_core import normalize_org_id
|
|
18
|
+
from sediment_derive import MirrorManager
|
|
19
|
+
from sediment_export import (
|
|
20
|
+
MergeRetentionReport,
|
|
21
|
+
RetentionScoreSummary,
|
|
22
|
+
generate_merge_retention_report_result,
|
|
23
|
+
merge_retention_to_export_rows,
|
|
24
|
+
write_jsonl,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from ..database import add_database_url_argument, one_shot_fact_store
|
|
28
|
+
|
|
29
|
+
_DEFAULT_MIRROR_PATH = "./mirrors"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _format_counts(counts: dict[str, int]) -> str:
|
|
33
|
+
return (
|
|
34
|
+
", ".join(f"{key}={value}" for key, value in sorted(counts.items())) or "none"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _print_score(name: str, summary: RetentionScoreSummary) -> None:
|
|
39
|
+
distribution = summary.distribution
|
|
40
|
+
assert distribution.mean is not None
|
|
41
|
+
assert distribution.median is not None
|
|
42
|
+
assert distribution.p10 is not None
|
|
43
|
+
assert distribution.p90 is not None
|
|
44
|
+
print(
|
|
45
|
+
f"{name}: n={distribution.count} mean={distribution.mean:.3f} "
|
|
46
|
+
f"median={distribution.median:.3f} p10={distribution.p10:.3f} "
|
|
47
|
+
f"p90={distribution.p90:.3f}"
|
|
48
|
+
)
|
|
49
|
+
for threshold in summary.thresholds:
|
|
50
|
+
rate = "no data" if threshold.rate is None else f"{threshold.rate:.3f}"
|
|
51
|
+
print(
|
|
52
|
+
f" below {threshold.threshold:.1f}: {threshold.below}/"
|
|
53
|
+
f"{threshold.total} ({rate})"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _print_table(report: MergeRetentionReport) -> None:
|
|
58
|
+
print(f"attributed file candidates: {report.attributed_file_candidates}")
|
|
59
|
+
print(f"candidates joined to merge: {report.candidates_joined_to_merge}")
|
|
60
|
+
print(f"scored rows: {report.scored_rows}")
|
|
61
|
+
print(f"pull requests with joined candidates: {report.joined_pull_requests}")
|
|
62
|
+
print(f"pull requests with scored rows: {report.scored_pull_requests}")
|
|
63
|
+
print(f"membership: {_format_counts(report.membership)}")
|
|
64
|
+
print(f"scoring skips: {_format_counts(report.scoring_skips)}")
|
|
65
|
+
for unit, counts in sorted(report.repository_skipped.items()):
|
|
66
|
+
print(f"repository skips ({unit}): {_format_counts(counts)}")
|
|
67
|
+
print(f"attribution skips: {_format_counts(report.attribution_skips)}")
|
|
68
|
+
print(
|
|
69
|
+
f"decision attachment skips: {_format_counts(report.decision_attachment_skips)}"
|
|
70
|
+
)
|
|
71
|
+
if report.scored_rows == 0:
|
|
72
|
+
print("scores: no data")
|
|
73
|
+
else:
|
|
74
|
+
_print_score("final head", report.head)
|
|
75
|
+
_print_score("merged commit", report.merge)
|
|
76
|
+
print(f"explicit accepted rows: {report.explicit_accept_rows}")
|
|
77
|
+
for name, thresholds in (
|
|
78
|
+
("explicit final head", report.explicit_accept_head_thresholds),
|
|
79
|
+
("explicit merged commit", report.explicit_accept_merge_thresholds),
|
|
80
|
+
):
|
|
81
|
+
for threshold in thresholds:
|
|
82
|
+
rate = "no data" if threshold.rate is None else f"{threshold.rate:.3f}"
|
|
83
|
+
print(
|
|
84
|
+
f"{name} below {threshold.threshold:.1f}: {threshold.below}/"
|
|
85
|
+
f"{threshold.total} ({rate})"
|
|
86
|
+
)
|
|
87
|
+
print(f"attribution provenance: {report.attribution_provenance}")
|
|
88
|
+
print(f"merge retention provenance: {report.merge_retention_provenance}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
92
|
+
"""Return the argv contract consumed by the generated CLI reference."""
|
|
93
|
+
parser = argparse.ArgumentParser(
|
|
94
|
+
prog="merge_retention_report",
|
|
95
|
+
description="Measure attributed file changes at pull request merge boundaries.",
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--org",
|
|
99
|
+
default=os.environ.get("SEDIMENT_ORG_ID"),
|
|
100
|
+
required=not os.environ.get("SEDIMENT_ORG_ID"),
|
|
101
|
+
help="org id (default: $SEDIMENT_ORG_ID)",
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--json", action="store_true", help="emit JSON instead of a text report"
|
|
105
|
+
)
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"--rows-out",
|
|
108
|
+
metavar="FILE",
|
|
109
|
+
help="write canonical merge-retention rows to FILE as JSONL",
|
|
110
|
+
)
|
|
111
|
+
add_database_url_argument(parser)
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"--mirror-path",
|
|
114
|
+
default=os.environ.get("SEDIMENT_MIRROR_PATH", _DEFAULT_MIRROR_PATH),
|
|
115
|
+
help="base dir for git mirrors (default: $SEDIMENT_MIRROR_PATH or "
|
|
116
|
+
f"{_DEFAULT_MIRROR_PATH!r})",
|
|
117
|
+
)
|
|
118
|
+
return parser
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def main(argv: list[str] | None = None) -> int:
|
|
122
|
+
args = build_parser().parse_args(argv)
|
|
123
|
+
try:
|
|
124
|
+
org_id = normalize_org_id(args.org)
|
|
125
|
+
except ValueError as exc:
|
|
126
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
127
|
+
return 2
|
|
128
|
+
with one_shot_fact_store(
|
|
129
|
+
args.database_url, operation="generate merge-retention report"
|
|
130
|
+
) as store:
|
|
131
|
+
result = generate_merge_retention_report_result(
|
|
132
|
+
store,
|
|
133
|
+
MirrorManager(args.mirror_path),
|
|
134
|
+
org_id,
|
|
135
|
+
)
|
|
136
|
+
report = result.report
|
|
137
|
+
if args.json:
|
|
138
|
+
print(json.dumps(asdict(report), indent=2))
|
|
139
|
+
else:
|
|
140
|
+
_print_table(report)
|
|
141
|
+
if args.rows_out:
|
|
142
|
+
try:
|
|
143
|
+
write_result = write_jsonl(
|
|
144
|
+
merge_retention_to_export_rows(result.rows),
|
|
145
|
+
args.rows_out,
|
|
146
|
+
split_enabled=False,
|
|
147
|
+
)
|
|
148
|
+
except (OSError, TypeError, ValueError) as exc:
|
|
149
|
+
print(f"error: couldn't write merge-retention rows: {exc}", file=sys.stderr)
|
|
150
|
+
return 1
|
|
151
|
+
if write_result.written:
|
|
152
|
+
print(
|
|
153
|
+
f"wrote {len(result.rows)} merge-retention rows to {args.rows_out}",
|
|
154
|
+
file=sys.stderr,
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
print(
|
|
158
|
+
f"wrote 0 merge-retention rows; left {args.rows_out} unchanged",
|
|
159
|
+
file=sys.stderr,
|
|
160
|
+
)
|
|
161
|
+
return 0
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
raise SystemExit(main())
|