Skip to content

Interleave Leaf asks with the explanation they turn on - #202

Merged
max-sixty merged 6 commits into
mainfrom
interleave-asks
Sep 1, 2026
Merged

Interleave Leaf asks with the explanation they turn on#202
max-sixty merged 6 commits into
mainfrom
interleave-asks

Conversation

@max-sixty

@max-sixty max-sixty commented Sep 1, 2026

Copy link
Copy Markdown
Owner

The skill said a decision has a control beside its evidence, and the decisions reference opened by making the ask self-contained. Nothing placed the ask relative to the explanation, so an author could finish each section and stack its asks at the foot, after the collapsed evidence. The guidance now says an ask sits where the reader has just read what it turns on, with the page going on after it, and that only an ask turning on the whole page comes last.

The heat-loss example was the one shipped page in the old shape (five chart sections, then its single ask); its ask now follows the survey of losses it turns on, and the corpus is regenerated. A second commit aligns the lf-tabs registry description with the tabs rule page-authoring.md adopted on 31 Aug, which it still contradicted, and a third carries into both, and into the revisions reference that owns it, the clause version check enforces: a passage whose id anchors an open thread or holds a standing decision moves into the collapsed details with its id. That commit also regenerates the gallery stills (heat-loss showed the replaced lede; release-notes predated the last asset change) and updates two nightly tests that still named pr-walkthrough by its old title.

Old vs new wording on three synthetic subjects, two runs each

Each arm's guidance was pasted into one prompt with the subject; every run completed without error.

Subject Old wording New wording
Review whose two asks each turn on one list item both asks stacked after the list and its evidence, 2/2 each ask after its item, 2/2
Report whose asks turn on whole sections each ask in its section, 2/2 each ask in its section, 2/2
PR packet with one whole-page sign-off ask last, 2/2 last, 2/2

version check accepts an lf-decision nested inside a list item, which is where the new wording puts one.

This was written by Claude Code on behalf of max-sixty

🤖 Generated with Claude Code

max-sixty and others added 3 commits September 1, 2026 13:33
The skill said a decision has a control beside its evidence, and the
decisions reference opened by making the ask self-contained. Read together,
they let an author finish each section and stack its asks at the foot, after
the collapsed evidence. Say instead that an ask sits where the reader has just
read what it turns on, with the page going on after it, and that only an ask
turning on the whole page comes last.

Move the heat-loss example's one ask up to follow the survey of losses it
turns on, and regenerate the corpus.

Paired runs of the old and new wording, pasted into one prompt with a
synthetic subject: on a review whose two asks each turn on one list item, the
old wording stacked both after the list and its evidence in 2 of 2 runs and
the new wording placed each after its item in 2 of 2. On a report whose asks
turn on whole sections, both wordings put each ask in its section. On a review
packet whose one ask turns on the whole page, all 4 runs kept it last.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
page-authoring.md stopped keeping earlier runs intact in a sibling tab and
now removes them, or collapses the context still needed inside the relevant
tab. The lf-tabs description still gave the old instruction; say the same
thing the reference does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@leaf-agent leaf-agent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Two findings, both about things sitting outside the diff's own lines.

The lf-tabs rewrite drops the clause that made "remove an earlier run" safe. The old description ended keep earlier context intact in a sibling panel so its stable ids retain their comments and decisions — that was the sentence saying the ids, not just the prose, are what has to survive a tab going away. The new text gives the removal criterion as "the current work no longer depends on it", while version check's criterion is "no id under it carries standing state". Those come apart on the ordinary case: an earlier run whose ask the reader already answered. I built one and removed the tab, and the check refuses it (protected ids present in revision r1 but dropped in index.html … ['run-one-http', 'run-one-options']). The route the rule points at does work — carrying the widget with its ids and chosen into the collapsed <details> passes — so what's missing is only the sentence saying the ids come with the context. Since this commit's purpose is to stop the registry contradicting page-authoring.md, the clause wants to land in both, or they re-diverge. Happy to push that if you want it.

