Commit Graph

1040 Commits

Author SHA1 Message Date
Edward Langley ec03f950b3 Merge remote-tracking branch 'gh/main' into fix/web-panel-clicks 2026-09-20 14:36:19 -07:00
Edward Langley 27580e4d64 docs(tests): a repair needs its own probe, because a repair can look like a fix
The two-step mutation recipe recorded alongside this finds a DISARMED
test. It says nothing about whether the repair you then write is real.
That gap is not hypothetical: the first repair of
panel_commands_address_the_named_panel_only folded [open:false,
open:true], read as a fix, and was still vacuous.

The probe that settles it is a ONE-DIRECTIONAL fault -- make the writer
`if open { set true }`, so the flag can be raised but never lowered. A
vacuous repair cannot see it, because a test that only ever asserts the
`true` direction is satisfied by a writer that only ever writes `true`.

Injected into both copies at once it fails set_panel_open_and_cursor AND
panel_commands_address_the_named_panel_only, each in its own copy --
which is what establishes both repairs are genuine rather than merely
differently spelled. Verified here rather than taken on report.

Prefer it to re-reading the diff: the diff contained `open: false` in
the version that was vacuous, which is exactly why reading it passed.

Docs only -- crates/, src/, web/ and harness/tests/ are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 14:33:37 -07:00
Ed Langley 74ccd6f030 Merge pull request #65 from fiddlerwoaroof/fix/socket-close-routing
fix(harness): restore the watchdog close-routing guard PR #61 deleted
2026-09-20 14:31:28 -07:00
Edward Langley 89f67afd4c fix(harness): make a vacuous watchdog denial fail, and single-home the window
PR #65's review found this PR had planted a fresh instance of the defect it
exists to repair. Both blocking findings fixed, each demonstrated.

FINDING 1. "a peer that cannot be reached is named, and the watchdog stays out
of it" could not see the property its comment claimed. `shell.arm` runs only
after `await transport.ready`, which never resolves on a socket that never
opens — so the watchdog is never armed there and denying "sent no document"
denies an unreachable event. Reverting PR #21's routing left that clause
passing.

The aggravating half: `context/test-evidence/clients.md` already said so, and
this PR EDITED that paragraph — inserting "runFor(10s) advances the watchdog"
immediately above the sentence explaining it is never armed. Not text the PR
left alone; text the PR walked past mid-sentence.

The clause is gone and the test keeps the naming property it really guards: a
`close` on a socket that never opened must not read as "disconnected". And the
shape cannot return — `outlastTheWatchdog` now asks the page `timesArmed() > 0`
before jumping, so a watchdog denial re-added to that path FAILS BY NAME. It
asks *ever armed* rather than *armed now* on purpose: by the time the disconnect
test jumps, a correct page has already disarmed, which is the property, so the
obvious spelling would have denied the passing case. Building the guard is what
surfaced that distinction; reading had not.

FINDING 2. The spec kept its own `HANDSHAKE_TIMEOUT_MS` beside a comment saying
a silent disagreement "would pass for the wrong reason". It did: the spec at
1000 against the page's 8000 made the suite green over the reverted routing,
because the jump landed inside the window. The constant is now exported from
`web/shell.js` and imported by both, so the disagreement is unconstructible.
Shrinking it in its one home still catches the routing revert — that is the
demonstration the guard did not go quiet.

Filed, not fixed: improvise-78v6 (#61 also deleted the help-overlay spec and
nothing replaced it — a PR about a dropped guard that did not diff the dropping
PR's test names) and improvise-5ray (--check-evidence collects only `### ` and
resolves only Rust fns, so every browser entry's `####` is invisible; appending
one that names nothing exits 0, which is how the dangling heading cleared the
gate for a release).

Verified on the merged tree: node 26/26, bundle 3/3, Playwright 17 passed + 3
skipped, cargo test --workspace 1378 passed, comment-runs clean,
--check-evidence 501 resolving, all cited ids resolve, no .beads diff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 14:24:25 -07:00
Edward Langley ad160b8b54 Merge remote-tracking branch 'gh/main' into fix/web-panel-clicks 2026-09-20 14:24:18 -07:00
Edward Langley ac6f17288c docs(tests): record how to know a launch default has stopped disarming tests
The question PR #60 left open was not which tests the launch default
disarmed, but HOW TO KNOW THE CLASS IS CLOSED. Grep cannot answer it: it
finds sites that NAME the flag, and the hazard is a test that merely
starts from a default-built state and asserts something downstream,
mentioning nothing. Finding the three disarmed tests by grep was luck.

Mutation answers it, because it does not care what a test mentions.
Recorded in context/test-evidence/clients.md: mutate every site that
writes, propagates or reads the value to the constant the default
supplies; sites where nothing fails are suspects; then RE-RUN EACH
SUSPECT WITH THE DEFAULT INVERTED. If it now fails, the test was
vacuous -- the default disarmed it, fix the fixture. If it still fails
nothing, it was simply unguarded -- pre-existing, file it.

That second step is the whole method. Without it the two species are
indistinguishable (both look like "nothing failed"), and it is what
established that the vacuity class is closed at TWO rather than three.

It found two further silent sites, neither caused by the ruling:

- App::cmd_context propagates the flag and nothing tests it. Stuck
  true, `C` on a CLOSED panel decides to close it -- the panel becomes
  unopenable by its own key -- with the workspace green. Invisible
  because the command test helper builds the value it then asserts on.
  Filed as improvise-qsmp; control run proves it predates the ruling.
- render.rs propagates it into RenderModel. Stuck true is improvise-wsaw
  reintroduced one layer up, and RUST SEES NOTHING -- only the Chromium
  harness catches it. A Rust-only run of the `test` job cannot see a
  reintroduced wsaw, which is the sharpest argument on file for the
  browser job being able to fail the build.

Also marks the_terminal_constructor_inherits_the_shared_default_rather_than_setting_it
PROSPECTIVE: with three panels its two siblings already cover every
Panel::ALL member, so it cannot fail alone today. It earns its place the
way gotcha 12 describes Panel::ALL -- a fourth panel reaches it from one
entry -- and saying so stops a green tick being read as present-tense
evidence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 14:24:08 -07:00
Ed Langley 8e6738af72 Merge pull request #62 from fiddlerwoaroof/feat/doc-site
feat(docs): publish the context/ prose as a documentation site folded into Pages
2026-09-20 14:20:43 -07:00
Edward Langley 702c1d43ff docs: correct the alt-text argument, which was right for the wrong reason
The commit that added `content: "\2630  " / ""` claimed Firefox ignores the
alt-text form and falls back to reading the content, making the change "an
improvement in two engines and a no-op in the third". The mechanism is wrong:
Firefox shipped the syntax in 133, and all three engines parse it -- measured
in Chrome 141, Firefox 142 and WebKit 26, each computing the declaration
intact. The conclusion survives; the reason given for it does not, and the
reason was the load-bearing half, because it was what made the change look
like a one-way door. It is one because every engine shipped to supports the
syntax, not because the unsupported path is harmless.

