Skip to content

Beat 018: the operational workspace - #25

Open
IanMayo wants to merge 11 commits into
claude/beat-017-addressablefrom
claude/beat-018-operational-layout
Open

IanMayo wants to merge 11 commits into
claude/beat-017-addressablefrom
claude/beat-018-operational-layout

Conversation

@IanMayo

@IanMayo IanMayo commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Answers the review of #24. Stacked on #24 → #23 → #22 → #21 → #20.

Beats 013–017 met every requirement in SRD-v2 and the surface still read as an article. Measured at 2560 × 1440 on #24's head: a 1,344 px scrolling controls pane in a 1,440 px window, about two fifths of the screen empty, and state reported in sentences where an instrument shows a readout.

The part worth recording: it was tested, and it passed

ADR-0012 called internal scroll "a declared property of a region, not an accident", and a test enforced that every scroller was one of the declared ones. The test proved the scrollers were intended. It said nothing about whether the content should have fitted — so the six-screen page of SRD-v2 §1.1 survived, folded into a column and stamped legitimate.

The doctrine is withdrawn and replaced by the distinction it lacked: a pane may scroll a list a reader scans, and may never scroll a body of text.

What landed

  • dockview. Pane headers, resize, re-dock, tabbing. Provenance is four tabs, not a column of disclosures. A status bar carries the disclaimer where nothing can close it.
  • The floor came down: 2038 × 728 → 1658 × 960. The old figure was 828 px of fixed chrome before six panels at a hard minimum — measured honestly, but of a layout that could not flex. A 2000 px window now shows the full row.
  • Zero scrolling elements at 2560 × 1440 and 2000 × 1200 in every state. Where anything scrolls it is a list, named: a profile's levels, the run's figures one to a line, the strip of horizons as controls.
  • Zero explanatory sentences on the surface, measured in seven states. The scorer's own words are one disclosure away, verbatim; the losing skill reads as a number with the sentence absent until opened.
  • The row keeps the room. The skill curve took 51% of the horizons pane; it takes a declared 30%.
  • Workspace persistence, held by a key-set test in the same shape as Beat 017: an addressable, operable surface #24's address grammar, with a planted seed failing by name.

Three findings

G-05 had a hole, and it was not the selectors. A vite preview survived the container's death and held port 4317; the gate correctly refuses a stale server, but reported it as "the rendered horizons are not the declared ones" — sending a reader hunting a defect in a row that was fine. Worse and silent: on the success path scanned was parsed from the "N passed" line and defaulted to 0, so a run matching no tests at all would exit zero with no violations. A pass over an empty scan, in the one gate whose walk is a browser run rather than a file count. Now classified by failure kind, with fewer than the expected two tests a failure by name.

The dead-space instrument prefers the layout FR-45 forbids. On one grid across both builds: #24's head 71.9% empty, this tree 56.0%, this tree with the skill curve uncapped 42.5%. A six-point plot stretched over 700 px is ink to a grid and not information to a reader. The requirement decided it and the 13.5 points are reported per pane rather than buried inside a chart.

The persistence amendment belonged to Principle IX, not Principle I. The clause it bounds is a bullet of Derived Artefacts, Not Fixtures. Corrected throughout, and the constitution's amendment log — which its own governance section requires — now carries the 1.2.0 line.

The persistence question, answered rather than assumed

Constitution 1.2.0: the prohibition is on forecast inputs and outputs — a persisted run would be a second way to bring a forecast back with none of the manifest's checks. A persisted pane width is a preference about furniture. A reader could reasonably have inferred otherwise, and one did, so the line is drawn rather than left to be read.

pnpm check green — 328 headless tests, 114 shell tests, 19 screenshots, nine gates, none failing.

ADR-0012's diagnosis stands. Dividing by rate of change is still right; what is superseded is the belief that a CSS grid with declared scrollers realised it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YTAVPvGLfa5sxH1u59opSr

…e pass

The author reviewed beat 017 and the surface still reads as an article.
Measured at 2560 by 1440 on that head: the controls pane is a 1,344 px
scroller in a 1,440 px window, about two fifths of the surface is empty
while the horizon panels are 200 px wide, and state is reported in
sentences where an instrument shows a readout.

The first of those was tested and passed, which is the part worth
recording. ADR-0012 called internal scroll "a declared property of a
region, not an accident", and a test asserted that every scrolling element
was a declared one. The test proved the scrollers were intended. It said
nothing about whether the content should have fitted, so the six-screen
page of SRD-v2 section 1.1 survived, folded into a column and stamped
legitimate. That is beat 007's finding for the third time in this project:
a test that asserts the easy property in place of the claim will pass while
the claim is false.

So the doctrine is withdrawn and replaced by a distinction it lacked: a
pane may scroll a list a reader scans, and may never scroll a body of text.

ADR-0014 records the workspace, dockview over golden-layout, and the
persistence question answered rather than assumed. The constitution goes to
1.2.0: Principle I's prohibition is on forecast inputs and outputs -- a
persisted run would be a second way to bring a forecast back with none of
the manifest's checks -- while a persisted pane width is a preference about
furniture. A reader of Principle I could reasonably have inferred
otherwise, and one did, so the line is now drawn rather than left to be
read.

ADR-0012's diagnosis stands. Dividing by rate of change is still the right
division; what is superseded is the belief that a CSS grid with declared
scrollers realised it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTAVPvGLfa5sxH1u59opSr
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Preview of this pull request: https://deepbluecltd.github.io/j-ocean/pr-preview/pr-25/

A static instance of the application built from this branch's head. It is a real
build with no fixtures: what it shows is what the code does.


Generated by Claude Code

github-actions Bot added a commit that referenced this pull request Sep 8, 2026
…h failure it found

The first attempt at this beat was interrupted with its work uncommitted: the dockview
workspace, the returned walkthrough, the persistence grammar and a repointed shell suite were
all in the tree and none of it was on a branch. This completes it. What it had built stood --
`pnpm test` and `pnpm test:shell` were green on that tree -- and five things it had not
finished are finished here. Each was a requirement the tree did not yet meet rather than a
tidy-up.