docs/example-heat-loss.jpg still shows the old lede. example-previews.py captures the first viewport, and the sentence this PR replaces — "The last section asks which measure to pay for first." — sits in it, so the published gallery now describes a page shape the example no longer has. This is the same failure mode examples/CLAUDE.md records for the demo stills ("a theme change once left the landing page arguing for a product whose picture showed the previous theme"), and nothing in the suite catches it: test_site.py asserts one preview file exists per example, not what it shows. scripts/example-previews.py regenerates it — say the word and I'll push the regenerated still.

Everything else checks out: the corpus regeneration matches scripts/corpus.py (test_corpus_is_generated_from_the_examples passes), the new registry wording does match page-authoring.md's tabs paragraph, heat-loss really was the only shipped page stacking its ask at the foot, and version check accepts an lf-decision nested in a list item — I ran the fixture and it renders inside the item without losing the marker.

What I ran

The tabs case, on a page with a decided ask in the earlier tab:

$ leaf version check /tmp/tabsdrop      # tab removed wholesale
✗ index.html: 1 issue(s)
  - protected ids present in revision r1 but dropped in index.html (unresolved threads, standing state, or widget retirement still need them): ['run-one-http', 'run-one-options']

$ leaf version check /tmp/tabsdrop      # same widget carried into a collapsed <details> in the current tab
✓ index.html: parses, widgets validate, one module script + theme link, protected ids and decisions carried over, nothing overflows the 720px column

The list-item ask, matching the new "an ask about one item of a list follows that item":

$ leaf version check /tmp/listask2
✓ index.html: parses, widgets validate, one module script + theme link, protected ids and decisions carried over, nothing overflows the 720px column

Rendered through scripts/preview.py --source … --export, the ask sits indented under its item with the sibling bullet intact.

Also run: uv run pytest tests/test_interact_product.py (41 passed), which covers the corpus generation check, test_examples_pass_check, and the registry lints. The lf-tabs string has one copy in the tree, and scripts/keydocs.py reads skills/leaf/assets/registry.json's $keys rather than the package registry, so docs/registry.html is untouched by that edit.

max-sixty and others added 2 commits September 1, 2026 13:56
The tabs rule in the registry and page-authoring.md said to remove an earlier
run once the current work no longer depends on it, but version check refuses
a version that drops an id anchoring an open thread or holding a standing
decision. Say in both, and in the revisions reference that owns the rule, that
such a passage moves into the collapsed details with its id.

Regenerate the gallery stills: heat-loss showed the lede this branch replaced,
and release-notes predated the last asset change. Two nightly tests still
named pr-walkthrough by its old title; they now name the current one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@max-sixty

Copy link
Copy Markdown
Owner Author

Both folded in at 3ececa2 (pushed as 0d062f5 with main merged in).

The tabs rule now says, in the registry entry and in page-authoring.md, that a passage whose id anchors an open thread or holds a standing decision moves into the collapsed details with the context; authoring-revisions.md, which owns the revision rule, states it with the version check refusal. The gallery stills are regenerated with scripts/example-previews.py; release-notes changed too, since its still predated the last asset change. Two nightly tests still named pr-walkthrough by its old title and now name the current one.

This was written by Claude Code on behalf of max-sixty

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@leaf-agent

Copy link
Copy Markdown
Collaborator

Checked both at 26938dc.

The tabs clause reads consistently in all three places now — the registry entry and page-authoring.md both carry the id-bearing passage into the collapsed <details> alongside the context, and authoring-revisions.md states it as the refusal, which is the wording that matches what version check actually enforces. That closes the gap between "the current work no longer depends on it" and "no id under it carries standing state": the earlier-run-with-an-answered-ask case now has a route the rule names.

docs/example-heat-loss.jpg shows the new lede, ending "After the survey of where it leaves, I ask which measure to book first." — the sentence the branch replaced is gone from the still.

test and review are still running on this head; lint passed.

@max-sixty
max-sixty merged commit 1bf7a43 into main Sep 1, 2026
6 checks passed
@max-sixty
max-sixty deleted the interleave-asks branch September 1, 2026 21:07
max-sixty added a commit that referenced this pull request Sep 1, 2026
## Problem

