Skip to content

Say a command hub roster name once, keep it on the printed sheet, and read three margin fixtures at the layout the examples now have - #239

Merged
max-sixty merged 4 commits into
mainfrom
fix/ci-33714432132
Sep 3, 2026
Merged

Say a command hub roster name once, keep it on the printed sheet, and read three margin fixtures at the layout the examples now have#239
max-sixty merged 4 commits into
mainfrom
fix/ci-33714432132

Conversation

@leaf-agent

@leaf-agent leaf-agent commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Problem

The nightly leg of ci failed on run 33714432132 (bf9dd47) with thirteen cases. Seven are the ones #236 already fixes. This takes three of the other six — each reproduces on every local run, and each is a fixture or a defect that c14fbd8 (#237) or bf9dd47 brought in. The remaining three pass locally and are reported below rather than guessed at.

Solution

A roster name was said twice, so neither copy could be pointed at. A command's stopped-work list and its fleet roster label each row with the target's own <strong> — a goal's title, a worker's name — and button() marked that label as the page speaking. Both copies stand fenced — the roster item is generated words, and the row's name sits between the state chip and the remit the widget builds — so two passages carried the same text and the same empty context, and findQuote cannot tell one from the other: repeated candidates detach rather than inheriting document order. A drag across a name therefore anchored nothing, on the roster and on the row alike. Both lists' labels are routes now, like the contents sidebar's links that name every heading and say none of them, and the words stay quotable where the page says them. Latent since before #237atlas-lead on command hub detaches the same way on bf9dd47 — but the sweep only reads blocks longer than twelve characters, and gallery-ready is the first agent name over that line.

A route inside a control is still the words its row is about, so paper keeps them. Demoting those labels out of the says reading also took them off the sheet: the chrome stylesheet's print block hides [data-lf-offer] that carries no data-lf-said, so all ten rows printed with no subject — · working · project-wide remit, · 12d — awaiting review; 5 downstream goals unreachable. The two markers answer two questions, and a copy of another element's words is the first case where they come apart: it must not anchor, and it must still print. relabel takes a third answer for it, says: "echo", which writes data-lf-echo — out of the says reading, and holding the same paper bargain data-lf-said strikes: the press goes, the words stay. A standalone copy is unaffected either way, since bake removes presses by the marker's value and an echoed route is a fragment link the browser still owns.

Ship review stands a contents map now, and the thread margin waits on the sidebar. theme.css grants --lf-thread-beside at 1208px of shell, or 1472px where the page has a > aside.sidebarbf9dd47 gave ship review one. At 1440 the shipped thread now opens Threads instead of a card beside its source, which is what both fixtures were reading. They read the sidebar floor now: 1600 for the walk, and 1472 where the case is the floor itself.

The gallery's first ask is a decision. #237 opened the core-surfaces section with bg-choice-ask, which stands before the suggestions, so one a no longer reaches bg-replace. The ask-address fixture presses twice and says what each press reaches.

Testing

uv run pytest tests --run-nightly over test_render_controls.py, test_render_margin.py, test_render_projection.py, test_render_anchors.py, test_render_pages.py, test_render_export.py, test_render_commands.py and test_render_widgets.py, plus the everyday uv run pytest tests (808 passed) and pre-commit run over the changed files. Five cases fail, none of them this branch's: the four test_render_anchors.py and test_render_controls.py cases #236 owns, and test_render_projection.py::test_escape_lets_go_of_the_ask_the_reader_is_standing_on, which reproduces deterministically with this branch stashed and again on a clean e0a0549 worktree, so it belongs to neither branch.

The paper half is measured rather than argued: with the roster labels marked says: "echo" the ten rows read the same under emulate_media(media="print") as on screen, and with says: false the same assertion reports the dropped subject. test_a_roster_row_names_its_target_without_saying_it_twice is that reading. The render gate cannot stand in for it: paperWords reads no text inside a declared offer, so a word that only ever stood in one is invisible to the screen-vs-paper comparison.

What the two card-width floors changed to, and why

