Commit Graph

1151 Commits

Author SHA1 Message Date
Edward Langley 2cf3303437 improvise-wpxr: a cell write naming a view the document lacks resolves nothing
Answers the ChatGPT Codex P2 on ba3d2875. Grids looked the view up through
resolve_view, whose read-side fallback to the default accepted SetCell
{ view: "Nope" } wherever Default drew the record. The view is now looked
up strictly, and a stored record is gated too, so a missing view is
RecordGone at the basis check for every record.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:58:33 -07:00
Edward Langley 7025ec2471 Merge #266 (fix/xd3q) into #279: one notify, and a rebase settles the status (improvise-0dbk)
Both PRs added ClientState::notify, so the merge failed with E0592. #266's
is kept: it queues the sentence through fold_outgoing, which runs
settle_status, so the owner is still recorded. #279's direct-write notify
is deleted.

#266's rebase() rebuilt the view without settle_status, so a relay that
moved the view off the editor left the message owned, and an equal
editor reopened afterwards took it back. rebase() now settles it, pinned
by a_rebase_off_the_editor_ends_the_message_standing (red before).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:57:06 -07:00
Edward Langley 5c69381b69 Walk every mode a browser can reach; an empty status is not a decline
From PR #280's review.

improvise-s2gb: every_silent_binding_is_on_the_list walked Normal and
search mode only, so a black hole bound in FormulaPanel (the reviewer's
Z -> commit-export) passed the whole suite. The modes are now derived:
reachable_modes starts from both halves of Normal (the grid, not a key,
picks RecordsNormal, e5pf) and closes over the modes a pressed binding
lands in. Doors `declined` shuts (the three stranding prompts, the
import wizard, records editing, search now that `/` declines) fall out
with no list to maintain. A floor assertion names seven modes the walk
must reach, so it cannot pass by shrinking. furnished_to_type seeds the
mode's own minibuffer as well as the search query.

`heard` counts a SetStatus as a decline only when it says something: a
resolver answering SetStatus("") left the user seeing nothing and passed
this guard, caught only incidentally by the Ctrl-Q tests.

Merges #275's new head (improvise-4k1j) and fixes the fmt gate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:52:49 -07:00
Edward Langley dffbf99ff7 improvise-y5eb/ik6l: evidence and the clients.md account
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:40:09 -07:00
Edward Langley 7ae871cb1e check-ci-graph: every value it reads is one of a few one-line shapes, or refused (0wn3)
A multi-line flow value (- name: [a, "b ... run: ... c: x"]) is one string to
go-yaml, PyYAML and Psych, but the gate counted the inner run:. Rather than
refuse one more construct, values are now an allowlist: empty, a block header,
a closed quote, a one-line flow with no quotes or nesting, or a plain scalar
not starting with an indicator. The real ci.yml needs nothing else.
Argument: context/code/tooling.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:38:35 -07:00
Edward Langley 51a61f4b17 improvise-kimw: derive a view's grid once per borrow, not once per question
App::grids() memoises the named view's layout for as long as it borrows the
App: once for a whole frame check, once in model_effects. A frame of n writes
to another view's empty records now derives 1 + 2n grids instead of about 5n.
Empty-record ClearCell on 18k cells with a formula, M4 Max: 1.44s -> 0.66s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:36:41 -07:00
Edward Langley bb2f889732 A retry whose cell is Gone keeps the typed text (improvise-joed on #251's decline)
renamed_away() is a Step::Declined, like still_waiting(), so the Enter
binding's welded clear-buffer no longer empties the editor the user must
Esc away from. Shared by the value and rename paths. Red first:
a_retry_whose_cell_is_gone_keeps_the_typed_text failed with Some("").

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:36:24 -07:00
Edward Langley 6609a743a4 Merge branch 'fix/fo0w-black-holes' into fix/oyaa-host-list-complete 2026-09-23 18:35:47 -07:00
Edward Langley 1e49bc0df7 H refuses on every fold the host refuses on; carry the fold flag
improvise-4k1j, from PR #275's review. hidden_row_item refused on a fold
only when the NEAREST group header was collapsed. Under two row
categories that header can be the inner one's: with Region's Coast
collapsed and Product's Kit expanded, the client composed
HideItem{Region, "Coast"} and a twin host wrote a group name into the
document, where the host refuses.

The frame cannot answer this on gh/main: FrameGroup names no level and
an inner GroupEnd reads as an outer one. So FrameEntry::Item now carries
the layout's own DataItem::collapsed flag (a fold, or under one, at any
level) and AxisFrame::folds reads it, the same bit
GridLayout::row_folds_a_group reads. FrameEntry::item builds an
unfolded one, as AxisEntry::item does.