## G-05 was repaired, not relaxed

It reported `FAIL  G-05 declared horizons rendered  (0 files scanned)` with the message *the
rendered horizons are not the declared ones*, and the row agreed with the declared file
exactly. A `vite preview` left running by an earlier command was holding the port, and the gate
deliberately refuses to reuse a server it did not start -- a gate asking its question of a build
that is not the tree can pass or fail for reasons nothing in the tree explains. Playwright never
opened a browser, and every non-zero exit was being read as the one failure this gate exists to
report.

Nothing it asserts changed. Two things were added, and the second is the one that mattered:

- **The exit is classified.** A missing browser, a preview server that would not start, and a
  row that disagrees are three different sentences now. Reporting the first two as the third
  sends whoever reads the log to look for a defect in a row that is correct, which is what
  happened.
- **A run that executes no tests is a failure.** `scanned` was taken from the `N passed` line
  and defaulted to zero, so a run matching no tests at all -- a file the `gate-g05` project's
  `testMatch` had stopped picking up -- would have exited zero with no violations and reported
  a **pass over an empty scan**. That is the hole `gate-lib` makes every other gate count files
  to avoid, and this gate had it because its walk is a browser run rather than a directory. It
  now requires both of its own tests to have run: the assertion, and the planted violation that
  is watched failing beside it.

## The score's statement stopped being drawn over the figures that say it

*better than persistence by 0.0 per cent; worse than climatology by 208.8 per cent* was still a
two-line sentence **above** `vs persistence 0.000` and `vs climatology -2.088`, so a reader read
prose to find two numbers already beneath it. That is the readout-written-as-prose the author's
review named, in the one place the beat had not moved it from.

It is the first line inside *Where this figure came from* now, byte for byte as the scorer
produced it -- `score.statement` is rendered whole, never reassembled or truncated. Principle VI
is met by the figure rather than by the sentence, and `tests/shell/figure-kinds.spec.ts` says so
in those terms: it reads the skill against climatology as a **number** with nothing opened,
requires it to be negative, and requires the statement not to be on screen until the disclosure
is. Recorded in `docs/narrative-disposition.json` as `score-statement`.

## A second exemption that meant "this is allowed"

Beat 014's prose test exempted any block carrying a figure, and this beat removed it. The same
mistake was one box smaller: `.legend` was exempt as a whole block -- fair of a legend, which
labels marks -- and the legend beneath the row is a paragraph, so the exemption covered
everything inside it. Two explanations of twenty words each were living under a class that means
*this labels a mark*:

    the row shows the field alone at this size: the attribution layer and each measurement
    at the depth it reached are drawn in the enlarged panel

    the same field on every panel: this run analyses once, at 2026-09-08T09:00:00.000Z.
    Attribution becomes per horizon when the forecast cycles.

Every **entry** of a legend is counted on its own now, against a stated ten words, and the
paragraph stays exempt because the paragraph is only its entries. Five clauses moved: two to
`centre/attribution`'s help, one to `centre/horizon-row`'s, and two dropped because the thing
they explained says it better itself -- a legend that named "the track's own range" beside the
two computed figures that are that range, and one that argued from its own label. Each has an
entry in the disposition record.

FR-051 and FR-007 both hold across that, which took reading them as asking different things.
FR-051 requires the row to **state** that it is showing the field alone, and it does, in eight
words: *field only -- depths in the enlarged panel*. What the enlarged panel adds is an
explanation, and explanation is help's.

## FR-045: the row is the payload, so it keeps the room

The skill curve was given whatever the row did not use and grew to **694 px in a 1 359 px
pane** -- taller than the six panels it annotates, to plot six points. FR-045 gives the dominant
space to the row.

The share it may take is now `presentation.workspace.skillCurveFraction`, declared like every
other figure of the layout, and `tests/shell/viewport-floor.spec.ts` measures the two
rectangles: a horizon panel is **525 px** tall at 2 560 x 1 440 and the curve beneath it is
**403**.

That decision is worth recording because the instrument disagreed with it. Dead space was
measured with one grid over both builds: **71.9 per cent on beat 017's head, 56.0 here** -- and
**42.5** on this tree with the curve left to fill the band. A stretched plot is ink to a grid of
eight-pixel cells and is not information to a reader, so the requirement decides and the
instrument does not. The 13.5 points that costs are reported in the plan's `## Measured`, by
pane, rather than hidden inside a chart with nothing more to say.

## One digest moved: `configuration`

Forty others are byte-identical and the seed is unchanged. The configuration digest is over the
whole file and what changed in it is `presentation` alone: the reference viewport, the floor,
the two pane widths, the gutter, the `workspace` block and its new `skillCurveFraction`. No
computed quantity moved, which is what G-07 exists to say.

## The record made true

`## Measured` is filled in from measurements taken on this tree, and the ADR and the beat's note
are corrected to them: a horizon panel is 280 px at the reference viewport and not 283, the
floor measures 1 658 x 959 against a declared 1 658 x 960, and nothing scrolls at the reference
viewport at all -- at the declared floor one pane scrolls the run's own term list, and below the
floor two scroll, the panes one under another and the strip sideways. Every one is a list.

The persistence amendment is cited against **Principle IX** throughout. The clause it bounds --
*no forecast input or output persists between visits* -- is a bullet of *Derived Artefacts, Not
Fixtures*, and this beat's drafts said Principle I because "nothing persists" is how the rule is
remembered. The constitution's amendment log gains the 1.2.0 line its own governance section
requires and did not have.

328 headless tests, 114 in a browser, nine gates, 19 screenshots recaptured at 2 560.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTAVPvGLfa5sxH1u59opSr
github-actions Bot added a commit that referenced this pull request Sep 8, 2026
The author opened the preview on an ordinary browser window and got a
6,584 px scrolling column beneath a fixed banner -- longer than the 5,757 px
page SRD-v2 was written to kill.