The shipped-thread case asserted a 460px card at 1440 and a 447px card at the 1208px shell floor. With a sidebar standing, the document keeps exactly --thread-margin (520px) from the right edge at every width — the surplus a page without a sidebar splits between its two strips all goes to the sidebar's side — and the marker (54px), the gutter (8px), main's own 24px padding and the card's 8px window inset all come out of that 520. So the card is 450px at any width where main has reached its 720px measure, and 426px at the 1472px floor where main is down to its own 640px floor. The assertions are 449 and 425 now, with the arithmetic written beside them.

Worth a maintainer's eye rather than a fix here: --thread-margin: 520px is documented as "the marker and the gutter between it and the 460px conversation", and 54 + 8 + 460 + 8 = 530. It has never quite held; only the surplus centring hid it, and the sidebar posture is the first arrangement that spends the strip exactly.

The three cases this does not take

None reproduces locally — each passed on every run of its file here, on bf9dd47 and on e0a0549 — and all three are the same shape: round_trip(page) returned before the gesture's own POST had been seen, so the assertion read the log one event early.

case what it read
test_render_options.py::test_a_pick_states_the_whole_set five choose events where the six clicks it made are six; the sixth click's DOM assertion had already passed
test_render_widgets.py::test_a_comment_on_a_wrapped_diff_line_names_the_line_an_unwrapped_one_names one anchor of two
test_render_reactions.py::test_selected_reactions_keep_neutral_button_furniture[dark] aria-pressed="true" after the take-back, with "Took back your ok — recorded" already in the banner

round_trip waits for the traffic watcher to hold nothing pending, which is empty both before a request starts and after it finishes; a gesture whose request event has not reached the watcher yet passes it instantly. Stating the ordering means waiting on what the gesture produces — the event count in the log, the state the reopened control reads — rather than on the watcher being quiet, and that is a change per case rather than one knob, which is why it is not in here.

One more ring fault appears once this and #236 are both in