hiding_agrees_with_the_host_on_every_row_of_nested_groups presses H on
all 25 rows of four nested grouped grids, in two windows each. It was
red on the nearest-header check at rows 4 and 5, as the review found.

Also fixes the fmt gate the review found red: fmt-workspaces.sh is a
check, not a formatter, and its diff output was misread as success.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:35:38 -07:00
Edward Langley 12f206fb0a Merge collapse-fu/wm20 (500d4609) into collapse-fu/h6z1
# Conflicts:
#	benches/grid_projection.rs
2026-09-23 18:33:53 -07:00
Edward Langley 12a92a8669 Give run_git's config nulls a witness no -c reaches (improvise-3j70)
GIT_CONFIG_GLOBAL=/dev/null and GIT_CONFIG_NOSYSTEM=1 in run_git were
witnessed by a config excludesFile, which vwjj's -c
core.excludesFile=/dev/null outranks. So dropping either line passed
every test from vwjj onwards.

The hostile child's config, read as both GIT_CONFIG_GLOBAL and
GIT_CONFIG_SYSTEM, now also sets core.sharedRepository = nonsense.
Measured on git 2.55: git init and git add die on it, git ls-files
does not read it (so the walk, which does not null the user's config,
is unaffected), and run_git passes no -c for it. init.defaultObjectFormat
and init.defaultRefFormat were tried first; they only warn.

Dropping either env line is now red. Every other rival in the combined
test is unchanged, and there is still one child per test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:32:41 -07:00
Edward Langley 500d46092b improvise-jq0m: pin both narrowings against the per-member definition; record the bench and the evidence
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:32:33 -07:00
Edward Langley 1572258e03 Merge #274's head (8d9de94e) into fix/joed-waiting-commit-keeps-text
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:31:51 -07:00
Edward Langley 8d9de94e08 A value retry writes only the cell it named; Enter follows the capture (improvise-8phw)
8phw: when A's Enter crossed B's HideItem, the LayoutMoved refusal replayed
the editor over South and settle_opened recaptured South/Cost, so the
retry wrote there. #251's Opened::Retry/Gone now covers values:
written_to() recognises SetCell/ClearCell as well as a rename, and Retry
and Gone carry the whole OpenedTarget, which the recapture must match.
A Gone value editor declines with RECORD_RENAMED_AWAY. seen stays
rename-only.

Codex P2 on #274: commit picked value/rename/create from the cell under
the cursor before it consulted the capture, so a reframe that left no
cell there sent a captured editor's Enter upstream. commit now
dispatches on opened.target(); only an uncaptured editor is dispatched by
the cursor. A records grid's measure cell is not captured as a value,
since the browser still declines it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:29:33 -07:00
Edward Langley b1715f49b4 improvise-i5z6: parse formula.pest once per test process
The two generator strategies re-parsed the grammar inside their
prop_map, once per generated case: the same defect improvise-8shu fixed
in improvise-io. A OnceLock takes the lib test binary from ~0.9s to
~0.06s on the same machine and load.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:23:45 -07:00
Edward Langley 5b8bdc19b1 One child per test, carrying every hostile git setting at once (improvise-eahe)
tests/shipped_documents_load.rs re-ran itself in 18 child processes, one
per hostile setting, and each child spawns several gits. So the binary
took 2.1-3.3s against design-principles 7's 2s-per-binary tripwire.

The six per-setting tests (53zz x2, vwjj, pr3i, a0i3 and 2iws/vav0) are
now one test, a_hostile_git_environment_changes_nothing. It has two
children: the gzip shape test and every_shipped_document_loads. Each
carries every setting at once:
- the template dir;
- the global and system config (with trace2 targets);
- XDG_CONFIG_HOME;
- GIT_DIR and GIT_INDEX_FILE;
- the five pathspec and attribute settings;
- the six trace variables.

Afterwards the test asserts the outer index is still empty.