Width was never the issue. The declared minimum is 1658 x 960, and 960 is
taller than any browser viewport a reader has: 1920 x 900, 1536 x 864 and
even 2560 x 900 all fall below the floor and into the stacked below-floor
column. The workspace itself is fine at 1920 x 1080 and 2560 x 1440, which
are the two heights it was measured at, and that is the whole of the
mistake: the surface was measured where it looked right rather than where a
reader stands.

The census that would have caught it reported the stacked column as "a list
-- the panes, one under another" and was believed. Stacking the whole
application vertically and scrolling it is not a list; it is the page.

So the spec gains a matrix of real browser sizes as a requirement rather
than a habit, the minimum height must come down below the shortest of them,
and the below-floor answer becomes the workspace with one horizon and the
strip rather than a column with a scrollbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTAVPvGLfa5sxH1u59opSr
github-actions Bot added a commit that referenced this pull request Sep 8, 2026
T070 to T073. The author opened the preview on an ordinary browser window and got a
6,584 px scrolling column beneath a fixed banner -- longer than the 5,757 px page SRD-v2
was written to kill. This is measured at the sizes a browser actually has: 1366 x 768,
1536 x 864, 1920 x 900, 1920 x 1080, 2560 x 900 and 2560 x 1440, declared once in
`tests/shell/declared-geometry.ts` and walked by `tests/shell/viewport-matrix.spec.ts`.

## What actually forced 960, which was none of the suspects

The declared minimum was 1658 x 960 and 960 is taller than any browser viewport a reader
has: a 1080-tall screen gives a window about 900 px of it once the browser's own chrome is
taken. So every ordinary window was below the floor.

`workspace.provenanceFraction` is not why, and neither is any other figure the layout
declares. The provenance pane's content is a term list that scrolls, so it fills whatever
height it is given and never asks for more; the selection pane's content is 89 px. The two
things that forced it were furniture:

- **the controls pane's own content at the 220 px it is narrowest: 823 px.** A control
  surface set at the body's line height of 1.5 -- a rule for a paragraph somebody reads,
  where a label, a button and a readout are read one at a time -- with its groups separated
  twice over, by a rule and by a gap. Set as furniture it is **648 px**, and no control,
  label, unit or declared bound left the pane.
- **the status strip, which wrapped to two rows and took 110 px.** 22 rem of its width went
  to an ellipsised 64-character digest, which at the floor's own width left no room for
  FR-58's statement beside it. Ellipsised to 8 rem the strip is one row: 66 px at the
  floor's width, 55 px at 1920 and above. The whole digest is still on the manifest tab,
  which is where a figure somebody copies belongs.

The floor now measures **1658 x 740** from the built workspace, declared 1658 x 740, and it
is tight: the controls pane clips by 10 px at 730. 740 is below 768, the shortest laptop in
the matrix.

## The column is deleted, not shortened

`BelowFloor` and every `.below-floor` rule are gone. Below the width the row needs, the
workspace is the workspace -- the same dock, the same panes, the same status strip -- and
the centre is forced to one horizon with the strip carrying the other five. That is beat
015's enlargement, which was already `CentreContent` forced to `enlarged`; what is gone is
the second arrangement that used to be wrapped around it. The statement of the size it
needs is one line in the horizons pane, which is the pane holding one horizon instead of
six, and it states a **width**.

Because that is what a small window is short of. The query that decides what the centre
holds asks the width alone now, and the height was measured to be the wrong question:
forced to one horizon at 1658 x 735 the horizons pane overflowed by 970 px, where the row
it replaced fitted with the controls pane five pixels over.

## What the matrix found, and what it is worth

Every viewport renders the workspace, the row builds and scores, the document does not
scroll on either axis, and nothing is clipped. The tallest scroll of any element in any
state is **653 px sideways** at 1366 x 768, and it is the horizon strip: a list of six
controls that says so on the element. Its slots may no longer go below the width a horizon
is legible at, because sharing a 548 px pane between six of them squeezed each horizon's
skill figures down to `0.0` and `-2.`, and a truncated figure is worse than a scrollbar on
a list.

Two faults surfaced only because the census was finally asked at those sizes, and both had
been in the tree since this beat's first pass. Neither produced a scrollbar.

- **The enlarged panel's depth elevation was given its intrinsic height.** `width: 100%`
  and `height: auto` on an `svg` with a viewBox of 100 x 170 is *width divided by the
  ratio*: 1398 px at the 823 px the aside has. It was laid out 879 px below the pane,
  painted nowhere, with its depth labels -- placed at `yOf(depth)` pixels, in the viewBox's
  own coordinates -- crowded into the top eighth of it, naming depths the lines beside them
  were not at. It fills the box it is given now and the labels are placed as a share of it.
- **The skill curve could be crushed below its own heading and caption**, 30 px of it in a
  636 px pane, with the caption hidden by the `overflow` that was there to protect the
  layout. Its minimum is `min-content` now; the plot contributes nothing to that, because
  `contain: size` says what the component already assumed -- it is measured by a
  `ResizeObserver` and drawn at whatever size it is given, so the `svg` carrying the last
  frame's size as attributes must not decide the next one.

## The test that would have caught it

`one-view.spec` asked its census at the declared floor and at one window below it, and the
census called the stacked column "a list -- the panes, one under another". Stacking a whole
application vertically and scrolling it is not a list; it is the page. That is this beat's
own finding one box larger: a class that means *this is allowed* is a licence and not a
category.

So the census is `tests/shell/census.ts`, asked by both files rather than copied, and the
list/prose test runs at every viewport in the matrix. Two shell tests lost their subject
and are recorded in the plan's `## Measured` against the claim each made: `one-view` ->
*holds the same discipline below the declared floor*, and `viewport-floor` -> *offers the
single-panel presentation, with the strip and the scores*, whose `pane-provenance` was four
sections stacked and is four tabs at every viewport now.

## One digest moved: `configuration`