Composed locally (912c99d, #236's head with main merged, plus this branch) the ring walk gets past the thread card and past page-map-search, and then stops on a new one:

in the page map sheet of corpus, the ring on div.lf-page-map-list "ask · Where should a ses" is not all there:
its top edge is 1.4px outside dialog.lf-ui.lf-page-map-sheet (ring 22,172,720,924 vs band 9,173,733,891)

The list runs 33px past the sheet's bottom and 1.4px over its top on the corpus, which is the same box #236 refits for the small-screen sheet — its measurement was taken at 760×900, and this is the 1200×900 walk. It is reachable only with both changes in, so neither branch's own run can see it. Left for #236 rather than duplicated here, since that PR already owns .lf-page-map-list's sizing.


Automated fix for failed run

…ee margin fixtures at the layout the examples now have

The nightly leg of ci went red on bf9dd47 with thirteen cases. Seven are the
ones #236 already fixes. Of the other six, three reproduce on every local run
and are what this changes; the remaining three pass locally and are reported on
the pull request rather than guessed at.

A command's fleet roster labelled each row with the worker's own <strong> and
marked that label as the page speaking. Both copies stand fenced — the roster
item is generated words, the row's name sits between the state chip and the
remit — so two passages carried the same text and the same empty context and
neither could be told from the other: a drag across a worker's name detached
instead of anchoring, on the roster and on the row alike. The link is a route to
the row now, like the contents sidebar's links, and the words stay quotable
where the page says them.

Ship review stands a contents map since bf9dd47, and a sidebar claims the
opposite strip, so its thread margin waits for 1472px of shell rather than
1208px. The two fixtures that open its shipped thread beside its source read
that floor now. At the floor the card is 426px rather than the 447px the same
floor affords a page with no sidebar, because the strip is exactly
--thread-margin there and the marker, the gutter, main's padding and the card's
own inset all come out of it.

The gallery's core surfaces open on a decision since #237, so the page's first
ask is that decision rather than the first suggestion. The ask-address fixture
presses a twice and says what each press reaches.
@leaf-agent

Copy link
Copy Markdown
Collaborator Author

The nightly leg of ci has now run on main itself at run 33718675826 (e0a0549, the merge of #238 and this branch's rebase base). Ten cases failed, and all ten belong to this PR or #236#238 brought in no new cause, and nothing appeared that neither branch owns.

The three cases this PR declined as race-shaped all passed on that run. They were the whole of the difference from the thirteen on bf9dd47, which is the first evidence for the classification from a run neither branch influenced.

The ten cases and which branch owns each

Verified against the branch diffs rather than the PR bodies: git diff origin/main...origin/fix/ci-33702158485 and ...origin/fix/ci-33714432132.

case owner
test_render_anchors.py::test_a_widgets_label_takes_a_comment_inside_the_control_it_labels #236
test_render_controls.py::test_a_closed_leaf_clears_itself_off_the_tray #236
test_render_controls.py::test_the_leaves_tray_takes_the_keyboard #236
test_render_navigation.py::test_the_g_chord_reaches_panels_and_document_lists #236
test_render_projection.py::test_escape_lets_go_of_the_ask_the_reader_is_standing_on #236
test_render_anchors.py::test_every_passage_in_a_real_page_can_be_quoted[feature-gallery] this PR
test_render_margin.py::test_ask_addresses_follow_the_feature_gallery_s_visible_margin_controls[1200] this PR
test_render_margin.py::test_ask_addresses_follow_the_feature_gallery_s_visible_margin_controls[390] this PR
test_render_margin.py::test_the_shipped_long_thread_opens_beside_its_source_in_the_right_margin this PR
test_render_controls.py::test_every_ring_the_layer_draws_is_shown_whole_somewhere_in_the_corpus both

The ring walk failed on the ship-review precondition — .lf-margin-marker[data-lf-kinds~="comment"] did not open a thread card on ship-review — which is the RING_SCOPE_WIDTH floor this PR raises from 1440 to 1600; #236 owns the walk itself past that point.

The three that did not recur, and one thing that is not a problem

test_render_options.py::test_a_pick_states_the_whole_set, test_render_widgets.py::test_a_comment_on_a_wrapped_diff_line_names_the_line_an_unwrapped_one_names, and test_render_reactions.py::test_selected_reactions_keep_neutral_button_furniture[dark] each failed on bf9dd47 and passed on e0a0549. That is one run, not a proof they are harmless, but it is consistent with the round_trip ordering account in this PR's body rather than with a deterministic regression, and it means the fixes here and in #236 are the whole of what a green nightly is waiting on.

The two branches merge cleanly despite both editing the RING_SCOPE_WIDTH region of test_render_controls.py — checked with a --no-commit --no-ff merge of origin/fix/ci-33714432132 into origin/fix/ci-33702158485, which auto-merged that file along with test_render_margin.py and skills/leaf/CLAUDE.md. Either can land first.

No fix PR for this run, since both causes are already proposed.

@leaf-agent leaf-agent left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The anchoring fix is the right shape — a copied label is a route, not a second place the page says the words, and findQuote detaching on two same-text/same-context passages is exactly the fault. But dropping data-lf-said is not a free demotion: it is also what paper reads, and the fleet roster and the stopped-work list now print with no subject at all.

Measured on examples/command-hub.html in Chrome, disclosures open, emulate_media(media="print"):

on e0a0549b on 466473cf
stopped row Choose the additive schema · 12d — awaiting review; 5 downstream goals unreachable · 12d — awaiting review; 5 downstream goals unreachable
fleet row atlas-lead · working · project-wide remit · working · project-wide remit

All ten rows, both lists. The contents-sidebar precedent the comment cites does not carry: the sidebar is .lf-chrome and leaves the sheet whole, where these rows stay on the sheet and lose their only identity.

Nothing in the suite sees it, and that is structural rather than a gap to fill with one more case. paperWords skips text inside [data-lf-offer] in both readings by construction — "Declared offers are excluded because paper has nothing to press" — so the gate's screen-vs-paper comparison cannot report a word that was only ever inside an offer. test_paper_takes_the_press_off_everything_it_cannot_press asks about dress, not words, and passes. I ran test_render_margin.py, test_render_commands.py, test_render_export.py and the paper cases green, and test_render_anchors.py + test_render_controls.py with only the four failures #236 owns.

leaf.js's own design note records this failure shape as already-fixed once: "Keying print on .lf-ui instead cost a printed decision the only words that stated it". The two markers answer two questions — is this the page speaking (anchoring), and are these words worth keeping on a sheet — and until now those had the same answer everywhere. A copy of another element's words is the first case where they come apart: it must not anchor, and it must still print. skills/leaf/CLAUDE.md closes the widget-level escape deliberately — "A wrong offer/said declaration is fixed where the label is created, not by naming its widget in print CSS" — so a @media print rule in command-hub/theme.css is not the remedy here. That makes this a call about whether the layer gains a third declaration, which is a maintainer's rather than something to settle in this PR.

Two smaller notes, neither a fix request:

  • The change reaches the stopped-work list too — button(goal.title, goal.element) in renderStopped — where the lost words are goal titles. The code comment covers it ("a route to a row"), but the PR title and Solution section name only the fleet roster, and the printed stopped list is the more visible half.
  • The new skills/leaf/CLAUDE.md paragraph states the rule layer-wide ("a contents link, a roster row naming a worker, any generated index entry stays chrome"). Written as it stands, following it puts every future generated index under the same print rule, so whatever answer the point above gets belongs in that paragraph.

The three fixture changes read correctly to me. The 1208/1472 floors and the > aside.sidebar condition are theme.css as written, ship review carries the sidebar on the base, and the two card-width floors match the arithmetic in the new comments. The --thread-margin shortfall you flagged is real on the file as it stands (54 + 8 + 460 + 8 = 530 against 520) and is worth its own look rather than a change here.

Comment thread skills/leaf/packages/command-hub/widgets/lf-command.js Outdated
A roster row names its target in the target's own words, so the name must not
anchor — two fenced passages with the same text and the same empty context
detach — and it must still print, being the only thing the row is about.
data-lf-said answered both questions until now; `says: "echo"` is the third
answer, writing data-lf-echo: out of the says reading, and keeping the paper
bargain data-lf-said strikes.
@leaf-agent leaf-agent changed the title Stop the fleet roster from saying a worker's name twice, and read three margin fixtures at the layout the examples now have Say a command hub roster name once, keep it on the printed sheet, and read three margin fixtures at the layout the examples now have Sep 3, 2026
@leaf-agent

Copy link
Copy Markdown
Collaborator Author

All three points are in 1e8d386, plus the title and description.

The layer gains the third declaration. I settled it here rather than leaving it open, because the code supplies the answer and the alternative that keeps both halves does not exist: reverting the demotion puts back the detaching drag this PR was opened to fix. The print block's own sentence — "what stays is a control whose label is one of the page's own words" — already covers a name a row is copied from; what it could not say was "these words, and no passage." So relabel takes true, false, or "echo", and "echo" writes data-lf-echo: skipped by the anchor pass exactly like an unsaid label, kept by print exactly like a said one, dress stripped with it. One value, one attribute, two selectors — reversible in one place if you want a different answer.

Two facts that fell out of the change rather than being designed for: a copy is not affected either way, because bake removes presses by the marker's value and an echoed route is an empty-valued a[href] the browser still owns, so the route keeps working in a standalone file (test_render_export.py green); and a route a widget builds outside any control — lf-toc's rows — needs none of this, since chrome carries no offer marker and no medium takes its words away. The skills/leaf/CLAUDE.md paragraph you flagged now states both cases, so a future generated index reads the rule that applies to it.

The stopped list is named now. Title and the Solution section both say roster and stopped list; the printed example in the second paragraph is a stopped row, which is the more visible half as you said.

The gate gap stands, and it is not one more case. I tried widening paperWords to compare text inside said and echoed offers — the corpus stayed green, so it is available — but it would not have caught this: a mis-declared label leaves both readings, so there is nothing for the screen-vs-paper comparison to disagree about. Left out rather than shipped as coverage it does not give. What the suite has instead is a direct paper reading of the rows.

Verification and the one failure that is neither branch's are in the description's Testing section.

@leaf-agent leaf-agent left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The print half is closed. I read it back rather than taking the note for it: on 1e8d386 the ten rows print with their subjects — Choose the additive schema · 12d — awaiting review; 5 downstream goals unreachable, atlas-lead · working · project-wide remit — and with says: "echo" swapped back to says: false in button() the same assertion reports exactly the loss the last review measured (· working · project-wide remit, and the four w-* rows the same way). So the new case is a real reading and not a restatement of the fix. -k "paper or print or bake or standalone or export" --run-nightly is green across the suite (40 cases), and the everyday ci gate is green on this head.

The two other notes are answered: the stopped list is named in the title and the Solution section, and the skills/leaf/CLAUDE.md paragraph now separates the route chrome builds from the route a control builds, so a future generated index reads the rule that applies to it.

One observation on the new declaration's reach, not a fix request. The design note says a copy is not the medium this divides, "because bake removes presses by the marker's value". That holds for what ships, because offer("a", …) writes the empty-valued marker and the export's removal pass selects [data-lf-offer]:not([data-lf-offer='']):not([data-lf-said]) in render-checks/standalone.js — the echoed route slips both that pass and the [data-lf-offer][data-lf-said] static-ising pass after it, and stays a live fragment link. But the value is a function of the tag, not of the declaration: an echo written on an offer("button", …) carries data-lf-offer="button", so that same pass would take the control out of the copy with its words inside it. The paper bargain data-lf-echo strikes would then hold on a sheet and not in an export, and nothing would report it — data-lf-said is the marker that pass reads. Unreachable today, since button() is the only caller and it builds an a; it is the seam the third answer opens, and worth knowing about before a second widget echoes a label.

`relabel`'s note claimed a copy is not the medium the third answer divides,
because bake removes presses by the marker's value. That is true of the one
caller — `button()` builds an `a`, whose marker is empty-valued, so the echoed
route slips the removal pass and stays a live fragment link — and false in
general: the value is a function of the tag, so an echo on an `offer("button",
…)` is valued, goes out of the copy with its words inside it, and nothing
reports the loss, because the pass that keeps a said press's words reads
`data-lf-said` alone.

No pass changes. The condition is unreachable while `button()` is the only
caller, and a guard for it would be machinery the second echo has to be written
around anyway. What the second echo needs is the constraint stated where it is
read: beside the declaration, and in the layer's own paragraph on it.
@leaf-agent

Copy link
Copy Markdown
Collaborator Author

The observation is right, and it is right for a reason the note as written denied, so the note is what I changed (e1c24db). offer writes the value off the tag — "button" for a button, the empty string for everything else — so "bake removes presses by the marker's value" is a fact about a vs button, not about the third answer. An echo on an offer("button", …) is valued, matches [data-lf-offer]:not([data-lf-offer='']):not([data-lf-said]), and leaves the copy with its words inside it; the static-ising pass that would have kept them reads data-lf-said alone, so nothing reports the loss. Claiming the division as a property of the declaration is the part that would have misled the second caller.

No pass changes. The condition is unreachable while button() is the only caller, and CLAUDE.md's Stage section reserves a guard for a reachable condition with a useful response — a :not([data-lf-echo]) on the removal plus an echo term on the static-ising pass would match nothing today, carry no test that could fail, and still have to be revisited by whoever writes the second echo. What that author needs is the constraint at the point of decision, so it is stated twice: beside the declaration in relabel, and in the layer's own echo paragraph in skills/leaf/CLAUDE.md, both naming the valued-marker case and what teaching the two passes would take.

Where the seam is, in the code
  • offer sets the value from the tag: node.dataset.lfOffer = tag === "button" ? "button" : ""widget-elements.js.
  • The removal pass and the static-ising pass that follows it — standalone.js.
  • Print reads both markers as one set already — [data-lf-offer]:not([data-lf-said], [data-lf-echo]) in chrome-style.js, which is why paper and the copy can disagree at all.

Making the copy read them as one set is not the fix, incidentally: it would catch today's echoed as in the static-ising pass, strip their href and turn a working fragment link into a span. The copy divides on whether the browser can still answer the press, which is what the value encodes; the echo declaration only says the words are not a passage. Closing the seam means an echo term constrained to valued markers, not a merge of the two.

The change is comment and prose only — no behavior, no selector. test_render_projection.py -k "echo or roster or paper" is green (5 cases) and the ci gate is running on this head.

max-sixty pushed a commit that referenced this pull request Sep 3, 2026
… from the press (#241)

## Problem

The nightly leg of `ci` failed on [run
33726571939](https://github.com/max-sixty/leaf/actions/runs/33726571939)
(`e71fa11`) with ten cases. Nine are already owned by open PRs — four by
#236
(`test_a_widgets_label_takes_a_comment_inside_the_control_it_labels`,
`test_a_selection_around_a_targets_buttons_does_not_deaden_them`,
`test_a_closed_leaf_clears_itself_off_the_tray`,
`test_the_leaves_tray_takes_the_keyboard`), four by #239
(`test_every_passage_in_a_real_page_can_be_quoted[feature-gallery]`,
both parameters of
`test_ask_addresses_follow_the_feature_gallery_s_visible_margin_controls`,
`test_the_shipped_long_thread_opens_beside_its_source_in_the_right_margin`),
and the ring walk by #239's `ship-review` precondition ahead of #236 and
#240. The tenth is unowned and is what this takes:
`test_render_widgets.py::test_a_wait_the_reader_would_notice_says_so_and_a_short_one_says_nothing`,
first seen on this run and absent from the three nightlies before it
([33722764498](https://github.com/max-sixty/leaf/actions/runs/33722764498),
[33718675826](https://github.com/max-sixty/leaf/actions/runs/33718675826),
[33714432132](https://github.com/max-sixty/leaf/actions/runs/33714432132)).

```
AssertionError: a wait worth noticing said nothing: [0.702361, 0.642718, 0.583432, 0.523789, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]
```

The test took `performance.now()` just before the press and asked that
every frame past 400ms had settled at 0.5. The rule it reads is
`[aria-busy="true"] { animation: … 140ms linear 200ms both; }` — 340ms
in all — but that clock starts at the first frame the busy attribute is
*painted* in, not at the press. `#inFlight` sets the attribute
synchronously and then renders the controls and updates the margin in
the same task, so the animation's start time is the first frame after
all of that. Measured here, the fade begins at 233ms and completes at
383ms: 17ms inside the deadline. The reported sample is that whole
timeline slipped about 120ms, which is what a loaded nightly worker
costs — the frames past 400ms are still mid-fade rather than absent.

## Solution

Read the fade on the rule's own clock. Each sampled frame now records
the animation's `currentTime` and `playState` beside the opacity. The
delay is the frames before `currentTime` reaches 150ms, including the
ones before the animation exists at all; the fade is the frames the
animation itself reports `finished`. Both halves of the claim survive
intact — nothing paints while the wait is too short to notice, and the
wait that is long enough ends at 0.5 — and neither is a deadline the
machine has to beat. This is `tests/CLAUDE.md`'s **A wait consumes a
fact the system states** applied to a reading that was inferring from
elapsed time, using the `getAnimations()` route that section names.

The sampling window goes 500ms → 700ms so a start that slips still
leaves frames on the far side of the fade; it is a bounded `setTimeout`
inside the page, not an awaited `animation.finished`.

## Testing

`uv run pytest tests/test_render_widgets.py --run-nightly` is green on
this branch (82 passed) and `uv run pytest tests` was green (808 passed,
6 skipped) on the commit the run failed on. `ruff check` and `ruff
format` at the pinned `v0.16.1` pass on the changed file.

Not vacuous, checked from both sides against the rule in
`chrome-style.js`:

<details><summary>The two negations</summary>

| rule changed to | what fails |
| --- | --- |
| `… 140ms linear 0ms both` (delay removed) | `the wait was announced
before it was one: {1, 0.94, 0.88, …}` — the early half |
| selector changed so nothing matches | `a wait worth noticing said
nothing: []` — the late half |

</details>

<details><summary>The frame samples, before and after</summary>

Sampled locally under three spinning cores, on the original wall clock
(`ms since t0`, opacity):

```
(200.3, 1), (217.0, 1), (223.8, 0.999939), (240.5, 0.940296), … (357.1, 0.523868), (373.7, 0.5), (390.5, 0.5), (407.1, 0.5)
```

The fade runs 223→383. The assertion's threshold is 400, so the margin
the reading had was one frame. On the animation's clock the same run
reads `currentTime` 0 at the frame the attribute paints in, 200 where
the fade starts and 340 where it ends and holds — the same numbers the
rule is written with.

Worth a maintainer's eye rather than a fix here: about 120ms of main
thread between the press and the frame the busy attribute paints in is a
lot for `SUGGESTION_PAGE`, which is a small fixture. `#inFlight` calls
`#renderControls` and `this.#margin?.update()` after setting the
attribute, and the animation cannot start until both have finished. The
rule is designed for exactly the heavy-page case, so it still does the
right thing for the reader — but the cost is the layer's, and it is not
visible from any assertion that reads the rule alone.

</details>

---

Automated fix for [failed
run](https://github.com/max-sixty/leaf/actions/runs/33726571939)

Co-authored-by: leaf-agent <318509791+leaf-agent@users.noreply.github.com>
max-sixty pushed a commit that referenced this pull request Sep 3, 2026
## Problem

The nightly leg of `ci` failed on [run
33735071949](https://github.com/max-sixty/leaf/actions/runs/33735071949)
(`a57ae29`) with seven cases. Six are already owned by open PRs:
`test_a_widgets_label_takes_a_comment_inside_the_control_it_labels`,
`test_a_closed_leaf_clears_itself_off_the_tray` and
`test_the_leaves_tray_takes_the_keyboard` by #236; the ring walk by
#239's `ship-review` precondition standing ahead of #240's
`lf-shotframe` fault; and both of
`test_page_fixture_renders[pr-walkthrough]` (the 4px sideways scroll on
`pr-scale-groups`) and
`test_nested_command_projections_stop_at_their_own_boundary` (`§
inner-worker`) by #243.

The seventh is new with `a57ae29` and nobody's yet:

```
FAILED tests/test_render_conversations.py::test_a_coined_class_cannot_reach_the_chromes_rules
AssertionError: the document-level class surface changed: widen the shared vocabulary on purpose
  Extra items in the left set:
  'lf-workspace'
```

#242 wrote the open workspace's tint as `.lf-btn.on,
.lf-btn.lf-workspace[aria-expanded="true"]` in `chrome-style.js`, at
document level. `lf-workspace` is worn by three controls and all three
are the chrome's own — the Threads toggle built at
[`leaf.js:817`](https://github.com/max-sixty/leaf/blob/064e67d5/skills/leaf/assets/leaf.js#L817),
and All leaves and Asks, which `trayIs` marks at
[`trays.js:194`](https://github.com/max-sixty/leaf/blob/064e67d5/skills/leaf/assets/runtime/trays.js#L194).
Every one of them is inside `chromeRoot`, the `.lf-chrome` container the
private stylesheet is scoped to. So the name reached document level
without being shared vocabulary, which is the seam the pinned set exists
to catch — the same one #152 answered for the keycap rules.

## Solution

Move the selector into the `@scope (.lf-chrome)` block, beside
`.lf-banner-more[data-lf-news]`, which paints the same accent face for
the same kind of banner state. `.lf-btn.on` keeps its document-level
rule, unchanged and now on its own: `on` is worn out in the page and is
the reason that line was there before #242.

Nothing about the paint moves. Specificity is unchanged, and the one
rule that could contend — `.lf-banner-menu > .lf-btn { border-color:
transparent }`, for a workspace control folded into the overflow menu —
is `(0,2,0)` against this selector's `(0,3,0)` on both sides of the
move, so scope proximity never gets a say. Fixing it at the pinned set
instead would have widened the shared vocabulary by a class no widget
can wear, which is what the assertion's own message asks you not to do.

## Testing

On this branch, rebased on `fdba9e1d`:

- `uv run pytest tests` — 808 passed, 6 skipped
- `test_a_coined_class_cannot_reach_the_chromes_rules` and both
parameters of #242's
`test_workspaces_replace_each_other_and_name_the_open_one` pass
- `uv run pytest tests/test_render_conversations.py
tests/test_render_controls.py --run-nightly` — 141 passed, and the three
failures are #236's two tray cases and the ring walk, all reproducing
identically on `main`

Not vacuous: deleting the scoped rule fails the workspace test at both
500 and 1200 with the resting face (`rgb(238, 234, 222)` where the
active face is `rgb(255, 254, 251)` under `--chip`) rather than passing
by construction. `chrome-style.js` is served rather than bundled, so
there is nothing to re-vendor, and no user-visible pixel changes, so
there is no before/after shot to hand over.

---
Automated fix for [failed
run](https://github.com/max-sixty/leaf/actions/runs/33735071949)

Co-authored-by: leaf-agent <318509791+leaf-agent@users.noreply.github.com>
@max-sixty
max-sixty merged commit 2b6b27f into main Sep 3, 2026
3 checks passed
@max-sixty
max-sixty deleted the fix/ci-33714432132 branch September 3, 2026 10:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants