patch-cc 0.2.2__tar.gz → 0.2.3__tar.gz
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.
- {patch_cc-0.2.2 → patch_cc-0.2.3}/PKG-INFO +1 -1
- {patch_cc-0.2.2 → patch_cc-0.2.3}/docs/PLAYBOOK.md +41 -11
- {patch_cc-0.2.2 → patch_cc-0.2.3}/pyproject.toml +1 -1
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/__init__.py +1 -1
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/bun/__init__.py +1 -1
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/bun/container.py +2 -2
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/bun/elf.py +4 -4
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/bun/macho.py +2 -1
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/cli.py +3 -3
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/codex/gateway.py +4 -2
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/codex/oauth.py +1 -1
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/locate.py +1 -1
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/menu.py +9 -8
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/patcher.py +1 -1
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/patches/agents.py +1 -1
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/patches/base.py +5 -5
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/patches/codex.py +2 -6
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/patches/streaming.py +147 -296
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/ui.py +1 -1
- patch_cc-0.2.3/uv.lock +422 -0
- patch_cc-0.2.2/uv.lock +0 -358
- {patch_cc-0.2.2 → patch_cc-0.2.3}/.github/workflows/ci.yml +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/.github/workflows/release.yml +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/.gitignore +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/AGENTS.md +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/CLAUDE.md +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/LICENSE +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/README.md +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/docs/CONDUCT.md +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/docs/INTERNALS.md +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/docs/demo.gif +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/bun/blob.py +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/bun/errors.py +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/cache.py +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/codex/__init__.py +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/codex/models.py +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/codex/translate.py +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/doctor.py +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/patches/__init__.py +3 -3
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/patches/chrome.py +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/patches/output.py +0 -0
- {patch_cc-0.2.2 → patch_cc-0.2.3}/src/patch_cc/patches/thinking.py +0 -0
|
@@ -46,6 +46,27 @@ constantly. These rules are what keep matchers alive:
|
|
|
46
46
|
every backup before removing any part of it: under the older, tighter callee
|
|
47
47
|
that same probe read 1 on all 8 builds, so a measurement taken before the
|
|
48
48
|
callee was dissolved does not survive it.
|
|
49
|
+
- **Rewrite at the dispatch point; never model the arm.** When an update is
|
|
50
|
+
valid at arm entry, splice it in where the code dispatches on the semantic
|
|
51
|
+
string — after a `case` label chain, or by wrapping the dispatch test
|
|
52
|
+
itself, `(test&&((update),!0))`, which preserves the test's value exactly
|
|
53
|
+
and so survives any surrounding composition (wrapping the *enclosing*
|
|
54
|
+
condition instead would run the update for the other side of a bare `||`) —
|
|
55
|
+
and say nothing about what the arm already does. Arm bodies are upstream's
|
|
56
|
+
busiest surface: 2.1.226 threaded one flag through five arms of the stream
|
|
57
|
+
reducer (`stmt1,stmt2;` became `if(stmt1,stmt2,d)call;`) and appended a
|
|
58
|
+
declarator to the head's `let`, and every literal that had modelled those
|
|
59
|
+
bodies — a cross-product of `mode(` vs `mode?.(`, braced vs unbraced arms,
|
|
60
|
+
four `displayTransform` spellings — died or was already dead weight. The
|
|
61
|
+
dispatch strings themselves (`"thinking_delta"`, `"message_stop"`) are the
|
|
62
|
+
API's vocabulary and do not churn. Same doctrine as the callee rule above:
|
|
63
|
+
the arm is *reached through*, the dispatch point is what the update is
|
|
64
|
+
*applied to*. Entry insertion does reorder against mid-arm work (the
|
|
65
|
+
message-stop reset now runs before upstream's `finalize()`), so it asks
|
|
66
|
+
that the update be independent of the arm's own channels — decide that,
|
|
67
|
+
don't assume it. Bound heads at grammar edges for the same reason — in a
|
|
68
|
+
minified bundle only `;` or `,` can follow `let{…}=t`, and which one is
|
|
69
|
+
noise (`(?=[;,])`, not `;`).
|
|
49
70
|
- Match an optional brace **pair** conditionally (`(\{)?…(?(1)\})`), never as
|
|
50
71
|
two independent `\{?` / `\}?`. A lone optional closing brace eats the
|
|
51
72
|
*enclosing* block's `}` on the unbraced shape, and a rewrite that emits its
|
|
@@ -174,7 +195,7 @@ first is how a manifest starts lying. The manifest is held to the same rule:
|
|
|
174
195
|
|
|
175
196
|
## Sub-steps, and why `live-thinking` has them
|
|
176
197
|
|
|
177
|
-
Most patches are one rewrite. `live-thinking` is ~
|
|
198
|
+
Most patches are one rewrite. `live-thinking` is ~20 named sub-steps, because
|
|
178
199
|
upstream has reshaped the stream reducer at least three times and a single hit
|
|
179
200
|
count cannot tell "all landed" from "half silently drifted".
|
|
180
201
|
|
|
@@ -183,9 +204,16 @@ kinds:
|
|
|
183
204
|
|
|
184
205
|
- **Independent fixes** (`memo-cache`, `linger`, `display-mode`, `bottom-row`,
|
|
185
206
|
…) — each may or may not be present on a given build.
|
|
186
|
-
- **Mutually-exclusive reducer variants** — `reducer-
|
|
187
|
-
|
|
188
|
-
|
|
207
|
+
- **Mutually-exclusive reducer variants** — `reducer-options` (2.1.138+, the
|
|
208
|
+
options-bag head; whether the bag still destructures `onStreamingThinking`
|
|
209
|
+
is the same shape with the prop present or absent, not a second variant) and
|
|
210
|
+
`reducer-legacy` (pre-2.1.138, positional parameters). On any one build
|
|
211
|
+
exactly one should land; its count is *recognition*, one per reducer. The
|
|
212
|
+
state updates themselves are per-dispatch-point steps (`request-start`,
|
|
213
|
+
`message-stop`, `text-clear`, `message-delta-clear`, `thinking-start`,
|
|
214
|
+
`thinking-append`), so a build that folds one arm away reads as that point's
|
|
215
|
+
absence by name, never as a bare count drop inside an aggregate. On
|
|
216
|
+
2.1.216–2.1.226 the variant is `reducer-options` with all six points.
|
|
189
217
|
|
|
190
218
|
An *optional* sub-step that finds nothing is reported as absent, not broken —
|
|
191
219
|
it is just a shape this build doesn't have. A sub-step that finds a shape but
|
|
@@ -201,11 +229,11 @@ The rest carry expectations, so their absence is checked rather than noted:
|
|
|
201
229
|
but *proof*: they are credited only when the rewritten reducer body actually
|
|
202
230
|
contains the state updates, which is what recognising a reducer does not by
|
|
203
231
|
itself establish.
|
|
204
|
-
- **The `reducer` group** — `reducer-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
232
|
+
- **The `reducer` group** — `reducer-options`, `reducer-legacy`. At least one
|
|
233
|
+
must land; none landing is the signal that upstream shipped a new reducer
|
|
234
|
+
head that needs a new variant. Two landing is allowed on purpose — a
|
|
235
|
+
transitional build carrying two reducers would have both correctly patched,
|
|
236
|
+
and that is no reason to cry wolf.
|
|
209
237
|
- **Optional** — `memo-cache`, `memo-removal`, `linger`, `bottom-row`,
|
|
210
238
|
`final-summary`. The first four match nothing on 2.1.216+ and are kept for
|
|
211
239
|
older builds; `discover` is a notes-only channel that records how far back the
|
|
@@ -354,11 +382,13 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
|
|
|
354
382
|
|
|
355
383
|
### Live thinking — `streaming.py`
|
|
356
384
|
|
|
357
|
-
- **`live-thinking`** — the ~
|
|
385
|
+
- **`live-thinking`** — the ~20-step patch above. Discovery anchor is
|
|
358
386
|
`onStreamingThinking:` → `useState(null)` (the older `hidePastThinking`
|
|
359
387
|
anchor is gone as of 2.1.216 — the fallback back-scan is load-bearing).
|
|
360
388
|
Reducer anchors: `type==="stream_request_start"`, `case"thinking_delta"`,
|
|
361
|
-
`content_block_start
|
|
389
|
+
`content_block_start` — the reducer is the function carrying all three, its
|
|
390
|
+
head bounded at the declarator edge, and every state update is an insertion
|
|
391
|
+
at a dispatch point (the rule above); the arm bodies are never matched.
|
|
362
392
|
The `display-mode` sub-step defaults the request's thinking display to
|
|
363
393
|
`"summarized"`; without it the API only streams summary text when the
|
|
364
394
|
`showThinkingSummaries` setting is on. Two shapes: the legacy inline env
|
|
@@ -85,7 +85,7 @@ def write(
|
|
|
85
85
|
compared against ``source`` -- *before* it is moved into place. A rebuild
|
|
86
86
|
bug therefore fails without ever touching the live binary.
|
|
87
87
|
"""
|
|
88
|
-
import shutil
|
|
88
|
+
import shutil
|
|
89
89
|
|
|
90
90
|
new_blob = blobmod.rebuild(
|
|
91
91
|
bundle.blob, source.encode("utf8"), drop_bytecode=drop_bytecode
|
|
@@ -120,7 +120,7 @@ def verify(path: str, expected: str) -> None:
|
|
|
120
120
|
"""Re-extract from a written binary and assert it round-trips exactly."""
|
|
121
121
|
try:
|
|
122
122
|
written = read(path)
|
|
123
|
-
except Exception as exc:
|
|
123
|
+
except Exception as exc:
|
|
124
124
|
raise ContainerError(f"patched binary could not be re-read: {exc}") from exc
|
|
125
125
|
if written.source != expected:
|
|
126
126
|
raise ContainerError(
|
|
@@ -224,11 +224,11 @@ def write_section(buf: bytes, payload: bytes, name: str = ".bun") -> bytes:
|
|
|
224
224
|
and seg.filesz
|
|
225
225
|
and seg.offset >= end
|
|
226
226
|
and seg.align
|
|
227
|
+
and (seg.offset + delta) % seg.align != seg.vaddr % seg.align
|
|
227
228
|
):
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
)
|
|
229
|
+
raise ElfError(
|
|
230
|
+
f"shifting LOAD segment {seg.index} would break its alignment"
|
|
231
|
+
)
|
|
232
232
|
|
|
233
233
|
new_size = bun.size + delta
|
|
234
234
|
body = bytearray(payload)
|
|
@@ -27,7 +27,7 @@ class MachOError(BunError):
|
|
|
27
27
|
|
|
28
28
|
def _lief():
|
|
29
29
|
try:
|
|
30
|
-
import lief
|
|
30
|
+
import lief
|
|
31
31
|
except ImportError as exc: # pragma: no cover - platform dependent
|
|
32
32
|
raise MachOError(
|
|
33
33
|
"LIEF is required to patch macOS binaries. Install it with "
|
|
@@ -107,6 +107,7 @@ def codesign(path: str) -> None:
|
|
|
107
107
|
["codesign", "--sign", "-", "--force", path],
|
|
108
108
|
capture_output=True,
|
|
109
109
|
text=True,
|
|
110
|
+
check=False,
|
|
110
111
|
)
|
|
111
112
|
if result.returncode != 0: # pragma: no cover - macOS only
|
|
112
113
|
raise MachOError(f"codesign failed: {result.stderr.strip()}")
|
|
@@ -36,7 +36,7 @@ from pathlib import Path
|
|
|
36
36
|
from rich.markup import escape
|
|
37
37
|
|
|
38
38
|
from . import cache, locate, patcher
|
|
39
|
-
from .bun import
|
|
39
|
+
from .bun import Bundle, BunError
|
|
40
40
|
from .codex import DEFAULT_PORT, is_valid_port
|
|
41
41
|
from .codex.models import CodexModel, catalogue, is_reserved_id, is_valid_id, reconcile
|
|
42
42
|
from .patches import (
|
|
@@ -549,7 +549,7 @@ def _doctor_target(path: str | None) -> tuple[Bundle, str] | None:
|
|
|
549
549
|
given -- that is how any kept backup becomes a regression corpus -- while the
|
|
550
550
|
installed binary falls back to its pristine copy when it is already patched.
|
|
551
551
|
"""
|
|
552
|
-
from .bun import container
|
|
552
|
+
from .bun import container
|
|
553
553
|
|
|
554
554
|
if path is not None:
|
|
555
555
|
# A name the user typed is data, not markup: an unescaped `claude[old]`
|
|
@@ -695,7 +695,7 @@ def _baked_port() -> int | None:
|
|
|
695
695
|
why ``serve`` asks once at startup rather than per request -- and why a re-bake
|
|
696
696
|
onto a different port needs a ``serve`` restart, as the apply report says.
|
|
697
697
|
"""
|
|
698
|
-
from .bun import container
|
|
698
|
+
from .bun import container
|
|
699
699
|
|
|
700
700
|
install = locate.find()
|
|
701
701
|
if install is None:
|
|
@@ -155,7 +155,7 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
155
155
|
def log_message(self, *_args) -> None: # keep the terminal clean
|
|
156
156
|
pass
|
|
157
157
|
|
|
158
|
-
def do_POST(self) -> None:
|
|
158
|
+
def do_POST(self) -> None:
|
|
159
159
|
# One request per connection: an SSE reply is open-ended (no
|
|
160
160
|
# Content-Length), so the stream ends by connection close.
|
|
161
161
|
self.close_connection = True
|
|
@@ -177,7 +177,9 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
177
177
|
# this API just as unparseable bytes are, and is answered the same
|
|
178
178
|
# way. Reaching `_messages` with one used to raise straight out of
|
|
179
179
|
# the handler thread: a traceback, a reset connection, no reply.
|
|
180
|
-
raise ValueError(
|
|
180
|
+
raise ValueError( # noqa: TRY004 - lands in json's own handler below
|
|
181
|
+
"not a JSON object"
|
|
182
|
+
)
|
|
181
183
|
except ValueError:
|
|
182
184
|
return self._error(
|
|
183
185
|
400, "invalid_request_error", "request body must be a JSON object"
|
|
@@ -74,7 +74,7 @@ class Credentials:
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
@classmethod
|
|
77
|
-
def from_json(cls, data: object) ->
|
|
77
|
+
def from_json(cls, data: object) -> Credentials | None:
|
|
78
78
|
if not isinstance(data, dict):
|
|
79
79
|
return None
|
|
80
80
|
access, refresh = data.get("access"), data.get("refresh")
|
|
@@ -78,7 +78,7 @@ def _resolve(path: Path) -> Installation | None:
|
|
|
78
78
|
|
|
79
79
|
def _is_native(binary: Path) -> bool:
|
|
80
80
|
"""Is this file one of the containers we can actually open?"""
|
|
81
|
-
from .bun import BunError, container
|
|
81
|
+
from .bun import BunError, container
|
|
82
82
|
|
|
83
83
|
try:
|
|
84
84
|
container.detect(str(binary))
|
|
@@ -27,8 +27,9 @@ from __future__ import annotations
|
|
|
27
27
|
import sys
|
|
28
28
|
import textwrap
|
|
29
29
|
import threading
|
|
30
|
+
from collections.abc import Callable
|
|
30
31
|
from dataclasses import dataclass, field
|
|
31
|
-
from typing import TYPE_CHECKING, Any
|
|
32
|
+
from typing import TYPE_CHECKING, Any
|
|
32
33
|
|
|
33
34
|
from blessed import Terminal
|
|
34
35
|
from rich import box
|
|
@@ -140,7 +141,7 @@ class MenuModel:
|
|
|
140
141
|
"""Everything the menu operates on, independent of the rendering engine."""
|
|
141
142
|
|
|
142
143
|
install: locate.Installation
|
|
143
|
-
status:
|
|
144
|
+
status: Status
|
|
144
145
|
pristine: Bundle
|
|
145
146
|
agents: list[BuiltinAgent]
|
|
146
147
|
models: list[str]
|
|
@@ -154,8 +155,8 @@ class MenuModel:
|
|
|
154
155
|
|
|
155
156
|
@classmethod
|
|
156
157
|
def build(
|
|
157
|
-
cls, install: locate.Installation, status:
|
|
158
|
-
) ->
|
|
158
|
+
cls, install: locate.Installation, status: Status, pristine: Bundle
|
|
159
|
+
) -> MenuModel:
|
|
159
160
|
agents = discover_agents(pristine.source)
|
|
160
161
|
models = [INHERIT, *discover_models(pristine.source)]
|
|
161
162
|
model = cls(
|
|
@@ -577,7 +578,7 @@ class MenuApp:
|
|
|
577
578
|
self.view = "select" # select | busy | report | doctor
|
|
578
579
|
self.stack: list[tuple[Modal, Callable[[object], None] | None]] = []
|
|
579
580
|
self.report: patcher.PatchReport | None = None
|
|
580
|
-
self.doctor_result:
|
|
581
|
+
self.doctor_result: DryRun | None = None
|
|
581
582
|
self.busy_message = ""
|
|
582
583
|
self.flash = ""
|
|
583
584
|
self.exit_code = 0
|
|
@@ -642,7 +643,7 @@ class MenuApp:
|
|
|
642
643
|
attrs = termios.tcgetattr(fd)
|
|
643
644
|
attrs[0] &= ~(termios.IXON | termios.IXOFF)
|
|
644
645
|
termios.tcsetattr(fd, termios.TCSANOW, attrs)
|
|
645
|
-
except Exception:
|
|
646
|
+
except Exception: # noqa: BLE001, S110 - best effort; no termios, no freeze risk
|
|
646
647
|
pass
|
|
647
648
|
|
|
648
649
|
def _key_name(self, keystroke) -> str:
|
|
@@ -1080,7 +1081,7 @@ class MenuApp:
|
|
|
1080
1081
|
|
|
1081
1082
|
def _apply(
|
|
1082
1083
|
self, selection: cache.Selection
|
|
1083
|
-
) -> tuple[patcher.PatchReport,
|
|
1084
|
+
) -> tuple[patcher.PatchReport, Status | None]:
|
|
1084
1085
|
options = selection.options
|
|
1085
1086
|
if options.codex_models:
|
|
1086
1087
|
# Fill in each model's name and window right before baking. A pick
|
|
@@ -1154,7 +1155,7 @@ class MenuApp:
|
|
|
1154
1155
|
def run() -> None:
|
|
1155
1156
|
try:
|
|
1156
1157
|
self._worker_result = (kind, work(), None)
|
|
1157
|
-
except Exception as exc:
|
|
1158
|
+
except Exception as exc: # noqa: BLE001 - the worker's failure IS the result
|
|
1158
1159
|
self._worker_result = (kind, None, str(exc) or exc.__class__.__name__)
|
|
1159
1160
|
|
|
1160
1161
|
threading.Thread(target=run, daemon=True).start()
|
|
@@ -326,7 +326,7 @@ def restore(install: locate.Installation) -> Path:
|
|
|
326
326
|
"for this version was never saved -- reinstall to get a clean binary."
|
|
327
327
|
)
|
|
328
328
|
# A full-file copy-back, so it works for both ELF and Mach-O.
|
|
329
|
-
from .bun.elf import atomic_write
|
|
329
|
+
from .bun.elf import atomic_write
|
|
330
330
|
|
|
331
331
|
atomic_write(str(install.binary), backup.read_bytes(), mode_from=str(backup))
|
|
332
332
|
return install.binary
|
|
@@ -79,7 +79,7 @@ def _subagent_prompt(content: str, _options: Options, outcome: Outcome) -> str:
|
|
|
79
79
|
transcript_var = transcript.group(1)
|
|
80
80
|
gate_pattern = compile_js(rf"{re.escape(transcript_var)}&&({IDENT})&&")
|
|
81
81
|
|
|
82
|
-
def drop_gate(match: re.Match[str]) -> str:
|
|
82
|
+
def drop_gate(match: re.Match[str], segment: str = segment) -> str:
|
|
83
83
|
prompt_var = match.group(1)
|
|
84
84
|
nearby = segment[match.end() : match.end() + 260]
|
|
85
85
|
if f"{{prompt:{prompt_var},theme:" not in nearby:
|
|
@@ -69,7 +69,7 @@ def derived_brand() -> str:
|
|
|
69
69
|
|
|
70
70
|
try:
|
|
71
71
|
user = getpass.getuser().strip()
|
|
72
|
-
except Exception:
|
|
72
|
+
except Exception: # noqa: BLE001 - however it fails, the answer is the same
|
|
73
73
|
user = ""
|
|
74
74
|
return f"{user}'s Code" if user else DEFAULT_BRAND
|
|
75
75
|
|
|
@@ -84,7 +84,7 @@ class Options:
|
|
|
84
84
|
#: Codex models to register and route -- ordinary patch configuration, like
|
|
85
85
|
#: every field here: chosen in the menu or with ``--codex``, and recorded in
|
|
86
86
|
#: the manifest by the patch that bakes it.
|
|
87
|
-
codex_models: list[
|
|
87
|
+
codex_models: list[CodexModel] = field(default_factory=list)
|
|
88
88
|
#: Localhost port shared by the redirect patch and the gateway. Defaulted
|
|
89
89
|
#: from the one home (:data:`patch_cc.codex.DEFAULT_PORT`) so the number
|
|
90
90
|
#: never lives in two places.
|
|
@@ -116,7 +116,7 @@ class Outcome:
|
|
|
116
116
|
applied: int = 0
|
|
117
117
|
notes: list[str] = field(default_factory=list)
|
|
118
118
|
#: Named sub-steps, for patches built from several independent rewrites.
|
|
119
|
-
steps: dict[str,
|
|
119
|
+
steps: dict[str, Outcome] = field(default_factory=dict)
|
|
120
120
|
#: What this sub-step's absence means (set via :meth:`step`). ``False`` --
|
|
121
121
|
#: a shape some builds simply lack; ``True`` -- the patch is broken without
|
|
122
122
|
#: it; a string -- name of a group of which at least one member must land.
|
|
@@ -153,7 +153,7 @@ class Outcome:
|
|
|
153
153
|
def note(self, message: str) -> None:
|
|
154
154
|
self.notes.append(message)
|
|
155
155
|
|
|
156
|
-
def step(self, name: str, expect: bool | str = False) ->
|
|
156
|
+
def step(self, name: str, expect: bool | str = False) -> Outcome:
|
|
157
157
|
"""Get (or create) a named sub-outcome.
|
|
158
158
|
|
|
159
159
|
A single scalar count cannot distinguish "all twelve rewrites landed"
|
|
@@ -167,7 +167,7 @@ class Outcome:
|
|
|
167
167
|
sub.expect = expect
|
|
168
168
|
return sub
|
|
169
169
|
|
|
170
|
-
def finalize(self) ->
|
|
170
|
+
def finalize(self) -> Outcome:
|
|
171
171
|
"""Roll sub-step totals up into this outcome."""
|
|
172
172
|
if self.steps:
|
|
173
173
|
self.candidates += sum(s.candidates for s in self.steps.values())
|
|
@@ -304,12 +304,8 @@ def _register_picker(
|
|
|
304
304
|
# because this is the only place in the picker where a model leaves Anthropic,
|
|
305
305
|
# and nothing else in the list would say so.
|
|
306
306
|
entries = ",".join(
|
|
307
|
-
"{value
|
|
308
|
-
|
|
309
|
-
js_string(value),
|
|
310
|
-
js_string(_display_name(value)),
|
|
311
|
-
js_string(_describe(model)),
|
|
312
|
-
)
|
|
307
|
+
f"{{value:{js_string(value)},label:{js_string(_display_name(value))},"
|
|
308
|
+
f"description:{js_string(_describe(model))}}}"
|
|
313
309
|
for value, model in picks
|
|
314
310
|
)
|
|
315
311
|
# Every row is emitted with no build-time "already present?" filter, and dedup
|