Forty others are byte-identical and the seed is unchanged. The diff on `config/j-ocean.json`
is a single line -- `minimumViewportHeightPx`, 960 to 740 -- and everything else this
commit changes is stylesheet, markup, tests and the record. No computed quantity moved,
which is what G-07 exists to say.

328 headless tests, 121 in a browser, nine gates, 20 screenshots -- including 1366 x 768,
which was the case that was broken, and a new one at 1920 x 900, which is the window the
author opened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTAVPvGLfa5sxH1u59opSr
github-actions Bot added a commit that referenced this pull request Sep 8, 2026
… is reversed

The author asked for it: *"the walkthrough should move around the UI, masking out the unrelated
elements/panels."* This beat had decided the opposite two passes ago and wrote the reasoning into
`Walkthrough.tsx` -- *"nothing is covered by a scrim. A reader may drag the ..."* -- so what is
reversed here is not an oversight but a stated decision. The old rationale is replaced rather
than left standing beside the thing that contradicts it, in the module, in the stylesheet and in
the spec.

It answered the wrong question. A workspace of docked panes raises **what am I looking at**, and
that question is answered by suppressing what you are **not** looking at; a card placed near the
pane it names, among seven equally bright rectangles, points at nothing in particular. Panel help
(FR-052) is untouched and still answers *what is this panel*. The two stay distinct.

## The hole is the pane, measured, and a deleted claim comes back with its subject

One fixed element with the rectangle cut out of it by `clip-path`, so what is dimmed and what is
lit are the same fact rather than two facts that agree today. The four lengths come from the
named pane's own `getBoundingClientRect` at the moment the step is shown: **0.36 px** from it at
the worst of the seven steps, against a stated tolerance of two.

Beat 016 struck out a test called `rings the panel its step is about, not some other part of the
page` and recorded against it *"Gone. There is no spotlight."* There is one again, so the claim
is back with its subject and at the two pixels that beat asserted -- at **every** step, in a
browser, against the pane the step names rather than against a figure.

`clip-path` clips hit-testing as well as paint. So the dim takes the reader's clicks and closes
on them, and the lit pane goes on working: what the pointer would reach is asked of the browser
at the middle of every pane, and `advance` is then pressed for real with the controls step open.
Through `grayscale(1)` the lit pane's paper is **255.0** of 255 and the dimmed pane's is
**110.0** -- a margin of **145.0** against the 40 beat 007 set, measured with beat 015's own
decoder rather than a second one.

## It follows the pane, or it is worse than no mask

A mask that is correct only until the reader touches something points confidently at the wrong
rectangle. So it is re-measured on a `ResizeObserver` over the pane and the dock, a
`MutationObserver` over the workspace's subtree, and the window's own resize and scroll,
coalesced to one measurement a frame and replacing state only when the rectangle differs.

A **sash drag**, a **tab moved into another group** and a **window resize** are each driven in a
browser, and each is asserted only after the pane has actually moved: the layout manager measures
its container asynchronously, so a rectangle read in the same task as a resize is the rectangle
before it, and a test that read one would report success over a workspace that had not moved yet.

The measurement is taken in a **layout** effect, and that is not a detail. With an ordinary
effect the frame painted after every Next showed the *previous* step's rectangle lit under the
new step's name.

## Three costs, said rather than absorbed

- **The hole is the pane's group, tab strip and all.** The element carrying `data-pane-id` is the
  pane's body; its tabs are in a strip 26 px above it. Lighting the body alone would leave the
  step that says *the run, the instruments, the record and the manifest, **as tabs*** with the
  tabs unpressable, and the lit pane staying operable is a requirement. Nothing is guessed: the
  strip belongs to an element with a rectangle of its own, and a pane with no group -- the status
  strip, which is not in the dock -- is lit as itself.
- **A reader cannot move a tab while a step is open**, because either the source or the target is
  dimmed, and a scrim that let them would be masking nothing. The layout manager's sashes sit
  above the scrim by its own stacking, so resizing a pane still works -- which matters, because
  the walkthrough's last step is the one that tells a reader to do it. The tab-move test is the
  single place in `walkthrough.spec.ts` that reaches past what a reader can do, and it says so.
- **Nothing animates, in either media state.** A mask that slid between steps for a reader who
  had expressed no preference would be the first thing on this surface that moved, so the test
  asks under `reduce` *and* under `no-preference`.

## A defect the no-reflow test found on its first run

Opening the walkthrough and advancing it may change no region's bounding rectangle, asserted to
the pixel as beat 015 asserts it for enlargement and beat 016 for panel help. It failed
immediately, on something that had been in the tree since the walkthrough returned.

The offer's label went from *Walk me through the workspace* to *Close the walkthrough* when it
was pressed. The control is in the status strip; the strip's height is what the dock does **not**
get; and at the declared floor the shorter label let the strip reflow by 11 px, so **every pane
in the dock moved** -- on the click that opens a walkthrough whose whole claim is that it moves
nothing. Both labels occupy one grid cell now, with the inactive one hidden. No word changed.

## One shell test lost its subject

`walkthrough` -> *names a pane that is on the surface at every step, and leaves the run usable*.
Its second half was *"The run underneath is still usable with the card open: nothing is behind a
scrim"*, which is the decision this commit reverses, so it is gone rather than corrected: a
reader may no longer act on a pane they cannot see, and that is the point. What replaces it is
the half that is still true and now matters more, and the first half moved into the test that
asks the mask about the same step. Recorded in the plan's `## Measured` against the claim it
made, as the eight before it were.

## One digest moved: `configuration`

Forty others are byte-identical and the seed is unchanged. The diff on
`scripts/gates/records/surface-invariance.json` is one line. What moved in `config/j-ocean.json`
is four figures in `presentation.workspace` -- the dim's opacity, and the card's width, minimum
height and gap -- because a dimension or a colour the surface needs is declared and never a
literal in a stylesheet (Principle X). A mask computes nothing, which is what G-07 exists to say.

328 headless tests, 130 in a browser, nine gates, 21 screenshots -- including
`018-walkthrough-mask.png`, which is the whole viewport, because what this commit added is what
happened to the other six rectangles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTAVPvGLfa5sxH1u59opSr
github-actions Bot added a commit that referenced this pull request Sep 8, 2026
…decide