[`ci` run
33516407327](https://github.com/max-sixty/leaf/actions/runs/33516407327)
came back **10 failed, 1614 passed, 6 skipped**. Eight belong to #189,
whose `ci` run was cancelled by #190's push before the nightly leg ever
ran on it. #188's `test_mcp_app.py` case arrived on the same collision
as the MCP test below and is carried here rather than left to red the
next nightly.

The everyday suite that gates a pull request does not run
`test_render_*` or `test_site.py`, so a PR can land its nightly half
unread — which is how these arrived, and how five more have arrived
since, from #193, #194 and #200. `main` has moved a long way under this
branch; the ones it now owns are taken on merge, and three of the new
ones are cleared here. #203 reached `tests/conftest.py`'s
`headless_shell` first, so the four `test_render_commands.py` setup
errors are its.

## Solution

Grouped by cause rather than by test, since several tests share one.

**The example the tests still name.** #189 rewrote
`examples/pr-walkthrough.html` into the Worktrunk review packet, and the
shipped patch is now a collapsed manifest that builds no lines until a
reader opens a file, which the scroller gate read as
`getComputedStyle(null)`. The gate reads the diffs that have drawn
lines, with its own non-vacuity floor kept.

**The new package's chrome and surfaces.** `.lf-pr-description` stacked
a second translucent accent onto a card that is already a tint of one,
and the layer's own `--muted` reads 4.32:1 there; the description now
sits on the raised-surface token. CallDiff's disclosure was a bare
`<button>` rather than an `offer`, so an exported copy kept a hand over
a press nothing can take. Its location anchor carried an `href` on the
header row, which names no location at all — and `reachScrollers` reads
a candidate for a focusable descendant before granting it a stop, so
that hidden anchor answered "there is already a way in here" with a link
nobody can reach, for a box whose words run off the side.

**A reading that counted clipped text as painted.** `coveredWords`
measured each run's whole rect, so an ellipsised name in a narrow column
read as covering its neighbour. It now intersects each run with the
boxes that clip it, stopping at an out-of-flow ancestor where a hidden
overflow further out need not reach. The one test that plants this fault
plants it properly now: the runtime's note is parked in a one-pixel box
with hidden overflow, so opacity alone never put its characters on the
screen.

**Three focus rings the corpus stopped painting.** `code-pre-light` and
`code-pre-shadow` are the tab stops on a scrolling code block and on a
diff's lines, and with the old example gone no example held either. The
page gets one rendered hunk and one code line long enough to scroll.
Then `.lf-diff-review`: it took the outward ring it shared with
`.lf-diff-next`, which stands on its own in the toolbar, while this one
rides a file's summary row inside a box that clips at the row's bottom
edge — its lower run fell 3px past what the sweep can see. It takes the
inset the summary beside it already takes, and splitting the rule made
each half declare the ring name neither had.

**`--diffs-fg-number` on changed rows.** Surfaced by that rendered hunk:
a changed row's number is drawn in the base its own fill is mixed from,
green on green at 4.03:1. Pierre's number overrides now take the layer's
tinted-surface inks.

**A sidebar the page got shorter under.** #190's zero-height ToC anchor
shortened the release page 58px, putting a named `scrollTo(0, 900)` past
the stretch where the box stands on its own offset. Both edges of that
stretch are the page's, so they are read off it, with a floor asserting
the stretch exists before a point halfway along it says anything.

**Playwright's loop against `asyncio.run` and `anyio.run`.**
`sync_playwright()` keeps an asyncio loop running in its thread for its
whole lifetime, and the `browser` fixture is session-scoped per xdist
worker — so an MCP test's own loop start raises in any worker that has
opened a browser and passes in one that has not, leaving the schedule to
decide. Verified directly: `asyncio.events._get_running_loop()` is
`None` before `sync_playwright()`, the loop inside it, `None` after.
`interact_support.run_async` runs the entry point on a thread with no
loop on it, and both MCP modules go through it — #204's three new
exchanges included, since they landed on the direct call while this was
in flight.

**A declared verb no page replayed.** #194 added `lf-diff`'s `review` to
the registry without an event for it on the standing-state page — the
one fixture built so that exactly this fails rather than going
unexercised. It gets a diff and a standing review.

**A focus contract two site tests never heard about.** #193 kept the
response field passive so a drag leaves the browser's own selection
alone and a native copy still has something to take, and aligned the
render tests with it. `test_site.py` is nightly-only. Its label test
turned on `document.activeElement === field`, which is now false for the
page's own words and the site's label alike — the discriminator was
gone, not inverted — so it reads whether the field was offered at all,
which is the difference it was always after.

## Testing

`uv run pytest tests --run-nightly` and `pre-commit run --all-files` on
this head. The everyday suite and lint are green on this branch in CI.
Every claim about what `main` does was taken from a control run of the
same tests in a clean checkout of `main`, not from reading.

<details><summary>Where each failure landed</summary>

| test | fix |
| --- | --- |
|
`test_render_gate.py::test_the_runtime_holds_a_scroller_the_page_wrote`
| reads the diffs that drew lines |
|
`test_render_export.py::test_an_exported_example_stands_on_its_own[pr-walkthrough]`,
`[corpus]` | widget, probe, theme |
|
`test_render_controls.py::test_every_ring_the_layer_draws_is_shown_whole_somewhere_in_the_corpus`
| example, then `.lf-diff-review`'s ring and both ring names |
|
`test_render_pages.py::test_a_left_sidebar_uses_the_margin_until_the_page_needs_it_back`
| scroll position read off the page |
|
`test_interact_mcp.py::test_stdio_protocol_carries_the_app_resource_and_private_tool_result`
| own thread |
|
`test_mcp_app.py::test_registered_server_prefers_full_page_and_keeps_snapshot_as_fallback`
| own thread |
|
`test_render_projection.py::test_the_render_gate_applies_every_standing_action_a_second_time`
| standing fixture (#194's) |
| `test_site.py::test_the_label_is_chrome_rather_than_words_to_quote`,
`::test_a_comment_lands_in_the_thread_with_its_quote` | the field is
offered, not entered (#193's) |
|
`test_render_anchors.py::test_a_data_bound_diff_aims_and_selects_one_source_line`
| **#194's and #193's, taken on merge** |
| `test_site.py`, `test_render_export.py` stale `<h1>` | **#202's, taken
on merge** |

The two exported-copy cases each carried four faults, three of them
masked: `offering` asserts before `covered`, which asserts before the
copy's axe run, so each fix uncovered the next. The ring sweep did the
same — the geometry fault stood in front of a naming check that had been
silent about a rule missing its name since it was written.

</details>

<details><summary>Three this leaves, all of them main's</summary>


**`test_render_controls.py::test_examples_have_no_serious_wcag_a_or_aa_violations[pr-walkthrough]`
stays red.** The `.lf-pr-description` contrast above is fixed and
confirmed gone. What holds the test red is 46 `nested-interactive`
findings from #194: each file's "Mark reviewed" `<button>` sits inside
that file's `<summary>`, and a disclosure with a focusable descendant is
a serious WCAG failure. Clearing it means taking the press out of the
disclosure and re-laying the row — `entry.node` is the `<details>` in
ten places, and the theme, print and export rules key on it — which is a
change to that widget's shape with visual verification of its own, not a
line this branch should slip in.


**`test_render_anchors.py::test_a_failed_fragment_hydration_waits_for_a_reader_retry`
fails whenever anything runs before it.** Confirmed on unmodified
`main`: three runs of the file at `-n4`, three failures, and the same
failure at `-n0` over the whole file. It passes alone every time.
`assert len(requests) == 2` sees one — the reopened disclosure's second
fragment fetch never reaches the route. Arrived with #189.


**`test_render_anchors.py::test_a_drag_released_mid_word_hugs_words_and_sentences`
fails over the whole file and passes alone.** `assert 'inside' ==
'inside it'` — the selection did not grow to the word boundary. It looks
like the next instance of what #191 diagnosed, Chromium 151 collapsing a
synthetic range on mouse release.

Also still behind: the gallery's `docs/example-pr-walkthrough.jpg`.
Regenerating here rewrites all nine stills in substituted faces, because
`theme.css` asks for Charter and this runner's `fc-match` has none of
the serif stack — the same reading #175 made. Left for the authoring
machine.

</details>

---

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

> _This was written by Claude Code on behalf of @max-sixty_

---------

Co-authored-by: leaf-agent <318509791+leaf-agent@users.noreply.github.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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