patch-cc 0.3.3__tar.gz → 0.4.0__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.3.3 → patch_cc-0.4.0}/PKG-INFO +11 -9
- {patch_cc-0.3.3 → patch_cc-0.4.0}/README.md +10 -8
- {patch_cc-0.3.3 → patch_cc-0.4.0}/docs/INTERNALS.md +85 -45
- {patch_cc-0.3.3 → patch_cc-0.4.0}/docs/PLAYBOOK.md +68 -6
- {patch_cc-0.3.3 → patch_cc-0.4.0}/docs/corpus.md +16 -7
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/__init__.py +1 -1
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/bun/blob.py +83 -30
- patch_cc-0.4.0/src/patch_cc/bun/container.py +208 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/cli.py +11 -1
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/js.py +388 -135
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/patcher.py +21 -22
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/patches/agents.py +9 -3
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/patches/chrome.py +19 -10
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/patches/codex.py +1 -3
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/patches/streaming.py +185 -37
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/patches/thinking.py +4 -4
- patch_cc-0.3.3/src/patch_cc/bun/container.py +0 -183
- {patch_cc-0.3.3 → patch_cc-0.4.0}/.github/workflows/ci.yml +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/.github/workflows/release-watch.yml +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/.github/workflows/release.yml +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/.gitignore +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/AGENTS.md +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/CLAUDE.md +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/LICENSE +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/docs/CONDUCT.md +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/docs/demo.gif +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/pyproject.toml +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/bun/__init__.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/bun/elf.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/bun/errors.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/bun/macho.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/cache.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/codex/__init__.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/codex/gateway.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/codex/models.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/codex/oauth.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/codex/translate.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/doctor.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/locate.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/menu.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/patches/__init__.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/patches/base.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/patches/output.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/src/patch_cc/ui.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.4.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: patch-cc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
@@ -198,14 +198,16 @@ startup name / `--version` marker are visible tells too.
|
|
|
198
198
|
## Why native-only, and why it stays small
|
|
199
199
|
|
|
200
200
|
Claude Code now ships only as a Bun single-file executable; the npm package is a
|
|
201
|
-
wrapper that downloads it. patch-cc edits the JavaScript
|
|
202
|
-
binary's `.bun` section in place
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
201
|
+
wrapper that downloads it. patch-cc edits the JavaScript modules embedded in the
|
|
202
|
+
binary's `.bun` section in place — since 2.1.242 the app is code-split across
|
|
203
|
+
~1,300 of them, and patch-cc treats every one as a single surface. It also drops
|
|
204
|
+
the stale precompiled bytecode of the modules it edits — editing a module's
|
|
205
|
+
source invalidates its bytecode anyway — so on Linux, where the ELF section is
|
|
206
|
+
rewritten in place, a patched binary is *smaller* than the original, not larger
|
|
207
|
+
(83 MB smaller on 2.1.243). (On macOS the freed bytes are not yet reclaimed, so
|
|
208
|
+
the file keeps its size; it still runs correctly.)
|
|
209
|
+
[docs/INTERNALS.md](docs/INTERNALS.md#the-bytecode-and-why-we-drop-it) has the
|
|
210
|
+
measurements; `patch-cc status` has yours.
|
|
209
211
|
|
|
210
212
|
See [docs/INTERNALS.md](docs/INTERNALS.md) for the container format and
|
|
211
213
|
[docs/PLAYBOOK.md](docs/PLAYBOOK.md) for repairing a patch after an update.
|
|
@@ -167,14 +167,16 @@ startup name / `--version` marker are visible tells too.
|
|
|
167
167
|
## Why native-only, and why it stays small
|
|
168
168
|
|
|
169
169
|
Claude Code now ships only as a Bun single-file executable; the npm package is a
|
|
170
|
-
wrapper that downloads it. patch-cc edits the JavaScript
|
|
171
|
-
binary's `.bun` section in place
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
170
|
+
wrapper that downloads it. patch-cc edits the JavaScript modules embedded in the
|
|
171
|
+
binary's `.bun` section in place — since 2.1.242 the app is code-split across
|
|
172
|
+
~1,300 of them, and patch-cc treats every one as a single surface. It also drops
|
|
173
|
+
the stale precompiled bytecode of the modules it edits — editing a module's
|
|
174
|
+
source invalidates its bytecode anyway — so on Linux, where the ELF section is
|
|
175
|
+
rewritten in place, a patched binary is *smaller* than the original, not larger
|
|
176
|
+
(83 MB smaller on 2.1.243). (On macOS the freed bytes are not yet reclaimed, so
|
|
177
|
+
the file keeps its size; it still runs correctly.)
|
|
178
|
+
[docs/INTERNALS.md](docs/INTERNALS.md#the-bytecode-and-why-we-drop-it) has the
|
|
179
|
+
measurements; `patch-cc status` has yours.
|
|
178
180
|
|
|
179
181
|
See [docs/INTERNALS.md](docs/INTERNALS.md) for the container format and
|
|
180
182
|
[docs/PLAYBOOK.md](docs/PLAYBOOK.md) for repairing a patch after an update.
|
|
@@ -13,7 +13,11 @@ binary:
|
|
|
13
13
|
- **Windows**: a PE `.bun` section (not supported here)
|
|
14
14
|
|
|
15
15
|
Inside that section is a *Bun module graph*: a flat arena of payloads, a module
|
|
16
|
-
table describing them, and a trailer.
|
|
16
|
+
table describing them, and a trailer. Before 2.1.242 the graph was the app in one
|
|
17
|
+
module plus a few asset modules; since then the app is **code-split** across
|
|
18
|
+
~1,300 `chunk-*.js` modules that the entry lazily imports (see
|
|
19
|
+
[the split](#the-21242-split-and-the-patchable-surface)). Either way patch-cc
|
|
20
|
+
treats every module the container declares to be JavaScript as one surface.
|
|
17
21
|
|
|
18
22
|
```
|
|
19
23
|
.bun section
|
|
@@ -35,52 +39,85 @@ A module record (new 52-byte format) is six such pairs — `name`, `contents`,
|
|
|
35
39
|
`sourcemap`, `bytecode`, `moduleInfo`, `bytecodeOriginPath` — followed by four
|
|
36
40
|
`u8` flags (`encoding`, `loader`, `moduleFormat`, `side`).
|
|
37
41
|
|
|
38
|
-
The module we patch is the entrypoint, which the offsets struct names by index
|
|
39
|
-
(`entry_point_id`) — the same index Bun itself resolves it by. Its `contents` is
|
|
40
|
-
the JS we edit. Its *name* is upstream's to change and we never read it:
|
|
41
|
-
2.1.229 renamed it `/$bunfs/root/src/entrypoints/cli.js` → `/$bunfs/root/cli`.
|
|
42
|
-
|
|
43
42
|
Code: `src/patch_cc/bun/blob.py`.
|
|
44
43
|
|
|
45
|
-
## The
|
|
46
|
-
|
|
47
|
-
The entry module also carries precompiled Bun **bytecode** — more than half the
|
|
48
|
-
binary. Every other module has none.
|
|
44
|
+
## The 2.1.242 split, and the patchable surface
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
Through 2.1.241 the entrypoint module *was* the app: one ~28 MB `contents`
|
|
47
|
+
carrying every line patch-cc anchors on. 2.1.242 turned on Bun code-splitting
|
|
48
|
+
with lazy loading, and the shape changed under the tool:
|
|
52
49
|
|
|
53
|
-
|
|
|
50
|
+
| build | modules | entrypoint `contents` |
|
|
54
51
|
|---|---|---|
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
`
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
52
|
+
| 2.1.241 | 11 | 28,249,679 bytes (the whole app) |
|
|
53
|
+
| 2.1.243 | 1,385 | 19,952 bytes (an argv shim) |
|
|
54
|
+
|
|
55
|
+
The entrypoint is now a ~20 KB shim that parses argv and lazily
|
|
56
|
+
`import()`s the app across ~1,300 `/$bunfs/root/chunk-*.js` modules; ~46 MB of
|
|
57
|
+
JS, the largest chunk 7.3 MB. The code did not disappear — every anchor is still
|
|
58
|
+
in the binary — but it left the one module patch-cc used to read, and it does not
|
|
59
|
+
concentrate in a single chunk (`branding` spans a dozen modules, `org-label` ten,
|
|
60
|
+
`spinner-tips` six).
|
|
61
|
+
|
|
62
|
+
So the patchable surface is **every module the container declares to be
|
|
63
|
+
JavaScript**, discovered the way the entrypoint itself is discovered — off the
|
|
64
|
+
artifact, never hardcoded. A module's *loader* (the second trailing flag) is how
|
|
65
|
+
Bun decides whether to compile it as source or hand it over as opaque bytes, and
|
|
66
|
+
the entrypoint is by definition the JS Bun runs, so its loader *is* the JS loader
|
|
67
|
+
(`Blob.js_modules`). The asset modules (the native addons, the bundled
|
|
68
|
+
`mermaid`/`hljs`, the HTML template) carry other loaders and are left alone. A
|
|
69
|
+
pre-split build is the one-module case of this — `js_modules()` returns just the
|
|
70
|
+
monolith — so [`js.Source`](PLAYBOOK.md#the-many-module-surface) spans one module
|
|
71
|
+
or a thousand through the same code.
|
|
72
|
+
|
|
73
|
+
The entrypoint still matters for one thing: it is where the manifest lives and
|
|
74
|
+
what `status` reads, named by the offsets struct's `entry_point_id` — the same
|
|
75
|
+
index Bun resolves it by. Its *name* is upstream's to change and we never read
|
|
76
|
+
it: 2.1.229 renamed it `/$bunfs/root/src/entrypoints/cli.js` → `/$bunfs/root/cli`.
|
|
77
|
+
|
|
78
|
+
## The bytecode, and why we drop it
|
|
79
|
+
|
|
80
|
+
Modules carry precompiled Bun **bytecode** — most of the binary. Before the
|
|
81
|
+
split only the entry module had any (~half of it); the code-split builds carry it
|
|
82
|
+
on nearly every chunk (232 MB of 377 on 2.1.243, across ~1,375 modules).
|
|
83
|
+
|
|
84
|
+
Any edit to a module's `contents` invalidates *that module's* bytecode; Bun
|
|
85
|
+
detects the mismatch and recompiles that module from source at launch. So keeping
|
|
86
|
+
a stale copy buys nothing — the recompile is paid either way — and dropping it
|
|
87
|
+
reclaims the space and guarantees our edits are what runs. patch-cc drops the
|
|
88
|
+
bytecode of exactly the modules it edited (`rebuild` over `changed_modules`) and
|
|
89
|
+
leaves every untouched module its bytecode and its fast start. On Linux, where
|
|
90
|
+
the ELF section is rewritten in place, the binary is smaller by exactly the
|
|
91
|
+
edited modules' bytecode.
|
|
92
|
+
|
|
93
|
+
Measured on 2.1.243, the full patch set:
|
|
94
|
+
|
|
95
|
+
| binary | size | bytecode | startup |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| pristine | 378 MB | 232 MB (every module) | ~13 ms `--version` |
|
|
98
|
+
| patched (edited modules' bytecode dropped) | **295 MB** | 150 MB (untouched modules) | ~15 ms |
|
|
99
|
+
|
|
100
|
+
The 83 MB reclaimed is the ~45 edited modules' bytecode; the rest stays, which is
|
|
101
|
+
why a split-build patched binary is smaller but not the *half* a patched monolith
|
|
102
|
+
was. The recompile is now per lazily-imported edited module rather than the whole
|
|
103
|
+
app at once, so startup barely moves. Read the current figures off any binary
|
|
104
|
+
with `patch-cc status` rather than off this table — the bytecode total grows every
|
|
105
|
+
few builds.
|
|
106
|
+
|
|
107
|
+
The size story is the **ELF** path: the `.bun` section is rewritten in place, so
|
|
108
|
+
the dropped bytecode is genuinely reclaimed (`container.verify` refuses a Linux
|
|
109
|
+
write that did not shrink by about that much). On **macOS** the file keeps its
|
|
110
|
+
original size: `macho.py` grows a segment but never shrinks one, so the freed
|
|
111
|
+
bytes stay as dead space. The binary still runs correctly (the bytecode is gone),
|
|
112
|
+
it is just not smaller — reclaiming it means shrinking the Mach-O segment and
|
|
113
|
+
re-laying `__LINKEDIT`, which is not done yet.
|
|
114
|
+
|
|
115
|
+
Every write asserts each **edited** module carries `bytecode == 0` in the binary
|
|
116
|
+
it produced (`container.verify`, beside the round-trip check), and `patch-cc
|
|
117
|
+
status` reports the total for an installed one. `doctor` cannot: a dry run is
|
|
118
|
+
handed a *clean* bundle, which still has all its bytecode by definition. If a
|
|
119
|
+
future Bun build makes bytecode authoritative over source, that assert is the
|
|
120
|
+
tripwire — every edit would silently no-op otherwise.
|
|
84
121
|
|
|
85
122
|
## Writing it back without ballooning
|
|
86
123
|
|
|
@@ -108,8 +145,10 @@ edit is followed by an ad-hoc `codesign` (mandatory on Apple Silicon).
|
|
|
108
145
|
|
|
109
146
|
## The manifest
|
|
110
147
|
|
|
111
|
-
Every patched bundle
|
|
112
|
-
|
|
148
|
+
Every patched bundle carries a single comment line — appended to the **entry
|
|
149
|
+
module**, the one module always present and always re-extracted, and the one
|
|
150
|
+
`status` reads — describing its shape; [PLAYBOOK.md](PLAYBOOK.md) covers what it
|
|
151
|
+
means for matcher health:
|
|
113
152
|
|
|
114
153
|
```
|
|
115
154
|
//patch-cc {"v":1,"tool":"<version>","patches":[...],"brand":...,"suffix":...,
|
|
@@ -157,7 +196,8 @@ rather than from a store of their own.
|
|
|
157
196
|
and dropped rather than aborting the run. See
|
|
158
197
|
[PLAYBOOK.md](PLAYBOOK.md#the-syntax-gate).
|
|
159
198
|
- Every write is verified: patch-cc re-extracts the JS from the binary it just
|
|
160
|
-
wrote and asserts
|
|
199
|
+
wrote and asserts every module equals what it meant to write, and that each
|
|
200
|
+
module it edited carries no leftover bytecode to run instead of the edit.
|
|
161
201
|
- Patching a binary that is already marked, when no pristine backup exists, is
|
|
162
202
|
refused outright — there is nothing clean to start from, and our edits change
|
|
163
203
|
lengths, so a second pass would corrupt rather than update. `restore` or a
|
|
@@ -152,6 +152,56 @@ gone green, and every one buys a branch against a single build. The counting
|
|
|
152
152
|
below is the net for drift; the rule above is the only thing standing between a
|
|
153
153
|
repair and the next repair.
|
|
154
154
|
|
|
155
|
+
## The many-module surface
|
|
156
|
+
|
|
157
|
+
Through 2.1.241 the bundle was one module — the entrypoint carried the whole app
|
|
158
|
+
— and every matcher searched that one buffer. 2.1.242 code-split it: the
|
|
159
|
+
entrypoint became a ~20 KB argv shim that lazily imports the app across ~1,300
|
|
160
|
+
`chunk-*.js` modules ([INTERNALS](INTERNALS.md#the-21242-split-and-the-patchable-surface)).
|
|
161
|
+
The anchors did not move — they scattered. So `js.Source` **spans every
|
|
162
|
+
JavaScript module the container declares** (`Blob.js_modules`, discovered off the
|
|
163
|
+
entrypoint's own loader), and the one-module world is exactly the many-module
|
|
164
|
+
world with one module: `find`/`literals`/`count` sweep every module, `apply`
|
|
165
|
+
routes each edit to the module its node came from, and a patch says
|
|
166
|
+
`source.find(name)` / `source.apply(edits)` unchanged. This is representation
|
|
167
|
+
change of the crudest kind — the same code in more files — so it *moved* no
|
|
168
|
+
invariant and abolished none; the surface widened, the rules held. Three things
|
|
169
|
+
the move made explicit, each a rule the single-module world could leave unsaid
|
|
170
|
+
because the module was the bundle:
|
|
171
|
+
|
|
172
|
+
- **A node carries its module; an offset does not.** An `Edit` is built from a
|
|
173
|
+
node, and the node's program root (`node.id`, distinct across parses) names the
|
|
174
|
+
module the edit belongs to, so a batch spanning a dozen chunks routes each
|
|
175
|
+
splice home and reparses only the modules it touched. It is why dedup keys are
|
|
176
|
+
the node's `id`, never its `start_byte`: a minified offset repeats in a
|
|
177
|
+
thousand modules, and two different sites at the same local offset are two
|
|
178
|
+
sites, not one to skip.
|
|
179
|
+
|
|
180
|
+
- **A minified local is scoped to its module.** Resolving one — a hoisted
|
|
181
|
+
`agentType:uu` constant read through its value — searches *its own* module
|
|
182
|
+
(`Source.find_local`), never the bundle, where `uu` binds a string in a dozen
|
|
183
|
+
unrelated chunks and `js.only` would rightly refuse to choose. Before the split
|
|
184
|
+
the module *was* the bundle, so a bundle-wide `find` was a module-wide one; the
|
|
185
|
+
two only came apart when the app was dealt across files. The same lesson the
|
|
186
|
+
playbook already states for identifiers within a build — "a name is a spelling
|
|
187
|
+
until its scope is said" — now reaches the module boundary too. A scope search
|
|
188
|
+
that climbs outward (`live-thinking`'s state and memo resolution) reads the
|
|
189
|
+
module wrapper by what it *is* — the pre-split monolith's whole-body IIFE is a
|
|
190
|
+
module of one top-level statement; a split module's `program` is dozens, and
|
|
191
|
+
its top-level components are real scopes to search — never by a fixed position,
|
|
192
|
+
which read every split component as the un-searchable wrapper and found nothing.
|
|
193
|
+
|
|
194
|
+
- **The gate reads code, not linkage.** tree-sitter's JavaScript grammar does not
|
|
195
|
+
model a reserved word as an import/export alias (`export{x as if}`,
|
|
196
|
+
`import{if as a}` — legal ES2015, which the split minifier emits when its
|
|
197
|
+
two-letter alias generator lands on `if`/`in`/`do`), so it plants a localized
|
|
198
|
+
`ERROR` inside the clause. patch-cc never locates in or edits module linkage —
|
|
199
|
+
every anchor is executable code — so `Source.defect` reads *past* an error
|
|
200
|
+
confined to an import/export statement and returns the first one that is not,
|
|
201
|
+
while a splice that broke real code still lands outside linkage and is still
|
|
202
|
+
caught. It is the gate's scope stated precisely, not a tolerance bolted on: the
|
|
203
|
+
eight such modules on 2.1.243 parse clean for every purpose the tool has.
|
|
204
|
+
|
|
155
205
|
## Discovery instead of hardcoding
|
|
156
206
|
|
|
157
207
|
Anything the binary can enumerate for us, it does:
|
|
@@ -313,7 +363,9 @@ re-deriving it.
|
|
|
313
363
|
|
|
314
364
|
Both numbers above are counts, and a count cannot tell a rewrite that landed
|
|
315
365
|
from a rewrite that landed *one prop-name to the left*. So the bundle is parsed,
|
|
316
|
-
and any `ERROR` or `MISSING` node aborts
|
|
366
|
+
and any `ERROR` or `MISSING` node **outside module linkage** aborts
|
|
367
|
+
(`src/patch_cc/js.py`; the linkage read-past is
|
|
368
|
+
[above](#the-many-module-surface)).
|
|
317
369
|
|
|
318
370
|
The parse is not a separate pass any more: locating already needs it, and an
|
|
319
371
|
edit costs one incremental reparse — ~75 ms on a 25 MB bundle against ~3 s for a
|
|
@@ -493,6 +545,10 @@ that broke a patch along with every note.
|
|
|
493
545
|
patch-cc extract ~/.local/share/claude/versions/<ver> > clean.js
|
|
494
546
|
```
|
|
495
547
|
|
|
548
|
+
Since 2.1.242 that is every JavaScript module concatenated, each behind a
|
|
549
|
+
`// ==== patch-cc module <n> ====` header, so `rg` still works over one file
|
|
550
|
+
and the header says which chunk a hit lives in.
|
|
551
|
+
|
|
496
552
|
2. Run `patch-cc doctor`. Note which patch dropped to `candidates == 0`, or —
|
|
497
553
|
for `live-thinking` — which sub-step.
|
|
498
554
|
|
|
@@ -856,11 +912,17 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
|
|
|
856
912
|
than carried over from whichever renderer the first step happened to see
|
|
857
913
|
last: a second component with the same four props donated its local to the
|
|
858
914
|
first one's memo, spliced an identifier that component cannot see, and
|
|
859
|
-
reported both steps green. The memo itself is found by what it computes
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
915
|
+
reported both steps green. The memo itself is found by what it computes — an
|
|
916
|
+
arrow whose whole body is a `flatMap` that wraps each streaming block as a
|
|
917
|
+
message, and the call that takes that arrow — never by the memo hook's name.
|
|
918
|
+
The hook is one more minified spelling: the monolith reaches it as
|
|
919
|
+
`<React>.useMemo(...)` (the property name survives a member read), but a
|
|
920
|
+
code-split module imports it under a local (`import{useMemo as te}` → `te(...)`),
|
|
921
|
+
so keying on `useMemo` read a build that memoizes exactly the same way as one
|
|
922
|
+
with no memo at all, and the rewrite reuses the memo's *own* callee rather than
|
|
923
|
+
re-spelling `.useMemo`. What a claim about the *text* around the block
|
|
924
|
+
(`.contentBlock]}`) only approximated is the computation itself: one sibling
|
|
925
|
+
property beside `content` read as the whole thing being gone.
|
|
864
926
|
|
|
865
927
|
**`display-mode`** defaults the request's thinking display to `"summarized"`;
|
|
866
928
|
without it the API only streams summary text when the `showThinkingSummaries`
|
|
@@ -21,11 +21,13 @@ matches means the file changed under you, not that a number moved.
|
|
|
21
21
|
|
|
22
22
|
## On disk now
|
|
23
23
|
|
|
24
|
-
The whole published span `2.1.210` → `2.1.
|
|
25
|
-
one pristine binary per version,
|
|
26
|
-
(
|
|
27
|
-
|
|
28
|
-
`
|
|
24
|
+
The whole published span `2.1.210` → `2.1.243` — 2.1.230 was never published —
|
|
25
|
+
one pristine binary per version, 33 in all. It straddles the **2.1.242 code
|
|
26
|
+
split** (INTERNALS.md): `2.1.242`/`2.1.243` are the first many-module builds, and
|
|
27
|
+
they jump ~35 MB over `2.1.241` for it. Several pairs share a byte count
|
|
28
|
+
(`2.1.225`/`2.1.226`, `2.1.229`/`2.1.231`, `2.1.239`–`2.1.241`, `2.1.242`/`2.1.243`)
|
|
29
|
+
and are still distinct binaries, which is why the identity column is the hash (of
|
|
30
|
+
the whole file, `sha256sum <version>.orig`) and never the size:
|
|
29
31
|
|
|
30
32
|
| version | size | sha256 |
|
|
31
33
|
|---|---|---|
|
|
@@ -55,10 +57,17 @@ which is why the identity column is the hash (of the whole file,
|
|
|
55
57
|
| `2.1.234` | 328 MB | `3473601ea695d5bf769c5b202844d4cb4fbf723ae995450fcb6973204775c84a` |
|
|
56
58
|
| `2.1.235` | 331 MB | `bfcf0ae2dbf94b2b6a106074aabf3938b9a10889c3b678e4cb5a00c03274d5d5` |
|
|
57
59
|
| `2.1.236` | 335 MB | `6c8818fa22187aa555c242be4abbacc44d6b71a32ac9631ee7b2b5d12f51f752` |
|
|
60
|
+
| `2.1.237` | 335 MB | `73975167f0108693cf6fd6614994781657ebb8456ebef5d247458734abfb3916` |
|
|
61
|
+
| `2.1.238` | 339 MB | `0933b286cf94e1b2504b35ac165ab76b8f822735d53371c56393988c23040d58` |
|
|
62
|
+
| `2.1.239` | 343 MB | `7de1b1576e2e0be73ce91c2b4dedf16a41058ea633b957a36fdc6044ddfc0f3c` |
|
|
63
|
+
| `2.1.240` | 343 MB | `1386169da77de19a655f07a86ab80f5775983a50eb0c9c27a7daf16e7320322d` |
|
|
64
|
+
| `2.1.241` | 343 MB | `0771bd866cff82b76581fc0499f6529e1a36845078f144f8c81dccb3bc7037b8` |
|
|
65
|
+
| `2.1.242` | 378 MB | `528ef039aa7d64d7b3fbc06925132755a516b4dcaad784cf0b51fe03167360d4` |
|
|
66
|
+
| `2.1.243` | 378 MB | `4b0dafeedd0b469c41988e200036fd773e7553ba960349c9f02a82c6d1f2ba27` |
|
|
58
67
|
|
|
59
68
|
This set covers the span the playbook's tree-move measurements were taken over
|
|
60
|
-
(`2.1.210` → `2.1.233`), so
|
|
61
|
-
historical.
|
|
69
|
+
(`2.1.210` → `2.1.233`) and the 2.1.242 split, so both are re-checkable here
|
|
70
|
+
rather than historical.
|
|
62
71
|
|
|
63
72
|
## Rebuild or extend it
|
|
64
73
|
|
|
@@ -24,6 +24,11 @@ from .errors import BunError
|
|
|
24
24
|
TRAILER = b"\n---- Bun! ----\n"
|
|
25
25
|
OFFSETS_SIZE = 32
|
|
26
26
|
|
|
27
|
+
#: Index of the ``loader`` byte in a module record's four trailing flags
|
|
28
|
+
#: (``encoding, loader, module_format, side``). It is how Bun decides whether a
|
|
29
|
+
#: module is JavaScript it compiles or an asset it hands over as bytes.
|
|
30
|
+
LOADER = 1
|
|
31
|
+
|
|
27
32
|
#: Module record field order. Old Bun (<1.3.7) stops after ``bytecode``.
|
|
28
33
|
FIELDS_NEW = (
|
|
29
34
|
"name",
|
|
@@ -86,18 +91,40 @@ class Blob:
|
|
|
86
91
|
"""
|
|
87
92
|
return self.modules[self.entry_point_id]
|
|
88
93
|
|
|
89
|
-
def
|
|
90
|
-
|
|
94
|
+
def js_modules(self) -> list[Module]:
|
|
95
|
+
"""Every module Bun loads the way it loads the entrypoint: the JS the
|
|
96
|
+
app is made of.
|
|
97
|
+
|
|
98
|
+
Since 2.1.242 the entrypoint is a ~20 KB argv shim that lazily imports
|
|
99
|
+
the app across ~1,300 ``chunk-*.js`` modules; before it, the entrypoint
|
|
100
|
+
*was* the whole app. Both are one question -- which modules carry the
|
|
101
|
+
JavaScript we patch -- and the loader byte answers it: a module's
|
|
102
|
+
``loader`` (the second trailing flag) says how Bun reads it, and the
|
|
103
|
+
entry is by definition the JS Bun runs, so its loader *is* the JS loader.
|
|
104
|
+
Read off the artifact, exactly as :meth:`entry_module` reads the
|
|
105
|
+
entrypoint -- never hardcoded to a value, and never by name -- so a build
|
|
106
|
+
that renumbers the loader still answers correctly. The assets (native
|
|
107
|
+
addons, the bundled ``mermaid``/``hljs`` minified files, the HTML
|
|
108
|
+
template) carry other loaders and are excluded: they are opaque bytes to
|
|
109
|
+
Bun and would not parse as our JavaScript.
|
|
110
|
+
|
|
111
|
+
Pre-split, this is the one-element list ``[entry_module()]`` -- the
|
|
112
|
+
monolith -- so the single-module world is exactly the many-module world
|
|
113
|
+
with one module, and every layer above flows through the same code.
|
|
114
|
+
"""
|
|
115
|
+
loader = self.entry_module().trailing[LOADER]
|
|
116
|
+
return [m for m in self.modules if m.trailing[LOADER] == loader]
|
|
91
117
|
|
|
92
118
|
def bytecode_size(self) -> int:
|
|
93
|
-
"""
|
|
119
|
+
"""Total precompiled Bun bytecode across every module, for either layout.
|
|
94
120
|
|
|
95
121
|
``bytecode`` is the fourth pair, so *both* record formats carry it --
|
|
96
|
-
``FIELDS_OLD`` is the first four of ``FIELDS_NEW``.
|
|
97
|
-
|
|
98
|
-
|
|
122
|
+
``FIELDS_OLD`` is the first four of ``FIELDS_NEW``. Before 2.1.242 only
|
|
123
|
+
the entrypoint carried any; the code-split builds carry it on every
|
|
124
|
+
chunk, so the figure ``status`` reports and the write reclaims is the sum,
|
|
125
|
+
not one module's.
|
|
99
126
|
"""
|
|
100
|
-
return
|
|
127
|
+
return sum(m.ranges["bytecode"][1] for m in self.modules)
|
|
101
128
|
|
|
102
129
|
|
|
103
130
|
def _detect_struct_size(modules_len: int) -> int:
|
|
@@ -192,20 +219,29 @@ def parse(data: bytes) -> Blob:
|
|
|
192
219
|
)
|
|
193
220
|
|
|
194
221
|
|
|
195
|
-
def rebuild(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
the
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
222
|
+
def rebuild(
|
|
223
|
+
blob: Blob, sources: dict[int, bytes], *, drop_bytecode: bool = True
|
|
224
|
+
) -> bytes:
|
|
225
|
+
"""Return a new blob with the given modules' source replaced.
|
|
226
|
+
|
|
227
|
+
``sources`` maps a module index to its new ``contents``; every other
|
|
228
|
+
module's bytes are re-emitted unchanged. Payloads keep their original file
|
|
229
|
+
order so the result stays as close to the input layout as possible. One
|
|
230
|
+
edited module or a thousand is the same code -- the pre-split monolith is
|
|
231
|
+
just ``{entry_index: new_bytes}``.
|
|
232
|
+
|
|
233
|
+
``drop_bytecode`` removes the precompiled Bun bytecode of exactly the
|
|
234
|
+
modules whose source changed. Editing a module's source invalidates its
|
|
235
|
+
bytecode -- Bun recompiles that module from source -- so keeping it would
|
|
236
|
+
run the recompile cost *and* the megabytes; dropping it reclaims the space
|
|
237
|
+
and, more importantly, guarantees our edits are what runs
|
|
238
|
+
(docs/INTERNALS.md). A module we did not touch keeps its bytecode and its
|
|
239
|
+
fast start. The set actually changed is returned by :func:`changed_modules`;
|
|
240
|
+
here we drop for every module handed new bytes, since a caller only passes
|
|
241
|
+
bytes it means to replace.
|
|
206
242
|
"""
|
|
207
|
-
entry = blob.entry_module()
|
|
208
243
|
fields = blob.fields
|
|
244
|
+
changed = set(sources)
|
|
209
245
|
|
|
210
246
|
# Every payload, in the order it appears in the source arena.
|
|
211
247
|
placed: list[
|
|
@@ -222,15 +258,15 @@ def rebuild(blob: Blob, source: bytes, *, drop_bytecode: bool = True) -> bytes:
|
|
|
222
258
|
new_ranges: dict[tuple[int, str], tuple[int, int]] = {}
|
|
223
259
|
prev_end = 0
|
|
224
260
|
for off, length, mod_index, field in placed:
|
|
225
|
-
|
|
226
|
-
if
|
|
261
|
+
edited = mod_index in changed
|
|
262
|
+
if edited and field == "bytecode" and drop_bytecode:
|
|
227
263
|
new_ranges[(mod_index, field)] = (0, 0)
|
|
228
264
|
prev_end = off + length
|
|
229
265
|
continue
|
|
230
266
|
|
|
231
267
|
payload = (
|
|
232
|
-
|
|
233
|
-
if (
|
|
268
|
+
sources[mod_index]
|
|
269
|
+
if (edited and field == "contents")
|
|
234
270
|
else blob.data[off : off + length]
|
|
235
271
|
)
|
|
236
272
|
# Preserve the 1-byte separators Bun emits between payloads.
|
|
@@ -268,16 +304,33 @@ def rebuild(blob: Blob, source: bytes, *, drop_bytecode: bool = True) -> bytes:
|
|
|
268
304
|
struct.pack_into("<I", out, offsets_at + 28, blob.flags)
|
|
269
305
|
|
|
270
306
|
if drop_bytecode:
|
|
271
|
-
#
|
|
272
|
-
# or it had none to begin with. Reading the outcome with a
|
|
273
|
-
# default is what lets the had-none case flow through the same
|
|
274
|
-
# instead of raising ``KeyError
|
|
275
|
-
#
|
|
276
|
-
#
|
|
277
|
-
|
|
307
|
+
# Each edited module now carries no bytecode -- whether we dropped a real
|
|
308
|
+
# payload or it had none to begin with. Reading the outcome with a
|
|
309
|
+
# ``(0, 0)`` default is what lets the had-none case flow through the same
|
|
310
|
+
# check instead of raising ``KeyError``: a field the ``placed`` loop never
|
|
311
|
+
# saw a length for is absent from ``new_ranges``, and absent *is*
|
|
312
|
+
# stripped.
|
|
313
|
+
for index in changed:
|
|
314
|
+
assert new_ranges.get((index, "bytecode"), (0, 0)) == (0, 0)
|
|
278
315
|
return bytes(out)
|
|
279
316
|
|
|
280
317
|
|
|
318
|
+
def changed_modules(blob: Blob, sources: dict[int, bytes]) -> dict[int, bytes]:
|
|
319
|
+
"""The subset of ``sources`` whose bytes actually differ from the blob.
|
|
320
|
+
|
|
321
|
+
A module the patches parsed but left byte-identical must not have its
|
|
322
|
+
bytecode dropped: that would trade a working fast-start module for a slow
|
|
323
|
+
recompile of code that never changed. So the write reclaims bytecode for the
|
|
324
|
+
modules that moved and no others, and the size tripwire measures against that
|
|
325
|
+
same set.
|
|
326
|
+
"""
|
|
327
|
+
return {
|
|
328
|
+
index: data
|
|
329
|
+
for index, data in sources.items()
|
|
330
|
+
if data != blob.payload(blob.modules[index].ranges["contents"])
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
281
334
|
def unwrap_section(section: bytes) -> tuple[bytes, int]:
|
|
282
335
|
"""Strip the length prefix a container section puts in front of the blob.
|
|
283
336
|
|