Each setting breaks the walk on its own, so a rival that drops one
defence leaves that setting live, and the child fails. Every rival that
was red against the per-setting tests is red against the combined one.
The rivals that were green before are green now:
- ICASE;
- the trace2 names under the 0 override;
- the GIT_CONFIG_GLOBAL and NOSYSTEM nulls, whose excludes witness vwjj's
  -c override masks. That last pair is filed as improvise-3j70.

Measured on an Apple M4 Max, three workspace runs each: this binary went
from 2.10-3.31s (load 37-39) to 0.57-0.91s (load 30-33). It is now
below the next-largest binary in every run.

The evidence sections keep their reasoning, under prose headings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:23:40 -07:00
Edward Langley 9dc8a643b2 check-ci-graph: a multi-line quoted value is refused, and the quote tracker is deleted (6zf1)
The tracker closed a multi-line quote on an escaped \" or a doubled '', so a
later quote reopened it over a job's real if:. Both styles exited 0 on the real
ci.yml. The opening line is refused instead; _opens_quote's escape handling
is now load-bearing and has a case per style. Argument: context/code/tooling.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:20:36 -07:00
Edward Langley 57b713cf15 A refusal's sentence queues at the tail, so a rebase cannot erase it (improvise-n0pb)
`fold_frame` wrote the rejection sentence straight into `view.status_msg`,
so it was in neither `settled` nor `in_flight`, and the xd3q rebase erased
it whenever a relay queued behind a frame still in flight: an in-flight
frame's own `SetStatus` was replayed over it.

`ClientState::notify` queues the sentence as a view-only entry at the tail
of `in_flight`, where `fold_outgoing` puts a frame the host never answers.
Frames sent before it replay first, relays queue ahead of it, frames sent
after it may replace it, and it settles when everything ahead is answered.
Writing it into `settled` too was tried as a rival and fails the same way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:20:12 -07:00
Edward Langley cc7e49ac67 Merge #251 (fix/20ek-milr-rename-races, ff8ea239) into fix/8xq0-editor-commits-opened-record
OpenedCoordinate becomes #274's OpenedCell, now carrying #251's seen
(filled for a rename only) inside Opened::Target. Retry/Gone keep #251's
rename-only semantics in this commit; the value-cell retry (improvise-8phw)
follows. #221's superseded "a retry after a refusal is a force" paragraph
is dropped in favour of #251's rule.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:17:20 -07:00
Edward Langley fbfe37802f Pin the GIT_TRACE prefix with all six variables; override config trace2 targets (improvise-2iws review, improvise-vav0)
The review of #276 found that an exact list of the three names the test
set passed it. The child now sets all six variables that write to their
target on git 2.55: GIT_TRACE, _SETUP, _PERFORMANCE, 2, 2_EVENT and
2_PERF. An exact three-name list leaves GIT_TRACE_SETUP behind and
fails.

vav0: a trace2.*Target in user config is not an environment variable,
so removal cannot reach it, and the walk's git does not null the user's
config. untraced_git now also sets GIT_TRACE2, GIT_TRACE2_EVENT and
GIT_TRACE2_PERF to 0, which outranks the config. The child adds a
GIT_CONFIG_GLOBAL whose [trace2] section sends all three targets to
/dev/stdout.

Rivals, each of which fails:
- no strip;
- the exact three names;
- the prefix minus GIT_TRACE, minus _SETUP, or minus _PERFORMANCE;
- no 0 override.

The evidence entry moves from the web_demo_model section to the
shipped_documents_load one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:16:22 -07:00
Edward Langley 689c8c6eb5 improvise-5m9z: an editor opened past the window settles on the host's answer
reopened_at opens provisionally where the landing cell is not projected
yet; ClientState::settle_declined_editor closes an untouched editor once
the host projects that cell as one i declines. Both ask one
resolve::declined_at. A touched editor is kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:11:33 -07:00
Edward Langley d09a27c7e7 EditOrDrill refuses a NonItem cell at open, and the four declines are pinned (improvise-yc9r)
`i`/Enter opened an editor on a NonItem cell and the refusal came only at
commit, under a minibuffer that hides the status. `EditOrDrill` now asks
`cursor_cell_id` after the formula check, as the browser already does.