The unsupported path is NOT harmless, and that is the risk neither the change
nor its first review named: `/ alt` in an engine that does not know it is a
parse error for the whole `content` declaration, so the glyph does not lose
its alt text -- it VANISHES. A silent visual regression, the opposite of the
assumed failure mode. The glyph is now verified painted in all three engines
by pixel diff against `::before { content: none }`, text starting 20px further
right in each.

This is written to context/ rather than left in the commit message because
design-principles.md §6 says an argument must not live only in a commit
message -- a message is write-once, unauditable, and no check in this repo can
read one. The rule landed in the same merge this corrects under, which is a
reasonable first exercise of it.

It also records this file's fourth wrong-property instance, and the sharpest,
because it produced a confident FALSE POSITIVE: the first probe measured the
label's width with and without ::before and reported the glyph missing in all
three engines. The label is display:block, so its width comes from the
container and cannot see inline content. Right question, wrong property --
and this time the wrong instrument accused working code rather than clearing
broken code.

Gates on the merged tree: 1379 rust, 26 node --test, 16 playwright (3 skipped,
webkit OPFS), fmt clean, clippy -D warnings clean, all three
check-comment-runs modes, 3473 site links / 0 broken, 0 nested anchors, 189
bead ids all resolving.

improvise-djoo

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 14:16:17 -07:00
Edward Langley d239a16245 Merge remote-tracking branch 'gh/main' into feat/doc-site
# Conflicts:
#	.github/workflows/ci.yml
2026-09-20 14:14:10 -07:00
Edward Langley 575e698203 Merge remote-tracking branch 'gh/main' into fix/web-panel-clicks 2026-09-20 14:12:47 -07:00
Edward Langley 1922beca06 fix(tests): a launch default disarms every test that asserts what it supplies
USER RULING (2026-09-20): the category sidebar is the launch default in the
TUI as well as the browser. `improvise model.improv` opening with the
sidebar showing is intended, not collateral. The behaviour is unchanged;
what changes here is that it is now a stated, tested decision.

Three tests asserted `category_panel_open` was TRUE after doing something
to make it true. The default now supplies that, so all three passed
without the code under them working.

**`set_panel_open_and_cursor` is the serious one.** With it vacuous,
`effect::SetPanelOpen`'s Category arm could be replaced by a no-op — `C`
doing nothing in the terminal — and the whole workspace stayed green at
1391 tests. Now closes the panel first and asserts it closed, so the
effect has to move the flag.

**`panel_commands_address_the_named_panel_only` is the subtle one, and my
first repair of it was ALSO vacuous.** Folding `[open:false, open:true]`
and asserting the final state is true still passes against a reducer that
folds nothing: the default already supplies `true`, so neither command
needs to land. Only asserting the FALSE direction discriminates. Caught by
injection reporting NONE failing after the "fix" — it would otherwise have
shipped looking repaired.

The rule this leaves, now in `context/code/clients.md`: **assert the
direction the default cannot supply.**

`tests/tui_launch_state.rs` gives the ruling a guard on the front end it
newly changes — the TUI's launch frame had no test at all. Not a rendered
frame: §7 says not to test `Widget::render` and the repo has no
`TestBackend`, so the target is `App::new`. One of the three walks
`Panel::ALL` comparing `App::new` against `ViewState::default()`, which is
what would catch a TUI-only launch state — the divergence wsaw closed.

**The view-row click test was vacuous for the property it is named for.**
`Default` is index 0 and holds `view_panel_cursor`, so clicking it made
`index` and the cursor the same number. Injecting the cursor-vs-index
fault into `click_view_row` passed the contract suite 3/3 — the exact
fault the ticket, the evidence and the Rust test exist for was invisible
to the browser half. Now switches to Default by keyboard, then clicks
Plan, and restores the two assertions the relocation had dropped.
Inherited from the deleted `pointer.spec.js`, not introduced here.