The last pass reported "one pre-existing flake: `provenance/manifest / dl scrolls vertically,
declaring null` at three viewports ... marginal and pre-existing" and left it standing. It was
neither marginal nor intermittent. The manifest tab's term list has **scrollHeight 59 px** at
every viewport in the matrix and a client box of **12, 20, 20, 30, 23 and 53 px**: the code
version and the results digest were cut off at all six. It looked like a flake because the
census read the pane in the same task as the click that opened its tab, and the layout manager
mounts a panel asynchronously -- so at the other three viewports what was measured was the tab
that was closing. `selectTab` now waits for the pane the tab names and for two frames in which
no pane's rectangle moves.

## Every element, no threshold, three outcomes

The census counted only elements whose computed `overflow` was `auto` or `scroll`, which is a
filter on the declaration and not on the fact: an element that runs past its box with
`overflow: visible` spills onto whatever is beside it, and one with `hidden` cuts its content
off, and neither has a scrollbar to count. It now walks **every** element at every viewport in
the declared matrix and reports anything more than 1 px past its own client box on either axis.

Each finding is then one of three things, decided in code, and there is no fourth: a **declared
list** (the manifest, the run's own figures, the horizon strip, a profile's levels), an
**exemption** in one reviewable list with a written reason each, or a **defect**. An exemption
whose reason is under 60 letters excuses nothing -- the census drops it and the element it names
is reported as a defect -- which is the whole difference between a list of reasons and a
threshold. Exempt: the visually hidden mark lists and the layout manager's live region, both
painted nowhere; its tab strip, which scrolls as a list of tabs on its own scrollbar (measured:
a wheel moves it to scrollLeft 90 and the Manifest tab comes fully inside); its tab label's 2 px
of leading; and the status strip's digest, ellipsised on purpose with the whole of it on the
manifest tab.

## What it found, on a green tree

- **The pre-row centre's field, 93 px over its box at every viewport.** `.analysed-field .field`
  was `display: flex` with no direction, which is a row: the field's own label and colour scale
  were laid out *beside* the picture and painted over the term list in the next column. That is
  the author's "the Horizons panel has text overwriting other text". The same rule letterboxed
  the picture -- 353 x 726 with the square canvas contained in the middle third, and an overlay
  canvas whose coordinates no longer matched the picture a reader clicks. A column, and the
  picture is now the square itself: 258, 633 and 903 px at the three widths measured.
- **The manifest tab's term list**, above. The two figures keep their height, the document gives
  way, and the paste box is folded into a disclosure -- at the floor the tab is 220 px wide and
  the figures, the document and the form together want 463 px of a 311 px pane.
- **The status strip's step time, 65 px over in the over-budget state.** The 8 rem ellipsis was
  written against every readout, so what fell off the end of *1.234 ms/step over 16 ms* was the
  word that says whether the budget was met. A prefix of a digest compares; a prefix of a verdict
  is a different verdict.
- **The controls pane, 8 px over at the floor**: its own bottom padding, which the floor's
  measurement had stopped short of. The declared floor is 1 658 x 748.
- **The skill curve's value axis**, six labels 6 px on top of each other in the 66 px band the
  row leaves. It carries as many gridlines as can be read at the tick text's own line height.

## An overlap census, because it is a different question

A box can overflow without landing on anything and a placed element can land on its neighbour
without overflowing. So the census asks both, and asks the overlap of the **words**: one `Range`
per text node gives a rectangle per painted line, cut down by every clipping ancestor, because
what a scroller or an ellipsis does not paint cannot land on anything. Measured on the boxes
instead it reported six help controls over six score figures -- a `dd`'s whole grid cell, not its
digits -- which would have bought the instrument's honesty for nothing.

## Watched failing

A defect (`.status-figures dt { max-width: 12px }`), an overlap (the row invitation's statement
shifted 28 px up), an exemption with no reason (`because: 'deliberate'`) and a list that does not
say which list (`data-list=""`) were each planted, watched failing by name, and removed. The
reasonless exemption fired both halves: the reviewability test named it and the digest it had
been excusing was reported as a defect.

## Which digest moved

`configuration`, and only that one: `5bac990a` -> `daeafe80`. Two presentation figures moved --
`minimumViewportHeightPx` 740 -> 748, and a new `fieldLabelHeightPx` of 72, which is the room the
field's label needs because a container query can ask its box how wide and how tall it is and
cannot ask how tall the words under the picture came out (Principle X; measured 46.4 px on one
line, 66.8 px wrapped). Neither computes anything and the other forty quantities in G-07's record
are byte-identical.

`pnpm check` green: 328 vitest, nine gates, none failing. 131 shell tests, 21 screenshots. The
plan's `## Measured` carries the full census -- every element, its classification and its reason
-- and corrects the flake note rather than deleting it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTAVPvGLfa5sxH1u59opSr
github-actions Bot added a commit that referenced this pull request Sep 8, 2026
… plant

The overlap census exists to catch a thing the overflow census was green on, so watching it fail
on a plant is only half the proof. The old stylesheet was put back and its 93 px of overflow
excused with an exemption -- the excuse a threshold, or an exemption nobody read, would have
given it -- and the overlap census failed anyway, on the words: the field's label 63 x 10 px over
`dt` *Influence radius*, 47 x 9 px over the row invitation's statement, and the colour scale
24 x 4 px over *Observations used*. That run is now in the plan's `## Measured` beside the three
planted classes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTAVPvGLfa5sxH1u59opSr
github-actions Bot added a commit that referenced this pull request Sep 8, 2026
…hell's path

The author pressed "Integrate 12 hours", was shown the over-budget notice, pressed "Integrate
anyway", and got no confirmation that anything had finished. The missing confirmation was the
smaller half: the run advanced **16 hours 48 minutes**.

