patch-cc 0.5.0__tar.gz → 0.5.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 (44) hide show
  1. {patch_cc-0.5.0 → patch_cc-0.5.1}/PKG-INFO +1 -1
  2. {patch_cc-0.5.0 → patch_cc-0.5.1}/docs/PLAYBOOK.md +98 -36
  3. {patch_cc-0.5.0 → patch_cc-0.5.1}/docs/corpus.md +6 -4
  4. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/__init__.py +1 -1
  5. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/patches/streaming.py +449 -90
  6. {patch_cc-0.5.0 → patch_cc-0.5.1}/.github/workflows/ci.yml +0 -0
  7. {patch_cc-0.5.0 → patch_cc-0.5.1}/.github/workflows/release-watch.yml +0 -0
  8. {patch_cc-0.5.0 → patch_cc-0.5.1}/.github/workflows/release.yml +0 -0
  9. {patch_cc-0.5.0 → patch_cc-0.5.1}/.gitignore +0 -0
  10. {patch_cc-0.5.0 → patch_cc-0.5.1}/AGENTS.md +0 -0
  11. {patch_cc-0.5.0 → patch_cc-0.5.1}/CLAUDE.md +0 -0
  12. {patch_cc-0.5.0 → patch_cc-0.5.1}/LICENSE +0 -0
  13. {patch_cc-0.5.0 → patch_cc-0.5.1}/README.md +0 -0
  14. {patch_cc-0.5.0 → patch_cc-0.5.1}/docs/CONDUCT.md +0 -0
  15. {patch_cc-0.5.0 → patch_cc-0.5.1}/docs/INTERNALS.md +0 -0
  16. {patch_cc-0.5.0 → patch_cc-0.5.1}/docs/demo.gif +0 -0
  17. {patch_cc-0.5.0 → patch_cc-0.5.1}/pyproject.toml +0 -0
  18. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/bun/__init__.py +0 -0
  19. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/bun/blob.py +0 -0
  20. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/bun/container.py +0 -0
  21. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/bun/elf.py +0 -0
  22. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/bun/errors.py +0 -0
  23. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/bun/macho.py +0 -0
  24. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/cache.py +0 -0
  25. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/cli.py +0 -0
  26. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/codex/__init__.py +0 -0
  27. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/codex/gateway.py +0 -0
  28. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/codex/models.py +0 -0
  29. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/codex/oauth.py +0 -0
  30. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/codex/translate.py +0 -0
  31. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/doctor.py +0 -0
  32. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/js.py +0 -0
  33. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/locate.py +0 -0
  34. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/menu.py +0 -0
  35. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/patcher.py +0 -0
  36. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/patches/__init__.py +0 -0
  37. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/patches/agents.py +0 -0
  38. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/patches/base.py +0 -0
  39. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/patches/chrome.py +0 -0
  40. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/patches/codex.py +0 -0
  41. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/patches/output.py +0 -0
  42. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/patches/thinking.py +0 -0
  43. {patch_cc-0.5.0 → patch_cc-0.5.1}/src/patch_cc/ui.py +0 -0
  44. {patch_cc-0.5.0 → patch_cc-0.5.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: patch-cc
3
- Version: 0.5.0
3
+ Version: 0.5.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
@@ -824,12 +824,15 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
824
824
 
825
825
  - **`live-thinking`** — the fourteen-step patch above.
826
826
 
827
- **`prop-threading`** inserts `streamingThinking:<state>,` before the
828
- `conversationId` property of every props bag *handed to a component* an
829
- argument that also carries `messages`. Being an argument is part of the
830
- identity: a module-level literal, a return-value payload, or a config object
831
- may legitimately carry the pair, and none of them is a render. Three things
832
- do the work, and each replaced something that had broken:
827
+ **`prop-threading`** hands the live-thinking state to every conversation
828
+ render a props bag *handed to a component* (an argument) that carries
829
+ `conversationId` and `messages`. Being an argument is part of the identity:
830
+ a module-level literal, a return-value payload, or a config object may
831
+ legitimately carry the pair, and none of them is a render. On a build whose
832
+ component owns the state it inserts `streamingThinking:<state>,` before the
833
+ bag's `conversationId`; on a store build it reroutes the render through a
834
+ wrapper component of ours that subscribes itself (below). Three things do
835
+ the work, and each replaced something that had broken:
833
836
 
834
837
  - **Identity is membership — of the essential props alone.** The props that
835
838
  make a render a conversation render, asked of one object rather than of a
@@ -868,37 +871,96 @@ for you. Each entry: what it changes, the stable anchor, and where it lives.
868
871
  *initialised* to is deliberately not asked: `useState(null)` is every
869
872
  build's spelling and `useState(void 0)` would be the same state, while the
870
873
  setter is the identity that matters.
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
874
+ - **The state has two semantic homes, and each is identified by the only
875
+ name it has.** Through 2.1.235 the scope that hands `onStreamingThinking`
876
+ declared the state itself, and the state is the array pattern binding the
877
+ handed setter — `useState`'s pair, which nothing else names; its value is
878
+ threaded into the bag as it stands. 2.1.236 moved the state into an
875
879
  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.
880
+ `useSyncExternalStore` shape), and there the question this step used to
881
+ ask *which idiom holds the state* is retired, because it broke on
882
+ every answer it ever gave. Each answer was a spelling of React's
883
+ data-access fashion, the busiest surface upstream owns: the handing ("the
884
+ call's only argument is the very expression the setter was read off")
885
+ died on 2.1.246, when the handing moved into the engine
886
+ (`this.stream.setStreamingThinking`) with the state unmoved and
887
+ `onStreamingThinking:` still twice in the bundle; the whole-snapshot
888
+ destructure that replaced it (`{streamingToolUses:…}=useX(<store>)`, the
889
+ pattern naming the store's own field) died on 2.1.247, which reads the
890
+ store through per-field selectors instead (`et(<store>,wv)` with
891
+ `wv=(s)=>s.streamingToolUses`) same store, same fields, a third
892
+ spelling, and a fourth break in this one question while the other
893
+ thirteen steps rode through both restructures untouched. A matcher
894
+ enumerating idioms is a whitelist against a fashion, each branch buying
895
+ exactly one build.
896
+
897
+ What survives every spelling is the **twin**. Live thinking is the
898
+ sibling of live tool-uses same store, same snapshot, same renderer
899
+ and the tool-use half is a feature upstream ships working, so every build
900
+ must carry, in the render's own reach, a read of `streamingToolUses` off
901
+ the store. That read is the *production*, found as whichever of the field
902
+ name's two grammar positions this build spells a snapshot pattern's own
903
+ key (the hook call its sole argument, since a second would make the
904
+ pattern something other than the snapshot), or a selector beside the
905
+ store (a function answering the field off its own parameter, inline or
906
+ hoisted behind a module-local name) — one answer or none per scope
907
+ (`js.only`), behind whatever `??` fallback the read wears (`js.values`;
908
+ 2.1.247 arrived wearing one). The production is then *reused verbatim* —
909
+ the reuse-their-expression rule `org-label` and the extras memo already
910
+ follow — so the state is read the way this build reads its twin, with one
911
+ field name swapped, and upstream can only break the claim by breaking
912
+ their own feature. That is the anchor-selection rule this step's whole
913
+ history teaches: prefer claims upstream cannot drop without paying for
914
+ them — the dispatch strings, the store's field names, the twin's dataflow
915
+ — over claims only this patch needs; a neighbour prop, a handing site and
916
+ a storage idiom were all the second kind, and every break above was one
917
+ of them.
918
+
919
+ - **On a store build the rewrite is a subscription, not a value.**
920
+ Threading the state's value was sound while the resolved render was
921
+ live-computed, and 2.1.247 retired that render too: the surviving
922
+ conversation render is react-compiler cached behind a fixed slot-test
923
+ chain (`if(Yr[15]!==ph||…)Du=r(Ih,{…})`), and a prop the compiler never
924
+ saw is a prop no slot tests — the element is reused, the child bails on
925
+ identity, and a perfectly threaded value renders exactly once.
926
+ Parse-green and frozen, a failure class `doctor` cannot see. So the
927
+ render is rerouted through a module-scope wrapper component
928
+ (`__cc_LiveConversation`) spliced beside it: the bag gains
929
+ `__cc_stream:<store>,` (the production's own store argument, whose
930
+ identity is as fresh as the conversation the cache already tests), the
931
+ render's component argument becomes the wrapper, and the wrapper
932
+ subscribes itself through the production's own hook with a selector of
933
+ ours in upstream's per-field shape, handing the component
934
+ `streamingThinking:` beside the spread props. The hook's optional
935
+ selector parameter is upstream's since the store era began — 2.1.236's
936
+ hook and 2.1.247's are the same two-parameter function, measured — and
937
+ where the destructure spelling leaves it undemonstrated, an in-module
938
+ hook is held to it (resolved, and required to declare a second
939
+ parameter), because a hook that ignored the argument would thread the
940
+ whole snapshot with every count green; an *imported* hook is accepted on
941
+ the measurement, a residue accepted by name rather than a cross-module
942
+ resolver built for a build that has never shipped. The transcript
943
+ renderer's own memo comparator compares unknown props by
944
+ identity (since 2.1.210), so a fresh value re-renders it and a quiet one
945
+ does not, cached parent or not: a component re-rendering from its own
946
+ subscription is the one contract in this chain no minifier or compiler
947
+ rewrites. Every name the wrapper re-spells — the JSX callee, the
948
+ component, the hook — is read off the site and owed module-scope meaning
949
+ (`_unshadowed`): any binding of the spelling on the way — a parameter, a
950
+ declarator, a function's or class's declared name, a catch clause's
951
+ parameter — would make the wrapper name something else that parses,
952
+ verifies, and throws at first render, so it refuses the wrap loudly
953
+ instead, and the store expression's own spellings are walked the same
954
+ way, bounded at the scope upstream wrote them in. The witness is
955
+ unchanged, paid the way `thinking-summaries` pays
956
+ it: the field the wrapper selects must still be named by the bundle's own
957
+ objects (the store's snapshot initialiser, its publish call —
958
+ `streamingThinking:null`, upstream's own since 2.1.236), so a store that
959
+ renames the field reads as the step reporting the store rather than
960
+ subscribing to `undefined`. Two things still come free with the store:
961
+ its setter takes functional updaters — React's own contract, which the
962
+ reducer splices already speak — and it hides a finished block itself
963
+ after 30 s, upstream's own linger.
902
964
 
903
965
  A render was once selected by *position* — the observation that the real sites
904
966
  fall after the state's `useState` declaration. That is worth recording as a
@@ -21,8 +21,8 @@ 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.246` — 2.1.230 and 2.1.244 were never
25
- published — one pristine binary per version, 35 in all. It straddles the
24
+ The whole published span `2.1.210` → `2.1.247` — 2.1.230 and 2.1.244 were never
25
+ published — one pristine binary per version, 36 in all. It straddles the
26
26
  **2.1.242 code split** (INTERNALS.md): `2.1.242`/`2.1.243` are the first
27
27
  many-module builds, and they jump ~35 MB over `2.1.241` for it. Several pairs
28
28
  share a byte count
@@ -67,10 +67,12 @@ the whole file, `sha256sum <version>.orig`) and never the size:
67
67
  | `2.1.243` | 378 MB | `4b0dafeedd0b469c41988e200036fd773e7553ba960349c9f02a82c6d1f2ba27` |
68
68
  | `2.1.245` | 392 MB | `16ad2b94deaf7b29abed966d981c9991a47af0420f5be8ed4a3f83bea9f678bc` |
69
69
  | `2.1.246` | 248 MB | `1a0a662dc1bb938eaec38545abce9a4a69113d7d7f7c5e1a553ea276617b906a` |
70
+ | `2.1.247` | 239 MB | `5fb321bf417ffc5cd4e3f36e7c9c7e029bf47aaa36d5621db979fcc5e6eabe15` |
70
71
 
71
72
  This set covers the span the playbook's tree-move measurements were taken over
72
- (`2.1.210` → `2.1.233`), the 2.1.242 split, and the 2.1.246 stream-store
73
- migration, so each is re-checkable here rather than historical.
73
+ (`2.1.210` → `2.1.233`), the 2.1.242 split, the 2.1.246 stream-store
74
+ migration, and the 2.1.247 selector reads it was read back through, so each is
75
+ re-checkable here rather than historical.
74
76
 
75
77
  ## Rebuild or extend it
76
78
 
@@ -1,3 +1,3 @@
1
1
  """patch-cc: an interactive patcher for the Claude Code native binary."""
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.5.1"
@@ -151,9 +151,14 @@ _TOOL_USES = "streamingToolUses"
151
151
  #: build in the corpus: the pair already names the same one renderer.
152
152
  _TRANSCRIPT_SIGNATURE = ("messages", _TOOL_USES)
153
153
  #: Our local for the threaded state, wherever one has to be minted: the
154
- #: renderer signatures `transcript-signature` extends, and the store-snapshot
155
- #: pattern `prop-threading` extends on builds that keep the state in a store.
154
+ #: renderer signatures `transcript-signature` extends, and the live wrapper's
155
+ #: own subscription on builds that keep the state in a store.
156
156
  _INJECTED = "__cc_streamingThinking"
157
+ #: The store handle the live wrapper reads, one prop of ours beside upstream's.
158
+ _STORE_PROP = "__cc_stream"
159
+ #: The component a store-era render is rerouted through: the subscription the
160
+ #: build makes for tool uses, made once more for thinking.
161
+ _WRAPPER = "__cc_LiveConversation"
157
162
 
158
163
 
159
164
  def _outermost(scope: js.Node) -> bool:
@@ -239,55 +244,242 @@ def _bound_in_scope(site: js.Node, prop: str) -> str | None:
239
244
  return None
240
245
 
241
246
 
242
- def _state_in_scope(site: js.Node) -> tuple[str, js.Node | None] | None:
243
- """The live-thinking state at this render: the name to thread, and the
244
- snapshot pattern to extend first when the state has to be read out of a
245
- store rather than a binding upstream already made.
247
+ def _bound_names(pattern: js.Node | None) -> set[str]:
248
+ """Every name a binding pattern binds -- and none it merely references.
246
249
 
247
- Resolved from the site outwards, so what is threaded is a variable actually
248
- in scope where it is threaded -- the one thing the old matcher named as
249
- owed and could not discharge without a parse. It mattered: on 2.1.232 the
250
+ A pattern's *keys* rename, its *defaults* are read, and only what lands on
251
+ the binding side is a name the scope owns; walking every identifier in the
252
+ subtree would claim both halves and refuse wraps over names nobody binds.
253
+ """
254
+ if pattern is None:
255
+ return set()
256
+ if pattern.type in ("identifier", "shorthand_property_identifier_pattern"):
257
+ return {js.text(pattern)}
258
+ if pattern.type == "pair_pattern":
259
+ return _bound_names(pattern.child_by_field_name("value"))
260
+ if pattern.type in ("assignment_pattern", "object_assignment_pattern"):
261
+ return _bound_names(pattern.child_by_field_name("left"))
262
+ if pattern.type in (
263
+ "object_pattern",
264
+ "array_pattern",
265
+ "formal_parameters",
266
+ "rest_pattern",
267
+ ):
268
+ found: set[str] = set()
269
+ for child in js.children(pattern):
270
+ found |= _bound_names(child)
271
+ return found
272
+ return set()
273
+
274
+
275
+ def _binds(scope: js.Node, name: str) -> bool:
276
+ """Does this scope bind that spelling, by any binding form it has?
277
+
278
+ One home for the question two callers ask -- :func:`_unshadowed` to refuse
279
+ a wrap, :func:`_module_function` to stop a resolve -- because a binding
280
+ form the list misses is a hole in both at once. The forms are the
281
+ grammar's own: the scope's parameters, its declarators, and the
282
+ declarations that bind by existing -- a function's name, a class's, a
283
+ catch clause's parameter. The first version listed parameters, declarators
284
+ and function declarations, and independent review demonstrated the gap the
285
+ same day: a catch parameter and a class name are bindings too, and a
286
+ wrapper re-spelling either would parse, verify, and throw at first render.
287
+ """
288
+ for holder in (
289
+ scope.child_by_field_name("parameters"),
290
+ scope.child_by_field_name("parameter"),
291
+ ):
292
+ if name in _bound_names(holder):
293
+ return True
294
+ for declarator in js.every(scope, js.of_type("variable_declarator"), scoped=True):
295
+ if name in _bound_names(declarator.child_by_field_name("name")):
296
+ return True
297
+ for declaration in js.every(
298
+ scope,
299
+ js.of_type(
300
+ "function_declaration",
301
+ "generator_function_declaration",
302
+ "class_declaration",
303
+ ),
304
+ scoped=True,
305
+ ):
306
+ bound = declaration.child_by_field_name("name")
307
+ if bound is not None and js.text(bound) == name:
308
+ return True
309
+ for clause in js.every(scope, js.of_type("catch_clause"), scoped=True):
310
+ if name in _bound_names(clause.child_by_field_name("parameter")):
311
+ return True
312
+ return False
313
+
314
+
315
+ def _module_function(source: Source, local: js.Node) -> js.Node | None:
316
+ """The function this spelling means *at this use*, resolved lexically.
317
+
318
+ A selector is usually hoisted (``function wv(sL){return
319
+ sL.streamingToolUses}``) and its call site carries only the minified name,
320
+ which is a spelling until its scope is said: in the monolith the same two
321
+ letters bind functions in a hundred unrelated scopes. So the walk out from
322
+ the use asks each scope in turn, and the innermost binding decides: a
323
+ function bound there is the answer, and any *other* binding of the name
324
+ blocks the resolve -- the engine would resolve the spelling to that
325
+ binding, not to a function further out, and the first version skipped
326
+ non-function bindings while claiming the engine's answer. Only when no
327
+ enclosing scope binds the name at all does the single module-scope
328
+ function answer from afar; anything still ambiguous answers ``None``.
329
+ This is a predicate's resolver, not a locator: an unresolvable name here
330
+ just means "not a provable selector", and the loudness belongs to the
331
+ step whose production then fails to resolve.
332
+ """
333
+ name = js.text(local)
334
+ owned: dict[int, js.Node] = {}
335
+ module_scope: dict[int, js.Node] = {}
336
+ for node in source.find_local(local, name):
337
+ parent = node.parent
338
+ if parent is None:
339
+ continue
340
+ fn = None
341
+ if (
342
+ parent.type == "function_declaration"
343
+ and parent.child_by_field_name("name") == node
344
+ ):
345
+ fn = parent
346
+ elif (
347
+ parent.type == "variable_declarator"
348
+ and parent.child_by_field_name("name") == node
349
+ and (value := parent.child_by_field_name("value")) is not None
350
+ and value.type in js.FUNCTIONS
351
+ ):
352
+ fn = value
353
+ if fn is None:
354
+ continue
355
+ owner = js.climb(fn.parent, lambda n: n.type in js.FUNCTIONS)
356
+ if owner is None or _outermost(owner):
357
+ module_scope[fn.id] = fn
358
+ else:
359
+ owned[owner.id] = fn
360
+ scope = js.climb(local, lambda n: n.type in js.FUNCTIONS)
361
+ while scope is not None and not _outermost(scope):
362
+ if scope.id in owned:
363
+ return owned[scope.id]
364
+ if _binds(scope, name):
365
+ return None
366
+ scope = js.climb(scope.parent, lambda n: n.type in js.FUNCTIONS)
367
+ found = list(module_scope.values())
368
+ return found[0] if len(found) == 1 else None
369
+
370
+
371
+ def _selects(source: Source, argument: js.Node, field: str) -> bool:
372
+ """Is this argument a *selector* for that snapshot field?
373
+
374
+ A selector is a function answering the field read off its own parameter --
375
+ 2.1.247's ``function wv(sL){return sL.streamingToolUses}``, handed to the
376
+ store hook beside the store. The field name is the identity, in the one
377
+ grammar position a per-field read gives it; whether the function is inline
378
+ or hoisted behind a name is spelling (:func:`_module_function`), and the
379
+ answer routes through :func:`js.values` like every other answer here.
380
+ """
381
+ fn = argument
382
+ if argument.type == "identifier":
383
+ resolved = _module_function(source, argument)
384
+ if resolved is None:
385
+ return False
386
+ fn = resolved
387
+ if fn.type not in js.FUNCTIONS:
388
+ return False
389
+ taken = [js.binding(parameter) for parameter in js.positional(fn)]
390
+ if len(taken) != 1:
391
+ return False
392
+ param = js.text(taken[0])
393
+ block = js.body(fn)
394
+ if block is None:
395
+ return False
396
+ answers = (
397
+ [block]
398
+ if block.type != "statement_block"
399
+ else [
400
+ expression
401
+ for statement in js.every(fn, js.of_type("return_statement"), scoped=True)
402
+ for expression in js.children(statement)
403
+ ]
404
+ )
405
+ return any(
406
+ js.reads(value, field) and js.text(js.receiver(value)) == param
407
+ for answer in answers
408
+ for value in js.values(answer)
409
+ )
410
+
411
+
412
+ @dataclass(slots=True)
413
+ class _PairState:
414
+ """The component-owned state (through 2.1.235): thread its value."""
415
+
416
+ state: str
417
+
418
+
419
+ @dataclass(slots=True)
420
+ class _StoreRead:
421
+ """A store-era production: wrap the render with a subscription through it.
422
+
423
+ ``selected`` records whether the spelling itself demonstrated the hook
424
+ taking a selector -- what licenses :func:`_wrap` to skip its arity check.
425
+ """
426
+
427
+ production: js.Node
428
+ store: js.Node
429
+ selected: bool
430
+
431
+
432
+ def _state_in_scope(source: Source, site: js.Node) -> _PairState | _StoreRead | None:
433
+ """The live-thinking state at this render: how this build's own scope
434
+ reaches the live stream data, resolved from the site outwards.
435
+
436
+ Outwards, so what is spliced is valid where it is spliced: on 2.1.232 the
250
437
  two conversation renders sit in *different* top-level components, only one
251
- of which declares the state, and threading a single bundle-wide answer into
252
- both put an out-of-scope identifier into a shipped binary. It parses, so no
253
- gate could see it; it throws when that component renders. The binding
254
- threaded has to be one this render can *see* (:func:`js.visible`), which a
255
- nested function's is not and a block's own is not either.
256
-
257
- The state has two upstream shapes, and each is proven by the only name it
258
- has:
259
-
260
- - **A ``useState`` pair** -- every build through 2.1.235. Nothing names the
261
- pair itself (``useState(null)`` and ``useState(void 0)`` are the same
262
- state, so the initialiser is never asked); what names it is its *setter*,
263
- handed to the reducer as ``onStreamingThinking:`` in this very scope. The
264
- state is the array pattern whose second binding is a handed setter. The
265
- handing is asked of the whole subtree on purpose -- a scope may hand it
266
- from inside a callback, and that says nothing about where the state
267
- lives.
268
- - **A store snapshot** -- 2.1.236 moved the state into an external stream
269
- store (``subscribe``/``getSnapshot``/``_publish``), which a component
270
- reads back by destructuring a hook call
271
- (``{streamingToolUses:…}=useX(<store>)``). That pattern *does* name
272
- itself: it binds the store's own ``streamingToolUses`` field -- the name
273
- the store publishes and the transcript signature already rests on -- and
274
- that membership is its identity, the same question every props bag here
275
- answers. Which scope hands the setter is deliberately no part of it:
276
- through 2.1.245 the same component handed ``<store>.setStreamingThinking``
277
- to the reducer, and 2.1.246 moved the handing into the engine
278
- (``this.stream.setStreamingThinking``) without moving the state -- an
279
- identity resting on the handing read four renders that plainly draw the
280
- conversation as having none in scope. The handed setter was
281
- `agentDefinitions` again: a neighbour standing in for the thing itself,
282
- one more fact upstream had to keep. The state is the pattern's own
283
- ``streamingThinking`` binding: upstream's, the day it takes one -- the
284
- goal achieved, same as every other judged-on-achievement step -- and
285
- until then ours, inserted into the pattern. The pattern is proven a
286
- *produced* snapshot by its value being a call with exactly one argument;
287
- sole argument is deliberate -- a second is a selector whose result is no
288
- longer the snapshot, and extending a pattern of unknowable provenance
289
- binds ``undefined`` with every count green, so that shape is refused
290
- loudly instead (one answer or none, :func:`js.only`).
438
+ of which declares the state, and a single bundle-wide answer put an
439
+ out-of-scope identifier into a shipped binary -- it parses, so no gate
440
+ could see it, and it throws when that component renders. A binding used
441
+ here has to be one this render can *see* (:func:`js.visible`).
442
+
443
+ The state has two semantic homes, and each is identified by the only name
444
+ it has:
445
+
446
+ - **The component's own pair** -- every build through 2.1.235. Nothing
447
+ names the pair itself (``useState(null)`` and ``useState(void 0)`` are
448
+ the same state, so the initialiser is never asked); what names it is its
449
+ *setter*, handed to the reducer as ``onStreamingThinking:`` in this very
450
+ scope. The state is the array pattern whose second binding is a handed
451
+ setter, and the handing is asked of the whole subtree on purpose -- a
452
+ scope may hand it from inside a callback, and that says nothing about
453
+ where the state lives. Answered as a :class:`_PairState`: the state is
454
+ a binding of this scope, and threading its value is the whole rewrite.
455
+ - **The stream store** -- 2.1.236 on. Here the question this function used
456
+ to ask -- *which idiom holds the state* -- is retired, because it broke
457
+ on every answer it ever gave: the whole-snapshot destructure it knew
458
+ (``{streamingToolUses:…}=useX(<store>)``, 2.1.236-2.1.246) was retired
459
+ by 2.1.247's per-field selector reads (``et(<store>,wv)``), the handing
460
+ it once rested on moved into the engine on 2.1.246, and each repair
461
+ bought exactly one build. Those are spellings of React's data-access
462
+ fashion, the busiest surface upstream owns, and a matcher enumerating
463
+ them is a whitelist against a fashion. What survives every spelling is
464
+ the **twin**: live tool-uses ride the same store, and that feature ships
465
+ working, so somewhere in this scope upstream reads ``streamingToolUses``
466
+ off the store -- as a snapshot pattern's own key, or through a selector
467
+ handed to the hook beside the store. That read is the **production**,
468
+ answered as a :class:`_StoreRead`: the hook and the store, upstream's
469
+ own, read out of whatever spelling this build uses and reused verbatim
470
+ by :func:`_wrap` -- the same
471
+ reuse-their-expression rule `org-label` and the extras memo follow,
472
+ because what is copied cannot drift from what it was copied from.
473
+
474
+ A production is a declarator this site can see whose value is one call
475
+ among its possible values (:func:`js.values` -- 2.1.247 arrived with a
476
+ ``??`` fallback on the read, and the exact-node question would have read
477
+ it as no call at all), taking the store alone (the destructure spelling:
478
+ the pattern names the field as its own key, sole argument since a second
479
+ would make the pattern something other than the snapshot) or the store
480
+ beside exactly one selector for the field (the selector spelling). One
481
+ answer or none per scope (:func:`js.only`): two reads of the store is a
482
+ cardinality change to be told about, not a first to win.
291
483
  """
292
484
  scope = js.climb(site, lambda n: n.type in js.FUNCTIONS)
293
485
  while scope is not None and not _outermost(scope):
@@ -300,7 +492,7 @@ def _state_in_scope(site: js.Node) -> tuple[str, js.Node | None] | None:
300
492
  if (pair := js.named(node)) is not None
301
493
  and (value := pair.child_by_field_name("value")) is not None
302
494
  }
303
- snapshots: dict[int, js.Node] = {}
495
+ productions: dict[int, _StoreRead] = {}
304
496
  for declarator in js.every(scope, js.of_type("variable_declarator")):
305
497
  name = declarator.child_by_field_name("name")
306
498
  if name is None or not js.visible(declarator, site):
@@ -308,49 +500,209 @@ def _state_in_scope(site: js.Node) -> tuple[str, js.Node | None] | None:
308
500
  if name.type == "array_pattern":
309
501
  bound = [js.text(child) for child in name.named_children]
310
502
  if len(bound) == 2 and bound[1] in setters:
311
- return bound[0], None
312
- continue
313
- value = declarator.child_by_field_name("value")
314
- if name.type != "object_pattern" or not js.children(name):
315
- continue
316
- if value is None or value.type != "call_expression":
503
+ return _PairState(bound[0])
317
504
  continue
318
- if len(js.arguments(value)) != 1:
505
+ calls = [
506
+ v
507
+ for v in js.values(declarator.child_by_field_name("value"))
508
+ if v.type == "call_expression"
509
+ ]
510
+ if len(calls) != 1:
319
511
  continue
320
- if _TOOL_USES not in js.props(name):
321
- continue
322
- snapshots[declarator.id] = name
323
- pattern = js.only(
324
- list(snapshots.values()), "snapshot reads of the live-thinking store"
325
- )
326
- if pattern is not None:
327
- carried = js.props(pattern)
328
- if _STREAMING in carried:
329
- return js.text(js.binding(carried[_STREAMING])), None
330
- return _INJECTED, pattern
512
+ production = calls[0]
513
+ args = js.arguments(production)
514
+ if name.type == "object_pattern" and js.children(name):
515
+ if _TOOL_USES not in js.props(name) or len(args) != 1:
516
+ continue
517
+ productions[declarator.id] = _StoreRead(production, args[0], False)
518
+ elif name.type == "identifier" and len(args) == 2:
519
+ chosen = [a for a in args if _selects(source, a, _TOOL_USES)]
520
+ if len(chosen) == 1:
521
+ store = args[0] if chosen[0] == args[1] else args[1]
522
+ productions[declarator.id] = _StoreRead(production, store, True)
523
+ found = js.only(list(productions.values()), "reads of the live stream store")
524
+ if found is not None:
525
+ return found
331
526
  scope = js.climb(scope.parent, lambda n: n.type in js.FUNCTIONS)
332
527
  return None
333
528
 
334
529
 
530
+ def _module_statement(node: js.Node) -> js.Node:
531
+ """The module-scope statement this node is part of -- where a declaration
532
+ meant for the whole module goes.
533
+
534
+ Module scope is a fact about the build, not a fixed depth: a split
535
+ module's ``program`` is the scope, and the monolith's is the wrapper
536
+ IIFE's own body (:func:`_outermost` -- inserting before the IIFE itself
537
+ would put the declaration outside every binding it needs).
538
+ """
539
+ statement = node
540
+ while statement.parent is not None:
541
+ parent = statement.parent
542
+ if parent.type == "program":
543
+ return statement
544
+ if parent.type == "statement_block":
545
+ owner = js.climb(parent, lambda n: n.type in js.FUNCTIONS)
546
+ if owner is not None and _outermost(owner):
547
+ return statement
548
+ statement = parent
549
+ return statement
550
+
551
+
552
+ def _root(expression: js.Node) -> js.Node | None:
553
+ """The identifier an expression resolves through -- ``X`` in
554
+ ``X.createElement`` -- or nothing when there is no lone name to check."""
555
+ node: js.Node | None = expression
556
+ while node is not None and node.type in (
557
+ "member_expression",
558
+ "subscript_expression",
559
+ "parenthesized_expression",
560
+ ):
561
+ node = node.child_by_field_name("object") or next(iter(js.children(node)), None)
562
+ return node if node is not None and node.type == "identifier" else None
563
+
564
+
565
+ def _unshadowed(name: str, site: js.Node, *, above: js.Node | None = None) -> bool:
566
+ """Does this spelling keep its meaning from the site up to ``above``?
567
+
568
+ The live wrapper is a module-scope function that re-spells names it read
569
+ off the render -- the JSX callee, the component, the hook -- and a
570
+ component-scope binding of the same spelling between the two would make
571
+ the wrapper name something else entirely: it parses, verifies, and throws
572
+ when the wrapper first renders, the 2.1.232 class of damage. So the name
573
+ is walked from the site outwards, and any scope that binds it
574
+ (:func:`_binds`, every binding form at once) refuses the wrap loudly
575
+ rather than shipping it. With no ``above`` the walk runs to module scope,
576
+ for a name the wrapper re-spells there; the store expression's spellings
577
+ name ``above`` instead -- the scope upstream wrote them in -- because a
578
+ spelling copied *down* into the bag only has to mean at the bag what it
579
+ meant where it was copied from.
580
+ """
581
+ scope = js.climb(site, lambda n: n.type in js.FUNCTIONS)
582
+ while scope is not None and not _outermost(scope):
583
+ if above is not None and scope.id == above.id:
584
+ return True
585
+ if _binds(scope, name):
586
+ return False
587
+ scope = js.climb(scope.parent, lambda n: n.type in js.FUNCTIONS)
588
+ return True
589
+
590
+
591
+ def _wrap(
592
+ source: Source, bag: js.Node, resolved: _StoreRead, serial: int
593
+ ) -> list[Edit] | str:
594
+ """Reroute this render through a component of ours that subscribes itself.
595
+
596
+ Threading the state's *value* into the props bag was sound while the
597
+ resolved render was live-computed, and 2.1.247 retired that render: the
598
+ surviving one is react-compiler cached behind a fixed slot-test chain
599
+ (``if(Yr[15]!==ph||…)Du=r(Ih,{…})``), and a prop the compiler never saw is
600
+ a prop no slot tests -- the element is reused, the child bails out on
601
+ identity, and a perfectly threaded value renders exactly once. The
602
+ compiler's cache is a representation this patch refuses to model; what
603
+ needs no modelling is React's own contract that a component re-renders
604
+ from its own subscription. So the render is rerouted through a wrapper
605
+ that makes the subscription this scope stopped making: it reads the store
606
+ through the *production's own hook* -- whose optional selector parameter
607
+ is upstream's since the store era began (2.1.236's hook and 2.1.247's are
608
+ the same two-parameter function, measured) -- with a selector of ours in
609
+ upstream's own per-field shape, and hands the component the state under
610
+ the prop every other step already speaks. The transcript renderer's own
611
+ memo comparator compares unknown props by identity (and has since
612
+ 2.1.210), so a fresh value re-renders it and a quiet one does not, cached
613
+ parent or not.
614
+
615
+ Everything the wrapper spells is read off the site -- the JSX callee, the
616
+ component, the hook -- so the one thing owed is that those names mean at
617
+ module scope what they mean at the render (:func:`_unshadowed`); a
618
+ shadowed name refuses the wrap loudly, and the answer is the *reason*, so
619
+ the step's note names what refused rather than one sentence covering
620
+ three facts. The store expression is not module-scope and never has to
621
+ be: it rides into the props bag (``__cc_stream:``), evaluated exactly
622
+ where the old threading evaluated its state, its spellings walked the
623
+ same way but bounded at the scope upstream wrote them in, and the bag
624
+ being cached is harmless for it -- the store's identity is as fresh as
625
+ the conversation the cache already tests. A bag that is not a JSX props
626
+ argument (the render call's second argument) is refused the same way:
627
+ rerouting a call this step misread would be rubble, not a wrap.
628
+
629
+ Where the destructure spelling leaves the selector undemonstrated, an
630
+ in-module hook is held to it -- resolved and required to declare a second
631
+ parameter -- because a hook that ignores the argument would thread the
632
+ whole snapshot as the state with every count green, the one silent
633
+ failure this rewrite could otherwise add. A hook defined elsewhere is
634
+ accepted on the measurement above: following the import hop would mean
635
+ cross-module function resolution built for a build that has never
636
+ shipped, and that residue -- a future destructure build importing a
637
+ selectorless hook -- is accepted here by name rather than guessed at.
638
+ """
639
+ handed = bag.parent
640
+ render = handed.parent if handed is not None else None
641
+ if render is None:
642
+ return "the conversation bag is not a component call's props"
643
+ args = js.arguments(render)
644
+ if len(args) < 2 or args[1] != bag:
645
+ return "the conversation bag is not a component call's props"
646
+ component = args[0]
647
+ jsx = render.child_by_field_name("function")
648
+ hook = resolved.production.child_by_field_name("function")
649
+ if jsx is None or hook is None:
650
+ return "the conversation bag is not a component call's props"
651
+ for expression in (jsx, component, hook):
652
+ root = _root(expression)
653
+ if root is None or not _unshadowed(js.text(root), bag):
654
+ return "a name the live wrapper needs is not the render's to use"
655
+ origin = js.climb(resolved.production, lambda n: n.type in js.FUNCTIONS)
656
+ references = {
657
+ js.text(node) for node in js.every(resolved.store, js.of_type("identifier"))
658
+ }
659
+ if resolved.store.type == "identifier":
660
+ references.add(js.text(resolved.store))
661
+ if origin is not None and any(
662
+ not _unshadowed(reference, bag, above=origin) for reference in references
663
+ ):
664
+ return "the store's spelling does not reach the render"
665
+ if not resolved.selected and hook.type == "identifier":
666
+ defined = _module_function(source, hook)
667
+ if defined is not None and len(js.positional(defined)) < 2:
668
+ return "the store hook takes no selector on this build"
669
+ name = _WRAPPER if serial == 0 else f"{_WRAPPER}{serial + 1}"
670
+ declaration = (
671
+ f"function {name}(__cc_props){{"
672
+ f"let {_INJECTED}={js.text(hook)}(__cc_props.{_STORE_PROP},"
673
+ f"(__cc_snapshot)=>__cc_snapshot.{_STREAMING});"
674
+ f"return {js.text(jsx)}({js.text(component)},"
675
+ f"{{...__cc_props,{_STREAMING}:{_INJECTED}}})}}"
676
+ )
677
+ return [
678
+ Edit.before(_module_statement(render), declaration),
679
+ Edit.replace(component, name),
680
+ Edit.before(
681
+ js.entry(js.props(bag)[_CONVERSATION[0]]),
682
+ f"{_STORE_PROP}:{js.text(resolved.store)},",
683
+ ),
684
+ ]
685
+
686
+
335
687
  def _step_prop_threading(source: Source, outcome: Outcome) -> Source:
336
688
  """Pass the live-thinking state into the renderers that need it."""
337
689
  step = outcome.step("prop-threading")
338
- edits = []
690
+ edits: list[Edit] = []
339
691
  renders = _conversation_renders(source)
340
- # The durable witness behind a snapshot insertion: the field it binds is
341
- # one the bundle's own objects still name (the store's snapshot initialiser
342
- # and its publish call, on 2.1.236). Extending the pattern is a rewrite the
343
- # matcher can only vouch for itself, and a store that renames the field
344
- # would leave it threading `undefined` with every count green -- the same
692
+ # The durable witness behind a store read: the field the wrapper selects
693
+ # is one the bundle's own objects still name (the store's snapshot
694
+ # initialiser and its publish call -- `streamingThinking:null`, upstream's
695
+ # own since 2.1.236). A store that renames the field would leave the
696
+ # wrapper subscribing to `undefined` with every count green -- the same
345
697
  # hole `thinking-summaries` pays a header-name count for.
346
698
  field_named = any(
347
699
  (pair := js.named(node)) is not None and pair.type == "pair"
348
700
  for node in source.find(_STREAMING)
349
701
  )
350
- extended: set[int] = set()
702
+ wrapped = 0
351
703
  unreached = 0
352
704
  for bag in renders:
353
- resolved = _state_in_scope(bag)
705
+ resolved = _state_in_scope(source, bag)
354
706
  if resolved is None:
355
707
  # By design on every build we hold: the resume view, the transcript
356
708
  # overlay and the message picker draw conversations too, in scopes
@@ -360,26 +712,33 @@ def _step_prop_threading(source: Source, outcome: Outcome) -> Source:
360
712
  # 2.1.232 out-of-scope threading).
361
713
  unreached += 1
362
714
  continue
363
- state, pattern = resolved
364
- if pattern is not None and not field_named:
365
- step.note(f"the stream store no longer names a {_STREAMING} field")
366
- continue
367
- step.candidates += 1
368
- step.applied += 1
369
715
  carried = js.props(bag)
370
716
  if _STREAMING in carried:
371
717
  # Upstream already threads it into this render; the goal achieved.
718
+ step.candidates += 1
719
+ step.applied += 1
372
720
  continue
373
- if pattern is not None and pattern.id not in extended:
374
- # Two renders in one scope read one snapshot: the pattern gains the
375
- # field once, at the front, which is valid whatever it ends with.
376
- extended.add(pattern.id)
721
+ if isinstance(resolved, _PairState):
722
+ step.candidates += 1
723
+ step.applied += 1
377
724
  edits.append(
378
- Edit.before(pattern.named_children[0], f"{_STREAMING}:{state},")
725
+ Edit.before(
726
+ js.entry(carried[_CONVERSATION[0]]),
727
+ f"{_STREAMING}:{resolved.state},",
728
+ )
379
729
  )
380
- edits.append(
381
- Edit.before(js.entry(carried[_CONVERSATION[0]]), f"{_STREAMING}:{state},")
382
- )
730
+ continue
731
+ if not field_named:
732
+ step.note(f"the stream store no longer names a {_STREAMING} field")
733
+ continue
734
+ step.candidates += 1
735
+ wrap = _wrap(source, bag, resolved, wrapped)
736
+ if isinstance(wrap, str):
737
+ step.note(wrap)
738
+ continue
739
+ step.applied += 1
740
+ wrapped += 1
741
+ edits += wrap
383
742
  # Printed every run, green ones included: an early warning held back until
384
743
  # something breaks arrives too late to be one.
385
744
  step.note(
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