Three rivals each passed the whole workspace before this: NonItem
writable in `value_cell_key`, a Record in `committable_cell_key`, and
`None` from the browser's open-editor arm. Each now fails its own test.
The commit guard is reached through `:enter-mode editing`, which opens
an editor past `EditOrDrill`.

command.md also records improvise-4daa's argument beside gk51's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:11:26 -07:00
Edward Langley 519c8ddefd Model::is_item: membership without building the item list (improvise-4daa)
`first_non_item` asked `effective_item_names(cat).contains(name)` per
coordinate per projected cell, cloning every item name each time, and the
TUI projects the whole grid every frame. `is_item` asks `contains_key`,
plus the formula targets for a measure category, and is pinned equal to
the list it replaces. #252's write path already goes through
`first_non_item`, so both callers get it.

Measured on an Apple M4 Max, interleaved 5x, minimum per build: pivot
18k cells 193 ms -> 136 ms (base 131 ms); records with a 2000-item label
145 ms -> 2.8 ms (base 1.9 ms).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:11:18 -07:00
Edward Langley 88e53aed73 Hold NEEDS_THE_HOST complete from the keymaps, not the help rows
improvise-oyaa. every_silent_binding_is_on_the_list presses every
binding the Normal and search-mode keymaps reach (parents unshadowed,
prefixes expanded) on the furnished fixture, and requires each silent
one to be on NEEDS_THE_HOST. The two earlier guards could not see an
unlisted binding that no help row names: dropping force-quit (Ctrl-Q)
from the list failed nothing. Now it fails this test.

every_binding_reaches_parents_and_prefixes checks the walk itself
against a hand-built keymap, because no silent binding in today's maps
sits in a parent or behind a prefix, so a walk that skipped either
passed the first test unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:09:54 -07:00
Edward Langley 9eb83538fe A message beside its open editor stands in the browser too (improvise-1x9h)
The toast hid every status after six seconds, so a refusal the user was
still fixing vanished with its prompt open: su4u's bug on a timer. The
browser now asks the TUI's question. EditorSession moves into
improvise-protocol, ClientState records which editor a status was
written in, and RenderModel::status_stands tells the toast to hold it
until that editor closes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:08:26 -07:00
Edward Langley d79a8bd5b0 improvise-wpxr: argue the named view in context/, and record the evidence
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:03:42 -07:00
Edward Langley c704ea1263 A commit that waits for the host keeps the typed text (improvise-joed)
Enter in Editing/RecordsEditing is [commit-cell-edit, clear-buffer edit].
The "still waiting" answer came back from composed() as an ordinary
Local step, so the sequence ran the welded clear after it and emptied
the editor it had left open, on the value path and on #221's rename
path alike.

The two cell commits are now answered by committed(), ahead of
composed(), and still_waiting() is a Step::Declined: the existing
channel by which a step stops a sequence. Under #227 (no weld; the
commit clears at its head) the binding is one command and the waiting
return precedes that clear, so the fix fits both.

Stacked on #274.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:03:39 -07:00
Edward Langley 588094b7bb check-ci-graph: a ' #' on a read line is refused, one string per line, read_graph opens wide (j49r)
A job header with a trailing comment was accepted by the readability check
(comment-stripped line) and never opened (raw line), so its if: went to the
previous job and read_graph never listed it. The audit found the stripper also
cut a quoted "x # y" open, hiding a later job-level if:. Both exited 0 on the
real ci.yml. Refusing the comment makes stripped == raw on every accepted
line, so the stripper is deleted. Argument: context/code/tooling.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:02:08 -07:00
Edward Langley ddcdf93a6b The shipped-document walk strips every GIT_TRACE* variable (improvise-2iws)
GIT_TRACE, GIT_TRACE2_EVENT and GIT_TRACE_PERFORMANCE are process-wide,
so --local-env-vars lists none of them. Pointed at /dev/stdout, git
wrote trace lines into the ls-files -z output, and the walk read them
as paths. git() now builds on untraced_git(), which removes every
variable starting with GIT_TRACE: a prefix, because the family is
open-ended. The rev-parse --local-env-vars call goes through it too,
since its stdout is parsed as well.

