improvise-2oxt. All four casts under docs/casts/ opened and closed on
`(0) Mac:<hostname> ~gf/u/edwlan/improvise` — a real hostname and home directory
on what improvise-kh8 made the site root — and `formulas.cast` and `import.cast`
showed that prompt as their visible OPENING frame. drill.cast also predated the
records columns entirely, showing a single `Value` column where the current
build draws a column per measure plus `+cat` and `+`.
The repair is structural, not careful. `docs/record-casts.py` drives `improvise`
on a pty it owns and writes asciicast v2 itself, so `improvise` is the pty's ONLY
child and there is no shell for a prompt to leak from — nothing to remember to
clean, and nothing that comes back when somebody re-records on another machine.
Three other things stopped being accidents with it: the geometry is an input
(120x37, the numbers docs/index.html already spelled, so they did not change),
the pacing is a per-step table, and the header carries no `env` (which held
$SHELL) and no `timestamp` (the recording machine's clock).
Two decisions that each cost a re-record. No `:q` is typed at the end: the player
loops, so the last frame is also the frame before the first, and a minibuffer
reading `:q` is what it would show — the process is signalled instead. And each
scenario had to be ARRANGED so the thing it names is visible: the drill cast
drilled into a cell with no records behind it, and the pivot cast pressed `c` on
a category already on the column axis. Both recorded cleanly and demonstrated
nothing — true frames about the wrong thing — and neither was visible from the
keystroke list.
That is the method worth keeping: a cast can be replayed through a small ANSI
reconstruction and READ AS TEXT, so "what does frame N show" has an answer rather
than a guess. It is how the `+cat`/`+` columns in the new drill.cast were
confirmed present, and how both dud scenarios were caught.
The guard is `landing.spec.js`, asserted over the cast FILES: no header carries
`env` or `timestamp`, the geometry is the one the page configures, and no chunk
contains an OSC window-title sequence — the leak's signature, emitted by a shell
once per prompt with the cwd in it and by `improvise` never. Asserted over the
file rather than the rendering because a player draws a prompt as ordinary text,
so a screenshot assertion would have to know what the hostname had been. BOTH
clauses were injected separately, because the `env` one would otherwise mask the
OSC one: the old cast fails on `env`, and the old cast with `env` stripped fails
on the title sequence.
docs/index.html loses the `p.dated` note admitting the drill cast was out of
date, which is now false.
Tests: 33 harness + 3 skipped (was 28 + 3). Rust and node unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
improvise-o31u. On a 390px viewport all four players computed a terminal font
size of 4.83258px — reproduced to five digits before the fix. The player DERIVES
its font size from the width it is given, and 120 columns into the ~356px inside
the page's gutters gives about 3px per character. Since improvise-kh8 this page
is the SITE ROOT, so that is the first thing a phone visitor sees.
Under `max-width: 900px` the player is allowed to overflow INSIDE `.player`
(`overflow-x: auto`, an 880px floor on its child) rather than being squeezed to
fit. Measured after: 11.9457px, matching PR #47's reverted measurement.
The instrument is the finding and is worth more than the CSS.
`getBoundingClientRect` reports a height for a `content-visibility`-hidden child
and heading text overflows its box without widening it, so neither "the element
is present" nor a box measurement can see this. The COMPUTED FONT SIZE is what a
reader sees, so that is what is asserted.
Two guards on the trade this fix must not make: the document's own
`scrollWidth - clientWidth` stays 0 — a page that scrolls sideways on a phone is
worse than one unreadable section — and `#cast-pivot` must be the thing that
scrolls, which separates "contained the overflow" from "removed it by shrinking
something else". The desktop arm is not ceremony either: the same query written
without a `max-width` bound, or with the floor on `.player` rather than its
child, changes 1280px too.
Tests: 4 landing tests (was 2). Rust and node unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
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>
Read the revert first, because this commit undoes the headline change of the
one before it and a subject line saying "caption" would have hidden that.
**REVERTED: 3fcb88b's `@media (max-width: 900px)` block.** User ruling: the
mobile defect is to be FILED, not fixed in this PR. It is improvise-o31u, P2,
carrying the measurement, the proposed fix (`overflow-x: auto` on `.player`
plus `fit: false` or an explicit `terminalFontSize`), and the note that a
working fix was written and measured here before being reverted, so the next
person starts from evidence. Measured both ways at 390x844, so the cost of the
revert is on the record rather than implied:
3fcb88b as committed font 11.9457px player width 880
this commit font 4.83258px player width 356
`document.documentElement.scrollWidth` is 390 and equal to `clientWidth` in
both, so the revert does not introduce horizontal page scroll — it restores an
unreadable font, and nothing else.
**F1, the stale drill cast, captioned rather than re-recorded or dropped** —
also a user ruling. `drill.cast` shows the narrow records format with a single
`Value` column, which improvise-2zq and improvise-c5v replaced with a column
per measure plus the `+cat` and `+` columns, so it demos a UI the software no
longer has, now at the site root. The caption is `p.dated`: body size, body
colour, set off with a rule, and ABOVE the player rather than under it —
verified by measuring its computed style against `body` and its box against the
player's, because "captioned" and "captioned in grey 10px under the fold" are
the same word and different things. improvise-2oxt stays open; improvise-e61
stays closed, with its notes now saying plainly that the caption is an honest
label on an UNMET criterion rather than a met one.
**F7: the harness readiness probe** was `/index.html`, which used to be the app
and is now the landing page — so it would answer 200 against a tree with no
bundle assembled at all. Now `/app/index.html`. Proved discriminating rather
than assumed: served a directory containing only the landing page, and the old
probe returns 200 while the new one returns 404.
`docs/index.html` is now 213 lines, past improvise-e61's "under 200". The
criterion is reinterpreted on the ticket rather than the file crammed to fit:
the number was a proxy for single file, vanilla, no build step, no framework,
and all four hold.
Also corrected, in the record rather than the tree: improvise-bck's evidence
cited the deployed root for its page-load and bundle-size measurements, and
this PR moved the app out from under those URLs.
Verified: 6/6 harness shell specs pass with the new probe, four casts still
play 37 lines each at the root, call to action still 200, cargo test 1311,
both comment checkers clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review findings on PR #47.
A 120-column cast shrunk into a 390px viewport computed a **4.83px** terminal
font — measured in Chromium against the assembled tree. That was tolerable when
the landing page sat at `/landing/`; since the user's ruling it is the first
thing a phone visitor sees. Under 900px each player now scrolls sideways at a
legible size instead: **11.95px**, measured the same way, with the page's own
`scrollWidth` still equal to its `clientWidth` — the overflow is contained
inside the player, so there is no horizontal PAGE scroll.
That takes `docs/index.html` to 209 lines, past improvise-e61's "under 200".
The criterion is reinterpreted rather than crammed to fit, and said so on the
ticket: the number was a proxy for single file, vanilla, no build step, no
framework, all of which hold. A limit met by one line and checked by nothing was
never really met.
`context/plan.md` carried two instructions that this PR made actively
misleading:
- §2.4 still told a re-recorder to use 100x30. That is the instruction whoever
picks up improvise-2oxt would follow, and it disagrees with both the existing
casts and the player. It now says 120x37, and says the two must be changed
together.
- §4.2 still said to set Pages to branch `main` / folder `/docs`. That used to
fail loudly — there was no `docs/index.html`, so it was a 404 — and since
improvise-e61 it fails QUIETLY: the landing page renders perfectly and its
call to action 404s, because `app/` exists only in the assembled tree. It now
says GitHub Actions, and says why the other setting is worse than it looks.
Verified: 6/6 harness shell specs still pass against the assembled tree, the
four casts still play 37 lines each at the root, and the call to action still
resolves 200. cargo test 1311, both comment checkers clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
User ruling on improvise-kh8, overturning the arrangement the previous commit
shipped: "The landing page should be served at the root of the repository and
link to the in-browser demo." The argument for the other arrangement is left
standing in the ticket rather than deleted — it is overruled, not wrong to have
considered — and the root is now `docs/index.html`.
The app moves under `app/` WHOLE, both bundle directories included. Every path
in `app.js` and `worker.js` is relative, so moving the bundle together keeps
every one of them correct and the wasm-pack generated glue untouched;
`worker.js`'s `fetch("demo.improv")` resolves against the worker's own URL, so
the seed follows for the same reason. The four `stamped` guards in
`assemble.sh` follow the files: they are the only check that the cache-bust
substitution worked, and a guard aimed at a path nothing writes fails in the
direction that looks like success.
`harness/tests/shell.spec.js` navigates to `/index.html` six times, including
the `?ws=` and `?doc=` variants, and every one of those now lands on the
landing page. All six are repointed at `/app/index.html`. That file is outside
this unit's declared scope and is the second deliberate stray in this PR.
VERIFIED AGAINST THE ASSEMBLED TREE, not against `docs/` directly, because a
path move is what re-arms the trap this page already caught once. With real
bundles from `web/build.sh`:
* `npx playwright test tests/shell.spec.js --project=chromium` — 6 passed,
with `/app/{index.html,app.js,style.css,worker.js,demo.improv}` and both
`?v=` stamped bundles all served 200;
* the four casts play 37 lines each at the site root, no failed request and
no page error;
* the full journey: load the root, click the call to action, land on `/app/`,
and the grid draws out of the in-page worker — 12 cells, all editable,
a "Revenue" header, `#status` reading "in-page worker · demo.improv".
The call-to-action check was proved able to fail by pointing the link at
`./aap/`: 404, while the page still looks entirely correct.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every cast header already carries idle_time_limit 2.0; repeating it in the
player config is a second answer to one question, and the one that drifts is
whichever is not the cast. Found reviewing against design-principles "a
derivation done twice is the defect".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
improvise-e61. Vanilla, single file, 192 lines, no build step: the four casts
improvise-d4w recorded, played by asciinema-player pinned from jsdelivr.
improvise-kh8 recorded a collision to resolve before starting — Pages already
serves the standalone web app at the root, so a landing page either replaces
it, sits beside it, or is dropped. It sits beside it at `/landing/`, because
kh8's own note is right that the live app is the better front door, and a page
the deploy never copies is a file nobody can reach. `web/assemble.sh` copies
it, so the browser harness serves the same tree the deploy publishes and there
is still one answer to "what ships".
The player config deviates from the tickets, deliberately. improvise-d4w's
description and improvise-e61's criteria both say 100x30; every cast header
says 120x37, so the recordings never complied with their own ticket. A 30-row
player clips the bottom seven rows of a 37-row cast, which is exactly where
the tile bar and the status line are, so the page declares the geometry the
casts actually have.
e61 also asked for the casts to be checked against the current UI before
embedding. They were: the tile bar's short_label vocabulary and the mode line
still match, but the casts predate improvise-2zq's `+cat` column by five
months and every one of them opens on the recorder's shell prompt. Filed as
improvise-2oxt rather than silently embedded as current.
Verified by driving the page in Chromium: four players construct, each renders
37 terminal lines after play, no failed request and no page error — against
both `docs/` directly and the assembled `site/landing/`. Counting the players
alone was proved worthless first: with autoPlay false the cast is not fetched
until the click, so a page whose every cast path 404s constructs four players
and throws nothing. Breaking `./casts/` to `./cats/` passes the construction
check and fails the play check with four 404s. improvise-bgtf carries that as
a harness spec, since nothing in CI asserts any of it yet.
README: `cargo install improvise` was false — the crate is not on crates.io —
and the Pages description named two of the three gates a deploy waits on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four recorded casts at 100x30 with 2s idle cap:
- pivot.cast: axis reassignment with tile mode
- drill.cast: drill-down into aggregated cells
- formulas.cast: formula panel and adding formulas
- import.cast: CSV import wizard walkthrough
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create docs/demo.tape scripting a ~15s pivot axis reassignment demo.
Hide shell startup and quit so the GIF opens directly on the TUI.
Set up git-lfs for docs/*.gif. Merge LFS hooks with existing beads hooks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Audited SPEC.md against code — mostly accurate but redundant with
repo-map.md and design-principles.md. Minor drift in storage internals,
wizard step count, and mode representation. Salvaged product vision and
non-goals into docs/design-notes.md with staleness disclaimer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>