`integrate` advanced a chunk in order to time it, *then* asked whether the projected time for
the longest declared horizon was inside the frame budget. On a refusal it returned -- leaving
those 72 steps applied and counting none of them -- and "Integrate anyway" began a fresh
`done = 0` and took a whole `stepsPerAdvance` from wherever the probe had left the run.
72 + 180 = 252 steps at the declared 240 s timestep; two presses landed on 504, which is
33 h 36 min for two requests of twelve hours each.

| | steps | valid at |
|---|---|---|
| before | 0 | 2013-09-01T00:00:00.000Z |
| the press, refused | 72 | 2013-09-01T04:48:00.000Z |
| proceeding, before | **252** | **2013-09-01T16:48:00.000Z** |
| proceeding, now | **180** | **2013-09-01T12:00:00.000Z** |
| pressed twice, now | **360** | **2013-09-02T00:00:00.000Z** |

The measured chunk is counted rather than thrown away: re-running it would do the same work
twice and drop the yield NFR-04 asks for. An advance is now a **target** -- the step the run is
going to -- carried across the reader's decision in `view.advanceToStep`, so resuming finishes
the remainder. Pressing the control again while a refusal stands continues that advance rather
than adding another twelve hours, so no sequence of presses puts the run past where the last
press said it would be.

## The gate never drove the shell's path

G-07 digests the state after a twelve-hour advance and produced it by calling `Run.advance(180)`
in `scripts/gates/surface-invariance.ts` -- a second implementation of the advance, written
beside the shell's and asserted against nothing. The shell drove the model chunked and behind a
probe, and that path was digested by nothing, which is how a control 40 per cent wrong passed
nine gates and 131 shell tests.

So the drive is `src/harness/advance.ts` and both callers ask it, the way `scoreEveryHorizon`
became the row's own arithmetic in beat 013. A change to how far an advance goes or how it is
chunked now moves the gate's digest, because the gate runs the shell's code. Beside it,
`tests/harness/advance.test.ts` asks what a digest of one path cannot: that every way a reader
can reach the end of an advance -- chunked, refused mid-way and resumed, or one straight call --
lands on the same bytes.

**No digest moved.** All 41 quantities are byte-identical: the gate's own walk was taking the
correct 180 steps already, and the defect was in the shell alone. The record's one changed line
is that quantity's producer sentence, which now names the module both callers go through.

## The assertion whose absence let this stand

`integrates the run and reports the step time as host time` asserted that `steps` was not `0`
and the instant had changed, on the reasoning that the exact count comes from configuration. A
figure derived from configuration can be derived in the test too, and a control 40 per cent
wrong satisfies "not zero". Three tests replace it, and the hours they check against are read
off the button's own label rather than written down, so what is asserted is that the run stands
where the control said it would put it.

## Confirmation, and a busy state that could not be seen

Nothing announced completion, and the figures that move are on the run tab, which may not be
the tab in view. The advance now says what it did in a live region beside the control that
asked for it, in the provenance-typed figure components and in five words once its figures are
removed. It is painted nowhere for a measured reason: the controls pane's content at the
floor's width *is* the declared floor, so a visible line costs 19 px of a budget with none left,
and buying it means re-declaring `presentation.minimumViewportHeightPx`. This change moves no
declared value.

`view.integrating` did disable the control, and it was set in the same task as the chunk it
described -- so the main thread was blocked by the work before the browser could paint the state
saying the work was happening, which is why the author measured `aria-busy` absent and the
button enabled at every sample. Every chunk runs after a yield now, the measured one included;
the group carries `aria-busy` and both controls that touch the run are disabled while it runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTAVPvGLfa5sxH1u59opSr
github-actions Bot added a commit that referenced this pull request Sep 8, 2026
… can be seen

The author asked for two things: *"when that long process is running, I need to see some kind
of busy cursor -- plus some verification that it's complete."*

## Three things take seconds, and one of them said so

An advance of twelve hours, building the horizon row -- six four-day integrations -- and
scoring every horizon against the truth record all block the same main thread. Beat 018's
sixth pass gave the advance `aria-busy` and a disabled control; the other two said nothing at
all, and none of them said anything at the pointer, which is where a reader who has just
clicked is looking.

So the notion is one module, `src/harness/working.ts`, and the stylesheet has one rule against
it -- `cursor: progress` on the workspace and everything in it -- because three rules written
beside three pieces of state are three rules that drift. `progress` and not `wait`: `wait` says
the application is blocked, `progress` says it is working and still usable, which is what the
chunked advance delivers and what the panes a reader can still read make true. Nothing
animates, so `prefers-reduced-motion` has nothing to turn off.

The two synchronous ones needed the sixth pass's own lesson one layer down: a flag set in a
click handler is committed to the DOM and **never painted**, because the browser's next chance
to draw comes after the work the flag describes is over. `begin` commits the flag and hands the
work to the frame after the one that paints it, bounded by a timer so that a tab which is never
painted cannot leave a control disabled for ever.

## The control says how far it has got, and moves nothing saying it

*Integrating 72 of 180* is a relabel, and a relabel costs no line in a pane whose content
height **is** the declared floor. What it could cost is a reflow -- beat 018's own walkthrough
offer changed its label and moved every pane in the dock by 11 px -- so the width is reserved:
both labels and the widest count the advance can show lie in one grid cell, and the button is
the widest of them in every state. Measured through a whole advance, at the floor's width and
at the reference width: **126.64 px, and 0 of 5 samples in which anything moved.** The
walkthrough's offer now uses the same rule rather than its own copy of it.

## The confirmation, in the status strip, for 0 px

`advance-report` existed with the right words and was painted nowhere, because a line beside
the control that asked for it is **30 px** of a pane whose content height is the floor -- two
wrapped lines at 220 px wide -- which would put the floor at 778 px, past the 768 px of the
shortest window in the matrix. In the status strip it costs **nothing**: the strip's height at
the floor is FR-58's statement, 57 px of it in a 66 px strip, and the readouts have 18 px under
them that nothing was using. It is also the better place -- on screen whatever pane has focus,
where an operational surface puts what just happened.