the_walk_ignores_a_git_trace_sent_to_stdout reruns the gzip shape test
and every_shipped_document_loads in children with all three set to
/dev/stdout. It failed at #269's head. Rivals, each of which fails it:
- no strip;
- the exact name only;
- keep GIT_TRACE_PERFORMANCE;
- keep GIT_TRACE2_EVENT.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:01:54 -07:00
Edward Langley cdefeeb9b8 Resolve Enter and H in the browser; decline / and n out loud
improvise-fo0w. Three black-hole keys closed over vocabulary that
already exists, each mirroring its Cmd line for line with a lockstep case:

- enter-advance (Normal Enter) resolves as cursor arithmetic over
  GridExtent; the host's bottom-right AbortChain is Ignored here, which
  loses nothing because Enter is never bound inside a Sequence.
- hide-selected-row-item (H) resolves to ModelCommand::HideItem over the
  outer row category and the selected tuple's first label, and refuses
  on a fold with the host's own FOLD_REFUSAL, now in improvise-protocol.
- search (/) and search-navigate (n) decline out loud as a pair: n needs
  the whole model, and / alone would open a box that finds nothing.

NEEDS_THE_HOST drops to exit-search-mode, force-quit and
toggle-group-under-cursor. Tests that used Enter or "Start search" as
their upstream example move to Ctrl-Q and "Exit search mode", including
shell.spec.js's not-sent case.

