Commit Graph

106 Commits

Author SHA1 Message Date
Edward Langley 0ae25b99c5 chore: updating missing dep 2026-09-25 01:08:55 -07:00
Claude (agent-hier) e09ec75226 docs: group hierarchies and fold lines, with the evidence for each rival (improvise-felo.6)
The harness fixtures open their groups explicitly now that groups start closed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-24 21:54:23 -07:00
Edward Langley f39a95709e Merge PR #279 (044e8d2d) into the 2026-09-24 integration wave
# Conflicts:
#	context/test-evidence/clients.md
2026-09-24 02:47:40 -07:00
Ed Langley 088d469fbb Merge branch 'main' into fix/oyaa-host-list-complete 2026-09-24 01:57:22 -07:00
Edward Langley 0b645a8f0e Merge fix/xd3q-reset-clobbers-pipelined-placement (75ce4230) into fix/1x9h-browser-refusal-stands 2026-09-23 23:15:08 -07:00
Ed L af44653f92 chore: setup nixos runners 2026-09-23 20:27:52 -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 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
Ed Langley 0ff17c0f0e Merge pull request #240 from fiddlerwoaroof/fix/ugj4-jibp-mouse-editor
Mouse editor: seed from the target, report the first edit (improvise-ugj4, improvise-jibp)
2026-09-23 10:49:45 -07:00
Edward Langley 463bef149d Merge commit '5932ad749f0df01d4321cc961f57a488ab637dde' into wave/km71-edit-formula
# Conflicts:
#	crates/improvise-protocol/src/lib.rs
2026-09-23 08:51:45 -07:00
Edward Langley 6d5b4e328a improvise-km71: serde back in improvise-formula for FormulaId alone
tt22 removed the formula crate's serde dependency with the legacy JSON loader,
keeping serde only where a type crosses the wire. FormulaId crosses the wire, so
it is the third such type. No derive goes back on the formula AST.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 08:50:53 -07:00
Ed Langley 5932ad749f Merge pull request #231 from fiddlerwoaroof/fix/oplp-tile-cursor
Tile select opens on the first regular category, not _Index (oplp)
2026-09-23 08:48:09 -07:00
Edward Langley fa0af07988 Merge remote-tracking branch 'gh/main' into fix/ugj4-jibp-mouse-editor 2026-09-23 08:45:28 -07:00
Edward Langley 37a64445e0 Merge commit 'c6de77dbde8d05ba287cb69d92c230089098916f' into wave/km71-edit-formula
# Conflicts:
#	context/code/command.md
2026-09-23 08:42:50 -07:00
Ed Langley aa96cd6404 Merge pull request #224 from fiddlerwoaroof/wave/tt22-no-legacy-json
.improv is the only native format: remove the legacy JSON document loader (improvise-tt22)
2026-09-23 08:41:29 -07:00
Edward Langley 5960fe8f0a Merge gh/main (e406edc8) into fix/oplp-tile-cursor
Clean merge, no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 08:35:24 -07:00
Edward Langley e5b18d3499 Merge remote-tracking branch 'gh/main' into wave/km71-edit-formula
# Conflicts:
#	context/test-evidence/command.md
#	crates/improvise-protocol/src/reduce.rs
2026-09-23 08:32:01 -07:00
Edward Langley c4877456a4 WIP red: mouse editor stale seed and touched (improvise-ugj4, improvise-jibp) 2026-09-23 08:30:51 -07:00
Ed Langley 194d8f529c Merge pull request #205 from fiddlerwoaroof/fix/ixxx-untouched-commit
An untouched cell edit writes nothing (improvise-ixxx)
2026-09-23 08:24:48 -07:00
Edward Langley 66359a2e04 Merge remote-tracking branch 'gh/main' into fix/oplp-tile-cursor
# Conflicts:
#	context/test-evidence/clients.md
2026-09-23 08:23:17 -07:00
Edward Langley 38ec60616c Merge remote-tracking branch 'gh/main' into wave/tt22-no-legacy-json
# Conflicts:
#	context/test-evidence/command.md
2026-09-23 08:18:38 -07:00
Ed Langley 0a4f906b1c Merge pull request #218 from fiddlerwoaroof/tooling/7epn-lnsh
tooling: pr-reviews polls past poll 2; stale harness lock names its fix (7epn, lnsh)
2026-09-23 08:14:05 -07:00
Edward Langley c6c2de96b2 Merge remote-tracking branch 'gh/main' into fix/ixxx-untouched-commit
# Conflicts:
#	context/test-evidence/clients.md
#	context/test-evidence/command.md
2026-09-23 08:13:40 -07:00
Edward Langley f85fddbb2f harness/opfs-contract: refresh Cargo.lock for the dropped serde features (improvise-tt22) 2026-09-23 08:13:25 -07:00
Edward Langley 2383c37f2f Merge remote-tracking branch 'gh/main' into fix/oplp-tile-cursor
# Conflicts:
#	context/test-evidence/clients.md
2026-09-23 08:10:09 -07:00
Edward Langley 27cbe2c1dd Tile select opens on the first regular category, not _Index
improvise-oplp: EnterTileSelect and the browser's resolver both opened the
tile cursor at 0, which is _Index, so `T Enter` or `T r` put _Index alone
on Row and emptied a pivot grid; the browser worker then persisted it.