Filed rather than fixed: improvise-56ek (`SetPanelOpen` maps a Panel to a
field twice, in two crates — each copy needed repairing independently, and
the harness passing against a no-op'd effect is correct rather than a gap)
and improvise-qf57 (the formula row's dropped `toHaveCount(0)`).
improvise-4ibz is closed by the ruling.

Every claim here proved by injection under `--no-fail-fast`, which is the
method note: `cargo test --workspace` is fail-fast and understates blast
radius.

Gates: 1394 Rust tests, 26 + 3 Node, 16 Playwright (3 WebKit OPFS skips),
clippy -D warnings, fmt, eight wasm crates, comment runs, 516 evidence
entries. `.beads/` untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 14:12:44 -07:00
Edward Langley 05589676f8 fix(docs): the nav toggle is named "Pages", not "trigram for heaven, Pages"
`::before` content participates in accessible-name computation, so the hamburger
glyph was part of the toggle's name and a screen reader would read it out. The
CSS alt-text form `content: "\2630  " / ""` suppresses the glyph from the name
while still painting it.

Taken from the reviewer's optional polish, and verified here rather than
accepted: at 390 the checkbox's accessible name is exactly "Pages" and its
`checked` state flips false -> true on Space, so the collapsed/expanded state is
announced rather than merely the control's existence. The glyph still renders --
computed `::before` content is present and the label lays out at 358px. At 1280
there is still no checkbox in the tree at all.

This is the same principle the previous commit wrote down, one step further:
focusability is necessary and not sufficient, because a control a screen reader
can reach but cannot name, or whose state it cannot read, is still broken. The
instrument that observes that is the accessible name and the checked flag, not
the tab order that was enough for the last fix.

Firefox ignores the alt-text form and falls back to reading the content, which
is exactly today's behaviour, so the change is an improvement in Chromium and
WebKit and a no-op elsewhere.

Gates: 1378 rust, 26 node --test, 16 playwright (3 skipped, webkit OPFS), all
three check-comment-runs modes, 3462 site links / 0 broken, 0 nested anchors,
0 of 23 pages overflow at 320/390/1280, nav operability unchanged at 390/900/
901/1280.

improvise-djoo

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 14:12:20 -07:00
Ed Langley 3bf240d187 Merge pull request #59 from fiddlerwoaroof/fix/web-drill-interim
fix(protocol): the advertised drill key says something true
2026-09-20 14:10:55 -07:00
Edward Langley c0b786cf0d fix(docs): the phone nav toggle needs a keyboard, not a mouse
A regression the previous commit introduced, found in review. The checkbox
driving the collapsed nav carried `hidden`, which is `display:none`, which
removes it from the tab order AND from the accessibility tree. A <label> is
not focusable on its own -- the checkbox it labels is what carries focus -- so
at <=900px the navigation could be clicked and reached no other way, and the
accessibility snapshot contained neither the checkbox nor any of the 25 links.

The toggle is now visually hidden but focusable at narrow widths, and
display:none at wide ones. Keeping display:none on the wide side matters:
merely un-hiding it puts focus on an invisible control whose label is
display:none, which is its own wart.

Measured at 390, 900, 901 and 1280, because the regression lives on one side
of a media query. At 390 and 900 the tab order is brand -> navtoggle, the
toggle is focusable, the a11y tree carries a checkbox node, and Space takes
visible nav links 1 -> 26. At 901 and 1280 the toggle is display:none, not
focusable, absent from the a11y tree, and tab runs straight through the 26
links -- desktop identical to before. 0 of 23 pages scroll horizontally at
320, 390 and 1280, so the absolutely-positioned 1px input introduces nothing.

WHAT IS WORTH MORE THAN THE FIX, and why doc-site.md gains a paragraph rather
than the code gaining a comment: my probe for the previous commit counted
VISIBLE nav links. That answers "does operating the toggle reveal the links" --
correct and complete for a mouse -- and is silent on whether any non-mouse
route exists. It could not have seen this, however sharp the visibility
instrument was. Last round's lesson was that a measurement which cannot
distinguish the two states is not a measurement; this round adds the half it
was missing: a measurement of the wrong PROPERTY is not a measurement either.
checkVisibility fixed the visibility instrument; it does not turn a visibility
count into an operability check. The instrument here is tab order, .focus()
and the accessibility snapshot.

Gates: 1378 rust, 26 node --test, 16 playwright (3 skipped, webkit OPFS), all
three check-comment-runs modes, 3462 site links / 0 broken, 0 nested anchors,
0 of 23 pages overflow at 320/390/1280.

improvise-djoo

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 14:09:37 -07:00
Edward Langley 69bb73a02e Merge remote-tracking branch 'gh/main' into fix/socket-close-routing 2026-09-20 14:07:25 -07:00
Edward Langley 692aa0b1d3 docs(clients): compare test time, not two invocations' wall clocks
The eleven deleted per-widget tests and the three contract tests replacing
them were compared at 3.3s against 3.4s, which reads as a near-tie. It is not
one: those were two separate `npx playwright test` invocations, each paying its
own cold start (~0.6s and ~1.9s), so the wall clocks measured startup as much
as tests and flattered the replacements. The per-test sums are 2.7s against
1.55s.

The conclusion is unchanged and has more margin: eleven tests at 2.7s cannot
buy a twenty-second saving, so the deletion of the eleven bought shape and the
two real-timer watchdog waits bought the speed. It also explains, without
needing anything else, why the contaminated baseline's clock barely moved —
1.55s of contract tests, warm inside a full suite, lands under a second against
a 36s total.

Same discipline as the row above it in the table, applied to the other side of
the comparison: two numbers are only a comparison when they were measured the
same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 14:06:00 -07:00
Ed Langley 2eadf5eebb Merge pull request #63 from fiddlerwoaroof/ci/build-once
ci: build the bundles once, and take the harness lint off the critical path
2026-09-20 14:05:46 -07:00
Edward Langley 481f1da444 docs(clients): measure the baseline on a clean tree, and quote only that
Third correction to one number, and this time by measurement rather than
arithmetic. `1af51c9` reached by a FULL checkout — which removes a tracked
file the target does not have, where `checkout -- <paths>` leaves it — then
built and run twice on a private port, and the same for this branch in the
same session:

  before  23 chromium, 26 passed + 3 skipped, 36.2s / 36.3s
  after   14 chromium, 17 passed + 3 skipped, 15.1s / 14.9s

Three things that changes.

The `before` row was internally inconsistent: it paired a corrected chromium
count (23) with the contaminated run count beside it (29), and 23 + 3 firefox
is 26. A row whose columns do not add up is a row built from two trees. The
note now says the columns must agree, because that identity is what caught
this every time it was caught at all.

`32.8s` is gone. It was not measured by anyone who could hand me its
provenance — I attributed it to the reviewer, who had only ever counted
`1af51c9` statically and never ran a full pre-PR suite — and it does not
reproduce here. Quoting an unattributable number inside the paragraph that
exists to explain an unattributable number was the wrong repair.

And the contamination's cost was the COUNT, not the clock: the contaminated
run read 29 against the clean tree's 26, but 36.9s against 36.2s, because the
three extra tests cost under a second between them. A wall clock absorbs a
contaminated tree without a trace; an arithmetic identity does not. That is
now the stated lesson, and the same error surviving three corrections — into
PR #61's body as "26 chromium", then through a fix that corrected one column
and left its neighbour — is the worked example of `clients.md`'s own rule that
a fact stated in more than one place is changed by a grep, not an edit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 14:05:04 -07:00
Edward Langley 41f694f535 fix(docs): collapse the phone nav, and stop the header comment lying
Round 2 of review. Three notes and the mobile-nav call.

THE NAV, and the correction that prompted it. I had called the 25-link
sidebar wall pre-existing and declined to fix it. That was wrong: the entire
CSS is new in this PR, so the wall ships for the first time here and it is
mine. Every phone page opened on ~1500px of nav before any content.

The mechanism is a checkbox and not <details>, and the reason is measured
rather than assumed. <details> was the obvious control, and it collapses
correctly on a phone -- but the sidebar layout has to force it OPEN, and a
closed <details> hides its content through ::details-content, which `display`
cannot override. Chromium showed the desktop sidebar collapsed to one link
with its toggle hidden: unusable. So the hiding is ours, in a media query,
where overriding it is just cascade.

Measured at both widths: 390px shows 1 link and the toggle, with the h1 at
189px instead of ~1500; 1280px shows all 26 links with no toggle and the h1
at 40px, unchanged; and the phone toggle really expands 1 -> 26 on click.
Note the first probe was wrong -- getBoundingClientRect reports a height for
content-visibility-hidden children, so it read 26 visible in both states and
would have passed the broken version. checkVisibility() is what separates them.

The same argument applies to the table of contents, which is equally new
here: repo-map opened on 1397px of TOC on a phone. Capped at 45vh so it
scrolls; 1397 -> 924, desktop identical at 652.

THE HEADER COMMENT. The generator stopped running standalone when the link
check began resolving ../index.html and ../app/ on disk, and its header still
advertised a default OUT_DIR that now exits 1. That is gotcha 48's third
paragraph exactly -- a fact stated in two places and updated in one, with the
copy that was updated being doc-site.md and the copy someone about to run the
script actually reads being the header. It now says so, keeps the strictness
(the hole it replaced was counting those links as checked while skipping
them), and records the other thing the reorder changed: the DIRECTION of a
failure, from an obviously-broken tree with no app/ to a complete-looking one
with a partial docs/.

And one documented subset: a \| inside a code span in a table cell keeps its
backslash where GFM would unescape it. Splitting is suppressed inside code
spans, so the escape is never needed there and never consumed. No corpus
instance; recorded as a decision rather than left to be rediscovered.

Gates: 1378 rust, 26 node --test, 16 playwright (3 skipped, webkit OPFS), all
three check-comment-runs modes, 3460 site links / 0 broken, 0 nested anchors,
0 of 23 pages overflow at 390 and 1280.

improvise-djoo

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 14:04:19 -07:00
Ed Langley 6b4689443c Merge pull request #45 from fiddlerwoaroof/docs/orchestrator-doc-pass
docs: correct two stale gotchas, and an assertion message that lied
2026-09-20 14:01:43 -07:00
Ed Langley 4aeed54232 Merge pull request #53 from fiddlerwoaroof/docs/wave2-batch-e
docs: follow the active_view split through repo-map and kb.pl
2026-09-20 14:01:39 -07:00
Ed Langley 147c30c42c Merge pull request #58 from fiddlerwoaroof/docs/testing-doctrine
docs: E2E pins the boundaries, unit tests work between them
2026-09-20 14:01:35 -07:00
Edward Langley bf01469cf8 docs(clients): label the test counts, and correct a contaminated baseline
Review of the fix found the speed table quotes one number where there are two:
every spec but `opfs.spec.js` is Chromium-only, so a count read off a run is
three larger than the count of tests in the files, with three more skipped. The
table now has a chromium column and an all-projects column, and says so — the
same "two numbers, not one" the Rust suite learned in design-principles §7.

Chasing that ambiguity turned up a worse error in the number itself. The
`before` figure was measured with `git checkout 1af51c9 -- harness web` inside
the working branch, which restores TRACKED files and leaves untracked ones
where they are — so `contract.spec.js` stayed behind and the "baseline" ran
main's 23 tests plus three of the branch's own. It read 29/36.9s, which looked
entirely plausible. The honest figure, measured on a clean tree, is 23/32.8s.
A baseline taken by checking paths out over your own work measures a tree that
never existed, and that is now written down where the number is.

Also records the probe for the open question the faked clock left. A peer that
opens, stays silent past the watchdog window and is closed afterwards produces
a byte-identical `#status` transcript under `page.clock` and under a real
nine-second wait — four states, same order, same text. So `runFor` does not
flatten that ordering. What stays uncovered is an event landing strictly INSIDE
the collapsed window, which a synchronous `runFor` cannot deliver and which is
a race the page promises nothing about. Stated rather than left to be
re-derived; the probe itself is not kept, being a characterization of
Playwright's clock rather than of this repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 14:01:16 -07:00
Edward Langley 57a25807af Merge gh/main (e8562f0) into fix/web-panel-clicks
PR #61 landed while this branch was open. It deleted
`harness/tests/pointer.spec.js` and `harness/tests/widgets.spec.js`, split
`web/app.js` into app/shell/serial, and added a Node suite under `web/tests/`.
Two conflicts, both resolved deliberately rather than by reflex.

**`harness/tests/pointer.spec.js` — modify/delete, resolved as DELETE.** The
habitual "keep both sides" would resurrect a file #61 removed on purpose.
`contract.spec.js` states its own scope as the RenderModel→DOM boundary and
the pointer's half of the JS→wasm one, so that is where these assertions
belong. Merged in, rather than restored:

- the category sidebar is open at launch and `C` still hides it;
- a first tile click selects and does NOT touch the document, a second cycles;
- the view row and formula row clicks improvise-iioi added, which
  `contract.spec.js` had no entry for at all.

**`context/test-evidence/clients.md` — content, resolved as BOTH SIDES.** The
two `pointer.spec.js` subsections are retargeted at the file that exists, with
a note saying why restoring the deleted one would have been wrong. The two
older `#### … the file's own scope` sections keep their history and gain a
forward pointer, because a reader following them would otherwise hunt for a
file that is gone.

**`web/tests/bundle/wasm_surface.test.js` now names `click_formula` and
`click_view`.** That test is an exact `deepEqual` over the page's `client.*`
calls and its comment says a new call "has to come and say so". It did: it
failed on the merge, which is the mechanism working. It could not have been
satisfied earlier — it did not exist when this branch was cut.

Two review findings fixed on top, neither a live bug:

- **The tile guarantee was overclaimed.** `T` leaves `tile_cat_idx` at 0, so a
  click on the LEFTMOST tile cycles on the first gesture. "A click never
  mutates on its own" and "a second click cycles" are both false against that
  probe. The behaviour is right — a click does what `Enter` does there — so the
  prose now states the implemented rule, and
  `t_then_a_click_on_the_leftmost_tile_cycles_it_immediately` names the case
  and asserts the neighbouring tile answers the same gesture differently.
- **`switch_view_at`'s `Option` had no guard on the keyboard side.** Removing
  the `unwrap_or_default()` makes `Enter` in an empty view panel `Upstream` —
  gotcha 47's black hole — and passed the whole workspace. That would make
  membership of the host-needed list state-dependent, which is the one property
  that list denies. One test closes it.

Gates on the merged tree: 1391 Rust tests, 26 + 3 Node tests, 16 Playwright
(3 WebKit OPFS skips), clippy -D warnings, fmt, harness fmt + wasm32 clippy,
eight wasm crates, comment runs and 513 evidence entries clean. `.beads/`
matches gh/main exactly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 14:00:19 -07:00
Edward Langley 9863d96eef fix(harness): restore the close-routing guard PR #61 dropped, on a faked clock
Adversarial review of PR #61 found one real coverage loss, and it is the
defect class that PR's own body names: an assertion that survives the deletion
of the thing it was about. `shell.spec.js` lost "a socket that opened and then
dropped reads as disconnected, and stays that way" and both MutationObserver
negative assertions. Reverting PR #21's routing half in `web/app.js` — the
socket `error` and `close` arms calling `setShell` instead of `fail` — left the
node suite 26/26 and Playwright 13/13 green.

`shell.test.js` cannot cover it, and why is the whole boundary argument:
"a loud failure DISARMS the watchdog" tests `createShell` in isolation and
cannot see whether `app.js` ROUTES the close event through `fail()`. That
routing is the half gotcha 48 says keeps getting lost; this was the third time,
after two earlier mis-confirmations that simulated the `opened` flag and not
the routing.

Both tests are back under their original names, so the two `####` headings in
context/test-evidence/clients.md name live tests again. They cost ~75ms each
rather than ~9s, because `page.clock` fakes the PAGE's timers: the page, the
socket, the listeners and the `setTimeout` call are all real and only the
passage of time is simulated. That is not a weakening, and the injections are
the proof in both directions — if `runFor` did not advance the page's timers, a
broken routing could not surface the watchdog's message and they would pass:

  - both arms reverted to `setShell`  -> "…and stays that way" fails
  - close arm only                    -> "…and stays that way" fails
  - close arm drops the `opened` flag -> "…the watchdog stays out of it" fails

Three injections, three outcomes, none of them the unit test.

The helper guards its own premise, because it turned out not to: `runFor`
without `clock.install()` does nothing AND throws nothing, so a test that
forgot the install asserted "the watchdog never spoke" against a watchdog that
never ran — 6/6 green. `outlastTheWatchdog` now requires the page's own
`Date.now()` to have moved, and that omission fails by name.

Also: `render_surface.test.js` matched `model.<field>` over source INCLUDING
comments, so un-painting a field and mentioning it in a comment passed —
demonstrated against the pre-fix version, then fixed and re-demonstrated.

And the PR's speed claim is corrected where it is documented. The eleven
deleted per-widget tests cost 3.3s between them against 3.4s for the three
contract tests replacing them: they were free. Essentially all of 36.9s -> 15.5s
was two real-timer waits, one of them this guard. The wait was the cost, not
the test.

Filed, not fixed: improvise-igxp (the help overlay's rows are asserted as
present, never as content — inherited verbatim from the test contract.spec.js
replaced) and improvise-4ktt (the js job's bundle-freedom is a shell-glob
convention, not a gate). The `####`-vs-`###` blind spot in --check-evidence,
which is why the dangling heading survived the gate, is a comment on
improvise-r0qa.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:59:06 -07:00
Edward Langley 1a0f04856e fix(docs): the review's four rendering defects, and the check that could see one
Review of PR #62 found four live defects and four smaller ones. The most
valuable is the first, because of what it says about the rest.

A TOC entry wrapping a heading whose rendered text already contained a link
emitted <a href="#anchor"><a href="github...">...</a></a>. The parser closes
the outer anchor early, so the entry became an empty unclickable stub and
clicking "src/draw.rs" in the TOC navigated to GitHub. 19 occurrences over 10
pages. Both hrefs resolved perfectly, so the link check, the whole-site link
check and a screenshot pass all missed it -- it answers "does every href
resolve" and this is not that question. So the fix is two things: the TOC keeps
a tag-free variant of each heading, AND a new check asserts no <a> appears
inside another <a>. Proved by reverting the first half: 66 nested anchors
reported, assembly exits 1. Clicking that TOC entry now lands on #src-draw-rs.

esc() escaped & < > but not the quote it was interpolated into title="..."
with. Five shipped bead titles contain one, and Chromium parsed the resulting
tag as having three junk attributes. Now escapes " and ' as well. Today's
corpus being friendly is not a reason to interpolate tracker text unescaped.

Seven pages overflowed at 390px by up to 92%. The reviewer attributed it to the
TOC; the TOC was real but not sufficient, and the first fix measured clean by
getBoundingClientRect while the page still overflowed, because the heading text
overflows its box without widening it. Only per-element scrollWidth names the
culprit: hundreds of snake_case test names as headings. overflow-wrap on the
headings and on .toc a, plus dropping display:table from .tickets, which had
been cancelling the mobile overflow-x guard. 0 of 23 pages overflow at 390 and
1280 now, measured.

table() split rows on every pipe before inline parsing, so `|Time Period|` --
this repo's own quoting syntax, in a corpus whose subject is that syntax -- was
shredded into extra columns. A row that still parses, so no column count could
see it. Now walked, respecting code spans and \|. Proved with a fixture: 9 tds
with the fix, 12 with the old splitter reinstated.

The four smaller ones: the link check counted 48 links it never verified behind
a comment promising they were "checked below" when there was no below -- those
are the links leaving the docs tree, and they are now resolved against the
filesystem, which is why assemble.sh runs the generator LAST, once its siblings
exist. The gotcha jump bar's hardcoded 48 is derived from the emitted anchors,
because it failed closed on deletion and silently on addition, and the list only
grows. context/code/doc-site.md cited improvise-xxxx and so broke the audit the
surrounding paragraph describes; the repo-wide audit is now 189 ids, all
resolving. And the Conventions paragraph's own example path now goes through
inline(), so the sentence demonstrates the linking instead of asserting it.

The ticket index keeps all 180 rows, open bugs and titles included, by the
user's ruling: the data is already public in .beads/issues.jsonl, the page only
makes it legible, and filtering it while the lede links the raw JSONL would be
theatre.

Gates: 1378 rust tests, 26 node --test, 16 playwright (3 skipped, webkit OPFS),
all three check-comment-runs modes, 3460 site links / 0 broken, 0 nested
anchors, 189 bead ids all resolving.

improvise-djoo

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:56:56 -07:00
Edward Langley db68b0ee5f Merge main into docs/orchestrator-doc-pass
Two conflicts. .beads/issues.jsonl restored to main's export so the branch
carries no beads diff. context/code/command.md: main rewrote the
Sessions::apply paragraph (now #[cfg(test)], improvise-s17o), which
supersedes this branch's 'verified at this commit' note; main's text is
kept, minus its closing pointer telling the reader to read
design-principles.md §5's 'Sessions::apply' as 'apply_checked', because
this branch corrects §5 itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:55:28 -07:00
Edward Langley 4f4c9642f2 Merge remote-tracking branch 'gh/main' into ci/build-once
# Conflicts:
#	.github/workflows/ci.yml
2026-09-20 13:55:12 -07:00
Edward Langley 6378551641 Merge main into docs/wave2-batch-e
Only .beads/issues.jsonl conflicted; restored to main's export so the
branch carries no beads diff. kb.pl auto-merged cleanly: doctrine/decision/
invariant counts 56/48/51 before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:54:52 -07:00
Edward Langley ab8d71df4f Merge main into docs/testing-doctrine
Only .beads/issues.jsonl conflicted; restored to main's export so the
branch carries no beads diff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:54:03 -07:00
Ed Langley e8562f082f Merge pull request #52 from fiddlerwoaroof/docs/kb-bindings-is-run
kb: bindings.rs is run, and the invariant's name was the false claim
2026-09-20 13:53:02 -07:00
Edward Langley f30d512169 Merge remote-tracking branch 'gh/main' into feat/doc-site 2026-09-20 13:50:42 -07:00
Edward Langley 50e4a37214 fix(web): a click is all of its intents, never mutates, and the panel opens
Four user rulings and three review findings, all in the pointer's one
resolver.

improvise-hkr8 — a click is all of its intents or none of them. Each
`click_*` now returns `Option<Vec<Command>>` and `click` does the single
`unwrap_or_default`. The `None` that matters is `BasisCache::claim`
answering that no basis has been published: the click used to keep its
navigation prefix, drop its activation and report success, while the
keyboard handled the identical `None` correctly as `KeyOutcome::Upstream`.

USER RULING — a tile click SELECTS; a second click on the already-selected
tile CYCLES. Every click in the UI is now non-mutating by default. The
argument that earns the two keystrokes is not tidiness: a cycle is
`LayoutGeometry`, so it bumps `cursor_epoch` and resets EVERY connected
client's cursor (gotcha 24) — the easiest input to trigger by accident was
wired to the one class of change that reaches other people. The divergence
from `Enter`, which still cycles in one keystroke, is chosen and recorded.
"Already selected" is mode AND index, because `tile_cat_idx` is 0 before
anything selects anything and tile 0 would otherwise cycle on first click.

USER RULING — the category panel is open at launch, as the shared
`ViewState::default()` rather than a browser special case. A browser-only
default is "the page overrides the flag", which is the same defect as the
"the page ignores the flag" bug improvise-wsaw fixed. This changes the TUI
too: `src/draw.rs` draws the panel off the same field.

Three review findings, all "a guard that would not catch the drift it is
named for":

- The formula-row `assert!` could never fail — the `assert_eq!` above it
  pins the same vector and aborts first. Replaced by an assertion aimed at
  the keymap itself, which is where the fact lives; the reviewer had shown
  that adding `Enter` to FormulaPanel left 1381 tests green while the click
  and the keyboard disagreed.
- `click_view_row` re-spelled `SwitchView + ChangeMode` inline instead of
  sharing a helper, so a change forced into the keyboard arm by lockstep
  left the click behind silently. Both now call `switch_view_at`.
- `context/test-evidence/clients.md` documented the behaviour PR #59 ships
  as one of the injected faults that disproved it, and had no entry for
  #59's new test. Both fixed; the checker's one-directional blind spot that
  hid it is filed as improvise-lyyc.

Every change is proved by injecting the fault: six injections, six distinct
failure signatures, none passing for another's reason.

Gates: 1389 workspace tests (was 1381), clippy -D warnings, fmt, harness
fmt, all eight wasm crates, 27/27 Chromium harness, 189 cited ticket IDs
resolve, comment runs and evidence headings clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:45:31 -07:00
Ed Langley 584dc3f4cf Merge pull request #64 from fiddlerwoaroof/chore/refresh-beads-export
chore(bd): refresh the issues export, which auto-export no longer maintains
2026-09-20 13:45:12 -07:00
Ed Langley 7a0b4bb636 Merge pull request #61 from fiddlerwoaroof/feat/js-node-tests
test(js): a Node suite between the boundaries, and Playwright pins only the seams
2026-09-20 13:44:52 -07:00
Edward Langley ba120df4cb chore(bd): refresh the issues export, which auto-export no longer maintains
The pre-commit hook that re-exported and staged .beads/issues.jsonl was removed
(adfc765 turned off git-add; the hook itself is gone), which was the right call:
the file used to ride along on every branch that did any bd work and conflicted
with every other branch that did the same — it left the index unpickable during
the PR #50 merge.

The consequence is that the export is now refreshed deliberately rather than
continuously, exactly like context/repo-map.md, and it had drifted: 314 records
committed against 338 in Dolt. Twenty-four beads filed since the hook went are
absent, including every one from today's web-UI and review rounds.

That staleness is load-bearing rather than cosmetic. The documentation site in
PR #62 links a ticket id only when it appears in this export, and renders it as
plain text otherwise — so every missing id is an unlinked citation on the public
site. The site is built to degrade rather than to assume, which is right, but
the degradation should reflect reality and not a stopped clock.

Dolt remains the source of truth; this file is a passive export of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:42:51 -07:00
Edward Langley 3c9668f9e2 ci: build the bundles once, and take the harness lint off the critical path
Measured on run 35535225637 (push to main, caches warm). The critical path
was the browser job at 263s plus an 11s deploy; everything else finished
inside it. Its decomposition: harness lint 37s, Playwright browser download
50s, `./harness/build.sh` 110s, `npx playwright test` 50s.

Three wasm-pack builds ran in that 110s, and two of them — the client and
the worker bundle — were the same two `pages-build` ran in its own 68s step,
in a parallel job with a separate rust-cache key (`add-job-id-key` defaults
on). Same work, twice per run.

- One `build` job now runs `./harness/build.sh` (all three bundles plus the
  `harness/serve` tree) and then `./web/assemble.sh site $GITHUB_SHA` from
  the pkg dirs it just produced. It uploads `harness/serve`, the two raw
  bundles, and the Pages artifact. `pages-build` is folded into it.
- `browser` downloads `harness/serve` whole and needs no Rust toolchain and
  no wasm-pack. Downloading the tree rather than reassembling it keeps one
  answer to "what does the harness serve" (improvise-vvl3): `harness/build.sh`.
  Verified locally that the two assemblies differ in exactly the four
  cache-bust-stamped files and nothing else — the .wasm bytes are identical.
- `harness-lint` is its own job. It gated nothing the harness needs and cost
  37s of the critical path, and `cargo fmt --all` run from inside
  `harness/opfs-contract` formats the WHOLE outer workspace (114 files,
  measured) despite that crate's empty `[workspace]` table, so a rustfmt
  violation anywhere in the repo failed the *Browser harness* job.
  `-p opfs-contract` scopes it to the one file it is about.
- Playwright's browser download is cached on the resolved package version.
- rust-cache in `build` and `harness-lint` covers `harness/opfs-contract`'s
  own target dir, which the default single-workspace config never did.

No gate is weakened. `pages-deploy` still needs every check that could
previously fail it; `harness-lint` is named explicitly because it used to be
a step inside `browser`. `kb`, `wasm` and `test` gain no dependency on
`build` — none of them consumes an artifact.

On improvise-864a: the gap between the Pages artifact upload and the deploy
narrows from ~182s to ~115s on this run's shape, so the window in which a
superseding run can expire it is smaller. The race is not closed, and not
widened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:40:08 -07:00
Edward Langley 04d8c1ff9a fix(docs): a source link names a directory as a tree, and . is not a path
Two ways the path linker could emit a URL that resolves by luck or not at all,
both found by reading the generated links back rather than by the link check,
which only sees the site's own pages.

A code span naming a directory -- `crates/gp-tree/`, `context/test-evidence`,
and about a dozen more -- was linked under /blob/. GitHub redirects that to
/tree/ today, so it worked, but the correctness of ~30 links rested on a
redirect nobody here controls. The kind is now read off the filesystem at
generation time, which is the same source the existence check already uses,
and the trailing slash is dropped.

And a code span of punctuation alone passed the path guard: `.` is matched by
both the character-class test and the "contains a slash or a dot" test, exists
on disk, and is a directory, so it became a link to the repository root from
the middle of a sentence. A path now has to contain at least one alphanumeric.

Verified with a 40-hex token: 195 source links, zero of them `blob/main`, no
blob link ending in a slash, no tree link naming a file, and no link to `.`.

improvise-djoo

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:40:00 -07:00
Edward Langley f1011d7f6f feat(docs): publish the context/ prose as a site folded into Pages
The landing page is the site root and the app is /app/; the prose now sits
at /docs/, assembled by the same web/assemble.sh the Pages job and the
browser harness both run, so what ships and what is tested are one tree.

Published: design-notes, design-principles, repo-map, the three ADRs, the
seven context/code/ arguments, and the 501 test-evidence entries as their
own clearly-labelled section. Not published: context/plan.md, which is an
agent prompt about a launch rather than a description of the program, and
context/cloud-agents.md, which is addressed to nobody who visits a website.
Neither is referenced by a published document outside a code fence, so
leaving them out dangles nothing. The reasoning is context/code/doc-site.md,
which is itself published.

Two things the corpus needed that a generator would not have given it. The
~190 improvise-xxxx citations resolve to a generated ticket index built from
the already-public .beads/issues.jsonl rather than to an invented URL that
would 404; an id the export does not yet carry stays plain text. And a code
span that is wholly a repo path links to GitHub at the assembled ref, but
only when the file exists on disk, so a stale citation degrades to text.

The generator link-checks its own output and exits non-zero on a break, and
assemble.sh runs under set -eu, so a dead cross-reference fails the assembly
rather than shipping. It earned that on its first two runs: it caught a
fence-close regex comparing a three-character marker to a one-character
string, which swallowed every document from its first code block onward, and
a list parser that ended the gotcha list at the first blank line, emitting 1
of the 48 anchors the jump strip links to. Proved by injection afterwards --
renaming the anchors reports 48 missing anchors, pointing the ticket links at
an ungenerated page reports 763 missing pages.

No framework, no node_modules for the site, no build step beyond one
zero-dependency node script, matching the landing page's posture.

improvise-djoo

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:37:20 -07:00
Edward Langley 36ade35240 test(web): a Node suite between the boundaries, Playwright only on them
E2E and integration tests are rare and pin boundary contracts; unit tests
assume those contracts and test between them (user, 2026-09-20). The browser
half has four boundaries — RenderModel→DOM, JS→wasm, page→peer, worker→OPFS —
and everything between them needed no browser at all.

`web/shell.js` and `web/serial.js` are the seams: pure modules that RETURN A
DESCRIPTION rather than take a DOM double. `gridPlan(model)` answers every
question `gridTable` used to answer while creating elements — the data-row
index across group headers, what a group header spans, a null group label
against an empty one, an empty edit buffer against no editor — and `app.js` is
now a loop that builds elements from it. Painting stays untested by Node; what
to paint stopped being untestable.

26 tests in `node --test web/tests` (0.12s, no Rust toolchain, no wasm bundle,
no npm install), plus three in `web/tests/bundle/` that read the generated
.d.ts and so run in the browser job. Playwright goes from 29 tests/36.9s to
16/15.5s on one machine: `widgets.spec.js` and `pointer.spec.js` are gone,
replaced by `contract.spec.js`, which asserts that EVERY field the render model
projects reaches the screen. The four widgets those specs were written for were
one failure at one boundary, and one assertion catches the fifth.

It caught it: `doc_name`/`doc_dirty`/`doc_stored_as` reach no element
(improvise-5jna, already open). They are named in `UNPAINTED` with the ticket,
so the list is the debt written down rather than an exemption.

Thirteen faults injected into the Node suite and eight into the served page,
each watched to fail the test it names; the last of those was strengthened
because the first version passed. `web/assemble.sh` grew an import-completeness
guard whose own first version was blind to the multi-line import it exists for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:33:49 -07:00
Edward Langley d7386b4308 fix(web): the view and formula panels are clickable too (improvise-iioi)
`categoryList` got a click listener and its two siblings did not, so `Click`
had three variants and no ViewRow/FormulaRow. Demonstrated in Chromium:
Shift-V, click a row, nothing changes — mode, row list, active marker and
panel cursor all byte-identical.

This is the one a user hits first, and shipping the cure for one panel and not
the other two REPRODUCES THE ORIGINAL REPORT: someone who has just learned
category rows are clickable clicks a view row and reads the silence as broken,
which is literally what "I can't expand categories" meant (improvise-7ybz).

Both follow the existing rule — go there, then do what Enter does there — and
the two panels answer it differently, which is the rule applying rather than
one of them being half done:

  - A VIEW row focuses, moves the cursor and switches, because ViewPanel binds
    Enter to switch-view-at-cursor. It ends in Normal; the panel stays open.
  - A FORMULA row focuses and moves the cursor and NOTHING ELSE, because
    FormulaPanel binds no Enter at all. Opening the editor on a click would be
    an activation invented for the pointer that the keyboard does not have.

The trap is the same one the category click had: the activation must resolve
against the row CLICKED, never the panel cursor, because the SetPanelCursor
ahead of it is not folded yet. Injected and run — reading the cursor instead
gives SwitchView("Default") beside SetPanelCursor { cursor: 1 }, so the cursor
lands on Plan while the document switches to Default. Silent, and wrong in the
half nobody watches. The fixture holds two views and clicks the second for
exactly that reason.

Browser tests proved against the bundle built immediately before the fix: both
new ones fail there while the six existing pointer tests pass.

Also folds in the harness port fix: reuseExistingServer is now opt-in via
IMPROVISE_HARNESS_REUSE rather than on by default. A default-on reuse adopts a
server another checkout left on the port and tests THAT tree — which fired in
both directions in one session, including a false PASS. A path-hash default was
rejected because it still silently reuses when two agents share a worktree
path; an explicit opt-in cannot. CI never reused, so it is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:26:35 -07:00
Edward Langley 0cb28c8de6 Merge branch 'fix/web-drill-interim' into fix/web-panel-clicks 2026-09-20 13:23:21 -07:00
Edward Langley d59ac99921 Merge remote-tracking branch 'gh/main' into fix/web-drill-interim 2026-09-20 13:20:59 -07:00
Edward Langley f80f0157d2 wip(b2): Click::ViewRow/FormulaRow resolvers and wasm entry points
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:20:55 -07:00
Ed Langley 1af51c99c6 Merge pull request #57 from fiddlerwoaroof/fix/web-review-findings
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Browser bundle + static site (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
fix(web): land the review findings main is missing
2026-09-20 13:20:23 -07:00
Edward Langley 7e9b418845 fix(protocol): the advertised drill key says something true
`>` is advertised in the Normal hint line as ">:drill", and it was a name
`resolve::step` did not handle — so `KeyOutcome::Upstream` -> `ClientFrame::Key`
-> `Host::handle`'s empty vec, a black hole rather than a slow path (gotcha 47).
Pressing the key the app tells you to press did nothing at all, which is why
drill read as broken rather than as refused.

It now answers "drill is not available in the browser yet". That wording is
deliberate and is the whole risk in this change: routing `>` through
`edit_or_drill`'s existing aggregate arm would have produced a TRUE-SOUNDING
sentence about the WRONG THING — drill is refused whatever the cell is, and a
message about aggregates would stop the reader looking. Two refusals, two
subjects.

This is an interim under the current architecture, not a design decision.
improvise-gd28 owns how drill crosses the wire (ADR 001 layer 2, addressed
command form); whether the advertised key says something true is not part of
that design, is strictly better than silence, and is reversible the moment
gd28 lands.

`>` therefore leaves `the_command_names_that_still_need_the_host_are_these`.
That list's own failure message says it is "where it stops being a gap", so
removing an entry is the mechanism working rather than a test being edited to
pass.

Guarded at both layers, each proved by injecting the exact fault warned about
— replacing the new message with the aggregate sentence:
  - `the_drill_key_and_an_aggregate_edit_refuse_for_different_reasons` fails on
    the assertion that `>` must not talk about aggregates;
  - `harness/tests/drill.spec.js` fails in Chromium on the same string, while
    its sibling (the `i` refusal) stays green — so neither passes for the
    other's reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:18:57 -07:00
Edward Langley 648a5fe79c docs: E2E pins the boundaries, unit tests work between them
User doctrine, 2026-09-20: 'E2E and integration tests should be rare and focus
on boundary contracts. Unit tests should assume the boundary contracts tested
by E2E and integration tests and test between the boundaries.'

Recorded with the worked example that makes it actionable rather than
aspirational. Four browser widgets had never worked and all four were ONE
boundary failure — a RenderModel field with no element painting it. Per-widget
specs cost four slow tests and would still miss the fifth; one contract
assertion that every exposed field is painted catches all four and the next.
opfs.spec.js is the shape already in the tree: it runs the storage contract
rather than a feature.

The corollary that is easy to miss: a unit test is cheap precisely BECAUSE it
assumes the contract, which means deleting a contract test silently invalidates
every unit test leaning on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:16:36 -07:00
Edward Langley 3f9a9d78b2 Merge remote-tracking branch 'gh/main' into fix/web-review-findings 2026-09-20 13:15:13 -07:00
Edward Langley f7d2214c7a test(harness): drill is refused in the browser, and one refusal is silent
The fourth widget reported, and the one with a different answer: there is no
wire vocabulary for drill. reduce.rs contains no Drill variant at all, neither
Command nor ModelCommand. That is improvise-gd28, and it is a decision rather
than an oversight — drilling writes drill_state, which gotchas 30 and 1 rule
session-local, and a Command::Model is routed with no SessionId. Adding
ModelCommand::Drill would reintroduce exactly what improvise-3x3u removed.

Everything below the command works, verified in the browser: CellId::Aggregate
is emitted and received, editable answers false for it, and edit_or_drill
reaches the aggregate arm. The TUI is the control and passes 12 drill tests, so
this is isolated to the client/wire.

The finding gd28 did not record is that the two entry points fail DIFFERENTLY.
`i`/`a` declines out loud with a status. `>` — the key the hint line actually
advertises as ">:drill" — is a name resolve::step does not handle, so it goes
KeyOutcome::Upstream -> ClientFrame::Key -> Host::handle's empty vec: a black
hole, not a slow path (gotcha 47). Silent. That asymmetry is why this reads as
broken rather than refused.

These tests document what is true rather than leaving folklore. They MAKE an
aggregate through the tile bar (web/demo.improv has none by construction —
gotcha 46) and assert the precondition, so they cannot pass against a grid of
Records. Both proved by injection: silencing the aggregate arm fails both, the
second on its check_the_checker control; making `>` resolve fails only the
second.

Evidence added to improvise-gd28 rather than filing a duplicate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:14:53 -07:00
Edward Langley adfc76572d chore: do not auto git add issues.jsonl 2026-09-20 13:13:35 -07:00