`presentation.minimumViewportHeightPx` stays **748**, re-measured with the confirmation on
screen, and **no digest moved**: all 41 of G-07's quantities are byte-identical.

## What the census found on the way

The integrating and integrated states are new to the census, at every viewport in the matrix
and at the declared floor, and holding a state that lasts a seventh of a second open enough to
measure took `model.chunkSteps` routed to 1 and the browser's own CPU throttle. It caught the
strip growing under a reader mid-advance -- 839 px of content in an 835 px dock, at three
viewports of six -- so the confirmation's line and the height of a readout are now held whether
or not there is anything in them, and the strip is the same height before an advance and after
one.

One finding is recorded and not fixed: at the floor the over-budget notice is 1,012 px of
content in a 656 px pane. No census had visited that state either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTAVPvGLfa5sxH1u59opSr
github-actions Bot added a commit that referenced this pull request Sep 8, 2026
…t of sight

The author asked for two things: *"I clicked 'Integrate 12 hours' -- the popup showed within the
'Controls' pane, but it's too tall for me to see the 'Integrate anyway' button. It used to be a
floating modal -- that seems like a better UI for this interaction."*

## The mechanism is the reader's font, and nothing had ever varied it

The notice's **height** scales with the reader's declared font. The pane's **width** does not. So
its paragraph wraps to more and more lines in a column that is getting shorter at the same time.
At the author's own window, 950 x 875, with a 310 px controls pane:

| root font | pane box | `proceed-anyway` bottom | room under it |
|---|---|---|---|
| 16 px | 724 | 403 | 321 |
| 20 px | 615 | 464 | 151 |
| 24 px | 523 | 510 | **39** |
| 28 px | 445 | 654 | **-183, out of sight** |

Every viewport in the matrix was censused. Every state but this one was censused. The surface is
written in `rem` throughout, and the reader's font was the one dimension of it nobody had ever
varied -- which SRD-v2's own edge cases name, *"deep browser zoom, or a large declared font"*.

The previous pass measured this state and **recorded it as a finding**. That was the wrong call
and the plan now says so where the note stands: recording is the right posture for a numeric
question under FR-40 and the wrong one for a control a reader cannot reach. It should have been
fixed when it was measured.

## The decision is a modal, and it is the platform's

`src/harness/OverBudget.tsx`: a native `<dialog>` opened with `showModal()`, so the top layer,
focus containment and Escape come from the browser rather than from a second implementation of
each that would drift from it. Being in the top layer is also what makes the requirement true by
construction -- it is laid out against the **viewport**, so no pane's width decides what a reader
sees of it. Measured at 950 x 875, the case that was broken: **420 x 330 px, centred at 265, 273,
with `Integrate anyway` at 293-449 x 533-579 and 296 px of window under it.** At 32 px there are
still 254 px under it. **No pane's rectangle moves by a pixel** when it opens or closes, asserted
at every viewport and at 20 and 24 px, and measured across the close rather than the open because
the advance's own first chunk changes the run's figures and the strip is sized from them.

The dim is the surface's **one** dim: `walkthroughMaskOpacity` became `maskOpacity` when a second
thing started using it, because two rules written for one claim are two rules that drift -- beat
015's lesson about the strip. Nothing animates, in any media state, so `prefers-reduced-motion` has
nothing to turn off, and the dim is luminance rather than a hue so a greyscale rendering still
separates the decision from the workspace.

The words are a decision now. Two figures with their provenance and neither truncated, and two
things a reader may do; the three sentences explaining what a frame budget is are `controls/run`'s
help -- the panel that had declared it had nothing to explain, which was true until one of its
buttons started asking a question -- with all three recorded in `docs/narrative-disposition.json`.
Escape declines, which integrates nothing beyond the chunk already measured, and returns focus to
the advance; pressing it again resumes the same target, so declining and then proceeding lands on
twelve hours and 180 steps.

## The instrument gap, which is the more important half

The census asked whether a **pane's** content fits the pane. That is a question about one element,
and it is why nothing was reported on the day the button was unreachable: the pane had grown to
hold the notice and it was the pane's **ancestor** that clipped it. There is a fifth check now --
every interactive element and every text-bearing element walked out through the ancestors that
actually clip it, respecting `position`, and intersected with each of their boxes. An ancestor a
reader can **scroll** is not a clip: the rectangle is moved into the scroller's own box, as a
wheel would move it, so an item below the fold of a list is judged by whether the list is on
screen. Watched failing on the defect before it was fixed, at 1 366 x 768 and a 28 px root font:

    controls/button[data-testid="proceed-anyway"] "Integrate anyway"
      100% outside div[data-testid="pane-controls"].pane-content.controls
    horizons/span.scale "0 1.00"
      100% outside div.dv-split-view-container.dv-vertical.dv-separator-border

The second is the ancestor case in its pure form, and the pane-content check said nothing about
the horizons pane there at all. It also caught two false readings of its own, both recorded: a
vertical scroller treated as a clip, and a `<pre>` that is its own scroller left out of its own
walk.

## The font axis, and the four defects it found

16, 20 and 24 px. Four defects, all fixed: a `16rem` floor on the term list that starved the
picture beside it to 110 px -- narrower than its own heading, which then ran 52 px outside its box;
a `minmax(8rem, 12rem)` label column that took 192 px of a 237 px list and pushed the figures out;
`fieldLabelHeightPx` reserving 72 **pixels** for a label written entirely in `rem`, which is right
at one font size and 7 px short at 24; and 3 px of controls pane the run panel's new help control
costs.

**Which combinations run is in the plan.** 16 px keeps the whole state walk at every viewport;
20 and 24 px run the arrival and over-budget states at every viewport; 28 px runs the decision
alone at the narrowest. **What that leaves out is the larger finding and the plan says it
plainly:** with the row built and scored, this surface does not hold at 20 or 24 px -- at
1 920 x 900 and 24 px the controls pane's content runs 299 px below the pane, 34 elements are past
their boxes, 16 pairs of text are painted on each other and 30 controls and figures are outside
what clips them, *Integrate 12 hours* and *New run* among them. That is the same class of defect
three panes wide, and it is a beat of its own; what is different from the note above is that there
is now an instrument that sees it.