Also improvise-0629: the help render test now asserts every ex row is
drawn dim, not just that its description is marked.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 18:01:08 -07:00
Edward Langley aab62a362a improvise-enz6: pin the commit door directly, and write up the evidence
The advance no longer reaches the commit door on a fold or a formula's
cell, so the two older advance tests stopped seeing it (measured: the
cell_key rival passes them). Two tests seat the editor there directly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:56:41 -07:00
Edward Langley 7bf8debd12 The keyboard editor commits to the cell it opened on (improvise-8xq0, improvise-xz2z)
resolve.rs::written read its operands at Enter: the record and measure
from the cell under the cursor, and the basis from claim(), i.e. the
version current at commit. So a HideItem from another client that
renumbered the rows under an open editor sent the write to whichever
cell the cursor then named (8xq0), and a write by another client to the
record while the editor was open was silently overwritten instead of
refused RecordMoved (xz2z, 0j0f's rule on the keyboard door).

The fix reuses #221's capture rather than adding a second one.
ClientState::opened becomes OpenedCell { record, target, version,
document_generation }, where OpenedTarget is Value { measure } or
Coordinate { category }. settle_opened captures a Record cell as a
Value target, and written builds the RecordCell and the claim_as_read
basis from it. With no capture while the host has yet to answer this
client's own last write, the commit declines with the rename's "still
waiting" status.

Red first: both round-trip probes failed on gh/main 202e7eac and on
#221's head 39821d5f. Unit-test fixtures that set Editing by assignment
now open the editor through the fold, which is where the capture is
taken.

Stacked on #221 (fix/fdk1-coordinate-rename).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:49:43 -07:00
Edward Langley 0c3ec183d3 Pin the column toggle's cursor-follow; a group lookup stops at its row (improvise-pb30, improvise-0r01)
pb30: gz puts the cursor on the collapsed group through landing and
place_cursor, and their column arms were untested. Three rivals passed
all 1948 tests: the arm swapped, landing returning None for columns, and
landing reading col_tuple(0). A new test uses two column levels
(Region over Month) with the cursor at (1, 7), South/May. It requires
the cursor to land on South's Q2 at (1, 5). Each rival fails it and
nothing else. The col_tuple(0) rival needs the two column levels, since
with one level the prefix is empty everywhere.

0r01 (Codex's P2 on #263): innermost_group built open_runs for the
whole axis, cloning a level stack per row, to answer for one row on
every z. open_runs is now a lazy iterator, still the one reader, and
the lookup takes .nth(idx). a_lookup_does_not_read_past_its_row gives
it an unreadable header after row 0 (level usize::MAX). The eager rival
panics with capacity overflow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:49:05 -07:00
Edward Langley 0947001b70 Each crate's tests compile alone; resolve.rs folds what it sends through the send door (improvise-a61f, improvise-aopa)
a61f: `cargo test -p improvise-wasm-client --lib` failed with E0277,
because `FormulaEntry: From<&str>` exists only under
improvise-protocol's `test-support` feature, which that crate's
dev-dependencies never asked for. `--workspace` hid it: feature
unification pulled the feature in through improvise-command's dev-dep.
A sweep of every member with `cargo test -p <crate> --no-run` found
the same failure in the root `improvise` crate
(tests/bug_hunt_reproductions.rs). Both now request the feature. The
normal wasm tree still carries no `test-support`.

aopa: eight helpers in resolve.rs's tests folded client-composed
commands through `fold`, the host's door, so a composed Command::Model
was never awaited in `in_flight`. All eight use `fold_outgoing`, one
frame per key, as web/app.js does.
`a_folded_browser_commit_waits_for_the_host` pins press_folded; the
rival that folds through `fold` again fails it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:35:27 -07:00
Edward Langley be99391818 Merge collapse-fu/wm20 (f668ddc9) into collapse-fu/h6z1
# Conflicts:
#	context/code/command.md
#	context/test-evidence/core.md
#	crates/improvise-core/src/model/types.rs
2026-09-23 17:34:11 -07:00
Edward Langley 5738e8e0c0 check-ci-graph: a job's keys start at indent 4, or its lines are refused (rjaa)
w0j4 read any line deeper than 4 outside steps:/needs: as content, so a
needed job whose own keys sat at indent 5 or 6 (valid YAML) had its if:
and needs: missed and the gate exited 0. A deeper line is now content
only once its job has a 4-space key. tooling.md names the three valid
styles the gate refuses loudly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:34:06 -07:00
Edward Langley 462471871b Pin Tab's rightmost-column clamp and bound the tile-bar walks
improvise-qnry: a lockstep case compares Tab at the rightmost column with
the host's real CommitAndAdvance, and the tile lockstep case now presses
past the last tile. improvise-ybum: browser_round_trip's tile and column
walks are bounded and fail on a stuck press instead of hanging.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:34:00 -07:00
Edward Langley 9872cc936f Every cell-editor opener awaits its seed while a write is unanswered (improvise-p6v6)
mdfc put the "any unanswered write" check inside `advanced` alone, so an
editor opened with i/a (`edit_or_drill`) still seeded from the cache the
write had made stale: `i 9 Tab Esc i 5 Enter`, typed faster than the
answer, stored 15 in the browser where the TUI stores 195.

The in-flight half now lives in `opened_on`, so no opener can forget it.
`advanced` keeps the "this frame writes" half, since the commit being
resolved is not in flight yet; dropping it fails the 3tt4/3uzg tests.

Stacked on #227 (fix/fy9o-reopened-editor-reseeded, de4a7d78).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:33:33 -07:00
Edward Langley 4cef40c951 Strip pathspec and attribute-source settings from the walk's git (improvise-a0i3)
GIT_LITERAL/GLOB/NOGLOB/ICASE_PATHSPECS and GIT_ATTR_SOURCE are
process-wide, so --local-env-vars names none of them. Remove them in
git() beside the local-vars loop.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:30:59 -07:00
Edward Langley d7dceef549 a grid evaluates through its own evaluator, never another's memo (opbo)
GridLayout::evaluate_in and column_total took any &FoldMemo, so a memo
from a layout showing Feb, handed to one hiding it, gave 5.0 where 2.0
was right. GridLayout::evaluator(model) now returns a GridEval that owns
a memo its layout built, with cell() and column_total(); the memo-taking
methods and the public fold_memo are gone. grid_body and prune_empty use
it, and the earlier rivals still fail when restated for the new API.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:30:41 -07:00
Edward Langley 2c16f7f2ce Guard App::restore's rebuild-after-view_state order
improvise-3til: rebuilding before view_state is restored passed every test.
A rolled-back frame that deleted the host's view now asserts the host is
back on that view with that view's layout shape.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:29:50 -07:00
Edward Langley fd221c6c6c prune_empty shares one fold memo per build (metf)
prune_empty evaluated every cell with a fresh memo, h6z1's shape on the
same keypress path. Measured on an M4 Max: a pruned build of the folded
chain went 705-900 ms -> ~350 ms at 18k and 6.4-6.6 s -> 3.0-5.0 s at
90k; the dense grid is unchanged within noise. A new bench line times
the pruned build. The test counts 9 evaluations where a fresh memo per
cell counts 45, and checks the pruned rows are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:24:34 -07:00
Edward Langley cf5c6fc711 Mark the help rows a browser cannot run, and guard it from one list
improvise-uln5. The browser painted every help-overlay row plainly,
including six whose keys go upstream to a host that answers nothing:
/, n, H, z, Enter, and search mode's Enter/Esc. Every ex-command row was
painted plainly too, although `:` opens a command line the browser
cannot run. A dead key is worse than a gap: the keys typed after it land
on live bindings (improvise-koh6).

- resolve::NEEDS_THE_HOST is now the one list of names this client has
  no arm for. KeyRow keeps the command it was built for.
- The browser's RenderHelpRow draws a listed row, and every ex row while
  the command line is stranded, as `dim` with "(not in the browser yet)".
  It marks rather than omits: both front ends keep drawing the same rows,
  the feature stays findable, and web/ needs no change.
- the_command_names_that_still_need_the_host_are_these now drives that
  list rather than a table of its own.
- every_help_row_the_browser_cannot_run_is_one_it_marks holds it from
  the other side: silent help rows == listed rows, and no hint line
  names one.
- every_advertised_command_answers_in_its_own_mode no longer restates
  the black holes.

Red: a_help_row_the_browser_cannot_run_is_drawn_marked at 202e7eac.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:22:48 -07:00
Edward Langley 1a9d73259c Merge gh/main (202e7eac) into collapse-fu/wm20
# Conflicts:
#	context/test-evidence/command.md
2026-09-23 17:19:55 -07:00
Edward Langley e4b5ce9046 A relay heard while frames are in flight rebases the view on them (improvise-xd3q)
A pruning commit is answered with ResetCursor and an echo of its own
placement. A frame sent before that answer had already been folded
optimistically, and fold applied the relays straight onto view on top of
it. The echo is per frame, so the later placement was never re-sent: the
client sat at (1,0) while its mirror sat at (2,0), and the next write
landed on West where the TUI writes East.

#246 already queues each relay behind the head frame, so the queue holds
the right order. fold now rebuilds view as settled plus everything still
in flight whenever a relay queues, the same replay a refusal uses.
Replaying from settled, rather than re-applying what queued behind the
relay, is pinned by a ViewBack test: re-applying stepped back twice.

Stacked on #246 (fix/hv0r-jpba-relays, 790a33ba).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:19:22 -07:00
Edward Langley adb6b81416 improvise-gk51: the pivot branch too, a shipped-document enumeration, and the argument
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:19:01 -07:00
Edward Langley 076ead7c7c improvise-yb4b/b5dp: pin the scan against the per-member fold; record the ruling, the bench and the evidence
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:18:55 -07:00
Edward Langley 09db8eb0cd The same mode is not the same editor: a message does not follow a commit (improvise-sx7h)
#257 restored a stale message whenever the mode after a key equalled the
mode before it. Editing and RecordsEditing commit with CommitAndAdvance,
which re-opens an equal AppMode on the next cell. So the message followed
the editor from cell to cell until Esc. The reviewer's repro was R, o, 5,
Tab: "Added new record row", or o's "That record row is already here"
refusal, survived the Tab.

press now compares an EditorSession, made of the things an opening sets:
- the whole AppMode, not its variant;
- the cursor cell, which is a cell editor's target;
- the editor's seed, which EnterEditAtCursor re-seeds from the cell, so a
  commit that re-opens on the same cell (Tab in the last column) is new.

A generation counter was rejected: editors are opened by more than one
effect, and a counter in each is a hand-kept list. The view was left out:
no minibuffer key can switch it and stay.

Tests:
- four press scenarios: the two records repros, Enter in a plain grid,
  and Tab in the last column;
- a direct check that ItemAdd on another category is another editor.

Rivals: mode-only (#257 as pushed), no cell, no seed and discriminant
each fail a distinct step.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:18:44 -07:00
Edward Langley ff8ea23909 A rename retry after any refusal may rename only the record it named
improvise-9ww8: a crossing rename by another client bumps layout_version,
so the refusal is LayoutMoved, not RecordGone, and the retry recaptured
whatever record was under the cursor. refused() now pins Opened::Retry(r)
for any refused rename of r; the recapture becomes the target only if it
is r, else Gone(r). RecordMoved on the same record stays a force. The
explicit RecordGone pin is subsumed and removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:13:58 -07:00
Edward Langley ca34cdf811 improvise-29w2: a CLOSED PR the loop waits on (MERGEABLE/BLOCKED) pins the state check at the loop top
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 17:11:23 -07:00