patch-cc 0.3.0__tar.gz → 0.3.1__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.
Files changed (43) hide show
  1. {patch_cc-0.3.0 → patch_cc-0.3.1}/PKG-INFO +1 -1
  2. {patch_cc-0.3.0 → patch_cc-0.3.1}/docs/PLAYBOOK.md +33 -10
  3. {patch_cc-0.3.0 → patch_cc-0.3.1}/docs/corpus.md +3 -2
  4. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/__init__.py +1 -1
  5. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/patches/codex.py +137 -31
  6. {patch_cc-0.3.0 → patch_cc-0.3.1}/.github/workflows/ci.yml +0 -0
  7. {patch_cc-0.3.0 → patch_cc-0.3.1}/.github/workflows/release.yml +0 -0
  8. {patch_cc-0.3.0 → patch_cc-0.3.1}/.gitignore +0 -0
  9. {patch_cc-0.3.0 → patch_cc-0.3.1}/AGENTS.md +0 -0
  10. {patch_cc-0.3.0 → patch_cc-0.3.1}/CLAUDE.md +0 -0
  11. {patch_cc-0.3.0 → patch_cc-0.3.1}/LICENSE +0 -0
  12. {patch_cc-0.3.0 → patch_cc-0.3.1}/README.md +0 -0
  13. {patch_cc-0.3.0 → patch_cc-0.3.1}/docs/CONDUCT.md +0 -0
  14. {patch_cc-0.3.0 → patch_cc-0.3.1}/docs/INTERNALS.md +0 -0
  15. {patch_cc-0.3.0 → patch_cc-0.3.1}/docs/demo.gif +0 -0
  16. {patch_cc-0.3.0 → patch_cc-0.3.1}/pyproject.toml +0 -0
  17. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/bun/__init__.py +0 -0
  18. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/bun/blob.py +0 -0
  19. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/bun/container.py +0 -0
  20. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/bun/elf.py +0 -0
  21. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/bun/errors.py +0 -0
  22. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/bun/macho.py +0 -0
  23. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/cache.py +0 -0
  24. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/cli.py +0 -0
  25. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/codex/__init__.py +0 -0
  26. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/codex/gateway.py +0 -0
  27. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/codex/models.py +0 -0
  28. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/codex/oauth.py +0 -0
  29. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/codex/translate.py +0 -0
  30. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/doctor.py +0 -0
  31. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/js.py +0 -0
  32. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/locate.py +0 -0
  33. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/menu.py +0 -0
  34. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/patcher.py +0 -0
  35. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/patches/__init__.py +0 -0
  36. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/patches/agents.py +0 -0
  37. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/patches/base.py +0 -0
  38. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/patches/chrome.py +0 -0
  39. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/patches/output.py +0 -0
  40. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/patches/streaming.py +0 -0
  41. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/patches/thinking.py +0 -0
  42. {patch_cc-0.3.0 → patch_cc-0.3.1}/src/patch_cc/ui.py +0 -0
  43. {patch_cc-0.3.0 → patch_cc-0.3.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: patch-cc
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Interactive patcher for the Claude Code native binary
5
5
  Project-URL: Homepage, https://github.com/anfreire/patch-cc
6
6
  Project-URL: Repository, https://github.com/anfreire/patch-cc
@@ -116,6 +116,18 @@ The tree removes the *mechanical* fragility. It does not decide these:
116
116
  that branch verbatim rather than constructing a flush call; `org-label` builds
117
117
  its line from upstream's own no-org string, so nothing here spells the
118
118
  separator. What you copy cannot drift from what you copied it from.
119
+ - **Match an identity by the weakest claim that still proves it; hold a rewrite
120
+ to the exact node.** The two jobs pull in opposite directions.
121
+ `thinking-inline`'s null-guard neutralises only a `return` that answers
122
+ exactly `null`, because deleting a conditional answer would delete behaviour
123
+ — for a rewrite, doubt means stop. The codex resolvers' *identity* is the
124
+ opposite case: "rejects an unknown model" was once asked as "the default
125
+ returns exactly `null`", and 2.1.234 wrapping that same rejection in a dead
126
+ recognizer (`return vXu(e)?xVe(t):null`, the stub answering `!1` — behaviour
127
+ identical) read the resolver as gone with every anchor count standing.
128
+ Identity asks what the node *can* answer — membership among its possible
129
+ values, the same rule props and arrays already follow — and exactness there
130
+ is a break waiting on upstream's next harmless addition.
119
131
  - **Never anchor on a minified local** (`A_`, `mET`, `wg6`). Unchanged, and now
120
132
  structural: you capture the node, so there is never a reason to spell one.
121
133
 
@@ -883,7 +895,7 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
883
895
  |---|---|---|
884
896
  | `enum`\* | the Task tool's `model` enum, so a subagent can be pinned to a Codex id | `agents.model_enums` — the same arrays `discover_models` reads |
885
897
  | `validator`\* | the known-model array — it gates *resolution*, not just acceptance | the array whose strings *include* `sonnet`/`opus`/`haiku`/`opusplan` — membership, never their order |
886
- | `resolver`\* | the override resolver (managed `availableModels` only) | the `"best"` arm whose `switch` answers `null` for an unknown model |
898
+ | `resolver`\* | the override resolver (managed `availableModels` only) | the `"best"` arm whose `switch` *can* answer `null` for an unknown model |
887
899
  | `general-resolver`\* | the resolver every ordinary request uses | the `"best"` arm whose `switch` has no answer of its own for one |
888
900
  | `redirect`\* | swaps the request origin to `127.0.0.1:<port>` | the `buildRequest` method that builds a URL (the others — two to four across the corpus — only delegate to `super`) |
889
901
  | `picker` | the `/model` list | the function every row list is handed to: it loops, adds, and returns its first parameter |
@@ -994,15 +1006,26 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
994
1006
  registered, with the manifest asserting the override. Now the dropped patch
995
1007
  takes its pins down with it.
996
1008
 
997
- The two resolvers are told apart by **what they answer, never by their
998
- minified names or by a statement form** — the override one rejects an unknown
999
- model with `default:return null`, the general one has no answer for it and
1000
- falls through to handing the name back. That is a fact about what each
1001
- resolver is *for*. Braces around an arm are not: they were the discriminator
1002
- once, and putting a `let` in the general resolver's `case"best"` was enough to
1003
- make every family shortcut vanish with the patch still green and seven of
1004
- seven steps applied. The `buildRequest` methods and the
1005
- `models`-shaped objects are likewise separated by what they *do* and *carry*.
1009
+ The two resolvers are told apart by **what each answers for an unknown
1010
+ model, never by their minified names or by a statement form** — `null` is
1011
+ among what the override one's default *can* answer, read off the grammar's
1012
+ own value routing (a ternary answers with either branch, `||`/`??` with
1013
+ their right side; 2.1.234 wrapped the same rejection in a dead recognizer,
1014
+ `return vXu(e)?xVe(t):null` with the stub answering `!1`, and the exact
1015
+ spelling read a behaviourally identical build as the resolver being gone),
1016
+ while the general one has no answer of its own — nothing in its default
1017
+ scoped to return or throw and falls through to handing the name back.
1018
+ Both identities are asked positively, of switches whose labels carry the
1019
+ built-in models (the membership `validator` asks of its array, asked of the
1020
+ labels — a throwaway `case"best":` elsewhere is nothing, not an arm to
1021
+ classify), and an arm answering neither *raises* instead of swelling the
1022
+ other side: under the complement this replaced, a reshaped override slid
1023
+ silently into the general list, and only the required step's zero was left
1024
+ to speak. Braces around an arm are no discriminator either: they were, once,
1025
+ and a `let` in the general resolver's `case"best"` was enough to make every
1026
+ family shortcut vanish with the patch still green and seven of seven steps
1027
+ applied. The `buildRequest` methods and the `models`-shaped objects are
1028
+ likewise separated by what they *do* and *carry*.
1006
1029
 
1007
1030
  **Two resolvers, and why both.** The **general** one turns `opus`
1008
1031
  into `claude-opus-4-8`, its return value *replaces* the model before the
@@ -20,7 +20,7 @@ matches means the file changed under you, not that a number moved.
20
20
 
21
21
  ## On disk now
22
22
 
23
- The reproducible set on the machine this was written on — 11 distinct binaries
23
+ The reproducible set on the machine this was written on — 12 distinct binaries
24
24
  by content (the SHA-256 is the whole file, `sha256sum <version>.orig`):
25
25
 
26
26
  | version | size | sha256 |
@@ -36,13 +36,14 @@ by content (the SHA-256 is the whole file, `sha256sum <version>.orig`):
36
36
  | `2.1.227` | 304 MB | `6832dc3f1797b890b71116e5f2dbbf9a83fd3d0498c235b4b0f9cd0e6e499ad6` |
37
37
  | `2.1.228` | 309 MB | `d535985e6941a3eb00179ccd7f52ceb0c6623a0305a518ebc4e6514f84a94c99` |
38
38
  | `2.1.233` | 325 MB | `55d281096f57d411ebbdd94dbf5e9ff3accb7c05713e37348c2c11d4b83bf9d9` |
39
+ | `2.1.234` | 328 MB | `3473601ea695d5bf769c5b202844d4cb4fbf723ae995450fcb6973204775c84a` |
39
40
 
40
41
  The backup directory holds a few more files than rows here, and that is not a
41
42
  discrepancy: a pre-0.2.0 backup doubled the version into its name
42
43
  (`2.1.216.2.1.216.orig`), and a binary installed under a non-version name is
43
44
  saved as `claude.unknown-<hash>.orig`. Both of the extras present hash-match the
44
45
  `2.1.216` row above — same bytes, a different filename — which is why the count
45
- of *files* (13) and the count of *distinct binaries* (11) differ.
46
+ of *files* (14) and the count of *distinct binaries* (12) differ.
46
47
 
47
48
  ## Rebuild or extend it
48
49
 
@@ -1,3 +1,3 @@
1
1
  """patch-cc: an interactive patcher for the Claude Code native binary."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.1"
@@ -165,52 +165,153 @@ def claimed_model_names(source: Source) -> set[str]:
165
165
  # ----------------------------------------------------------------- resolvers
166
166
  #
167
167
  # The binary has TWO model resolvers and a shortcut needs an arm in both. They
168
- # are told apart by what they *answer*, never by their minified names or by a
169
- # statement form: the override resolver (reached only when managed
170
- # `availableModels` are active) rejects an unknown name with `null`; the general
171
- # resolver -- the one every ordinary request uses, which turns `opus` into
172
- # `claude-opus-4-8` -- has no answer of its own for one and falls through to
173
- # passing it straight back.
168
+ # are told apart by what each *answers* for an unknown model, never by their
169
+ # minified names or by a statement form: the override resolver (reached only
170
+ # when managed `availableModels` are active) has a closed list and rejects --
171
+ # null is among what its default can answer -- while the general resolver (the
172
+ # one every ordinary request uses, which turns `opus` into `claude-opus-4-8`)
173
+ # has no answer of its own and falls through to passing the name straight back.
174
+ # Both identities are asked positively, of every model resolver, and an arm
175
+ # answering neither raises instead of swelling the other side: a complement
176
+ # cannot report its own absence.
174
177
 
175
178
 
176
179
  def _best_arms(source: Source) -> list[js.Node]:
177
- """Every arm labelled ``"best"``, which is where new model arms are spliced.
180
+ """Every model resolver's ``"best"`` arm -- where new model arms are spliced.
178
181
 
179
182
  The label is the name and the arm is the node; ``case"best":`` written out
180
- is the two of them with the minifier's spacing in between.
183
+ is the two of them with the minifier's spacing in between. The switch also
184
+ has to *be* a model resolver -- its labels carry the built-in models, the
185
+ same membership `_validators` asks of its array -- because everything here
186
+ applies to every arm of the kind: a throwaway ``case"best":`` in unrelated
187
+ code must be nothing to a registration that would splice into it and to a
188
+ classifier that would be asked what it is (`_resolvers`).
181
189
  """
182
190
  found = []
183
191
  for node in source.find(_BEST):
184
192
  arm = js.up(node, "switch_case")
185
- if arm is not None and arm.child_by_field_name("value") == node:
193
+ if (
194
+ arm is not None
195
+ and arm.child_by_field_name("value") == node
196
+ and set(_BUILT_IN_MODELS) <= _labels(arm)
197
+ ):
186
198
  found.append(arm)
187
199
  return found
188
200
 
189
201
 
190
- def _rejects_unknown(arm: js.Node) -> bool:
191
- """Does this arm's ``switch`` answer ``null`` when nothing matched?
202
+ def _labels(arm: js.Node) -> set[str]:
203
+ """The string labels of this arm's whole ``switch``, as their values.
192
204
 
193
- The two resolvers differ in what an unknown model means to them, which is a
194
- fact about what each is *for*: the override resolver has a closed list and
195
- rejects, the general one passes the name through for something downstream to
196
- judge. Whether either spells an arm as ``case"best":return f()`` or
197
- ``case"best":{return f()}`` is the minifier's business -- and telling them
205
+ A set: which names the switch dispatches on is the question, and neither
206
+ their order nor what sits between them is part of it.
207
+ """
208
+ return {
209
+ js.text(value)[1:-1]
210
+ for case in js.children(arm.parent)
211
+ if case.type == "switch_case"
212
+ for value in [case.child_by_field_name("value")]
213
+ if value is not None and value.type == "string"
214
+ }
215
+
216
+
217
+ def _answer(statement: js.Node) -> js.Node | None:
218
+ """The expression a ``return`` answers with -- nothing for a bare ``return;``."""
219
+ parts = [child for child in js.children(statement) if child.type != "comment"]
220
+ return parts[0] if parts else None
221
+
222
+
223
+ def _may_answer_null(expr: js.Node | None) -> bool:
224
+ """Can this expression's value be ``null``?
225
+
226
+ Asked of the grammar's own value routing, so the rejection keeps counting
227
+ however much recognition upstream composes in front of it: a ternary
228
+ answers with either branch, parentheses and a sequence with their last
229
+ expression, ``||`` and ``??`` with their right side (a null on their left
230
+ is exactly what both exist to pass over), ``&&`` with either side (null is
231
+ falsy), an assignment with the value it assigns. Everything else -- a call,
232
+ an identifier, an ``await`` -- is opaque: it may well evaluate to null, but
233
+ the grammar does not say so, and a rejection hidden behind one is a new
234
+ shape to be told about (`_resolvers` raises), never one to absorb.
235
+
236
+ 2.1.234 is why this is a question about *possible* answers and not the
237
+ answer's spelling: the override default became ``return vXu(e)?xVe(t):null``
238
+ with ``vXu`` a stub returning ``!1`` -- behaviour identical to ``return
239
+ null`` -- and "exactly a bare null" read that build's rejection as gone.
240
+ """
241
+ if expr is None:
242
+ return False
243
+ if expr.type == "null":
244
+ return True
245
+ if expr.type == "ternary_expression":
246
+ return any(
247
+ _may_answer_null(expr.child_by_field_name(field))
248
+ for field in ("consequence", "alternative")
249
+ )
250
+ if expr.type in ("parenthesized_expression", "sequence_expression"):
251
+ parts = [child for child in js.children(expr) if child.type != "comment"]
252
+ return bool(parts) and _may_answer_null(parts[-1])
253
+ if expr.type == "assignment_expression":
254
+ return _may_answer_null(expr.child_by_field_name("right"))
255
+ if expr.type == "binary_expression":
256
+ operator = expr.child_by_field_name("operator")
257
+ sides = {"&&": ("left", "right"), "||": ("right",), "??": ("right",)}
258
+ return any(
259
+ _may_answer_null(expr.child_by_field_name(field))
260
+ for field in sides.get("" if operator is None else operator.type, ())
261
+ )
262
+ return False
263
+
264
+
265
+ def _resolvers(source: Source) -> tuple[list[js.Node], list[js.Node]]:
266
+ """Every model resolver, classified: (override, general).
267
+
268
+ Each identity is asked positively of the arm's own ``switch``: the override
269
+ resolver's default *can answer null* (`_may_answer_null`, over the
270
+ default's scoped returns -- a callback's ``return`` answers for the
271
+ callback); the general resolver's default has no answer of its own --
272
+ absent, or with nothing scoped to return or throw -- so an unknown model
273
+ falls out of the switch and back to the caller's passthrough. Whether
274
+ either spells an arm as ``case"best":return f()`` or
275
+ ``case"best":{return f()}`` is the minifier's business -- telling them
198
276
  apart by that was a silent failure waiting: brace the general resolver (a
199
277
  `let` in the arm is enough) and every family shortcut disappeared with the