## Digests

One moved: `configuration`, `daeafe80` to `c39db1ba`, and the other 40 of G-07's quantities are
byte-identical. Four declared presentation figures changed and nothing else may:
`workspace.maskOpacity` renamed from `walkthroughMaskOpacity`, `workspace.modalWidthPx` new at
420, `fieldLabelHeightRem` at 5 replacing `fieldLabelHeightPx` at 72, and
`minimumViewportHeightPx` re-measured at 752 against the 752 the built workspace needs and the
768 of the shortest viewport in the matrix. Nine gates, none failing; 335 vitest, 151 shell tests
and 21 screenshots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTAVPvGLfa5sxH1u59opSr
github-actions Bot added a commit that referenced this pull request Sep 9, 2026
…ol that costs seconds

The author asked for two things: *"It happens a lot quicker than the dialog warned. But, for
the other buttons in that panel, there is no indication of progress -- which makes it difficult
to engage with."*

## The notice quoted the wrong operation's cost

`projectedHorizonMs` projects the **longest declared horizon** -- 96 h, 1 440 steps -- and the
over-budget decision printed it, alone, under the heading of a press of *Integrate 12 hours*,
which is 180 steps. Measured with one step time behind both: **1 542 ms** for the row's
integration where **193 ms** is what the pressed control costs. A factor of 7.99, and the
author's report is the evidence that the heading was read the way it invited.

The arithmetic was never wrong and the **check has not moved**: NFR-04 gauges the machine
against the worst case and FR-008 is written about the longest declared horizon, so a check on
the advance would pass on a machine that cannot draw the row. What was wrong was the
attribution -- a figure presented as being about something it is not about, which is the
Principle V fault rather than an error. The decision prints three readouts now, each named for
the work it is the cost of: *this advance (12 h, 180 steps)*, *the horizon row (96 h, 1 440
steps)*, and the declared frame budget the second is held against. The test asserts the
**ratio** -- 1 440 / 180, declared arithmetic -- so it holds the attribution rather than the
speed of the machine running it. Why the check is gauged against the worst case is an
explanation, so it is a fourth paragraph of `controls/run`'s help, with a row in
`docs/narrative-disposition.json`.

## The count is a position in the work, because the work was already made of pieces

`runForecast` walked the declared horizons in order and `scoreEveryHorizon` walked the same
six. Both are generators now -- `forecastInChunks`, `scoreHorizonByHorizon` -- yielding between
the pieces they were always made of, with a driver over them that every caller which is not
watching still uses: the gate, the headless tests, the manifest replay. **No digest moved on
that**, which is the whole of the claim that it is a restructuring and not a computation.

So the totals are asked of the work before it starts, and they differ between presses because
the work does: **8** to build the row (the issue analysis, six horizons, the quay-side brief
computed once), **7** to re-issue it, **14** to apply an edit (the forecast and the baseline it
is differenced against), **7** to revert, **6** to score. A fixed denominator would have been
the invented figure this pass exists to remove. Every long operation turned out to be
chunkable, so `working.ts` has one entry point, it takes a total, and there is no second one
that runs work without a count.

Each control says how far it has got in its own label, is disabled while it runs, and confirms
itself in the status strip -- `advance-report` is `surface-report` now, generalised from *what
the advance did* to *what the surface just finished*, in the same place and for whichever
operation finished. The bias field and *Quality control* carry no count: a count inside a
control's **name** is a readout in the wrong place, so those are disabled and the strip says
*Applying the edit: 3 of 14* for them.

## Saying so moves nothing, and two controls had to be paid for first

Every relabelled control reserves the width of the widest thing it can say. Nothing moved at
1 658 px or at 2 560 px, on any sample of any run -- every pane's rectangle and every control in
the pane the author was reading, against the resting layout. The test also asserts the count
**counts up**, and saw every value from 0 to the total on each.

Two controls cost the layout before that held, and the floor is where it showed:

- `revert` went 147 x 24 px to **188 x 55**, because `.run-status button { display: block }` and
  `button.reserving { display: inline-grid }` have the same specificity and the first is later
  in the file, so the three labels that belong in one grid cell stacked into three lines;
- `.issue-control` went four rows to **five**, because *Re-issuing 0 of 7* takes the control
  from 58 px to 113 and the row it shared with the issue offset has 197 px of which the offset
  takes 94.

Together **58 px** of a controls pane whose content height at the floor's width *is* the
declared floor -- enough to take it to 810 px, past the 768 of the shortest window in the
matrix. The first is a `:not(.reserving)` split; the second is paid for by the row the label
was wasting, since `flex: 1 1 100%` put the slider alone on a line with *Issued* alone above it.

**One digest moved: `configuration`, and only because one declared presentation figure did.**
`presentation.minimumViewportHeightPx` is **753**, from 752 -- the one pixel the reflowed issue
control needs, measured from the built workspace rather than chosen. The other 40 of G-07's
quantities are byte-identical across a change that turned two of the shell's arithmetic walks
inside out.

## The censuses, in the states this pass invented

A control that relabels itself while it works is exactly what overflows a box or wraps a pane to
an extra row, and this beat has been caught by that three times. Those states had never been
censused because until the work was chunked they could not be: a blocking call holds the main
thread for the whole of itself. `viewport-matrix.spec.ts` now censuses *building the horizon
row* and *scoring every horizon* at every viewport in the matrix, and `reader-font.spec.ts`
censuses the building state at 20 and 24 px. The scoring and re-issuing states are not there,
and the reason is the finding the plan already records: with the row built this surface does not
hold at those sizes, so a census of them would fail on that rather than on anything here.

335 vitest, 153 shell tests, 21 screenshots, nine gates, none failing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTAVPvGLfa5sxH1u59opSr
github-actions Bot added a commit that referenced this pull request Sep 10, 2026

This branch has not been deployed

No deployments
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