The rule is improvise_protocol::opening_tile, over &[TileEntry], called by
both ends. TileEntry gains `regular`, filled host-side from CategoryKind in
project::tiles_of, because a thin client cannot tell _Index from a user
category without sniffing names. _Index/_Dim stay tileable (#192).

A lockstep case presses `T Enter` on both ends and pins which category
moved; it was red at base (_Index on Row), and fails for a TUI-only revert
and for a `regular` filled true everywhere. contract.spec.js counted three
ArrowRights from tile 0 and now asserts `T` alone selects Product.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 08:08:54 -07:00
Ed Langley 33cf496927 Merge pull request #215 from fiddlerwoaroof/fix/5xbq-1c5m
One page rule for both ends; the help overlay shows how to page (5xbq, 1c5m)
2026-09-23 07:58:59 -07:00
Edward Langley 82bbc74c13 Merge remote-tracking branch 'gh/main' into wave/km71-edit-formula 2026-09-23 07:57:04 -07:00
Edward Langley ebf3dd2ba2 tooling: pr-reviews polls past the second poll; stale harness lock names its fix
improvise-7epn: two cases on the one-tick-per-read clock. One resolves on
poll 3, and one times out after polling up to the deadline. Replacing the
post-poll deadline check with `break` now fails exactly those two
(23 passed, 2 failed), where before it left the suite 23/23.

improvise-lnsh: when `cargo metadata --locked` refuses the opfs-contract
lock, build.sh now says not to remove --locked and names
`cargo update -w --manifest-path harness/opfs-contract/Cargo.toml`.
Checked by hand against a lock with pest pinned to 2.9.1. Evidence is in
context/test-evidence/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 07:49:40 -07:00
Edward Langley a8737f3fa5 One page rule for both ends; the help overlay shows how to page
improvise-5xbq: resolve::page_rows is now the one home of the page-scroll
rule; the TUI's Move calls it instead of repeating visible_rows * 3 / 4.
A lockstep case pages (PgDn/PgUp, Ctrl-d/Ctrl-u) on a non-square window,
8 rows by 3 columns, and pins the TUI's landing rows as well as comparing
host and client, since one home moves both ends together. Both faults
the ticket injected (page_rows = window_rows / 2; paging by window_cols)
now fail at the first PgDn, before and after the move to one home.

improvise-1c5m: the help overlay draws the mode's own hint (hint_line,
the string #hint carries) in a sticky footer, so a narrow window whose
app bar wraps under #help still says how to page. Playwright case at
600px, red at base, and red again with the footer made non-sticky.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 07:41:03 -07:00
Edward Langley a91db1d857 improvise-km71: edit the formula under the cursor, in place
Enter or e in the formula panel opens FormulaEdit on the formula under the
cursor, seeded with its text. The editor names the formula by a FormulaId
captured when it opens, and commits through a new ReplaceFormula effect and
ModelCommand. The edit keeps the formula's position, renames it in place when
the target changes, and refuses out loud if the formula was removed or
rewritten meanwhile, if the new target belongs to another formula, or if the
text does not parse. A refusal leaves the editor open over the user's text.

FormulaCache now carries each formula's id, so the browser composes the same
edit, and a formula-row click opens the editor on the clicked row.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 23:42:33 -07:00
Edward Langley 1550d390cb pr-reviews re-polls after a slow tick; the harness lock fails loudly
improvise-qnpi: `pr-reviews --wait` checked its deadline before the first
re-poll, so a second boundary between two reads of the clock skipped the
only poll (exit 1 with polls=1, reproduced under CPU load). The deadline
is now checked after each poll. A new case drives it with a PATH shim
whose `date` ticks one second per read, so it fails deterministically on
the old loop rather than under load.

improvise-mkbp: harness/opfs-contract/Cargo.lock pinned pest 2.9.1 while
its path deps require 2.9.2, so every build rewrote it. The resolved lock
is committed and build.sh starts with `cargo metadata --locked`; the
wasm-pack `-- --locked` route was tried and does not work (wasm-pack
re-resolves first). opfs.spec.js now names the fix when harness/serve/opfs
is missing, which is what running web/assemble.sh alone leaves.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 23:36:00 -07:00
Edward Langley 7628770d79 Merge remote-tracking branch 'gh/main' into fix/ixxx-untouched-commit
# Conflicts:
#	context/test-evidence/command.md
2026-09-22 22:31:19 -07:00
Edward Langley 4c45166b8a WIP: the mouse editor hands wasm its captured seed (improvise-ixxx)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 22:24:17 -07:00
Edward Langley c960713f8c The worker's status claims success only on its first frame (improvise-i0v9)
The worker transport wrote "in-page worker · <doc>" when it spawned the
worker, before anything had answered, so a tab whose worker could not start
showed a success-looking status over a blank page. It now writes "starting
the worker · <doc>…", and createShell's arm() takes an `answer` that sawFrame
writes on the first frame, but only while the watchdog is still armed. A
ReadOnly or SeedMissing notice lands BEFORE the Snapshot, and fail() has
already disarmed, so the claim cannot overwrite the precise reason.

The two-tab spec now logs every #status and denies both the premature claim
and "did not answer" before the page clock is walked. A new spec pins the
exact status sequence on the writable path. Reasoning:
context/code/clients.md, "The watchdog".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 22:10:29 -07:00
Ed Langley 1c184a5cc2 Merge pull request #176 from fiddlerwoaroof/fix/6qet-collapse-row
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
improvise-6qet: a collapsed group is one aggregate row, and z's cursor follows it
2026-09-22 21:36:41 -07:00
Edward Langley 216b14860d improvise-6qet: the grouped harness spec expects the collapsed group's row
grouped.spec.js asserted a collapsed group contributes no rows (8 data rows).
It now contributes one per Store (10), labelled Upland, and its cells are
not editable. The editable assertion was checked against cell_id without
folds_a_group: it fails with 6 editable cells where 0 are expected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:31:06 -07:00
Edward Langley ef7775efc0 Merge remote-tracking branch 'gh/main' into wire2/j4qt
# Conflicts:
#	context/test-evidence/clients.md
2026-09-22 21:28:26 -07:00
Edward Langley 3039ce8e58 Merge gh/main into fix/df2w-i0v9-browser-page
Conflict only in context/test-evidence/clients.md, both appended at EOF:
kept df2w's two entries and ktlk's section, whole.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:12:41 -07:00
Edward Langley a77e3a9146 A read-only tab refuses edits instead of painting what it cannot save (improvise-j4qt)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:12:24 -07:00
Edward Langley 942211b921 df2w: an open, empty formula panel is drawn
views/formulas were empty-means-closed, so an open panel over a document
with no formulas built the same RenderModel as a closed one. Option<Vec<_>>
carries both facts in one field; the page hides on null.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 20:54:26 -07:00
Edward Langley 334e218c7b s8h7: i on an aggregate names drill, in help and in the browser
In the TUI, EditOrDrill hands an aggregated cell to DrillIntoCell, so `i`
on an aggregate is drill by another door. The help page called it only
"Edit current cell", and the browser refused it in words unrelated to `>`'s,
so a user could not tell the two were one missing feature.

- help.rs: the edit-or-drill row says it drills into an aggregate.
- help.rs: Normal-mode Enter was captioned "Edit current cell (same as i)"
  but is bound to enter-advance alone, which only moves down; the row moves
  to Cursor movement and says so.
- resolve.rs: the Aggregate arm now reads "an aggregate is edited by
  drilling in — " + not_yet("drill"), the same sentence `>` uses.
- context/code/clients.md and the evidence entry reverse the recorded
  "different sentences on purpose" rationale, which was false about the TUI.
- harness/tests/drill.spec.js follows the new wording.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 20:53:18 -07:00
Edward Langley c8d715346f fix(web): a narrow window must not widen the document (improvise-tg27)
The report was "a narrow window clips grid columns without usable horizontal
scroll", and the ticket suspected a flex `min-width: auto` trap swallowing
`#grid`'s overflow. It was not that: `#grid` has `overflow: auto`, which
already zeroes a flex item's automatic minimum. What could not shrink was the
app bar.

Measured at 22ab58f on the grouped harness fixture, `documentElement.scrollWidth`
read 1011 at every window narrower than 1011px — a floor, not a slope, which is
the signature of a sum of `flex: none` reserves. Below it the PAGE grew a
horizontal scrollbar, and what it scrolled to was the bar's own right-hand end
sitting outside the window, while the grid slid off to the left.

`header` takes `flex-wrap: wrap`, chosen over a `@media` breakpoint because a
flex item wraps exactly when the line cannot hold it, so the desktop bar is
untouched by construction rather than by a number someone picked. `main` gets
the `min-width: 0` the ticket asked about, and the three side panels are capped
at 40vw so the grid keeps the larger share of a phone window.

What this deliberately does NOT do: make `#grid` scroll to columns beyond the
announced extent. The browser's Viewport is a real window (gotcha 44/47), so
those columns are never projected and no scrolling can reveal them; the
keyboard already reaches every one, and a spec now asserts that.

`harness/tests/narrow.spec.js` asserts `scrollWidth <= clientWidth`, which is a
question a browser answers exactly. `body { overflow-x: hidden }` was rejected
for making that assertion vacuous.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 15:50:35 -07:00
Edward Langley 6de56e6ba1 Merge branch 'main' into ui/header-stable 2026-09-22 00:17:27 -07:00
Edward Langley d750b433b8 fix: the app bar is reserved, not fitted, and a message is a toast (improvise-n6t8)
The rule is the deliverable: nothing a user expects to be fixed may move.

Four children of <header> were sized by their own content and all four
changed on the SAME event, so the bar reflowed on a mode change with no
message at all: #mode (mode_label, 4-14 chars), #hint (hint_line, one
line per mode), #doc-name (gains and loses " [+]") and #view-name, which
had :empty { display: none } and so appeared out of nothing.

