From 75d2aa64592b67eb0a3e4dc1e03de2bb7f662e27 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 23:35:40 -0400 Subject: [PATCH 1/5] Scope shared layer inheritance at original Widget owner --- .../SCOPE.md | 21 ++ .../owner-before.json | 189 ++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 evidence/native-layer-acquisition-20261004/SCOPE.md create mode 100644 evidence/native-layer-acquisition-20261004/owner-before.json diff --git a/evidence/native-layer-acquisition-20261004/SCOPE.md b/evidence/native-layer-acquisition-20261004/SCOPE.md new file mode 100644 index 0000000000..2a997af4c3 --- /dev/null +++ b/evidence/native-layer-acquisition-20261004/SCOPE.md @@ -0,0 +1,21 @@ +# One layer inheritance decision in Widget, one native traversal + +Same finished checkout normally joined Text51/main2fac. Source first: Widget.layers +and Compositor._arrange_root.get_layers independently choose the outermost authored +layer declaration. The compositor additionally maintains inherited_layers for every +visited node and recursive resolver custody until the scene completes. + +Extend existing Widget behavior to resolve its authored declaration against the +original inherited order. Public layers and native layout traversal use that same +behavior. Carry original resolved order through existing add_widget/arrange_widget, +removing the recursive get_layers algorithm, per-pass inheritance map and duplicate +public selection. Preserve subtree key order, external-root ancestry, explicit empty +and default layer declarations, custom layers property behavior, detached nodes, +placement order, capture/projection lifetime and custom override context. + +No new cache/map/type/flag/registry, no Toad edits, WT/env/native copy or film. +NRA Package AST covers entire native/Toad/tools roots before editing; omissions +and dynamic limitations explicit. Original420 ProfileTrace arrangement activation +is source context, not callcounts/CPU time/dominance. Batch affected native sanity +last, then one changed joined installed journey owned by Heisenberg; no unchanged +51 capture or physical speed claim. Full performance remains active. diff --git a/evidence/native-layer-acquisition-20261004/owner-before.json b/evidence/native-layer-acquisition-20261004/owner-before.json new file mode 100644 index 0000000000..cb4c187548 --- /dev/null +++ b/evidence/native-layer-acquisition-20261004/owner-before.json @@ -0,0 +1,189 @@ +{ + "packages": [ + { + "repo": "/home/ts/wt/textual-native-subtree-strips-20261002", + "head": "2fac5395d37454296e9fa9b0cd342b820bacc25a", + "root": "src/textual", + "modules": 249, + "unparsed": [], + "sites": [ + { + "path": "src/textual/_compositor.py", + "line": 82, + "kind": "declaration", + "source": "from_widget" + }, + { + "path": "src/textual/_compositor.py", + "line": 779, + "kind": "declaration", + "source": "_arrange_root" + }, + { + "path": "src/textual/_compositor.py", + "line": 1129, + "kind": "declaration", + "source": "layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 79, + "kind": "Store", + "source": "inherited_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 837, + "kind": "declaration", + "source": "get_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 818, + "kind": "Store", + "source": "inherited_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 90, + "kind": "Load", + "source": "inherited_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 1124, + "kind": "Del", + "source": "get_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 762, + "kind": "Load", + "source": "self.layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 838, + "kind": "Load", + "source": "inherited_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 844, + "kind": "Load", + "source": "inherited_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 1062, + "kind": "Load", + "source": "get_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 839, + "kind": "Load", + "source": "inherited_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 841, + "kind": "Load", + "source": "get_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 945, + "kind": "Load", + "source": "type(widget).layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 945, + "kind": "Load", + "source": "Widget.layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 843, + "kind": "Load", + "source": "widget.styles.layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 946, + "kind": "Load", + "source": "get_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 952, + "kind": "Load", + "source": "widget.layers" + }, + { + "path": "src/textual/css/styles.py", + "line": 1232, + "kind": "Load", + "source": "self.layers" + }, + { + "path": "src/textual/screen.py", + "line": 360, + "kind": "declaration", + "source": "layers" + }, + { + "path": "src/textual/screen.py", + "line": 371, + "kind": "Load", + "source": "super().layers" + }, + { + "path": "src/textual/screen.py", + "line": 1414, + "kind": "Load", + "source": "self._compositor.layers" + }, + { + "path": "src/textual/screen.py", + "line": 1376, + "kind": "Load", + "source": "self._compositor.layers" + }, + { + "path": "src/textual/widget.py", + "line": 2830, + "kind": "declaration", + "source": "layers" + }, + { + "path": "src/textual/widget.py", + "line": 2841, + "kind": "Load", + "source": "node.styles.layers" + } + ] + }, + { + "repo": "/home/ts/wt/toad-viewport-raster-cpu-continuation-20261001", + "head": "aa75b3aa28ec89f127b4aabb3a65802b91481445", + "root": "src/toad", + "modules": 288, + "unparsed": [], + "sites": [] + }, + { + "repo": "/home/ts/wt/toad-viewport-raster-cpu-continuation-20261001", + "head": "aa75b3aa28ec89f127b4aabb3a65802b91481445", + "root": "tools", + "modules": 41, + "unparsed": [], + "sites": [] + } + ], + "limits": [ + "Lexical layers references include Styles declarations and Compositor paint order; source classification distinguishes them from Widget inheritance.", + "AST does not resolve dynamic plugin overrides." + ] +} From 6b4326827d9258875f973af61390dee746966c81 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 23:38:52 -0400 Subject: [PATCH 2/5] Carry Widget-owned layer declaration through native arrangement --- src/textual/_compositor.py | 53 +++++++++++++++++++++----------------- src/textual/widget.py | 28 +++++++++++++++++--- 2 files changed, 54 insertions(+), 27 deletions(-) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index f83249e8ae..08a34c8667 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -812,10 +812,13 @@ def store_geometry(node: Widget, geometry: MapGeometry, clip: SceneClip) -> None ) layer_order = root_geometry.order[-1][2] no_clip = RootSceneClip(root_geometry.region) - # Layer names normally inherit from the outermost declaring ancestor. - # Resolve that once per node for this reflow, instead of rebuilding an - # ancestors list and a layer dictionary for every nested widget. - inherited_layers: dict[Widget, dict[str, int] | None] = {} + # Widget owns layer inheritance. Acquire external ancestry only at the + # root, then carry that original declaration through this traversal. + root_layers = root._get_layer_order() + root_layer_order = ( + None if root_layers is None + else {name: index for index, name in enumerate(root_layers)} + ) default_layers = {"default": 0} retained_paths: set[Widget] = set() if visible_only: @@ -834,16 +837,6 @@ def store_geometry(node: Widget, geometry: MapGeometry, clip: SceneClip) -> None if node is root or node in retained_paths: retained_paths.update(path) - def get_layers(widget: Widget) -> dict[str, int] | None: - if widget in inherited_layers: - return inherited_layers[widget] - parent = widget.parent - layers = get_layers(parent) if isinstance(parent, Widget) else None # noqa: F821 -- closure cleared only after traversal - if layers is None and widget.styles.has_rule("layers"): - layers = {name: index for index, name in enumerate(widget.styles.layers)} - inherited_layers[widget] = layers - return layers - def arrange_widget( widget: Widget, virtual_region: Region, @@ -854,6 +847,8 @@ def arrange_widget( visible: bool, dock_gutter: Spacing, complete: bool, + layer_names: tuple[str, ...] | None, + inherited_layers: Mapping[str, int] | None, _MapGeometry: type[MapGeometry] = MapGeometry, ) -> None: """Called recursively to place a widget and its children in the map. @@ -943,8 +938,7 @@ def arrange_widget( ] if type(widget).layers is Widget.layers: - resolved_layers = get_layers(widget) # noqa: F821 -- closure cleared only after traversal - layers_to_index = default_layers if resolved_layers is None else resolved_layers + layers_to_index = default_layers if inherited_layers is None else inherited_layers else: # Preserve custom widget layer policies. layers_to_index = { @@ -1001,6 +995,8 @@ def arrange_widget( visible, arrange_result.scroll_spacing, complete, + layer_names, + inherited_layers, ) else: if widget._anchored and not widget._anchor_released: @@ -1053,25 +1049,34 @@ def arrange_widget( dock_gutter, ), clip) - def add_widget(widget, virtual_region, region, order, layer_order, clip, visible, dock_gutter, complete): + def add_widget(widget, virtual_region, region, order, layer_order, clip, visible, dock_gutter, complete, + layer_names, inherited_layers): nonlocal map, widgets, invisible_widgets + resolved_layers = widget._inherit_layer_order(layer_names) + if resolved_layers is not layer_names: + # Only a newly reached declaration needs a rank projection. + # The owner either returns inherited unchanged or authored names. + inherited_layers = { + name: index for index, name in enumerate(cast(tuple[str, ...], resolved_layers)) + } + layer_names = resolved_layers if (not self.max_subtree_geometry_entries or not widget.CACHE_SUBTREE_GEOMETRY or not widget._is_mounted): - arrange_widget(widget, virtual_region, region, order, layer_order, clip, visible, dock_gutter, complete) # noqa: F821 -- closure cleared after traversal + arrange_widget(widget, virtual_region, region, order, layer_order, clip, visible, dock_gutter, + complete, layer_names, inherited_layers) # noqa: F821 -- closure cleared after traversal return - inherited = get_layers(widget) # noqa: F821 -- closure cleared after traversal resource_type = widget.subtree_geometry_resource() complete = resource_type.complete_arrangement(complete) # Retention requires an uncropped path only for partial resources. # Complete native arrangements already own every descendant box. if widget in retained_paths and not complete: arrange_widget(widget, virtual_region, region, order, layer_order, clip, visible, dock_gutter, - complete) # noqa: F821 -- closure cleared after traversal + complete, layer_names, inherited_layers) # noqa: F821 -- closure cleared after traversal return key = SubtreeGeometryKey.from_widget(widget, virtual_region, region, order, layer_order, clip.region, visible, dock_gutter, size, visible_only and not complete, - tuple(inherited.items()) if inherited is not None else ()) + tuple(inherited_layers.items()) if inherited_layers is not None else ()) cached = self._subtree_geometry.get(widget) if cached is not None and cached.matches(key): cached.restore_into(map, widgets, invisible_widgets, key, clip, clips, widget._render_widget) @@ -1087,7 +1092,7 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible map, widgets, invisible_widgets = {}, set(), set() try: arrange_widget(widget, virtual_region, region, order, layer_order, clip, visible, dock_gutter, - complete) # noqa: F821 -- closure cleared after traversal + complete, layer_names, inherited_layers) # noqa: F821 -- closure cleared after traversal geometry = map added_widgets, added_invisible = frozenset(widgets), frozenset(invisible_widgets) finally: @@ -1114,6 +1119,8 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible True, root_geometry.dock_gutter, False, + root_layers, + root_layer_order, ) finally: self._arranging = False @@ -1121,7 +1128,7 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible # their closure cells, keeping old maps and entire widget trees # alive until cyclic GC. Reflow is finished, so break those local # recursion links before returning the authoritative scene map. - del add_widget, arrange_widget, get_layers + del add_widget, arrange_widget widgets -= invisible_widgets return map, widgets diff --git a/src/textual/widget.py b/src/textual/widget.py index d52e9afad9..f5ba4ed85a 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2833,13 +2833,33 @@ def layers(self) -> tuple[str, ...]: Returns: Tuple of layer names. """ - layers: tuple[str, ...] = ("default",) + order = self._get_layer_order() + return ("default",) if order is None else order + + def _inherit_layer_order( + self, inherited: tuple[str, ...] | None, + ) -> tuple[str, ...] | None: + """Resolve the outermost authored declaration in native tree order. + + An explicitly empty declaration still owns the order. Custom ``layers`` + overrides remain local arrangement policy, as before; they do not + replace the underlying inherited style declaration. + """ + if inherited is None and self.styles.has_rule("layers"): + return self.styles.layers + return inherited + + def _get_layer_order(self) -> tuple[str, ...] | None: + """Acquire original ancestry once at an external traversal boundary.""" + ancestry: list[Widget] = [] for node in self.walk_ancestors(with_self=True): if not isinstance(node, Widget): break - if node.styles.has_rule("layers"): - layers = node.styles.layers - return layers + ancestry.append(node) + order = None + for node in reversed(ancestry): + order = node._inherit_layer_order(order) + return order @property def link_style(self) -> Style: From 2e8ce3336b7b2ff83eb98cc545c08265b2a70856 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 3 Oct 2026 23:41:39 -0400 Subject: [PATCH 3/5] Record layer inheritance closure and final native App controls --- .../SCOPE.md | 44 ++++ .../native-sanity.exit | 1 + .../native-sanity.log | 2 + .../owner-after.json | 216 ++++++++++++++++++ tests/test_compositor.py | 22 +- 5 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 evidence/native-layer-acquisition-20261004/native-sanity.exit create mode 100644 evidence/native-layer-acquisition-20261004/native-sanity.log create mode 100644 evidence/native-layer-acquisition-20261004/owner-after.json diff --git a/evidence/native-layer-acquisition-20261004/SCOPE.md b/evidence/native-layer-acquisition-20261004/SCOPE.md index 2a997af4c3..eb46d7b525 100644 --- a/evidence/native-layer-acquisition-20261004/SCOPE.md +++ b/evidence/native-layer-acquisition-20261004/SCOPE.md @@ -19,3 +19,47 @@ and dynamic limitations explicit. Original420 ProfileTrace arrangement activatio is source context, not callcounts/CPU time/dominance. Batch affected native sanity last, then one changed joined installed journey owned by Heisenberg; no unchanged 51 capture or physical speed claim. Full performance remains active. + +## Published implementation and final native controls + +Production `6b4326827d9258875f973af61390dee746966c81`: Widget and Compositor, +54 added / 27 deleted lines against merged51. Existing Widget owns +`_inherit_layer_order`; public `layers` and the native tree traversal consume +it. `_get_layer_order` acquires external-root ancestry once, using the original +Widget/App boundary and traversal order. The transient optional tuple means +an actually absent authored declaration, retaining the original optional +contract; it is not a new durable/runtime state or mirror. Explicit empty and +duplicate tuples are preserved exactly by the public property. Native rank +mapping is a derived synchronous resource, projected only when a declaration +is reached and shared through descendants. Custom `layers` overrides remain +local arrangement policy, preserving the original raw inherited key semantics. + +Deleted the independent compositor resolver and recursion, all its callers, +per-widget inherited-order registry and completion cleanup; deleted public +Widget's separate declaration-choice loop. Original subtree key rank snapshot, +scene map, clip/placement/capture publication and custom property dispatch +remain their existing contracts. No new class/map/cache/state field. Root +public acquisition still requires inspecting ancestry to find the outermost +source; its temporary ordered ancestor sequence is not a semantic registry. + +Whole roots before: native249/26 relevant sites, Toad288/0, tools41/0, +zero omissions. Native after249/31 sites, zero omissions; sole authored +inheritance choice is Widget._inherit_layer_order. Added sites are shared +owner methods and traversal parameters, not independent decisions. Full search +has no get_layers declaration/caller in current source; original GC control +now names the actual two recursive arrangement closures. Other `.layers` +reads are Styles declarations or Compositor front-to-back order, semantically +separate resources. Dynamic external overrides are not resolved by AST. + +Final batched native controls: **21 passed in 1.94s**, real App/compositor and +subtree resources, covering outermost inheritance/change, custom property, +empty/duplicate public names, detached Widget default/declaration, nested +capture with external ancestor, clipping/scroll/retention/invalidation, and +recursive closure release without GC. No production changes after this batch; +no old51/420 controls or film repeated. Resource warning with12.3GiB available +RAM supported this single short existing-dependency process; no memory caps, +new environment, native copies, installed writes or provider calls. + +**Source-qualified draft; installed changed-path qualification pending** the +next meaningful joined Heisenberg workflow. No CPU gain, dominance, smoothness, +physical FPS or 144Hz claim. Existing 51/420 original evidence remains frozen. diff --git a/evidence/native-layer-acquisition-20261004/native-sanity.exit b/evidence/native-layer-acquisition-20261004/native-sanity.exit new file mode 100644 index 0000000000..573541ac97 --- /dev/null +++ b/evidence/native-layer-acquisition-20261004/native-sanity.exit @@ -0,0 +1 @@ +0 diff --git a/evidence/native-layer-acquisition-20261004/native-sanity.log b/evidence/native-layer-acquisition-20261004/native-sanity.log new file mode 100644 index 0000000000..b93b219e8f --- /dev/null +++ b/evidence/native-layer-acquisition-20261004/native-sanity.log @@ -0,0 +1,2 @@ +..................... [100%] +21 passed in 1.94s diff --git a/evidence/native-layer-acquisition-20261004/owner-after.json b/evidence/native-layer-acquisition-20261004/owner-after.json new file mode 100644 index 0000000000..09d0d3d951 --- /dev/null +++ b/evidence/native-layer-acquisition-20261004/owner-after.json @@ -0,0 +1,216 @@ +{ + "repo": "/home/ts/wt/textual-native-subtree-strips-20261002", + "head": "6b4326827d9258875f973af61390dee746966c81", + "root": "src/textual", + "modules": 249, + "unparsed": [], + "sites": [ + { + "path": "src/textual/_compositor.py", + "line": 82, + "kind": "declaration", + "source": "from_widget" + }, + { + "path": "src/textual/_compositor.py", + "line": 779, + "kind": "declaration", + "source": "_arrange_root" + }, + { + "path": "src/textual/_compositor.py", + "line": 1136, + "kind": "declaration", + "source": "layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 79, + "kind": "Store", + "source": "inherited_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 90, + "kind": "Load", + "source": "inherited_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 817, + "kind": "Load", + "source": "root._get_layer_order" + }, + { + "path": "src/textual/_compositor.py", + "line": 762, + "kind": "Load", + "source": "self.layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 1055, + "kind": "Load", + "source": "widget._inherit_layer_order" + }, + { + "path": "src/textual/_compositor.py", + "line": 1059, + "kind": "Store", + "source": "inherited_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 1066, + "kind": "Load", + "source": "inherited_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 1074, + "kind": "Load", + "source": "inherited_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 1095, + "kind": "Load", + "source": "inherited_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 940, + "kind": "Load", + "source": "type(widget).layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 940, + "kind": "Load", + "source": "Widget.layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 1079, + "kind": "Load", + "source": "inherited_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 941, + "kind": "Load", + "source": "inherited_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 941, + "kind": "Load", + "source": "inherited_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 999, + "kind": "Load", + "source": "inherited_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 1079, + "kind": "Load", + "source": "inherited_layers" + }, + { + "path": "src/textual/_compositor.py", + "line": 946, + "kind": "Load", + "source": "widget.layers" + }, + { + "path": "src/textual/css/styles.py", + "line": 1232, + "kind": "Load", + "source": "self.layers" + }, + { + "path": "src/textual/screen.py", + "line": 360, + "kind": "declaration", + "source": "layers" + }, + { + "path": "src/textual/screen.py", + "line": 371, + "kind": "Load", + "source": "super().layers" + }, + { + "path": "src/textual/screen.py", + "line": 1414, + "kind": "Load", + "source": "self._compositor.layers" + }, + { + "path": "src/textual/screen.py", + "line": 1376, + "kind": "Load", + "source": "self._compositor.layers" + }, + { + "path": "src/textual/widget.py", + "line": 2830, + "kind": "declaration", + "source": "layers" + }, + { + "path": "src/textual/widget.py", + "line": 2839, + "kind": "declaration", + "source": "_inherit_layer_order" + }, + { + "path": "src/textual/widget.py", + "line": 2852, + "kind": "declaration", + "source": "_get_layer_order" + }, + { + "path": "src/textual/widget.py", + "line": 2836, + "kind": "Load", + "source": "self._get_layer_order" + }, + { + "path": "src/textual/widget.py", + "line": 2849, + "kind": "Load", + "source": "self.styles.layers" + }, + { + "path": "src/textual/widget.py", + "line": 2861, + "kind": "Load", + "source": "node._inherit_layer_order" + } + ], + "deleted_authorities": [ + "Compositor._arrange_root.get_layers declaration, recursion and all callers", + "Compositor._arrange_root per-widget inherited_layers registry", + "Widget.layers independent bottom-up declaration selection" + ], + "preserved_resources": [ + "SubtreeGeometryKey.inherited_layers immutable rank snapshot", + "Derived rank mapping carried synchronously through original arrangement", + "Default/custom property policies and raw authored tuple including duplicates" + ], + "new_owner": "Widget._inherit_layer_order decides outermost authored inheritance; public property and compositor consume it. Widget._get_layer_order acquires external ancestry once at original traversal entry.", + "limits": [ + "Lexical AST includes unrelated Styles/Compositor.layers projections; source reading classified these.", + "Dynamic plugin dispatch is not proven by AST; existing layers overrides are consumed exactly as before.", + "Native controls are last; installed changed qualification not yet claimed." + ], + "production_delta": { + "files": 2, + "added": 54, + "deleted": 27 + } +} diff --git a/tests/test_compositor.py b/tests/test_compositor.py index 747c8c7753..ea6338b60e 100644 --- a/tests/test_compositor.py +++ b/tests/test_compositor.py @@ -58,7 +58,7 @@ def compose(self): async def test_reflow_releases_recursive_closures_without_gc(): app = App() names = {"Compositor._arrange_root..add_widget", - "Compositor._arrange_root..get_layers"} + "Compositor._arrange_root..arrange_widget"} def retained_closures(): return {id(value) for value in gc.get_objects() @@ -208,6 +208,26 @@ def compose(self): geometry = app.screen._compositor.full_map assert geometry[low].order > geometry[high].order + # Capturing a nested root must inherit the same original declaration + # outside its scope, including explicit empty and duplicate public names. + for names in ((), ("default",), ("low", "high", "low")): + outer.styles.layers = names + assert inner.layers == names + await pilot.pause() + compositor = app.screen._compositor + geometry = compositor.full_map + captured, _ = compositor._arrange_root( + inner, app.screen.size, visible_only=False, + root_geometry=geometry[inner], + ) + assert captured[low].order == geometry[low].order + assert captured[high].order == geometry[high].order + + detached = Container() + assert detached.layers == ("default",) + detached.styles.layers = ("high", "low", "high") + assert detached.layers == ("high", "low", "high") + async def test_custom_widget_layers_are_respected(): class CustomLayers(Container): From 819f8642690a23e60fa339716cf36ff557ef5232 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 4 Oct 2026 00:51:19 -0400 Subject: [PATCH 4/5] Record scoped installed layer and style qualification with original physical failure --- .../SCOPE.md | 30 ++ .../SCOPED-READY.json | 276 ++++++++++++++++++ 2 files changed, 306 insertions(+) create mode 100644 evidence/native-layer-acquisition-20261004/SCOPED-READY.json diff --git a/evidence/native-layer-acquisition-20261004/SCOPE.md b/evidence/native-layer-acquisition-20261004/SCOPE.md index eb46d7b525..37ce94dd07 100644 --- a/evidence/native-layer-acquisition-20261004/SCOPE.md +++ b/evidence/native-layer-acquisition-20261004/SCOPE.md @@ -63,3 +63,33 @@ new environment, native copies, installed writes or provider calls. **Source-qualified draft; installed changed-path qualification pending** the next meaningful joined Heisenberg workflow. No CPU gain, dominance, smoothness, physical FPS or 144Hz claim. Existing 51/420 original evidence remains frozen. + +## Original joined422 installed qualification + +**Scoped installed Ready for review**, using corrected native53a52e5b/wheela805 +which includes this unchanged52source. Original installed App contracts passed +retained A/B/A/editorUndo/shellclose, native left/right pointer+slider sidebar +controls, rapidcoldPageDown/End and bounded preparation/runway. Original physical +seven input checks and drag/scroll/Up/Down/reverse/End completed. + +The actual raw journey **FAILED** at peer click: the target was clipped outside +the captured roster after sidebar-up, although sidebar-down showed it. No16warm +or full physical A/B/A/Undo/IRC pass. Both raw completed flags remainFALSE; no +replay, new movie or oracle relaxation. Original owner identity unchanged; owned +cleanup remaining/errors empty; transferredUI custody/encoder255 preserved. +All14original keeper hashes matched; source proof records exact339Core/319Toad/ +266Text/3Diff files, Core611d416/native2ea/SDK.12.1/normal69. + +Heis reviewed actualUp52.7559..53.5559DURING originalUI; Down/reverseAFTER. +Kepler personally reviewed18-8SIDEBAR32.2385..33.0385 while sameUI alive, +notUp; earlier personal-up filenames are corrected by original live_review +phase. Selected bands show painted body/chrome with repeatedpositions/stepped +movement. Up writer23.05median/63.44p95/205.67worstms; Down13.22/27.12/ +222.82; observer-inclusiveCPU76.06/64.23%, unmatched workloads. No causal +gain/smoothness/FPS/144Hz claim. Last10s idle zero viewport churn is separate +from full idle marker18.60%UI. Fullnativeperformance remainsactive. + +F0 main normally integrated WITHOUT native source change. Required Debt ratchet +must pass before merge; this installed receipt cannot waive any growth. Full +native suite remainsdeferred. Existing holder/code imports handed back toSchF1; +no ongoing borrowed package/App/client/native claim from this qualification. diff --git a/evidence/native-layer-acquisition-20261004/SCOPED-READY.json b/evidence/native-layer-acquisition-20261004/SCOPED-READY.json new file mode 100644 index 0000000000..ef08677b0e --- /dev/null +++ b/evidence/native-layer-acquisition-20261004/SCOPED-READY.json @@ -0,0 +1,276 @@ +{ + "status": "SCOPED_INSTALLED_READY_FOR_REVIEW; required Debt ratchet remains a merge gate", + "scope": "Native52 layer inheritance/traversal resource + Native53 ordered rule publication/repaint intent, from original joined422 installed App and completed physical input/sidebar phases", + "qualified_textual_head": "a52e5b122b09e6414f8adb1fa04995ac9ad6961b", + "qualified_toad_head": "9138bc6a3fb2d5e7d5fa517f139272a900192a50", + "qualified_core_head": "d41601c16d74e9e3e4195b6527fb218b8fe14af9", + "native_manifest": "2ea0a4d4ff2e362864c156e4b11642775b3f2a96e7f01522b346b6f6d9526936", + "source_receipt": "/home/ts/wt/toad-viewport-raster-cpu-continuation-20261001/evidence/session-native-custody-owner-20261004/SCOPED-READY.json", + "source_receipt_sha256": "feec53af541b617ceef802ff43eb1c9e58d7f58632da6873665e5ef763a4d9db", + "installed_app_checks": { + "blank_ABA_editor_Undo_shell_shutdown": true, + "left_right_sidebar_pointer_drag_slider_hover_collapse": true, + "rapid_PageDown_End_cold_tail_frame_editor_Undo": true, + "runway_configuration_reversal_End_resource_bounds": true + }, + "physical_input_checks": { + "input_remains_focused": true, + "original_editor_window_and_mode": true, + "draft_and_caret_preserved": true, + "input_pageup_moves_history": true, + "input_pagedown_moves_history": true, + "input_pageup_reaches_original_window": true, + "input_pagedown_reaches_original_window": true + }, + "raw_completed": false, + "raw_capture_completed": false, + "raw_error": "CalledProcessError: Command '['xdotool', 'exec', '--sync', '/home/ts/wt/toad-viewport-raster-cpu-continuation-20261001/.artifacts/runtime-native-style22-20261002/bin/python', '/home/ts/wt/toad-viewport-raster-cpu-continuation-20261001/tools/performance/click_history.py', '--target', 'thread', '--state', 'phase-switch-b-state.pickle', '--name', 'nra-domain-mapping']' returned non-zero exit status 1.", + "warm_physical_A_B_A": "NOT REACHED; peer clipped outside captured roster, no16warm/wholejourneyPASS", + "cleanup": { + "remaining": [], + "errors": [] + }, + "original_owner_before": { + "root": "/var/tmp/agent-comms-live-20260927-wzjtqhza", + "mode": "existing_thread", + "name": "nra-architecture", + "identity": { + "pid": 288457, + "start_time": 55741087 + }, + "original_owner_alive": true + }, + "original_owner_after": { + "root": "/var/tmp/agent-comms-live-20260927-wzjtqhza", + "mode": "existing_thread", + "name": "nra-architecture", + "identity": { + "pid": 288457, + "start_time": 55741087 + }, + "original_owner_alive": true + }, + "raw_keepers": [ + { + "path": "/home/ts/.cache/agent-scratch/session422-sidebar-native53-public-20261004-01/capture/receipt.json", + "sha256": "c9f6a898c17a72fc36b35d47a120c9136ad5cb960f0870ab0acab68840f6ec1b" + }, + { + "path": "/home/ts/.cache/agent-scratch/session422-sidebar-native53-public-20261004-01/capture/events.jsonl", + "sha256": "4441bdd91b3e2afc16a99dde49f1b723d4c085da8fd88fadd4e65f3fe5571873" + }, + { + "path": "/home/ts/.cache/agent-scratch/session422-sidebar-native53-public-20261004-01/capture/driver.log", + "sha256": "ebeac2499364f2a891ad9dc0c99163f2db35f51566b1f042619703a3c44516d1" + }, + { + "path": "/home/ts/.cache/agent-scratch/session422-sidebar-native53-public-20261004-01/capture/actions.xdo", + "sha256": "bf1d93b1d1d91671c2f89a97952061edd2e4cd95dfcdfa0fdc420516b3ccad58" + }, + { + "path": "/home/ts/.cache/agent-scratch/session422-sidebar-native53-public-20261004-01/capture/frame-delivery.json", + "sha256": "90ab1a66340ddf86190cc7cea801ece0951e9734d9361b49a1047dfe70c657af" + }, + { + "path": "/home/ts/.cache/agent-scratch/session422-sidebar-native53-public-20261004-01/capture/frame-delivery.svg", + "sha256": "5d3330a21692b4640178eb57696856b4e6778cb8b2f1038ef84d94bcf25b92a6" + }, + { + "path": "/home/ts/.cache/agent-scratch/session422-sidebar-native53-public-20261004-01/capture/cpu-profile.json", + "sha256": "362b75eee34353df45af684ab74e9f3bb230bc44a582282a1c72d5610627dbdc" + }, + { + "path": "/home/ts/.cache/agent-scratch/session422-sidebar-native53-public-20261004-01/capture/profile-review.json", + "sha256": "af9b68db4d6229a0d0d5001e6e6c103106933c70438d46611712a0350529411d" + }, + { + "path": "/home/ts/.cache/agent-scratch/session422-sidebar-native53-public-20261004-01/capture/profile-launch.json", + "sha256": "0644e38dd12f11351cfc61c59b11b2899264f18f09820d0a2382633554ca8d90" + }, + { + "path": "/home/ts/.cache/agent-scratch/session422-sidebar-native53-public-20261004-01/capture/phase-input-held-up-done-state.pickle", + "sha256": "13835ffd1245a9d65b06993564ed60f8a3eb1c5c36afea6c6774b05263d590f9" + }, + { + "path": "/home/ts/.cache/agent-scratch/session422-sidebar-native53-public-20261004-01/capture/phase-switch-b-state.pickle", + "sha256": "3f65f07f95d20fe961041bc5c8a0828b026fb39f9d80573db0286707143b604b" + }, + { + "path": "/home/ts/.cache/agent-scratch/session422-sidebar-native53-public-20261004-01/capture/live-29-14-frames.png", + "sha256": "108904c16d7b2b8e7492ba091585e386d7a189bca586ef32b3d87be1189c22f0" + }, + { + "path": "/home/ts/.cache/agent-scratch/session422-sidebar-native53-public-20261004-01/capture/live-36-19-frames.png", + "sha256": "bf557e7e15ace19a62d241f95975a87eb984316144126830438bac79ff776261" + }, + { + "path": "/home/ts/.cache/agent-scratch/session422-sidebar-native53-public-20261004-01/capture/live-54-29-frames.png", + "sha256": "365359ae640c3ab843a5b53b09f8fb83720875ffcbde41ec2de49db3c1533500" + } + ], + "writer_phases": [ + { + "action": "input-held-up", + "intervals": { + "count": 50, + "median_ms": 23.05, + "p95_ms": 63.44, + "p99_ms": 198.57, + "max_ms": 205.67 + }, + "writer_receipts": 51 + }, + { + "action": "input-held-down", + "intervals": { + "count": 108, + "median_ms": 13.22, + "p95_ms": 27.12, + "p99_ms": 39.38, + "max_ms": 222.82 + }, + "writer_receipts": 109 + }, + { + "action": "up", + "intervals": { + "count": 131, + "median_ms": 11.76, + "p95_ms": 27.13, + "p99_ms": 38.48, + "max_ms": 124.65 + }, + "writer_receipts": 132 + }, + { + "action": "down", + "intervals": { + "count": 124, + "median_ms": 12.68, + "p95_ms": 26.58, + "p99_ms": 30.55, + "max_ms": 114.11 + }, + "writer_receipts": 125 + }, + { + "action": "reverse", + "intervals": { + "count": 147, + "median_ms": 10.11, + "p95_ms": 23.17, + "p99_ms": 40.79, + "max_ms": 106.02 + }, + "writer_receipts": 148 + }, + { + "action": "end", + "intervals": { + "count": 6, + "median_ms": 485.34, + "p95_ms": 501.7, + "p99_ms": 501.7, + "max_ms": 503.43 + }, + "writer_receipts": 7 + }, + { + "action": "sidebar-scroll-down", + "intervals": { + "count": 14, + "median_ms": 11.69, + "p95_ms": 95.48, + "p99_ms": 95.48, + "max_ms": 99.11 + }, + "writer_receipts": 15 + }, + { + "action": "sidebar-scroll-up", + "intervals": { + "count": 11, + "median_ms": 4.53, + "p95_ms": 102.56, + "p99_ms": 102.56, + "max_ms": 103.37 + }, + "writer_receipts": 12 + } + ], + "motion_review": { + "during": { + "phase": "input-held-up", + "frames": "/home/ts/.cache/agent-scratch/session422-sidebar-native53-public-20261004-01/capture/live-29-14-frames.png", + "interval": [ + 52.75589658, + 53.55589658 + ], + "before": { + "monotonic_ns": 563947734770750, + "phase": "before-personal-Up-view", + "identities": [ + { + "pid": 1747649, + "expected_birth": "56383659", + "actual_birth": "56383659", + "state": "S", + "same_birth_alive": true + }, + { + "pid": 1748988, + "expected_birth": "56384474", + "actual_birth": "56384474", + "state": "S", + "same_birth_alive": true + } + ] + }, + "after": { + "monotonic_ns": 563975320055112, + "phase": "after-personal-Up-view", + "identities": [ + { + "pid": 1747649, + "expected_birth": "56383659", + "actual_birth": "56383659", + "state": "S", + "same_birth_alive": true + }, + { + "pid": 1748988, + "expected_birth": "56384474", + "actual_birth": "56384474", + "state": "S", + "same_birth_alive": true + } + ], + "review": "48 consecutive Up frames52.75589658..53.55589658: body/chrome present; repeated positions then discrete advances and internal whitespace. No full-body blank in this band; no smoothness/FPS/CPU claim." + }, + "assessment": "Consecutive recorded frames reviewed personally during original UI lifetime. Body/chrome present, repeated positions and discrete advances/internal whitespace; no full body blank seen in this selected band. Not live continuous playback/smoothness/FPS." + }, + "after": [ + "Down79.502229452..80.302229452 live-36-19", + "reverse117.15386885..117.95386885 live-54-29" + ], + "after_assessment": "Both sheets reviewed after original UI exit; body/chrome painted with stepped advances/repeated positions. No full-body blank in these selected bands; not whole-screen text legibility or smoothness proof." + }, + "personal_kepler_review": { + "sheet": "live-18-8", + "actual_phase": "sidebar-scroll-up", + "source_interval_seconds": [ + 32.238479831954464, + 33.03847983195446 + ], + "ui_identity": { + "pid": 1748988, + "birth": 56384474 + }, + "before_monotonic_ns": 563951721818363, + "after_monotonic_ns": 563986554172980, + "assessment": "Original recorded sidebar sheet reviewed while same UI alive; painted body/chrome, repeated positions/stepped motion. Not heldUp; earlier mislabel explicitly corrected." + }, + "performance_claim": "No overall CPU gain, smoothness, physicalFPS or144Hz; fullnativeperformance active", + "remaining": "Stepped motion, high main-thread CPU, true presentation/input-to-paint/144Hz, busyIRC/tab-open/growingEnd/void/globalCPU and TC1/T9/T4 remain active. No causal/historical CPU gain or smoothness claim.", + "checks_not_repeated": "No new App/capture/build/environment/provider/native or package write for qualification; existing wheels preserved" +} From 87baaf7975be43ecba4631b9ff1d681ceed0af18 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 4 Oct 2026 01:04:50 -0400 Subject: [PATCH 5/5] Let Widget fold layer sources and carry only native ranks --- .../SCOPE.md | 23 +++++++++++++ src/textual/_compositor.py | 23 ++++--------- src/textual/widget.py | 34 ++++--------------- 3 files changed, 37 insertions(+), 43 deletions(-) diff --git a/evidence/native-layer-acquisition-20261004/SCOPE.md b/evidence/native-layer-acquisition-20261004/SCOPE.md index 37ce94dd07..780883c152 100644 --- a/evidence/native-layer-acquisition-20261004/SCOPE.md +++ b/evidence/native-layer-acquisition-20261004/SCOPE.md @@ -93,3 +93,26 @@ F0 main normally integrated WITHOUT native source change. Required Debt ratchet must pass before merge; this installed receipt cannot waive any growth. Full native suite remainsdeferred. Existing holder/code imports handed back toSchF1; no ongoing borrowed package/App/client/native claim from this qualification. + +## F0 correction: one source fold and one carried resource + +Parent's original packaged ratchet against main919 reported Compositor +7 and +Widget +20. This supersedes the earlier source-qualified assessment; original +installed422 evidence does not waive growth and remains unchanged. + +Widget._get_layer_order now folds supplied original sources in native leaf-to-root +order, keeping the last (outermost) authored declaration. Public layers supplies +its lazy ancestry; root acquisition supplies the same traversal; descendant +admission supplies its one local source only while no ancestor declaration owns +rank order. The same behavior handles explicit empty orders and the Widget/App +boundary. Public duplicate names remain exact; rank projection preserves their +last ordinal. Custom layers property dispatch remains local arrangement policy. + +Deleted _inherit_layer_order, the temporary ancestry list/reverse walk and every +layer_names parameter/caller. Native traversal carries only the original derived +rank mapping; cache keys snapshot that resource. No new type, registry, map, +compatibility path or policy change. The corrected property documentation describes +the actual outermost contract; no unrelated class behavior is removed to offset +metrics. Relative main919, Widget has no class-span growth and Compositor shrinks +by two lines. Before/after packaged ratchet and changed App controls remain the +final required confirmation, not a source-only Ready claim. diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 08a34c8667..6be7df87cb 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -814,7 +814,7 @@ def store_geometry(node: Widget, geometry: MapGeometry, clip: SceneClip) -> None no_clip = RootSceneClip(root_geometry.region) # Widget owns layer inheritance. Acquire external ancestry only at the # root, then carry that original declaration through this traversal. - root_layers = root._get_layer_order() + root_layers = root._get_layer_order(root.walk_ancestors(with_self=True)) root_layer_order = ( None if root_layers is None else {name: index for index, name in enumerate(root_layers)} @@ -847,7 +847,6 @@ def arrange_widget( visible: bool, dock_gutter: Spacing, complete: bool, - layer_names: tuple[str, ...] | None, inherited_layers: Mapping[str, int] | None, _MapGeometry: type[MapGeometry] = MapGeometry, ) -> None: @@ -995,7 +994,6 @@ def arrange_widget( visible, arrange_result.scroll_spacing, complete, - layer_names, inherited_layers, ) else: @@ -1050,20 +1048,14 @@ def arrange_widget( ), clip) def add_widget(widget, virtual_region, region, order, layer_order, clip, visible, dock_gutter, complete, - layer_names, inherited_layers): + inherited_layers): nonlocal map, widgets, invisible_widgets - resolved_layers = widget._inherit_layer_order(layer_names) - if resolved_layers is not layer_names: - # Only a newly reached declaration needs a rank projection. - # The owner either returns inherited unchanged or authored names. - inherited_layers = { - name: index for index, name in enumerate(cast(tuple[str, ...], resolved_layers)) - } - layer_names = resolved_layers + if inherited_layers is None and (order_names := widget._get_layer_order((widget,))) is not None: + inherited_layers = {name: index for index, name in enumerate(order_names)} if (not self.max_subtree_geometry_entries or not widget.CACHE_SUBTREE_GEOMETRY or not widget._is_mounted): arrange_widget(widget, virtual_region, region, order, layer_order, clip, visible, dock_gutter, - complete, layer_names, inherited_layers) # noqa: F821 -- closure cleared after traversal + complete, inherited_layers) # noqa: F821 -- closure cleared after traversal return resource_type = widget.subtree_geometry_resource() complete = resource_type.complete_arrangement(complete) @@ -1071,7 +1063,7 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible # Complete native arrangements already own every descendant box. if widget in retained_paths and not complete: arrange_widget(widget, virtual_region, region, order, layer_order, clip, visible, dock_gutter, - complete, layer_names, inherited_layers) # noqa: F821 -- closure cleared after traversal + complete, inherited_layers) # noqa: F821 -- closure cleared after traversal return key = SubtreeGeometryKey.from_widget(widget, virtual_region, region, order, layer_order, clip.region, visible, dock_gutter, @@ -1092,7 +1084,7 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible map, widgets, invisible_widgets = {}, set(), set() try: arrange_widget(widget, virtual_region, region, order, layer_order, clip, visible, dock_gutter, - complete, layer_names, inherited_layers) # noqa: F821 -- closure cleared after traversal + complete, inherited_layers) # noqa: F821 -- closure cleared after traversal geometry = map added_widgets, added_invisible = frozenset(widgets), frozenset(invisible_widgets) finally: @@ -1119,7 +1111,6 @@ def add_widget(widget, virtual_region, region, order, layer_order, clip, visible True, root_geometry.dock_gutter, False, - root_layers, root_layer_order, ) finally: diff --git a/src/textual/widget.py b/src/textual/widget.py index f5ba4ed85a..78ec964737 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2828,37 +2828,17 @@ def layer(self) -> str: @property def layers(self) -> tuple[str, ...]: - """Layers of from parent. - - Returns: - Tuple of layer names. - """ - order = self._get_layer_order() + """Names from the outermost authored layer declaration, or the default.""" + order = self._get_layer_order(self.walk_ancestors(with_self=True)) return ("default",) if order is None else order - def _inherit_layer_order( - self, inherited: tuple[str, ...] | None, - ) -> tuple[str, ...] | None: - """Resolve the outermost authored declaration in native tree order. - - An explicitly empty declaration still owns the order. Custom ``layers`` - overrides remain local arrangement policy, as before; they do not - replace the underlying inherited style declaration. - """ - if inherited is None and self.styles.has_rule("layers"): - return self.styles.layers - return inherited - - def _get_layer_order(self) -> tuple[str, ...] | None: - """Acquire original ancestry once at an external traversal boundary.""" - ancestry: list[Widget] = [] - for node in self.walk_ancestors(with_self=True): + def _get_layer_order(self, sources: Iterable[DOMNode]) -> tuple[str, ...] | None: + order = None + for node in sources: if not isinstance(node, Widget): break - ancestry.append(node) - order = None - for node in reversed(ancestry): - order = node._inherit_layer_order(order) + if node.styles.has_rule("layers"): + order = node.styles.layers return order @property