patch-cc 0.3.3__tar.gz → 0.5.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.5.0}/AGENTS.md +12 -6
- {patch_cc-0.3.3 → patch_cc-0.5.0}/PKG-INFO +14 -12
- {patch_cc-0.3.3 → patch_cc-0.5.0}/README.md +13 -11
- patch_cc-0.5.0/docs/INTERNALS.md +266 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/docs/PLAYBOOK.md +149 -28
- {patch_cc-0.3.3 → patch_cc-0.5.0}/docs/corpus.md +27 -9
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/__init__.py +1 -1
- patch_cc-0.5.0/src/patch_cc/bun/blob.py +544 -0
- patch_cc-0.5.0/src/patch_cc/bun/container.py +276 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/cli.py +44 -8
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/doctor.py +109 -2
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/js.py +431 -135
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/menu.py +38 -4
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patcher.py +34 -29
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patches/agents.py +9 -3
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patches/base.py +14 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patches/chrome.py +52 -12
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patches/codex.py +1 -3
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patches/streaming.py +283 -124
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patches/thinking.py +4 -4
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/ui.py +31 -5
- patch_cc-0.3.3/docs/INTERNALS.md +0 -164
- patch_cc-0.3.3/src/patch_cc/bun/blob.py +0 -305
- patch_cc-0.3.3/src/patch_cc/bun/container.py +0 -183
- {patch_cc-0.3.3 → patch_cc-0.5.0}/.github/workflows/ci.yml +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/.github/workflows/release-watch.yml +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/.github/workflows/release.yml +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/.gitignore +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/CLAUDE.md +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/LICENSE +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/docs/CONDUCT.md +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/docs/demo.gif +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/pyproject.toml +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/bun/__init__.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/bun/elf.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/bun/errors.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/bun/macho.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/cache.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/codex/__init__.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/codex/gateway.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/codex/models.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/codex/oauth.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/codex/translate.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/locate.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patches/__init__.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/src/patch_cc/patches/output.py +0 -0
- {patch_cc-0.3.3 → patch_cc-0.5.0}/uv.lock +0 -0
|
@@ -17,9 +17,15 @@ point it at the pristine copies in `~/.local/share/patch-cc/backups/` to sweep
|
|
|
17
17
|
older builds). There is no test suite by design — doctor against real bundles
|
|
18
18
|
is the check.
|
|
19
19
|
|
|
20
|
-
Be exact about what that covers: doctor runs the **matchers
|
|
21
|
-
bundle they produce
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
Be exact about what that covers: doctor runs the **matchers**, parses the
|
|
21
|
+
bundle they produce, then **bakes it into a temp binary and executes
|
|
22
|
+
`--version`** — so the sweep also proves every build writes and boots (the
|
|
23
|
+
container layer included; 2.1.246 is why). It still never runs the gateway,
|
|
24
|
+
the translator, the OAuth flow or the menu, so a change under
|
|
25
|
+
`src/patch_cc/codex/` (the bridge — all of it runtime, none of it a patch; the
|
|
26
|
+
Codex *patch* is `patches/codex.py`, which the sweep does cover) or in
|
|
27
|
+
`menu.py` is checked by exercising it — a real `codex serve` against a real
|
|
28
|
+
turn — and a green sweep says nothing about it. Nor does `--version` exercise
|
|
29
|
+
a patch's *feature* at runtime: it proves the patched code loads and runs, not
|
|
30
|
+
that a streamed turn renders — behaviour changes still want a real turn
|
|
31
|
+
watched.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: patch-cc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -94,7 +94,7 @@ patch-cc # then just run it
|
|
|
94
94
|
| Chrome & branding | Disable spinner tips | No rotating tips on the spinner |
|
|
95
95
|
| | Mark `--version` | Appends `(patched)` — or any marker you choose |
|
|
96
96
|
| | Custom startup name | Defaults to `<your username>'s Code` |
|
|
97
|
-
| | Startup org/email label | Replace the org/email on the welcome screen — or hide it (demo mode keeps the stock line) |
|
|
97
|
+
| | Startup org/email label | Replace the org/email on the welcome screen — or hide it (demo mode keeps the stock line). Upstream stopped drawing the segment in 2.1.246, so newer builds offer this dimmed as *not on this build* |
|
|
98
98
|
|
|
99
99
|
## Usage
|
|
100
100
|
|
|
@@ -113,8 +113,8 @@ uvx patch-cc apply --org-label "Ada's Lab" # ...or show this instead
|
|
|
113
113
|
uvx patch-cc apply --codex gpt-5.6-sol # + a Codex model (see below)
|
|
114
114
|
uvx patch-cc apply --from-cache # replay your last remembered selection
|
|
115
115
|
uvx patch-cc status # exactly what is applied
|
|
116
|
-
uvx patch-cc doctor # do all patches match this build?
|
|
117
|
-
uvx patch-cc doctor path/to/claude # ...
|
|
116
|
+
uvx patch-cc doctor # do all patches match this build, and does it bake + boot?
|
|
117
|
+
uvx patch-cc doctor path/to/claude # ...same, for some other binary
|
|
118
118
|
uvx patch-cc list # every patch, described
|
|
119
119
|
uvx patch-cc restore # put the original back
|
|
120
120
|
```
|
|
@@ -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.
|
|
@@ -63,7 +63,7 @@ patch-cc # then just run it
|
|
|
63
63
|
| Chrome & branding | Disable spinner tips | No rotating tips on the spinner |
|
|
64
64
|
| | Mark `--version` | Appends `(patched)` — or any marker you choose |
|
|
65
65
|
| | Custom startup name | Defaults to `<your username>'s Code` |
|
|
66
|
-
| | Startup org/email label | Replace the org/email on the welcome screen — or hide it (demo mode keeps the stock line) |
|
|
66
|
+
| | Startup org/email label | Replace the org/email on the welcome screen — or hide it (demo mode keeps the stock line). Upstream stopped drawing the segment in 2.1.246, so newer builds offer this dimmed as *not on this build* |
|
|
67
67
|
|
|
68
68
|
## Usage
|
|
69
69
|
|
|
@@ -82,8 +82,8 @@ uvx patch-cc apply --org-label "Ada's Lab" # ...or show this instead
|
|
|
82
82
|
uvx patch-cc apply --codex gpt-5.6-sol # + a Codex model (see below)
|
|
83
83
|
uvx patch-cc apply --from-cache # replay your last remembered selection
|
|
84
84
|
uvx patch-cc status # exactly what is applied
|
|
85
|
-
uvx patch-cc doctor # do all patches match this build?
|
|
86
|
-
uvx patch-cc doctor path/to/claude # ...
|
|
85
|
+
uvx patch-cc doctor # do all patches match this build, and does it bake + boot?
|
|
86
|
+
uvx patch-cc doctor path/to/claude # ...same, for some other binary
|
|
87
87
|
uvx patch-cc list # every patch, described
|
|
88
88
|
uvx patch-cc restore # put the original back
|
|
89
89
|
```
|
|
@@ -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.
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# Internals
|
|
2
|
+
|
|
3
|
+
How patch-cc gets from a Claude binary to a patched, smaller one.
|
|
4
|
+
|
|
5
|
+
## The shape of a native Claude binary
|
|
6
|
+
|
|
7
|
+
Claude Code ships as a [Bun](https://bun.sh) single-file executable. The whole
|
|
8
|
+
app — a ~20 MB minified JS bundle plus a few asset modules — is embedded in the
|
|
9
|
+
binary:
|
|
10
|
+
|
|
11
|
+
- **Linux**: an ELF section named `.bun`
|
|
12
|
+
- **macOS**: a Mach-O section `__BUN,__bun`
|
|
13
|
+
- **Windows**: a PE `.bun` section (not supported here)
|
|
14
|
+
|
|
15
|
+
Inside that section is a *Bun module graph*: a flat arena of payloads, a module
|
|
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.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
.bun section
|
|
24
|
+
└── [u64 size prefix] (u32 on Bun < 1.3.4)
|
|
25
|
+
└── Bun blob
|
|
26
|
+
├── payload arena name / contents / sourcemap / bytecode / ... bytes,
|
|
27
|
+
│ plus the record chain's own payloads (Bun >= 1.4.1)
|
|
28
|
+
├── module table N records × 52 bytes (36 on old Bun)
|
|
29
|
+
├── record chain flag-gated records (Bun >= 1.4.1), see below
|
|
30
|
+
├── compileExecArgv
|
|
31
|
+
├── offsets struct 32 bytes: byteCount, modulesPtr, entryId, argvPtr, flags
|
|
32
|
+
└── "\n---- Bun! ----\n" 15-byte trailer
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Every pointer is a `(u32 offset, u32 length)` pair relative to the blob start,
|
|
36
|
+
and pointers live in exactly three places: the module table, the offsets
|
|
37
|
+
struct, and the record chain. That is what makes rewriting tractable — move a
|
|
38
|
+
payload, fix the handful of pointers that describe it.
|
|
39
|
+
|
|
40
|
+
Two invariants ride on payload *positions* rather than pointers, and `rebuild`
|
|
41
|
+
preserves both by keeping every payload's inter-payload gap and its offset
|
|
42
|
+
phase modulo 128 — so a rebuild with no edits reproduces the blob byte for
|
|
43
|
+
byte, and one with edits moves payloads only in whole alignment steps:
|
|
44
|
+
|
|
45
|
+
- Bytecode payloads (module bytecode, and the record chain's bytecode blobs)
|
|
46
|
+
sit at blob `offset % 128 == 120`, which is 128-byte alignment once the
|
|
47
|
+
section's 8-byte size prefix is in front. Bun ≥ 1.4.1 deserializes bytecode
|
|
48
|
+
in place and calls misalignment "a runtime assertion error or segfault"
|
|
49
|
+
(`append_bytecode_aligned`); older Bun quietly tolerated the phase drift the
|
|
50
|
+
rewriter used to introduce.
|
|
51
|
+
- `count_z` payloads (names, contents) carry a NUL terminator in the gap
|
|
52
|
+
after them.
|
|
53
|
+
|
|
54
|
+
A module record (new 52-byte format) is six such pairs — `name`, `contents`,
|
|
55
|
+
`sourcemap`, `bytecode`, `moduleInfo`, `bytecodeOriginPath` — followed by four
|
|
56
|
+
`u8` flags (`encoding`, `loader`, `moduleFormat`, `side`).
|
|
57
|
+
|
|
58
|
+
Code: `src/patch_cc/bun/blob.py`.
|
|
59
|
+
|
|
60
|
+
## The record chain (Bun ≥ 1.4.1)
|
|
61
|
+
|
|
62
|
+
2.1.246 moved to Bun 1.4.1, whose `StandaloneModuleGraph.rs` chains optional
|
|
63
|
+
records directly after the module table, each announced by a new `flags` bit
|
|
64
|
+
and read back in flag order:
|
|
65
|
+
|
|
66
|
+
| bit | record |
|
|
67
|
+
|---|---|
|
|
68
|
+
| 5 | `[u32; modules]` — each module's WTF hash of its source text (0 = none) |
|
|
69
|
+
| 6 | `u32 count`, then `count` × `{u32 id, ptr}` — internal-module bytecode |
|
|
70
|
+
| 7 | one pointer: the **shared bytecode string table** |
|
|
71
|
+
| 8 | `u32` — how many leading modules load before the first `import()` |
|
|
72
|
+
| 9 | one pointer: the string table `moduleInfo` bodies index |
|
|
73
|
+
|
|
74
|
+
The pointers point back into the arena: the shared string table (~9.9 MB on
|
|
75
|
+
2.1.246) is the string data **every chunk's bytecode references by ordinal**,
|
|
76
|
+
so it is load-bearing for every module we did *not* touch. The hash is JSC's
|
|
77
|
+
SourceCodeKey hash, how a launch that runs from bytecode avoids paging in
|
|
78
|
+
source text just to hash it.
|
|
79
|
+
|
|
80
|
+
patch-cc parses the chain record for record (`_parse_records` — a build with
|
|
81
|
+
none of the bits, which is every Bun before 1.4.1, walks zero records through
|
|
82
|
+
the same code), carries the pointed-at payloads through `rebuild` like any
|
|
83
|
+
module payload, copies the whole tail between table and offsets struct
|
|
84
|
+
verbatim, and re-points the pointers in place. Two details matter:
|
|
85
|
+
|
|
86
|
+
- An **edited** module's hash word is zeroed — upstream's own "none, compute
|
|
87
|
+
it" value — because the pristine text's hash must not key our bytes in JSC's
|
|
88
|
+
source cache.
|
|
89
|
+
- **Unknown** record bits are refused at parse: a record of unknown size
|
|
90
|
+
cannot be walked past nor re-pointed, and rewriting around it is exactly how
|
|
91
|
+
a graph gets corrupted. 2.1.246 against patch-cc ≤ 0.4.0 is the lesson: the
|
|
92
|
+
chain-blind rewriter dropped the records and zero-filled the string table
|
|
93
|
+
while every *module* round-tripped byte-perfect — matcher-green, dead at
|
|
94
|
+
launch, `SIGSEGV` from inside Bun's graph loader.
|
|
95
|
+
|
|
96
|
+
## The 2.1.242 split, and the patchable surface
|
|
97
|
+
|
|
98
|
+
Through 2.1.241 the entrypoint module *was* the app: one ~28 MB `contents`
|
|
99
|
+
carrying every line patch-cc anchors on. 2.1.242 turned on Bun code-splitting
|
|
100
|
+
with lazy loading, and the shape changed under the tool:
|
|
101
|
+
|
|
102
|
+
| build | modules | entrypoint `contents` |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| 2.1.241 | 11 | 28,249,679 bytes (the whole app) |
|
|
105
|
+
| 2.1.243 | 1,385 | 19,952 bytes (an argv shim) |
|
|
106
|
+
|
|
107
|
+
The entrypoint is now a ~20 KB shim that parses argv and lazily
|
|
108
|
+
`import()`s the app across ~1,300 `/$bunfs/root/chunk-*.js` modules; ~46 MB of
|
|
109
|
+
JS, the largest chunk 7.3 MB. The code did not disappear — every anchor is still
|
|
110
|
+
in the binary — but it left the one module patch-cc used to read, and it does not
|
|
111
|
+
concentrate in a single chunk (`branding` spans a dozen modules, `org-label` ten,
|
|
112
|
+
`spinner-tips` six).
|
|
113
|
+
|
|
114
|
+
So the patchable surface is **every module the container declares to be
|
|
115
|
+
JavaScript**, discovered the way the entrypoint itself is discovered — off the
|
|
116
|
+
artifact, never hardcoded. A module's *loader* (the second trailing flag) is how
|
|
117
|
+
Bun decides whether to compile it as source or hand it over as opaque bytes, and
|
|
118
|
+
the entrypoint is by definition the JS Bun runs, so its loader *is* the JS loader
|
|
119
|
+
(`Blob.js_modules`). The asset modules (the native addons, the bundled
|
|
120
|
+
`mermaid`/`hljs`, the HTML template) carry other loaders and are left alone. A
|
|
121
|
+
pre-split build is the one-module case of this — `js_modules()` returns just the
|
|
122
|
+
monolith — so [`js.Source`](PLAYBOOK.md#the-many-module-surface) spans one module
|
|
123
|
+
or a thousand through the same code.
|
|
124
|
+
|
|
125
|
+
The entrypoint still matters for one thing: it is where the manifest lives and
|
|
126
|
+
what `status` reads, named by the offsets struct's `entry_point_id` — the same
|
|
127
|
+
index Bun resolves it by. Its *name* is upstream's to change and we never read
|
|
128
|
+
it: 2.1.229 renamed it `/$bunfs/root/src/entrypoints/cli.js` → `/$bunfs/root/cli`.
|
|
129
|
+
|
|
130
|
+
## The bytecode, and why we drop it
|
|
131
|
+
|
|
132
|
+
Modules carry precompiled Bun **bytecode** — most of the binary. Before the
|
|
133
|
+
split only the entry module had any (~half of it); the code-split builds carry it
|
|
134
|
+
on nearly every chunk (232 MB of 377 on 2.1.243, across ~1,375 modules).
|
|
135
|
+
|
|
136
|
+
Any edit to a module's `contents` invalidates *that module's* bytecode; Bun
|
|
137
|
+
detects the mismatch and recompiles that module from source at launch. So keeping
|
|
138
|
+
a stale copy buys nothing — the recompile is paid either way — and dropping it
|
|
139
|
+
reclaims the space and guarantees our edits are what runs. patch-cc drops the
|
|
140
|
+
bytecode of exactly the modules it edited (`rebuild` over `changed_modules`) and
|
|
141
|
+
leaves every untouched module its bytecode and its fast start — which is why
|
|
142
|
+
the shared bytecode string table those modules' bytecode indexes
|
|
143
|
+
([the record chain](#the-record-chain-bun--141)) is never droppable. On Linux,
|
|
144
|
+
where the ELF section is rewritten in place, the binary is smaller by exactly
|
|
145
|
+
the edited modules' bytecode.
|
|
146
|
+
|
|
147
|
+
Measured on 2.1.243, the full patch set:
|
|
148
|
+
|
|
149
|
+
| binary | size | bytecode | startup |
|
|
150
|
+
|---|---|---|---|
|
|
151
|
+
| pristine | 378 MB | 232 MB (every module) | ~13 ms `--version` |
|
|
152
|
+
| patched (edited modules' bytecode dropped) | **295 MB** | 150 MB (untouched modules) | ~15 ms |
|
|
153
|
+
|
|
154
|
+
The 83 MB reclaimed is the ~45 edited modules' bytecode; the rest stays, which is
|
|
155
|
+
why a split-build patched binary is smaller but not the *half* a patched monolith
|
|
156
|
+
was. The recompile is now per lazily-imported edited module rather than the whole
|
|
157
|
+
app at once, so startup barely moves. Read the current figures off any binary
|
|
158
|
+
with `patch-cc status` rather than off this table — the bytecode total grows every
|
|
159
|
+
few builds.
|
|
160
|
+
|
|
161
|
+
The size story is the **ELF** path: the `.bun` section is rewritten in place, so
|
|
162
|
+
the dropped bytecode is genuinely reclaimed (`container.verify` refuses a Linux
|
|
163
|
+
write that did not shrink by about that much). On **macOS** the file keeps its
|
|
164
|
+
original size: `macho.py` grows a segment but never shrinks one, so the freed
|
|
165
|
+
bytes stay as dead space. The binary still runs correctly (the bytecode is gone),
|
|
166
|
+
it is just not smaller — reclaiming it means shrinking the Mach-O segment and
|
|
167
|
+
re-laying `__LINKEDIT`, which is not done yet.
|
|
168
|
+
|
|
169
|
+
Every write asserts each **edited** module carries `bytecode == 0` in the binary
|
|
170
|
+
it produced (`container.verify`, beside the round-trip check), and `patch-cc
|
|
171
|
+
status` reports the total for an installed one. `doctor`'s dry run cannot — a
|
|
172
|
+
clean bundle still has all its bytecode by definition — but its smoke bake
|
|
173
|
+
writes a temp binary through the same `container.write` and then *executes* it,
|
|
174
|
+
so the sweep exercises the assert, and the loader itself, on every corpus
|
|
175
|
+
build. If a future Bun build makes bytecode authoritative over source, that
|
|
176
|
+
assert is the tripwire — every edit would silently no-op otherwise.
|
|
177
|
+
|
|
178
|
+
## Writing it back without ballooning
|
|
179
|
+
|
|
180
|
+
`.bun` is the last *allocated* ELF section; only non-allocated metadata
|
|
181
|
+
(`.comment`, `.symtab`, `.strtab`, `.shstrtab`) follows it. patch-cc rewrites
|
|
182
|
+
the ELF bytes in place:
|
|
183
|
+
|
|
184
|
+
1. Splice the new (smaller) blob over the old `.bun` bytes.
|
|
185
|
+
2. Shift `e_shoff`, `e_phoff`, and the trailing non-alloc sections/segments by
|
|
186
|
+
the size delta.
|
|
187
|
+
3. Grow or shrink the containing `PT_LOAD` segment's `filesz`/`memsz` to match.
|
|
188
|
+
|
|
189
|
+
`.bun` keeps its original file offset. This is deliberately *not* done with a
|
|
190
|
+
general ELF library: LIEF rebuilds the binary and relocates `.bun` so its file
|
|
191
|
+
offset equals its virtual address (`0x20000000`), which inflates the file to
|
|
192
|
+
~715 MB. Raw in-place surgery avoids that entirely.
|
|
193
|
+
|
|
194
|
+
Guards refuse anything that could corrupt the mapping: allocated sections after
|
|
195
|
+
`.bun`, growth into a header table, an unrelated spanning segment, or a
|
|
196
|
+
misaligned `PT_LOAD` shift. If any fires, the write aborts rather than guesses.
|
|
197
|
+
|
|
198
|
+
Code: `src/patch_cc/bun/elf.py`. macOS uses LIEF (`macho.py`) — Mach-O segment
|
|
199
|
+
growth is page-aligned and bounded, with no relocation pathology, and every
|
|
200
|
+
edit is followed by an ad-hoc `codesign` (mandatory on Apple Silicon).
|
|
201
|
+
|
|
202
|
+
## The manifest
|
|
203
|
+
|
|
204
|
+
Every patched bundle carries a single comment line — appended to the **entry
|
|
205
|
+
module**, the one module always present and always re-extracted, and the one
|
|
206
|
+
`status` reads — describing its shape; [PLAYBOOK.md](PLAYBOOK.md) covers what it
|
|
207
|
+
means for matcher health:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
//patch-cc {"v":1,"tool":"<version>","patches":[...],"brand":...,"suffix":...,
|
|
211
|
+
"models":{...},"org":...,"codex":{"port":8817,"models":["gpt-5.6-sol"]}}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Every key after `patches` is a configurable patch's own, declared in one place
|
|
215
|
+
(`Patch.setting`) so the manifest here, the cache, and the menu cannot spell it
|
|
216
|
+
three ways. Each is written only when *that* patch landed **and** has a value
|
|
217
|
+
worth recording, so `status` can never assert a name, marker, or model the
|
|
218
|
+
bundle does not contain — and so this is the *widest* the line gets, not its
|
|
219
|
+
fixed shape.
|
|
220
|
+
|
|
221
|
+
That line is why `patch-cc status` can name exactly what is applied: several
|
|
222
|
+
patches are value flips (`verbose:!0`) that leave no other trace. A comment
|
|
223
|
+
can't collide with code and travels with the bundle through extract/repack.
|
|
224
|
+
The menu also reads it to pre-select the current patch set — the binary is the
|
|
225
|
+
state.
|
|
226
|
+
|
|
227
|
+
Each key records what was *asked for*, never what was derived from it. `codex`
|
|
228
|
+
carries model ids and a port and nothing else: a Codex model's display name and
|
|
229
|
+
context window are already baked into the bundle, and repeating them here would
|
|
230
|
+
be a second copy — one that a relabelling upstream could make disagree with the
|
|
231
|
+
binary it claims to describe. That is also what makes the manifest the single
|
|
232
|
+
home for the gateway port: `codex serve` and `codex status` read it from here
|
|
233
|
+
rather than from a store of their own.
|
|
234
|
+
|
|
235
|
+
## Safety
|
|
236
|
+
|
|
237
|
+
- Before the first patch of a version, the pristine binary is copied to
|
|
238
|
+
`~/.local/share/patch-cc/backups/`. `restore` copies it back — never an
|
|
239
|
+
inverse patch (insertions cascade, so a reverse diff is meaningless).
|
|
240
|
+
- Patching always starts from that pristine copy, so re-applying never stacks
|
|
241
|
+
edits on edits, and an apply where **nothing lands** — every selected patch
|
|
242
|
+
broken, so the manifest would claim nothing — leaves the binary untouched
|
|
243
|
+
entirely (stripping bytecode for nothing would only slow startup). A patch
|
|
244
|
+
that *lands* still writes even where it changed no bytes, because landing
|
|
245
|
+
includes an override the build already satisfies: the manifest records what
|
|
246
|
+
was asked and verified present, so `status` can report it.
|
|
247
|
+
- The bundle is parsed, and any syntax error aborts before the binary or the
|
|
248
|
+
backup is touched — the two checks below answer "did we write what we meant
|
|
249
|
+
to", which a corrupt splice satisfies perfectly. The parse is the same one
|
|
250
|
+
the patches locate with, so it costs nothing extra and sits at *every* batch
|
|
251
|
+
of edits as well as on the final bytes: a patch that produces rubble is named
|
|
252
|
+
and dropped rather than aborting the run. See
|
|
253
|
+
[PLAYBOOK.md](PLAYBOOK.md#the-syntax-gate).
|
|
254
|
+
- Every write is verified: patch-cc re-extracts the JS from the binary it just
|
|
255
|
+
wrote and asserts every module equals what it meant to write, that each
|
|
256
|
+
module it edited carries no leftover bytecode to run instead of the edit, and
|
|
257
|
+
that the graph around the modules survived — the record chain kept its
|
|
258
|
+
length and flags, its arena payloads (the shared bytecode string table above
|
|
259
|
+
all) round-trip byte-identical, and every source-hash word is the pristine
|
|
260
|
+
one, except an edited module's, which must be zero. The chain checks exist
|
|
261
|
+
because 2.1.246 failed *only* there: every module compared equal while the
|
|
262
|
+
written binary was dead.
|
|
263
|
+
- Patching a binary that is already marked, when no pristine backup exists, is
|
|
264
|
+
refused outright — there is nothing clean to start from, and our edits change
|
|
265
|
+
lengths, so a second pass would corrupt rather than update. `restore` or a
|
|
266
|
+
reinstall are the only honest fixes; there is deliberately no override.
|
|
@@ -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:
|
|
@@ -259,6 +309,20 @@ isolation was cheaper and could not see the one ordering this project calls
|
|
|
259
309
|
load-bearing — `codex-models` registering the ids `subagent-models` then pins —
|
|
260
310
|
nor anything at all about the bundle the run produced, which it discarded.
|
|
261
311
|
|
|
312
|
+
The composed result is then **baked and executed**: doctor writes it into a
|
|
313
|
+
temp binary through the same `container.write` as a real apply — staging,
|
|
314
|
+
round-trip verification, the container checks — and runs `<binary> --version`,
|
|
315
|
+
expecting the version-marker suffix in the output (our own edit's print, so the
|
|
316
|
+
check proves patched code *executes*, not merely that the binary boots around
|
|
317
|
+
it). Matching and running are different truths with different failure owners:
|
|
318
|
+
matchers break in the patches, the run breaks in the container layer, and
|
|
319
|
+
2.1.246 is the build where they split — every matcher green, every module
|
|
320
|
+
round-tripping byte-perfect, and the written binary `SIGSEGV`ing in Bun's graph
|
|
321
|
+
loader because what the rewrite had destroyed (the Bun 1.4.1 record chain and
|
|
322
|
+
the shared bytecode string table, docs/INTERNALS.md) lives in bytes no module
|
|
323
|
+
owns. The temp binary is always removed; the backup under test is never
|
|
324
|
+
written to.
|
|
325
|
+
|
|
262
326
|
Three readings of the two numbers, kept distinct:
|
|
263
327
|
|
|
264
328
|
| symptom | meaning |
|
|
@@ -313,7 +377,9 @@ re-deriving it.
|
|
|
313
377
|
|
|
314
378
|
Both numbers above are counts, and a count cannot tell a rewrite that landed
|
|
315
379
|
from a rewrite that landed *one prop-name to the left*. So the bundle is parsed,
|
|
316
|
-
and any `ERROR` or `MISSING` node aborts
|
|
380
|
+
and any `ERROR` or `MISSING` node **outside module linkage** aborts
|
|
381
|
+
(`src/patch_cc/js.py`; the linkage read-past is
|
|
382
|
+
[above](#the-many-module-surface)).
|
|
317
383
|
|
|
318
384
|
The parse is not a separate pass any more: locating already needs it, and an
|
|
319
385
|
edit costs one incremental reparse — ~75 ms on a 25 MB bundle against ~3 s for a
|
|
@@ -493,6 +559,10 @@ that broke a patch along with every note.
|
|
|
493
559
|
patch-cc extract ~/.local/share/claude/versions/<ver> > clean.js
|
|
494
560
|
```
|
|
495
561
|
|
|
562
|
+
Since 2.1.242 that is every JavaScript module concatenated, each behind a
|
|
563
|
+
`// ==== patch-cc module <n> ====` header, so `rg` still works over one file
|
|
564
|
+
and the header says which chunk a hit lives in.
|
|
565
|
+
|
|
496
566
|
2. Run `patch-cc doctor`. Note which patch dropped to `candidates == 0`, or —
|
|
497
567
|
for `live-thinking` — which sub-step.
|
|
498
568
|
|
|
@@ -552,6 +622,12 @@ that broke a patch along with every note.
|
|
|
552
622
|
enumerated with its hashes in [corpus.md](corpus.md) — so both sides are
|
|
553
623
|
recomputed from the binaries themselves.
|
|
554
624
|
|
|
625
|
+
Each build's report ends with its baked binary's own `--version` output (the
|
|
626
|
+
smoke run above), so the sweep also proves every corpus build still *bakes
|
|
627
|
+
and boots* — the half matcher counts cannot see, and the half 2.1.246 broke
|
|
628
|
+
while every count held. A moved runtime line is as much a finding as a moved
|
|
629
|
+
count.
|
|
630
|
+
|
|
555
631
|
## Patch reference
|
|
556
632
|
|
|
557
633
|
Grouped by source module. The menu's three groups (Output & display, Models &
|
|
@@ -792,28 +868,37 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
|
|
|
792
868
|
*initialised* to is deliberately not asked: `useState(null)` is every
|
|
793
869
|
build's spelling and `useState(void 0)` would be the same state, while the
|
|
794
870
|
setter is the identity that matters.
|
|
795
|
-
- **The state has two homes, and the
|
|
796
|
-
2.1.235 the scope that hands `onStreamingThinking` declared the
|
|
797
|
-
itself, and the state is the array pattern binding the handed setter
|
|
798
|
-
`useState`'s pair. 2.1.236 moved it into an
|
|
799
|
-
(`subscribe`/`getSnapshot`/`_publish` — the
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
871
|
+
- **The state has two homes, and each is proven by the only name it has.**
|
|
872
|
+
Through 2.1.235 the scope that hands `onStreamingThinking` declared the
|
|
873
|
+
state itself, and the state is the array pattern binding the handed setter
|
|
874
|
+
— `useState`'s pair, which nothing else names. 2.1.236 moved it into an
|
|
875
|
+
external stream store (`subscribe`/`getSnapshot`/`_publish` — the
|
|
876
|
+
`useSyncExternalStore` shape) that a component reads back by destructuring
|
|
877
|
+
a hook call (`{streamingToolUses:…}=useX(<store>)`), so the state is that
|
|
878
|
+
pattern's own `streamingThinking` binding — upstream's the day it takes
|
|
879
|
+
one, the goal achieved, and until then ours, inserted at the front of the
|
|
880
|
+
pattern. The pattern names *itself*: it binds the store's own
|
|
881
|
+
`streamingToolUses` field — the name the store publishes and the
|
|
882
|
+
transcript signature already rests on — and that membership is its whole
|
|
883
|
+
identity, provided its value is a call taking exactly one argument, one
|
|
884
|
+
answer or none (`js.only`). It was once proven by the handing instead —
|
|
885
|
+
"the call's only argument is the very expression the setter was read off"
|
|
886
|
+
— and 2.1.246 moved the handing into the engine
|
|
887
|
+
(`this.stream.setStreamingThinking`), out of every component, without
|
|
888
|
+
moving the state: four renders that plainly draw the conversation read as
|
|
889
|
+
having none in scope, with `onStreamingThinking:` still twice in the
|
|
890
|
+
bundle. The handed setter was `agentDefinitions` again — a neighbour
|
|
891
|
+
standing in for the thing itself, one more fact upstream had to keep.
|
|
892
|
+
Sole argument stays deliberate — a second is a selector whose result is
|
|
893
|
+
no longer the snapshot, and extending a pattern of unknowable provenance
|
|
894
|
+
binds `undefined` with every count green. The insertion also pays for a
|
|
895
|
+
witness the way `thinking-summaries` does: the field it binds must still
|
|
896
|
+
be named by the bundle's own objects (the store's snapshot initialiser,
|
|
897
|
+
its publish call), so a store that renames the field reads as the step
|
|
898
|
+
reporting the store rather than threading `undefined`. Two things come
|
|
899
|
+
free with the store: its setter takes functional updaters — React's own
|
|
900
|
+
contract, which the reducer splices already speak — and it hides a
|
|
901
|
+
finished block itself after 30 s, upstream's own linger.
|
|
817
902
|
|
|
818
903
|
A render was once selected by *position* — the observation that the real sites
|
|
819
904
|
fall after the state's `useState` declaration. That is worth recording as a
|
|
@@ -856,11 +941,29 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
|
|
|
856
941
|
than carried over from whichever renderer the first step happened to see
|
|
857
942
|
last: a second component with the same four props donated its local to the
|
|
858
943
|
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
|
-
|
|
944
|
+
reported both steps green. The memo itself is found by what it computes — an
|
|
945
|
+
arrow whose whole body is a `flatMap` that wraps each streaming block as a
|
|
946
|
+
message, and the call that takes that arrow — never by the memo hook's name.
|
|
947
|
+
The hook is one more minified spelling: the monolith reaches it as
|
|
948
|
+
`<React>.useMemo(...)` (the property name survives a member read), but a
|
|
949
|
+
code-split module imports it under a local (`import{useMemo as te}` → `te(...)`),
|
|
950
|
+
so keying on `useMemo` read a build that memoizes exactly the same way as one
|
|
951
|
+
with no memo at all, and the rewrite reuses the memo's *own* callee rather than
|
|
952
|
+
re-spelling `.useMemo`. What a claim about the *text* around the block
|
|
953
|
+
(`.contentBlock]}`) only approximated is the computation itself: one sibling
|
|
954
|
+
property beside `content` read as the whole thing being gone. The wrapped
|
|
955
|
+
block is asked by membership among the element's *possible values*
|
|
956
|
+
(`js.values` — the same value routing the codex resolvers read), never asked
|
|
957
|
+
to be the read: 2.1.246 minted stable ids for streamed blocks
|
|
958
|
+
(`ce?{...S.contentBlock,id:V}:S.contentBlock`), and the exact-node question
|
|
959
|
+
read the same wrap one choice deeper as no wrap at all — nine required steps
|
|
960
|
+
to zero, every anchor count standing. The rewrite reuses upstream's callback
|
|
961
|
+
and dependencies verbatim — the callback invoked per entry with the same
|
|
962
|
+
three arguments `flatMap` hands it, each dependency spread rather than
|
|
963
|
+
transcribed — because a rebuilt copy is a copy free to shed whatever
|
|
964
|
+
upstream adds next: the re-spell this replaced would have silently dropped
|
|
965
|
+
the 2.1.246 minting, and with it went the uuid-stamp identity that re-spell
|
|
966
|
+
had to prove and the reuse no longer asks.
|
|
864
967
|
|
|
865
968
|
**`display-mode`** defaults the request's thinking display to `"summarized"`;
|
|
866
969
|
without it the API only streams summary text when the `showThinkingSummaries`
|
|
@@ -1251,6 +1354,24 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
|
|
|
1251
1354
|
touched: the `/status` Organization/Email rows, the login screen, and the
|
|
1252
1355
|
org's startup message (`"Message from <org>:"`) still show the real account.
|
|
1253
1356
|
|
|
1357
|
+
**2.1.246 retired the surface.** Upstream deleted the welcome-banner variant
|
|
1358
|
+
that composed the segment; the surviving banner draws `model · billing` with
|
|
1359
|
+
no org anywhere — the hidden state an empty `--org-label` asks for, now
|
|
1360
|
+
upstream's own default. That is a semantic change, not a matcher to repair
|
|
1361
|
+
(the `help-title` precedent), and it is *absence*, reported apart from broken
|
|
1362
|
+
(CONDUCT): the patch declares its surface (`Patch.absence`, answered by the
|
|
1363
|
+
same `_org_segments` its candidates count off, so the two cannot disagree),
|
|
1364
|
+
and every surface derives its answer from the bundle in hand — the menu shows
|
|
1365
|
+
the row dimmed (`not on this build`, unselectable), an explicit `--org-label`
|
|
1366
|
+
is refused at the front door with the same sentence, a cached replay skips it
|
|
1367
|
+
with a note, and `doctor` prints a `-` row apart from ✓/✗ and stays green.
|
|
1368
|
+
Nothing is keyed on a version, so a build that composes the segment again
|
|
1369
|
+
un-dims the row with no code change. What absence cannot tell apart is a
|
|
1370
|
+
composition respelled out of the locator's sight — that build would read as
|
|
1371
|
+
absent, visibly dimmed on a screen that still draws the segment — and the
|
|
1372
|
+
sweep over the corpus, where thirty-four builds carry the surface, is what
|
|
1373
|
+
keeps the locator honest.
|
|
1374
|
+
|
|
1254
1375
|
## Removed patches
|
|
1255
1376
|
|
|
1256
1377
|
Kept here so nobody reintroduces them without knowing why they left:
|