Every child that changes with a keystroke now has an allocated width, and
#hint is the single elastic child, so nothing is positioned by it. The
mode pill's reserve is a FLOOR and deliberately not a cap: a cap absorbs
an over-long label inside a box that never grows, which is a reserve that
hides its own failure. AppMode's exhaustive-match methods make the
compiler demand mode_label for a new variant; nothing checked the CSS
still fit it. harness/tests/header.spec.js is that check.

The spec found what the ticket did not predict: the compounding one was
not the pill. #status was flex: 0 1 auto beside a flex: 1 1 auto hint, so
a longer hint took the shrink out of #status, which WRAPPED to two and
three lines and grew the header by a row -- 52.5px to 71.0px. Reserving
the pill alone would have left that untouched.

ViewState::status_msg is now a toast in a position: fixed dock anchored at
its bottom, so it takes part in no layout and stacks ABOVE the minibuffer:
the prompt is neither covered nor displaced. composeStatus is gone -- the
two facts go to two elements, so a repaint reaches onModel and cannot
address onShell at all. That is gotcha 46's rule made structural rather
than kept by a join's convention; its unit test was replaced, not deleted.

Dismissal keys off message IDENTITY, not a timer. status_msg is STATE and
repaint() re-offers it on every revision, so a setTimeout alone resurrects
the toast on the next keypress. Cost stated rather than hidden: two
identical messages in a row show once, because nothing carries a sequence
and the alternative reading of an ambiguous offer is exactly the bug.