200
278
  patch still green.
279
+
280
+ The identities exclude each other by construction, and an arm holding
281
+ neither -- a default that answers something, never null -- raises, which
282
+ `Patch.run` turns into one broken patch naming the new shape (the promise
283
+ `js.only` keeps for cardinality, kept here for classification). Under the
284
+ complement this replaced, such an arm slid silently into the general list,
285
+ and only a required step's zero was left to say anything at all.
201
286
  """
202
- default = next(
203
- (
204
- sibling
205
- for sibling in js.children(arm.parent)
206
- if sibling.type == "switch_default"
207
- ),
208
- None,
209
- )
210
- if default is None:
211
- return False
212
- answer = js.first(default, js.of_type("return_statement"))
213
- return answer is not None and [c.type for c in answer.named_children] == ["null"]
287
+ override: list[js.Node] = []
288
+ general: list[js.Node] = []
289
+ for arm in _best_arms(source):
290
+ default = next(
291
+ (
292
+ sibling
293
+ for sibling in js.children(arm.parent)
294
+ if sibling.type == "switch_default"
295
+ ),
296
+ None,
297
+ )
298
+ answers = js.every(default, js.of_type("return_statement"), scoped=True)
299
+ refusals = js.every(default, js.of_type("throw_statement"), scoped=True)
300
+ if not answers and not refusals:
301
+ general.append(arm)
302
+ elif any(_may_answer_null(_answer(statement)) for statement in answers):
303
+ override.append(arm)
304
+ else:
305
+ answered = [_answer(statement) for statement in answers]
306
+ kinds = sorted(
307
+ {"nothing" if value is None else value.type for value in answered}
308
+ | ({"a throw"} if refusals else set())
309
+ )
310
+ raise ValueError(
311
+ "a model resolver neither rejects an unknown model nor passes "
312
+ f"it through: its default answers {', '.join(kinds)}"
313
+ )
314
+ return override, general
214
315
 
215
316
 
216
317
  def _existing_arms(arm: js.Node) -> set[str]:
@@ -241,12 +342,12 @@ def _arms(resolution: dict[str, str]) -> str:
241
342
 
242
343
  def _override_resolvers(source: Source) -> list[js.Node]:
243
344
  """Every resolver reached only when managed ``availableModels`` are active."""
244
- return [arm for arm in _best_arms(source) if _rejects_unknown(arm)]
345
+ return _resolvers(source)[0]
245
346
 
246
347
 
247
348
  def _general_resolvers(source: Source) -> list[js.Node]:
248
349
  """Every resolver an ordinary request goes through."""
249
- return [arm for arm in _best_arms(source) if not _rejects_unknown(arm)]
350
+ return _resolvers(source)[1]
250
351
 
251
352
 
252
353
  def _register_arms(
@@ -760,6 +861,11 @@ def _codex_models(source: Source, options: Options, outcome: Outcome) -> Source:
760
861
  return source
761
862
 
762
863
  model_ids = [m.id for m in options.codex_models]
864
+ # Classified once here for the gate and the note; the registrations below
865
+ # re-derive from the source each hands the next, since every batch of edits
866
+ # is a new parse. A count that moves on a green run is the early warning.
867
+ override, general = _resolvers(source)
868
+ outcome.note(f"resolvers: {len(override)} override, {len(general)} general")
763
869
  # Family shortcuts (`sol` -> the newest `gpt-<ver>-sol`) resolve to an id
764
870
  # before the request is built, so they ride only the surfaces that accept,
765
871
  # resolve, and show a model -- validator, both resolvers, picker. They need no
@@ -770,7 +876,7 @@ def _codex_models(source: Source, options: Options, outcome: Outcome) -> Source:
770
876
  # so absent it, no shortcut is registered anywhere and the ids (which need none
771
877
  # of this) carry on untouched.
772
878
  shorts = family_aliases(options.codex_models)
773
- if shorts and not _general_resolvers(source):
879
+ if shorts and not general:
774
880
  outcome.note("general model resolver anchor drifted; shortcuts skipped")
775
881
  shorts = {}
776
882
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes