6Compose two committed result files into one: the first decides, the second is
7consulted only where the first is unsure.
11Every leg of this bench so far has measured one model alone. The escalation
12column says what a model would hand to a human below a confidence threshold,
13and plan 293 showed that Jev, asked to flag below 0.9, catches every one of its
14own permission-granting errors. The fine-tuned encoders beat Jev on agreement
15at half the latency and no per-call cost, and at 0.9 they hand 42 to 51 of 300
16hard-set decisions upwards. Nobody has measured what happens when those
17escalated decisions go to Jev instead of to a human.
19Every piece of that measurement is already on disk, so this is a join rather
20than a run: the encoder result files and the Jev result files carry the same
21commands in the same order, and a combination is a pure function of two rows
22and a threshold. Nothing here opens a socket, loads a checkpoint or touches a
23GPU; the two inputs are read-only and the output is a result file in the bench
24schema that :mod:`score` reads unchanged.
26A row is UNSURE when the first model's top probability is strictly below the
27threshold or the row has no usable ``probs``. At or above the threshold every
28rule keeps the first model's verdict and ``probs`` untouched, so the four rules
29differ only in what they do with the unsure rows:
32 The unsure row becomes ``confirm`` with no ``probs`` and nothing is
33 consulted. This is the CONTROL: the encoder alone with a human behind it,
34 which is the operating point plans 293 and 300 already recommend.
36 The unsure row takes the second model's verdict and ``probs``.
38 The unsure row takes whichever of the two verdicts grants LESS permission,
39 with the ``probs`` of the model whose verdict was taken.
41 The unsure row keeps the first's verdict and ``probs`` where the two agree,
42 and becomes ``confirm`` with no ``probs`` where they differ.
44``--sweep`` writes ``results/combined_sweep.md``: the same compositions at six
45thresholds, and the movement across the repeated Jev runs, scored in memory so
46that a threshold curve costs no committed files. It carries no generation date
47precisely so that a second run reproduces it byte for byte.
49Run it under the project venv from the subproject root::
51 python bench/argv_policy/combine.py --first results/encoder_small-7-c299_hard.json \\
52 --second results/jev_direct_hard.json --rule replace --threshold 0.9 \\
53 --out results/combined_encoder_small-7-c299_replace_t90_hard.json
55 python bench/argv_policy/combine.py --sweep
59- :func:`rule_escalate` / :func:`rule_replace` / :func:`rule_stricter` /
60 :func:`rule_disagree` -- the four rules, each a pure function of one first
61 row, one second row and the threshold.
62- :func:`join_rows` -- the command-keyed join, and the refusal when the two
63 files do not carry the same commands.
64- :func:`build_payload` -- two payloads to one result payload with provenance.
65- :func:`render_sweep` -- the generated markdown of the threshold sweep and the
66 repeated-run movement.
67- :func:`main` -- the CLI.
73 L2 - combination lifecycle (files read, file written, sweep rendered)
74 L3 - per-combination counts (consulted, disagreeing)
79- Read-only in both inputs: a combination never rewrites a result file, and the
80 only paths written are the ``--out`` target and the sweep document.
81- No network, no torch, no key: the Jev files ARE the Jev runs.
82- The permission ordering is :data:`score.PERMISSIVENESS` itself, imported
83 rather than restated, so ``stricter`` can never disagree with the direction
84 columns the scorer reports.
85- A threshold comparison is STRICT: a top probability exactly equal to the
86 threshold is sure, which is the same reading ``score.py`` uses for its
88- Provenance stores each input path RELATIVE to the bench directory when it
89 lies inside it, so a committed combination is reproducible from any checkout
90 rather than naming the worktree it was made in.
91- Console output is ASCII only; commands are printed through :func:`ascii`.
95- oc_diagnostics (structured debug logging via _dbg)
96- score (the bench scorer, for PERMISSIVENESS and for the sweep's arithmetic)
98from __future__
import annotations
105from dataclasses
import dataclass
106from pathlib
import Path
107from typing
import Any, Callable
109from oc_diagnostics
import _dbg
111from score
import PERMISSIVENESS, Score, score_payload
113_dbg(
"OC_CHAT.MAIN",
"module",
"loaded", 4)
119Payload = dict[str, Any]
122RULES: tuple[str, ...] = (
"escalate",
"replace",
"stricter",
"disagree")
126CONSULTING_RULES: tuple[str, ...] = (
"replace",
"stricter",
"disagree")
129ESCALATION_VERDICT =
"confirm"
134UNKNOWN_RANK: int = max(PERMISSIVENESS.values()) + 1
138DEFAULT_THRESHOLD = 0.9
141SWEEP_FIRSTS: tuple[str, ...] = (
142 "encoder_small-7-c297",
143 "encoder_small-7-c299",
144 "encoder_xsmall-7-c297-sc4-dw1",
148SWEEP_SECONDS_HARD: tuple[str, ...] = (
150 "jev_direct_hard_rep1",
151 "jev_direct_hard_rep2",
155SWEEP_SECONDS_HAND: tuple[str, ...] = (
"jev_direct_hand",
"jev_direct_hand_ref")
158SWEEP_THRESHOLDS: tuple[float, ...] = (0.5, 0.7, 0.8, 0.9, 0.95, 0.99)
162 """A combination that cannot be made: a bad join, or a missing input."""
165@dataclass(frozen=
True)
167 """What one rule did with one pair of rows."""
170 probs: dict[str, float] |
None
175 """The row's top forecast probability, or ``None`` when it has none.
177 The same reading ``score.py`` takes of the same field, and the tests assert
178 that equality on a battery of rows rather than trusting this comment: a row
179 whose ``probs`` is missing, empty or not a mapping has no forecast at all,
180 and is not a zero-confidence one.
183 probs = row.get(
"probs")
184 if not isinstance(probs, dict)
or not probs:
186 values = [float(value)
for value
in probs.values()]
187 return max(values)
if values
else None
191 """Whether the first model is unsure enough on this row to be combined.
193 Strictly below the threshold, so a top probability exactly equal to it is
194 SURE and the row passes through untouched. A row with no usable forecast is
195 unsure: a model that produced no distribution has expressed no confidence,
196 and reading that as certainty is the one direction this must not round in.
200 return top
is None or top < threshold
204 """A copy of the row's ``probs``, or ``None`` when it has none worth carrying."""
206 probs = row.get(
"probs")
207 if not isinstance(probs, dict)
or not probs:
213 """How much permission one verdict grants, under the scorer's own ordering."""
215 return PERMISSIVENESS.get(prediction, UNKNOWN_RANK)
219 """The sure case, which every rule shares: the first model, untouched."""
225 threshold: float) -> Decision:
226 """The control: an unsure row goes to a human, and nothing is consulted.
228 ``second`` is accepted and deliberately unread. Every rule shares one
229 signature so that the sweep can run all four over the same joined rows, and
230 the control is the rule that has a second opinion available and declines to
231 ask for it -- which is exactly the operating point it controls for.
236 return Decision(ESCALATION_VERDICT,
None,
False)
240 threshold: float) -> Decision:
241 """An unsure row takes the second model's verdict and its ``probs``."""
246 raise CombineError(
"rule 'replace' needs a second result file")
251 threshold: float) -> Decision:
252 """An unsure row takes whichever verdict grants less permission.
254 The ``probs`` travel with the verdict that was taken, so the forecast a row
255 carries is always the forecast of the model that decided it. Where the two
256 agree -- including where they agree on a verdict neither label set knows --
257 the first model's forecast is kept, because it is the model that would have
264 raise CombineError(
"rule 'stricter' needs a second result file")
265 first_pred, second_pred = str(first[
"pred"]), str(second[
"pred"])
272 threshold: float) -> Decision:
273 """An unsure row survives agreement and goes to a human on disagreement."""
278 raise CombineError(
"rule 'disagree' needs a second result file")
279 if str(second[
"pred"]) == str(first[
"pred"]):
281 return Decision(ESCALATION_VERDICT,
None,
True)
287RULE_FUNCTIONS: dict[str, Callable[..., Decision]] = {
288 "escalate": rule_escalate,
289 "replace": rule_replace,
290 "stricter": rule_stricter,
291 "disagree": rule_disagree,
296 """Index one file's rows by command, refusing a duplicate.
298 A repeated command would make the join ambiguous -- two truths, two
299 forecasts, and no rule for which one wins -- so it is refused here rather
300 than resolved silently. No case file on this bench carries one.
303 index: dict[str, Row] = {}
305 command = str(row[
"command"])
307 raise CombineError(f
"{label}: command appears twice: {ascii(command)}")
313 second_rows: list[Row] |
None,
314 first_label: str, second_label: str) -> list[Row |
None]:
315 """Line the second file's rows up with the first file's, in the first's order.
317 Returns one entry per first row: the matching second row, or ``None`` for
318 every row when there is no second file. Refuses when the two files do not
319 carry exactly the same commands, or when they disagree about the truth of
320 one -- both of those mean the two runs are not the same measurement, and
321 every figure downstream would be a join artefact rather than a reading.
324 if second_rows
is None:
325 return [
None] * len(first_rows)
328 missing = sorted(set(first_index) - set(second_index))
329 extra = sorted(set(second_index) - set(first_index))
332 f
"{first_label} and {second_label} do not carry the same commands: "
333 f
"{len(missing)} only in the first, {len(extra)} only in the second"
334 + (f
"; first missing: {ascii(missing[0])}" if missing
else "")
335 + (f
"; first extra: {ascii(extra[0])}" if extra
else "")
337 for command, row
in first_index.items():
338 other = second_index[command]
339 if str(row.get(
"truth")) != str(other.get(
"truth")):
341 f
"{first_label} and {second_label} disagree on the truth of "
342 f
"{ascii(command)}: {row.get('truth')!r} against {other.get('truth')!r}"
344 return [second_index[str(row[
"command"])]
for row
in first_rows]
348 consulted: bool) -> float |
None:
349 """The latency of the combined decision: the first's, plus the second's when asked."""
351 values = [float(first[
"ms"])]
if first.get(
"ms")
is not None else []
352 if consulted
and second
is not None and second.get(
"ms")
is not None:
353 values.append(float(second[
"ms"]))
354 return sum(values)
if values
else None
358 """The threshold as the two digits the file names and model names carry."""
360 return f
"t{round(threshold * 100):02d}"
364 """The SHA-256 of one input file, which is what pins a combination to its inputs."""
366 return hashlib.sha256(Path(path).read_bytes()).hexdigest()
370 """One input path, relative to the bench directory when it lies inside it.
372 A committed combination names ``results/jev_direct_hard.json`` rather than
373 the worktree it happened to be made in, so anyone can rerun it from their
374 own checkout and compare digests instead of paths.
377 resolved = Path(path).resolve()
379 return resolved.relative_to(Path(bench_dir).resolve()).as_posix()
381 return resolved.as_posix()
385 seconds: list[Row |
None],
386 rule: str, threshold: float) -> list[Row]:
387 """Apply one rule down the two joined row lists, in the first file's order."""
389 apply_rule = RULE_FUNCTIONS[rule]
391 for first, second
in zip(first_rows, seconds):
392 decision = apply_rule(first, second, threshold)
394 "command": str(first[
"command"]),
395 "truth": str(first[
"truth"]),
396 "slice": str(first.get(
"slice",
"")),
397 "pred": decision.pred,
399 if decision.probs
is not None:
400 row[
"probs"] = decision.probs
402 row[
"consulted"] = decision.consulted
403 row[
"first_pred"] = str(first[
"pred"])
404 row[
"second_pred"] =
None if second
is None else str(second[
"pred"])
410 second_payload: Payload |
None,
411 *, rule: str, threshold: float,
412 first_path: Path, second_path: Path |
None,
413 bench_dir: Path, date: str) -> Payload:
414 """Two loaded result payloads to one combined result payload.
416 The output is a bench result file and nothing more exotic: ``score.py``
417 reads it with no change, and the three fields it does not read --
418 ``consulted``, ``first_pred``, ``second_pred`` -- are there so a reader can
419 take any row apart without rerunning the combination.
422 func =
"build_payload"
423 first_rows = list(first_payload.get(
"results", []))
424 second_rows = list(second_payload.get(
"results", []))
if second_payload
else None
425 first_model = str(first_payload.get(
"model",
"?"))
426 second_model = str(second_payload.get(
"model",
"?"))
if second_payload
else None
427 seconds =
join_rows(first_rows, second_rows, first_model,
428 second_model
or "(no second file)")
429 rows =
combine_rows(first_rows, seconds, rule, threshold)
431 consulted = sum(1
for row
in rows
if row[
"consulted"])
432 disagreeing = sum(1
for row
in rows
433 if row[
"consulted"]
and row[
"first_pred"] != row[
"second_pred"])
434 cost_usd: float |
None =
None
435 if second_payload
is not None and second_payload.get(
"cost_usd")
is not None and second_rows:
436 cost_usd = consulted * float(second_payload[
"cost_usd"]) / len(second_rows)
438 "model": f
"combined_{first_model}_{rule}_{_threshold_tag(threshold)}",
439 "condition": second_model
or "none",
441 "cost_usd": cost_usd,
446 "threshold": threshold,
448 "first_model": first_model,
449 "first_condition": str(first_payload.get(
"condition",
"-")),
450 "first_sha256":
_digest(first_path),
451 "second_file":
None if second_path
is None else _relative_path(second_path, bench_dir),
452 "second_model": second_model,
453 "second_condition": (str(second_payload.get(
"condition",
"-"))
454 if second_payload
else None),
455 "second_sha256":
None if second_path
is None else _digest(second_path),
456 "consulted": consulted,
457 "disagreeing": disagreeing,
458 "confirm_issued": sum(1
for row
in rows
if row[
"pred"] == ESCALATION_VERDICT),
459 "confirm_required": sum(1
for row
in rows
if row[
"truth"] == ESCALATION_VERDICT),
462 _dbg(
"OC_CHAT.MAIN", func,
463 f
"{payload['model']}: consulted {consulted}, disagreeing {disagreeing}", 3)
468 """Read one result file, refusing a path that is not there."""
471 if not target.is_file():
473 return json.loads(target.read_text(encoding=
"utf-8"))
481@dataclass(frozen=True)
483 """One scored combination, as the sweep tables print it."""
494 """What the consulted decisions would have cost at the second's own rate."""
496 return self.score.cost_usd
500 """Result files read once and reused across the sweep's 130-odd combinations."""
507 def path(self, stem: str) -> Path:
508 """The result file for one stem."""
510 return self.
_dir / f
"{stem}.json"
513 """One result payload, loaded at most once."""
521 """The SHA-256 of one result file, computed at most once."""
527def _sweep_row(cache: _PayloadCache, first: str, second: str, rule: str,
528 threshold: float, bench_dir: Path) -> SweepRow:
529 """Compose and score one combination in memory, writing nothing."""
532 cache.payload(first), cache.payload(second), rule=rule, threshold=threshold,
533 first_path=cache.path(first), second_path=cache.path(second),
534 bench_dir=bench_dir, date=
"",
536 provenance = payload[
"provenance"]
537 assert isinstance(provenance, dict)
538 return SweepRow(first=first, rule=rule, threshold=threshold, second=second,
539 score=score_payload(payload, f
"{first}|{rule}", threshold),
540 consulted=int(provenance[
"consulted"]))
544 """A latency in milliseconds, to one decimal, or a dash."""
546 return "-" if value
is None else f
"{value:.1f}"
550 """A cost in US dollars, to four decimals, or a dash."""
552 return "-" if value
is None else f
"{value:.4f}"
556 """The threshold sweep: one line per first, rule and threshold."""
558 lines = [
"| first | rule | threshold | agreement | granted more | granted less "
559 "| consulted | confirm issued | median ms | cost USD |",
560 "|---|---|---:|---:|---:|---:|---:|---:|---:|---:|"]
564 f
"| `{row.first}` | `{row.rule}` | {row.threshold:.2f} | {score.agreement} "
565 f
"| {score.granted_more} | {score.granted_less} | {row.consulted} "
566 f
"| {_confirm_issued(score)} | {_ms_cell(score.median_ms)} "
567 f
"| {_cost_cell(row.cost_usd)} |"
569 return "\n".join(lines) +
"\n"
573 """How many ``confirm`` verdicts one scored combination issued."""
575 return int(score.provenance[
"confirm_issued"])
if score.provenance
else 0
579 key: Callable[[SweepRow], tuple[object, ...]]
580 ) -> list[tuple[tuple[object, ...], list[SweepRow]]]:
581 """Group scored rows by a key, in the order the keys first appear.
583 Grouping on the row's OWN fields rather than on the module constants is
584 what keeps a range table honest: a key spelled by hand can miss every row
585 and print an empty table, which is exactly what a reader would read as "no
586 movement between runs".
589 groups: dict[tuple[object, ...], list[SweepRow]] = {}
591 groups.setdefault(key(row), []).append(row)
592 return list(groups.items())
596 """Every (first, rule, run) reading at one threshold, one line each."""
598 lines = [
"| first | rule | second | agreement | granted more | granted less "
599 "| consulted | confirm issued |",
"|---|---|---|---:|---:|---:|---:|---:|"]
603 f
"| `{row.first}` | `{row.rule}` | `{row.second}` | {score.agreement} "
604 f
"| {score.granted_more} | {score.granted_less} | {row.consulted} "
605 f
"| {_confirm_issued(score)} |"
607 return "\n".join(lines)
611 """The range each (first, rule) covers across the repeated runs at one threshold."""
613 lines = [
"| first | rule | agreement | granted more | granted less | consulted |",
614 "|---|---|---|---|---|---:|"]
615 for (first, rule), group
in _grouped(rows,
lambda row: (row.first, row.rule)):
617 f
"| `{first}` | `{rule}` "
618 f
"| {_range_cell([item.score.correct for item in group])}"
619 f
" of {group[0].score.total} "
620 f
"| {_range_cell([item.score.granted_more for item in group])} "
621 f
"| {_range_cell([item.score.granted_less for item in group])} "
622 f
"| {group[0].consulted} |"
624 return "\n".join(lines)
628 """The same range, at every swept threshold, for reading an operating point.
630 A recommendation made at one threshold needs its noise band at THAT
631 threshold. The pinned table above is at 0.90 because that is where the
632 plan's main tables sit; this one is what says whether a point chosen
633 elsewhere on the curve survives a second run of the second model.
636 lines = [
"| first | rule | threshold | agreement | granted more | granted less "
637 "| consulted |",
"|---|---|---:|---|---|---|---:|"]
638 for (first, rule, threshold), group
in _grouped(
639 rows,
lambda row: (row.first, row.rule, row.threshold)):
640 assert isinstance(threshold, float)
642 f
"| `{first}` | `{rule}` | {threshold:.2f} "
643 f
"| {_range_cell([item.score.correct for item in group])}"
644 f
" of {group[0].score.total} "
645 f
"| {_range_cell([item.score.granted_more for item in group])} "
646 f
"| {_range_cell([item.score.granted_less for item in group])} "
647 f
"| {group[0].consulted} |"
649 return "\n".join(lines)
653 """``n`` when every run agrees, ``lo-hi`` when they do not."""
655 low, high = min(values), max(values)
656 return str(low)
if low == high
else f
"{low}-{high}"
660 """The whole generated sweep document.
662 Deliberately DATELESS. The document is a pure function of the committed
663 result files it names, and it carries their digests instead of a timestamp,
664 so a second run reproduces it byte for byte and a diff means an input
668 func =
"render_sweep"
670 hard_first = SWEEP_SECONDS_HARD[0]
672 hand_first = SWEEP_SECONDS_HAND[0]
673 sweep_rows = [
_sweep_row(cache, f
"{first}_hard", hard_first, rule, threshold, bench_dir)
674 for first
in SWEEP_FIRSTS
for rule
in RULES
675 for threshold
in SWEEP_THRESHOLDS]
676 hand_sweep_rows = [
_sweep_row(cache, f
"{first}_hand", hand_first, rule, threshold,
678 for first
in SWEEP_FIRSTS
for rule
in RULES
679 for threshold
in SWEEP_THRESHOLDS]
680 hard_rows = [
_sweep_row(cache, f
"{first}_hard", second, rule, DEFAULT_THRESHOLD, bench_dir)
681 for first
in SWEEP_FIRSTS
for rule
in RULES
for second
in SWEEP_SECONDS_HARD]
682 hand_rows = [
_sweep_row(cache, f
"{first}_hand", second, rule, DEFAULT_THRESHOLD, bench_dir)
683 for first
in SWEEP_FIRSTS
for rule
in RULES
for second
in SWEEP_SECONDS_HAND]
684 hard_curve = [
_sweep_row(cache, f
"{first}_hard", second, rule, threshold, bench_dir)
685 for first
in SWEEP_FIRSTS
for rule
in RULES
686 for threshold
in SWEEP_THRESHOLDS
for second
in SWEEP_SECONDS_HARD]
688 inputs = sorted({row.first
for row
in sweep_rows + hard_rows + hand_rows}
689 | set(SWEEP_SECONDS_HARD) | set(SWEEP_SECONDS_HAND))
691 "# Combining two opinions: the threshold sweep and the repeated runs",
693 "Generated by `python bench/argv_policy/combine.py --sweep` (plan 302). Do not edit"
694 " by hand: every figure below is composed in memory from the committed result files"
695 " named at the foot of this document and scored by `score.py`, and the file carries"
696 " no generation date so that a second run reproduces it byte for byte.",
698 "The first model decides. A row is unsure when its top probability is strictly below"
699 " the threshold, and only then is the second model consulted. `escalate` consults"
700 " nothing and is the control: it is the encoder alone with a human behind it.",
702 "## The threshold sweep, hard set, first Jev run",
704 f
"Second: `{hard_first}`. Thresholds: "
705 +
", ".join(f
"{value:.2f}" for value
in SWEEP_THRESHOLDS) +
".",
709 "## The threshold sweep, hand set, first Jev run",
711 "The 104 hand-written commands are too few for a threshold curve to be read on its"
712 " own -- one command is almost a percentage point -- so this table is here to say"
713 " whether an operating point chosen on the hard set above survives on the other case"
714 f
" set, not to choose one. Second: `{hand_first}`.",
718 "## The movement between repeated runs of the second model",
720 "Plan 293 measured two identical Jev requests moving probabilities on 60 of 104 rows"
721 " by up to 0.13, so a combination that consults Jev inherits that slack. These are"
722 " the same compositions at "
723 f
"{DEFAULT_THRESHOLD:.2f} against each repeated run. `escalate` consults nothing, so"
724 " its range is zero by construction and is the control for the reading: where an"
725 " `escalate` range is not zero the fault is in this document, not in the model.",
727 f
"### Hard set, 300 commands, every run at {DEFAULT_THRESHOLD:.2f}",
731 f
"### Hard set, 300 commands, the range across "
732 f
"{len(SWEEP_SECONDS_HARD)} runs at {DEFAULT_THRESHOLD:.2f}",
736 f
"### Hand set, 104 commands, every run at {DEFAULT_THRESHOLD:.2f}",
740 f
"### Hand set, 104 commands, the range across "
741 f
"{len(SWEEP_SECONDS_HAND)} runs at {DEFAULT_THRESHOLD:.2f}",
745 "## The same range at every swept threshold, hard set",
747 "An operating point read off the sweep above needs its noise band at the threshold"
748 " it was read at, not only at "
749 f
"{DEFAULT_THRESHOLD:.2f}. This is the range each combination covers across the"
750 f
" {len(SWEEP_SECONDS_HARD)} hard-set Jev runs, at each of the six thresholds.",
756 "| result file | SHA-256 |",
759 parts += [f
"| `results/{stem}.json` | `{cache.digest(stem)}` |" for stem
in inputs]
761 _dbg(
"OC_CHAT.MAIN", func,
762 f
"rendered {len(sweep_rows)} sweep rows and "
763 f
"{len(hard_rows) + len(hand_rows) + len(hard_curve)} repeat rows", 2)
764 return "\n".join(parts)
768 """Construct the CLI argument parser."""
770 parser = argparse.ArgumentParser(
771 prog=
"python bench/argv_policy/combine.py",
772 description=
"Combine two argv-policy result files: the first decides, the "
773 "second is consulted where the first is unsure.",
775 parser.add_argument(
"--first", default=
None,
776 help=
"Result file of the model that DECIDES.")
777 parser.add_argument(
"--second", default=
None,
778 help=
"Result file of the model CONSULTED where the first is "
779 "unsure. Optional for --rule escalate, which consults "
780 "nothing and only records which pairing it controls for.")
781 parser.add_argument(
"--rule", choices=RULES, default=
None,
782 help=
"How an unsure row is decided.")
783 parser.add_argument(
"--threshold", type=float, default=DEFAULT_THRESHOLD,
784 help=f
"Confidence below which the first model is unsure "
785 f
"(default: {DEFAULT_THRESHOLD}). The comparison is strict.")
786 parser.add_argument(
"--out", default=
None,
787 help=
"Where to write the combined result file, or the sweep "
788 "document under --sweep.")
789 parser.add_argument(
"--date", default=
None,
790 help=
"The date recorded in provenance (default: today). Pin it "
791 "to reproduce a committed file byte for byte.")
792 parser.add_argument(
"--sweep", action=
"store_true",
793 help=
"Render the threshold sweep and the repeated-run movement "
794 "to results/combined_sweep.md instead of combining once.")
795 parser.add_argument(
"--results-dir", default=
None,
796 help=
"Directory the sweep reads (default: results/ beside "
801def _run_sweep(args: argparse.Namespace, bench_dir: Path) -> int:
802 """The ``--sweep`` path: render the document and write it."""
805 results_dir = Path(args.results_dir)
if args.results_dir
else bench_dir /
"results"
806 if not results_dir.is_dir():
807 print(f
"combine: results directory not found: {results_dir}", file=sys.stderr)
809 out = Path(args.out)
if args.out
else results_dir /
"combined_sweep.md"
810 out.write_text(
render_sweep(results_dir, bench_dir), encoding=
"utf-8")
811 print(f
"combine: wrote {out}")
812 _dbg(
"OC_CHAT.MAIN", func, f
"wrote {out}", 2)
817 """The single-combination path: compose two files and write the result."""
819 func =
"_run_combination"
820 if not args.first
or not args.rule
or not args.out:
821 print(
"combine: --first, --rule and --out are required without --sweep",
824 if args.second
is None and args.rule
in CONSULTING_RULES:
825 print(f
"combine: --rule {args.rule} consults a second model, so --second "
826 "is required", file=sys.stderr)
828 first_path = Path(args.first)
829 second_path = Path(args.second)
if args.second
else None
831 second_payload =
load_payload(second_path)
if second_path
else None
832 date = args.date
or datetime.date.today().isoformat()
833 payload =
build_payload(first_payload, second_payload, rule=args.rule,
834 threshold=args.threshold, first_path=first_path,
835 second_path=second_path, bench_dir=bench_dir, date=date)
836 Path(args.out).write_text(json.dumps(payload, indent=1, sort_keys=
True),
838 provenance = payload[
"provenance"]
839 assert isinstance(provenance, dict)
840 print(f
"combine: wrote {args.out}")
841 print(f
"combine: {payload['model']} against {payload['condition']}, "
842 f
"{provenance['consulted']} of {len(payload['results'])} consulted, "
843 f
"{provenance['disagreeing']} disagreeing")
844 _dbg(
"OC_CHAT.MAIN", func, f
"wrote {args.out}", 2)
848def main(argv: list[str] |
None =
None) -> int:
849 """CLI entry point: one combination, or the whole sweep document."""
852 bench_dir = Path(__file__).resolve().parent
857 except CombineError
as error:
858 print(f
"combine: {error}", file=sys.stderr)
862if __name__ ==
"__main__":
str digest(self, str stem)
Path path(self, str stem)
Payload payload(self, str stem)
None __init__(self, Path results_dir)
float|None cost_usd(self)
argparse.ArgumentParser _build_parser()
Decision rule_disagree(Row first, Row|None second, float threshold)
Payload build_payload(Payload first_payload, Payload|None second_payload, *, str rule, float threshold, Path first_path, Path|None second_path, Path bench_dir, str date)
int _rank(str prediction)
str _threshold_tag(float threshold)
str _cost_cell(float|None value)
str render_sweep(Path results_dir, Path bench_dir)
list[tuple[tuple[object,...], list[SweepRow]]] _grouped(list[SweepRow] rows, Callable[[SweepRow], tuple[object,...]] key)
Decision _keep_first(Row first)
Decision rule_stricter(Row first, Row|None second, float threshold)
int _confirm_issued(Score score)
dict[str, float]|None _usable_probs(Row row)
str _relative_path(Path path, Path bench_dir)
list[Row] combine_rows(list[Row] first_rows, list[Row|None] seconds, str rule, float threshold)
Payload load_payload(Path path)
Decision rule_replace(Row first, Row|None second, float threshold)
SweepRow _sweep_row(_PayloadCache cache, str first, str second, str rule, float threshold, Path bench_dir)
str _ms_cell(float|None value)
float|None top_probability(Row row)
bool is_unsure(Row row, float threshold)
float|None _effective_ms(Row first, Row|None second, bool consulted)
str _range_cell(list[int] values)
str _repeat_range_table(list[SweepRow] rows)
Decision rule_escalate(Row first, Row|None second, float threshold)
dict[str, Row] _index_by_command(list[Row] rows, str label)
str _threshold_range_table(list[SweepRow] rows)
int _run_combination(argparse.Namespace args, Path bench_dir)
str _repeat_rows_table(list[SweepRow] rows)
list[Row|None] join_rows(list[Row] first_rows, list[Row]|None second_rows, str first_label, str second_label)
int _run_sweep(argparse.Namespace args, Path bench_dir)
int main(list[str]|None argv=None)
str _threshold_table(list[SweepRow] rows)