Nine of seventeen modes are reachable in a browser and the spec names them
individually; three injected faults are recorded in the evidence file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 00:17:22 -07:00
Edward Langley 260f6e0b48 test: the page-menu spec reads the demo under the decimal default
`harness/tests/pages.spec.js` arrived on `main` with #123 while this branch was
open, and asserts `toHaveText("20,300")` against `web/demo.improv`. The merge
was clean and the combination was wrong — design-principles §7's third case
exactly: `git merge-tree` answers "can git combine these texts", not "does the
combination do what both sides meant". CI caught it on the merge ref, which is
the only place the two sides had ever been run together.

The assertion is unchanged in what it discriminates: it exists so that a menu
moving only the label fails, and `"20,300.00"` separates Q4 from Q1's
`"18,400.00"` exactly as the old literal did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 23:59:35 -07:00
Edward Langley faa2f28f29 Merge remote-tracking branch 'gh/main' into fix/e0ex-decimal-default 2026-09-21 23:50:06 -07:00
Edward Langley 9156b48f12 fix: the page menu must not claim a modified key (ChatGPT Codex, PR #123)
The menu's early return sat ABOVE `app.js`'s `metaKey` guard, so with the menu
open `Cmd-R` reached `menuKey`, was answered `ignore`, and was then defaulted
away by `if (!e.ctrlKey && !e.altKey) e.preventDefault()` — true for Meta.
Reload was blocked. `Ctrl-j`/`Alt-Home` had the mirror fault and MOVED the menu,
two lines below a comment saying Ctrl and Alt stay the browser's.

Ordering plus one bail: the `metaKey` guard goes back above the menu branch,
`pageMenuKeydown` returns on `ctrlKey || altKey`, and what remains claims
unconditionally — every unmodified key, `ignore` included, which is the rule the
menu exists to keep.

The harness assertion installs its `defaultPrevented` probe AFTER load so it
runs second and sees what the page decided, and ends on an unmodified `j`
asserting `["j", true]` — without which "nothing was prevented" is satisfied by
a page that claims nothing. Proved by restoring the reviewed code: it fails on
`Control+j` moving the active row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 23:36:43 -07:00
Edward Langley d00cea2477 fix: show decimals by default, and let a formula name its own format
improvise-e0ex. `Margin = Profit / Revenue` drew as `1`: `View::new` seeded
`,.0` — zero decimals — so a 0.66 ratio rounded half-away-from-zero. The cause
is the format, not the arithmetic; exact decimal math would not change the
rendering by one character, so nothing here touches the evaluator and gotcha 8
is unchanged.

Two halves, per the user's ruling:

* `View::new` seeds `,.2`. Revenue draws `18,400.00` and Margin `0.66`. The
  noise on whole numbers is the cheaper error — a currency column with two
  redundant zeroes is legible, a ratio with none is wrong — and a format cannot
  be guessed from a value.
* A formula may carry its own format, which wins for its target; a raw measure
  keeps the view's. `formula.pest` gains
  `format_clause = ${ ^"FORMAT" ~ sp+ ~ format_spec }`, with `sp+` for `WHERE`'s
  reason (a predicate is invisible to the grammar-walking generator) and
  `FORMAT` added to `quote::KEYWORDS`.

`improv.pest` is deliberately unchanged: `formula_raw` already carries the
`## Formulas` line verbatim, so the format rides in the formula text with
nothing to add — and the generator, which reads `improv.pest`, sees no new
spelling to violate.

Resolution has one home. `FormatPlan::of(model, view)` replaces the
`(comma, decimals)` pair every render path threaded — `GridLayout::display_text`,
`viewport::grid_body`/`compute_col_widths`, `project::grid_in` and
`persistence/export.rs::render_csv` — rather than being added beside it, which
would be two answers to "what format is this cell". It is asked of the cell's
`CellKey`, so a cell aggregating over `_Measure` keeps the view's format by
construction. `GridBody::col_formats` carries the answer to the totals strip,
which is formatted on its own path.

Persistence is untouched except for metadata: `format_f64` is display,
`persistence/format.rs::format_number` writes round-trip precision, and the two
have no call between them. Newly created views write `format: ,.2`; existing
documents are unaffected, because `format_md` has always written the line
explicitly. `web/demo.improv` moves to `,.2` on both views.

The trap this sets is gotcha 34's: `format_md` writes `Formula::raw`, so a
`FORMAT` suffix round-trips whether or not the walker parsed it. The round-trip
test therefore asserts the parsed field — injecting a walker that drops the
clause fails that one test and nothing else in `improvise-io`, 242 tests
including the whole characterization suite and the grammar properties.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 23:33:52 -07:00
Edward Langley a913390322 feat: the page bar becomes a control, and [/] reach the modes about the document
Three tickets on one subject: paging.

improvise-rw6l — clicking a page chip opens a menu of that axis's items, with
a down-arrow saying so. Almost nothing had to be derived: `PageAxis::items` was
already projected and `ModelCommand::SetPageSelection` was already on the wire.
The one gap was that `RenderPageAxis` dropped the names, keeping the position
and count improvise-p1ym needed for `[`/`]`. `items` is relayed INSIDE `cycle`
rather than beside it, so there is one predicate and not two: a Filter axis has
no cycle, therefore no arrow, no menu, no items, and composes nothing if a click
reaches it — all four off `PageAxis::cycle`, the one home of gotcha 27's split.

improvise-771e — the browser tile bar said "Quarter Pag". `short_label` is three
characters so the TUI aligns in a fixed-width terminal (gotcha 26); a browser has
no column to fit. `render.rs` asks `plain_label` instead; `short_label` is
untouched. Note what pinned the truncation was `harness/tests/drill.spec.js`,
not `tests/grid_frame_agreement.rs`, which does not mention the tile bar.

improvise-wiew — `[`/`]` move from Normal onto a new `document_keymap` between
the global and the modes whose subject IS the document (Normal, the three
panels, TileSelect; RecordsNormal through Normal). Help and the import wizard
take the global directly. Per the user's ruling: an overlay has a subject of its
own, and these are the first shared keys that MUTATE the document — under the
help overlay that is an invisible change to the grid it is covering.

Boundary crossings to flag, both outside the brief's stated write scope and
neither a wire change:
  - `resolve.rs` gains `Click::PageItem` and `lib.rs` a `click_page_item`
    binding. Composing the command anywhere else would put a derivation in the
    page, which design-principles §5 forbids; `Command`/`ModelCommand` are
    untouched.
  - `improvise-command/src/command/keymap.rs` gains three tests. The two
    existing guards were the mechanism that forced the wiew argument, so they
    had to be answered where they live.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 23:24:38 -07:00