Commit Graph

1259 Commits

Author SHA1 Message Date
Edward Langley 173d9820a6 feat(gp): decline the artefact families separately, in their own words
improvise-wd6n. Spelling::Artefact was one variant standing for five distinct
families, the last five reached through a `_ =>` wildcard in spell(). The one
refusal they fed said the tree "splits on a col.missing flag or a date part",
so three of the five were declined in words naming something they are not.

Artefact now names which family, spell() is exhaustive (a sixth FeatureRole is
a compile error rather than a sixth thing that message misdescribes), and the
refusal gives one clause per family.

The two families the ticket asks about are declined in writing in
context/code/gp.md §11, separately, because each needs different vocabulary:
col.missing needs a word for absence that reaches apply_agg and eval_agg, and
the date parts need a date type improvise does not have anywhere. Nothing new
is emitted, so improvise-4zl7's route-then-aggregate rule is untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:51:17 -07:00
Edward Langley 7646986841 Merge gh/main (PR #83) into orchestrator main 2026-09-20 21:49:14 -07:00
Ed Langley 8bb1d539d8 Merge pull request #83 from fiddlerwoaroof/wave/docs-evidence-gates
Ten gates that could not fail, and the prose that said they could (wave E)
2026-09-20 21:49:02 -07:00
Edward Langley 97177112a3 perf(io): the grammar generator re-parsed improv.pest per case, not per run
improvise-8shu. improvise-io's lib binary was the run's outlier at 1.42s
against a 0.01s median. The ticket's diagnosis was an unbounded grammar walk
generating deep documents; measuring the two halves separately falsified it.
load_grammar() x1000 was 2.50s and the walk x1000 was 15.7ms — improv_file()
called load_grammar() inside its prop_map, so pest_meta re-parsed the grammar
once per generated case.

The strategy's input space is unchanged. The grammar is read once through a
OnceLock, and a guard counts parses rather than calls so it is independent of
which test in the binary runs first. Case counts go 500 -> 256, the default
design-principles §7 prescribes and never below it.

1.42s -> 0.21s; improvise-io is no longer the run's longest binary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:48:34 -07:00
Edward Langley 38a270ac70 docs: the door is Sessions::apply_checked, and close 26 landed tickets
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-ahdf. Gotchas 36 and 43 named Sessions::apply as the production
door. It is not pub, it delegates to apply_checked after Checked::against,
and its only call sites are tests — Host calls apply_checked. Verified by
reading the signatures rather than the prose: apply at project.rs:878 has no
pub, apply_checked at :887 does. design-principles.md §5 already said
apply_checked, so the repo map was the stale copy of a fact stated twice.

Two of the four remaining mentions were live claims and are corrected; one is
historical ("demonstrated against ... before removal") and stands, and one is
the new sentence saying apply is not pub.

Also closes 26 tickets whose fixes are in merged main, each verified against
the code or against an evidence entry rather than against a PR body. Left
open deliberately: h074 (reported half-done), s17o (a decision in #78's
ground), mcq4 (fixed to 1.9x, residual documented), lltt (P1, detector only).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:44:32 -07:00
Ed Langley b2320ff080 Merge pull request #85 from fiddlerwoaroof/docs/kb-pr82-facts
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
kb: five facts from PR #82, and the negative arm's own anti-vacuity guard
2026-09-20 21:42:15 -07:00
Edward Langley 3542d6f717 fix(tooling): a leaf is not an identity, and a convenience flag was a bypass
Both blockers from the adversarial review of #83, both in the mode this PR
rewrote, both producing a pass while the gate did not do what its prose said.

1. cargo_test_names reduced every listed path to its last segment. Six leaf
names collide in this workspace and two are cited in evidence, so a heading was
satisfiable by a test in another module answering another question -- renaming
ui::project::tests::a_load_that_fails_is_not_a_rebase, the one its entry
describes, left the checker at exit 0 with the ui::host namesake answering for
it. Paths are kept whole; a bare leaf resolves only when unique, and an
ambiguous one is REFUSED with both candidates named. _RUST_NAME accepts `::`, so
a qualified heading is a claim rather than prose, and a citation is matched on
the leaf so qualifying a heading cannot orphan its own test. The two colliding
entries are qualified. Neither half suffices alone: rename one of a colliding
pair and the leaf becomes unique again, which is why qualification is the fix
and the refusal is what forces it.

2. --rust-tests called cargo_test_names with one argument, so the --ignored
subtraction never ran, and it printed a byte-identical success line. It now
requires --rust-tests-ignored (one alone exits 2) and the line names its source.

Also, from the same review: a missing context/test-evidence/ exited 0 while 170
citations pointed at nothing (early return, now gone); the doc-site "runs last"
check accepted a trailing `echo` that merely NAMED the generator, so it asks for
an invocation; bundle_freedom gains an arm for a transitive `import "../app.js"`,
which reaches the bundle while naming nothing forbidden; and two prose overclaims
are corrected -- the prose-heading count is a convenience rather than a guard
(git diff is the mechanism), and the ratio rule says "above the run MEDIAN",
which is defensible where "above the others" is false against second place.

scripts/check-ci-graph.py makes claim 6's rule a gate: pages-deploy must `needs`
every other job in ci.yml. The rule was stated in the last commit and a
zz-new-check: job added during review was still ungated with nothing noticing.
README now states the rule and lists no jobs, the second copy being what went
stale twice.

Every fault re-injected and observed; figures in context/test-evidence/tooling.md
rather than only here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:42:07 -07:00
Edward Langley 565edc5964 kb: the obvious fix bought nothing, and the clean merge kept both mechanisms
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-mcq4's obvious narrowing — fingerprint the active view rather than
every view — was the ticket's first option, the orchestrator's suggestion and
the author's plan, and moved 12.41ms to 12.56ms. Nothing. The per-view
multiplier was never the cost; two expensive builds were, and the bench's
second view was a fresh one costing nothing to build. It would have shipped as
the fix had the author not re-measured after implementing. A fix three people
agree is obvious has had its plausibility checked and not its effect.

And the merge note: when two branches replace one mechanism with two different
ones, the dangerous file is the one that does NOT conflict. app.rs conflicted
loudly and was resolved by hand; effect.rs auto-merged clean and left both
cursor mechanisms live, 1435 tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:38:27 -07:00
Edward Langley 95839fce43 docs: gotcha 49 for the origin check, and the inventory is a dated snapshot
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
Gotcha 49 records improvise-kc7n: a websocket handshake is exempt from the
same-origin policy, so no network control can see cross-site hijacking — an
IP allow-list, a private network and loopback binding all answer "may this
client reach the endpoint" and the honest answer is yes. Loopback is the
least protected case, not the safest. Records why all three arms are decided
rather than defaulted, and that none of it is authentication.

The File Inventory line counts were audited: 26 of 61 resolvable entries had
drifted past 10%, several by a third. Refreshed, and relabelled as a dated
snapshot with the method, because false precision in a carefully-checked
document is worse than an obvious estimate. Two traps found while doing it,
both recorded: a path-walking refresh resolves a bare name like lib.rs
against the first matching crate and rewrites the wrong number, and the
ws-server/ entries use a shorthand prefix no such walk resolves at all.

The code-review skill's gate list now runs BOTH spellings of
check-comment-runs.py. The flag selects rather than adds, so --check-evidence
never runs the comment cap and does not say so; three separate agents hit it
in one campaign (improvise-ijql).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:36:29 -07:00
Edward Langley 29cdab1a4a Merge remote-tracking branch 'gh/main' into worktree-agent-a97453f7fa65d51b9 2026-09-20 21:36:21 -07:00
Edward Langley 384f55c636 Merge gh/main (PRs #81, #84) into orchestrator main 2026-09-20 21:32:52 -07:00
Ed Langley fb405a6301 Merge pull request #84 from fiddlerwoaroof/fix/ws-origin-check
fix: check Origin at the websocket handshake (improvise-kc7n)
2026-09-20 21:32:32 -07:00
Edward Langley ec236da7c5 Merge remote-tracking branch 'gh/main' into wave3/protocol-session 2026-09-20 21:32:02 -07:00
Edward Langley 313e02f522 Merge #79, and fix improvise-mcq4: the frame observation costs one build, not four
Merge of `gh/main` at 23f6def (#79, #80, #82) together with the fix for the
regression this branch measured and filed. One conflict, in
`context/test-evidence/clients.md`, where both sides appended a section: both
kept.

improvise-mcq4. The observation was building every view's pruned layout, before
and after every model effect, and `with_frozen_records` applies `prune_empty`,
which walks cells. Two changes, and the first one is the one that mattered — not
the one that looked obvious:

- **Narrowing to the active view bought nothing.** It was the coordinator's
  suggestion and mine, and measuring it is what showed why: the bench's second
  view is a fresh one with everything on `Axis::None`, so it costs nothing to
  build. The per-view multiplier was never the cost; two *expensive* builds were.
  Kept anyway — `active_view_frame` is the honest shape, and the audience half is
  now the config diff `observe_reshape` already computes for free, which
  over-reports in the safe direction and is read only behind the epoch gate.
- **The opening basis is free.** At batch start `self.layout` already IS the
  active view's pruned layout, so `opening_view_frame` hashes it instead of
  building a second one. `frame_of` is the shared hash; `view_frame` builds then
  calls it. Guarded on `drill_state.is_none()`, because a drill freezes the record
  list and the held layout is then not what `view_frame` would build. Opening use
  only — `self.layout` is stale mid-batch (gotcha 23).

MEASURED, one `CycleAxis` batch, `prune_empty` ON, against the same fixtures:

                     before fix   after fix   baseline
      400 cells         283.2us     167.0us      93.6us
    9,600 cells          7.34ms     3.879ms     1.967ms
   18,000 cells         12.41ms     5.752ms     3.043ms
   90,000 cells         91.69ms     56.54ms     28.52ms

4.1x down to 1.9x at the canonical size, and back inside the 16.7ms frame budget
where it had been outside it. What remains is one pruned build per model effect,
which is inherent to observing a pruned frame; the last one duplicates
`rebuild_layout`'s work and could be elided only by moving the check after it,
which is the position that clobbers a deliberate cursor placement in the same
batch. Not done, and improvise-mcq4 carries the residual.

BOTH CONSTRAINTS RE-RUN AFTER THE CHANGE, because narrowing is exactly what could
quietly stop covering things:
- Subsumption: #76's own `a_page_flip_under_prune_empty_invalidates_the_cursor`
  and its negative twin both still pass with no per-effect reset anywhere.
- Breadth: `AddFormula`/`RemoveFormula` and `SortData` still covered.
- Disabling the narrowed observation fails ten tests, so it still does the work.
- `a_reshape_of_one_view_leaves_a_client_on_another_view_alone` still passes, so
  the cheap audience set has not widened into nynk's negative arm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:31:58 -07:00
Edward Langley 29af67499e Merge gh/main (PR #81) into orchestrator main 2026-09-20 21:31:36 -07:00
Ed Langley 0947fd0c6c Merge pull request #81 from fiddlerwoaroof/wave/core-formula-io
core/formula/io: a duplicate-section round trip, the WHERE word boundary, and a parameter that promised a filter
2026-09-20 21:31:04 -07:00
Edward Langley 664b4a7780 fix: the --allow-origin help was a three-line comment run
CI's `check-comment-runs.py` runs with no flags and the local gate in my brief
runs it with `--check-evidence`, which reports the evidence audit and not the
run cap — so a three-line clap doc comment passed locally and failed in CI.
Run both spellings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:27:25 -07:00
Edward Langley 01d48f2ce9 Merge remote-tracking branch 'gh/main' into fix/ws-origin-check
# Conflicts:
#	context/test-evidence/clients.md
2026-09-20 21:26:39 -07:00
Edward Langley d5995814d1 review: the round-trip test could not see a merge that dropped the section
Adversarial review injected the other fault -- Some(_existing) => {}, which
drops the duplicate section outright -- and the whole improvise-io suite stayed
green at 2000 proptest cases while the fixture's item and its group line
vanished from the document. A test that compares two printed forms cannot tell
unstable-but-lossless from stable-and-lossy, because both sides of the equality
lose the same thing. Three contains assertions now run before the equality, and
they fail under that injection.

Two more from the same review. push_category destructures PCategory, so a fourth
field has to be named rather than silently dropped on the duplicate path only.
And design-principles.md's formula-tokenizer section described a tokenizer that
no longer exists -- a split_where pre-parse step and Token::Ident/Token::Str,
none of which are in the tree -- and claimed bare identifiers allow spaces,
which bare_ident forbids. This branch changes that production, so correcting the
sibling copy is the grep gotcha 48 asks for.

improvise-349m improvise-r08u

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:26:34 -07:00
Edward Langley b30487fccd Merge gh/main (PRs #77, #79, #82) into orchestrator main
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
2026-09-20 21:24:28 -07:00
Ed Langley 23f6defb19 Merge pull request #79 from fiddlerwoaroof/wave/browser-page
The browser page: a title bar, a live pointer, a scrolling selection, and casts with no shell in them
2026-09-20 21:23:36 -07:00
Edward Langley 1351ecc9b8 kb: five facts from PR #82, and the negative arm's own anti-vacuity guard
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
PR #82 disproved the replacement claim this orchestrator had handed its
author, not only the original: of the four prepare steps documented as
"the reason the test is not vacuous", only DeleteView and ShowItem are.
ToggleGroup::apply toggles collapse unconditionally — the membership check
gates only reset_cursor — and TransposeAxes' step restates a precondition
View::on_category_added already establishes. Verified against the source
before recording.

Also records the detector half of the N-1-guarded-edits shape, named by
the PR #79 reviewer: a comment explaining why something is correct is weak
evidence that anything checks it, and the more carefully argued the
comment, the more likely it is standing in for a test. Five instances
across two independent reviews.

The two new keys are dated slugs rather than improvise- ids, because bd
issued none. An ID-shaped non-ID is worse than a dead id: the diagnosis is
identical and the repair is not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:23:06 -07:00
Ed Langley 580e273c4c Merge pull request #82 from fiddlerwoaroof/fix/geometry-fingerprint-guards
test: close two guard holes and correct an evidence claim measurement contradicts
2026-09-20 21:22:24 -07:00
Edward Langley cead3210e5 fix: two Origin headers state none a policy can answer for
Review of the previous commit. `HeaderMap::get` is first-wins, and the
deployment README recommends puts a reverse proxy in front of the socket — one
that APPENDS `Origin` rather than setting it leaves two, and answering from the
first lets the second be anything at all. No browser sends two, so two is not a
state any policy can answer for and the handshake is refused, exactly as an
`Origin` that is not text already was.

Guarded by two_origins_state_none_this_policy_can_answer_for, proved by
restoring the first-wins read, which fails that test alone across the
workspace. The test also asserts the allowed origin BY ITSELF is still allowed,
so the refusal is demonstrably the second header rather than the first.

Second review finding: a_handshake_from_a_foreign_origin_is_refused matched
"403" in a Display string, which cannot tell a refused upgrade from a dropped
connection. It now matches Error::Http and asserts the status. Proved by
changing forbidden() to UNAUTHORIZED, which fails it and
a_refusal_is_a_forbidden_response.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:21:49 -07:00
Ed Langley 08afd185a3 Merge pull request #80 from fiddlerwoaroof/docs/gotcha-corrections
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
docs: correct two stale gotchas, and record the N-1-guarded-edits shape
2026-09-20 21:21:40 -07:00
Edward Langley a808c3d7fd Merge #76: one cursor mechanism, not two, and the cost that exposes
`main` moved to #76 (improvise-748f, improvise-7kvw) while this branch was in
review. #76 approaches cursor invalidation from the per-effect side; this
branch (improvise-ni6k) replaced that table with an observation. The merge is
the interesting part.

TWO CONFLICTS, both resolved keeping both sides:
- `document_content` (#76's new O(cells) hash for the CellData arm) lands
  beside `document_config`, which stays `pub(crate)` for improvise-1xf4's test.
- Three pre-loop bases now: #76's `content_basis` (writes_cells) and
  `config_basis` (moves_geometry, their narrower gate), plus this branch's
  `frame_basis`. Mine stays on `touches_model` and the comment says why: a
  `CellData` write under `prune_empty` reframes.

THE ONE THAT DID NOT CONFLICT IS THE ONE THAT MATTERED. `effect.rs` auto-merged
clean and left #76's new per-effect `SetPageSelection` reset_cursor standing
beside the observation — two live mechanisms, 1435 tests green, nothing failing.
That is the failure mode, not a merge error. The stray is removed; one mechanism
remains, and `reset_cursor` now has exactly one caller in the crate.

#76's OWN TESTS ARE NOW THE GUARD, which is the outcome to want:
`a_page_flip_under_prune_empty_invalidates_the_cursor` and its negative twin
both pass against the observation with the per-effect call deleted, and
disabling the observation fails the positive arm while the negative one still
passes. So the observation subsumes the per-effect fix rather than merely
coexisting with it.

A MEASURED REGRESSION, FILED AS improvise-mcq4, NOT HIDDEN. 748f exists to keep
a no-cell batch off an O(cells) walk. `view_frame` builds through
`with_frozen_records`, which applies `prune_empty`, which walks cells. One
`CycleAxis` batch with pruning on, fingerprint off vs on: 3.0ms -> 12.4ms at
18,000 cells and 28.5ms -> 91.7ms at 90,000 — 3-4x, crossing the 16.7ms budget
where the tree before it sat at 3.0ms.

It was nearly missed twice, both times by a fixture that could not reach the
expensive branch: the SetCell bench shows +2-5% because #76's own walk dominates
that arm, and the geometry bench, written first against the existing fixture,
had `prune_empty` OFF and read 17-176us. `benches/reshape_observation.rs` now
carries the held-key case with pruning ON, and context/code/command.md carries
the argument.

Test function names diffed BY NAME across the merge, whole workspace including
harness/: nothing lost from either side. The seven present at 6978a4e and absent
after are this PR's own renames (j7v6, 8uj8, 1xf4), the two list helpers
improvise-g9jb replaced, and the two no-op guards improvise-ni6k orphaned.

Note: `git stash list` shows an `autostash` holding ci.yml, AGENTS.md and skills
files. It is not this session's — the tree was verifiably clean before the merge
— and it has been left untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:20:50 -07:00
Edward Langley ee19afbdd8 Merge branch 'main' into fix/ws-origin-check 2026-09-20 21:20:21 -07:00
Edward Langley d100639348 fix: check Origin at the handshake, so a page cannot drive someone's session
improvise-kc7n. A websocket handshake is exempt from the same-origin policy —
there is no CORS preflight — so any page a user visited could open a socket to
improvise-ws-server from that user's own allowed address and read and edit every
document served. An address check cannot see it: the request genuinely comes
from an allowed client, and only Origin says what is driving it. Loopback is the
least protected case, having no proxy in front of it at all.

`src/origin.rs` is the decision and `transport::admitted` its one caller, which
refuses the upgrade with a 403 rather than accepting and ignoring. The default
allows pages served from loopback — which is the shipped local recipe, page on
`http://localhost:8080` and socket on another loopback port — and refuses
everything else; `--allow-origin` names the deployed page, which reaches the
server through a TLS proxy and is not loopback. An absent Origin is allowed and
decided rather than defaulted: no browser omits it, so absent is a non-browser
client, and refusing those would break this crate's own tests while buying
nothing that --bind does not already buy.

Five edits, five guards, each reverted alone against the named test:

- origin.rs permits() present arm  -> a_foreign_origin_is_refused
- origin.rs permits() None arm     -> a_handshake_with_no_origin_is_accepted
- origin.rs is_loopback()          -> a_handshake_from_a_loopback_page_is_accepted
- transport.rs handshake callback  -> a_handshake_from_a_foreign_origin_is_refused
- main.rs --allow-origin wiring    -> allow_origin_widens_the_policy_and_nothing_else_does

`transport::serve` takes the policy rather than defaulting one, so a host cannot
forget to pass it. The argument is context/code/clients.md §Origin and the
evidence context/test-evidence/clients.md; README no longer says the server
never looks, and the proxy's Origin allow-list is now defence in depth.

This is not authentication — it answers what page is driving a client, never who
is connecting. improvise-kpnr is untouched and still open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:20:18 -07:00
Edward Langley e9b8b708c5 fix(core): the Aggregation enum stole Model's doc comment
Inserting the enum between Model's doc comment and its derive left the comment
documenting the enum and Model with none -- a four-line run, which is how the
comment-run cap caught it. Note the cap check and --check-evidence are two
different modes of the same script, and running only the second is what let it
through locally.

improvise-5jm5

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:19:56 -07:00
Edward Langley 45229693e5 Merge main into wave/browser-page
Brings in #76, #77 and the repo-map/kb corrections on top of them. No
conflicts; the o31u evidence entry was re-read after the merge rather than
trusted to it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:19:13 -07:00
Edward Langley 158beb3bc9 test: read the view the effect writes, not the document's default
Self-review finding on the assertion the previous commit moved.
`selecting_the_page_already_selected_invalidates_no_cursor` read its page
precondition off `Workbook::active_view()` — the document's *default* view —
while `SetPageSelection` writes through `App::view_mut()`, this client's view.
Gotcha 29 makes those two deliberately free to differ, so an assertion about
what an effect did was reading a view that effect need not have touched.

Latent, not live: the fixture is single-client and the two agree, and the Q2
injection fails identically either way. But the sibling `prune_empty` guard
added alongside it already reads `App::view()`, and one test reading one view
through two accessors is the drift this repo names as the disease.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:17:48 -07:00
Edward Langley 1d4d9107cf fix(tooling): three findings from reviewing this branch against itself
1. A self-test case that was a duplicate wearing different clothes. "a heading
cargo's listing does not carry is an orphan however the source reads" passed on
an empty listing -- which is the first case in the table, already. Once defined
comes from cargo, the Rust beside a fixture is not read at all, so four cases
for the four source shapes would be four copies of one assertion dressed as
coverage. Deleted; the evidence for those shapes is the real-tree injection, and
context/code/tooling.md now says which half a fixture can carry and which it
cannot. This is exactly the shape improvise-84wp's prose, applied two commits
ago, tells a reviewer to look for.

2. assemble-stamp.sh grepped its patterns as REGEXES, inherited from the inline
version it was extracted from. The `.` in improvise_wasm_client.js?v=abc matches
any character, so a stamp corrupted in that position read as present -- a pass in
the one direction a guard must not fail in. grep -F, plus a case proved by
injection: dropping the -F flips it.

3. A missing cargo raised a traceback out of --check-evidence. It now names the
flag that takes a saved listing instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:17:41 -07:00
Edward Langley 381a2f55d3 docs: the two thirteens differ in both directions, and my own awk proved why
Verifying the correction before merging it found the claim true but
understated. The reset_cursor set and the LayoutGeometry set are both
thirteen and differ THREE EACH WAY: CreateView, DeleteView and
SetNumberFormat declare the kind and never reset; AddItem, AddItemInGroup
and RemoveItem reset while declaring CellData, because adding or removing
an item renumbers the grid even though the change is data.

The naive awk over LayoutGeometry reported fourteen, picking up
HelpPageSet — the exact artefact this document already warns about two
paragraphs away, walked into while writing the correction. That warning
is now attached to the derivation it applies to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:17:31 -07:00
Edward Langley a69b0bca86 test: the third edit of improvise-0x9u's repair, and two evidence corrections
`close` restores the DRAWN cell text rather than the seed, and reverting that
one line left the whole suite green while `18400` sat where `18,400` had been
on the shipped demo. Asserted after the second click, proved by that revert.

improvise-o31u's desktop arm cannot fail for the reason its evidence gave:
both named mechanisms were measured and neither moves it (`#cast-pivot` is
966px at 1280, so an 880px floor is inert wherever it is placed). The entry
now says it is forward-looking, and credits the phone arm with the fault it
actually catches.

Plus three one-liners: UNCALLED's source-scan blindness, the gzip -9 proxy in
the improvise-cl7y table, and why `view-back` is not one of its five siblings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:15:49 -07:00
Edward Langley b78299036c test: close two guard holes and correct an evidence claim measurement contradicts
Three findings from adversarial review of PR #76, after it merged. None is a
shipped-behaviour defect; the arguments live in context/test-evidence/command.md.

1. The entry for every_layout_geometry_effect_is_visible_to_the_narrow_fingerprint
   said the `dirty` clear after `prepare` is what stops the preparation satisfying
   the assertion. It is not: `dirty` is raised only inside `App::apply_effects`,
   and no `prepare` in that test runs an effect. Removing the clear and blinding
   `document_config` to 0 together leaves all thirteen cases reading false. The
   clear stays as hygiene for a future `prepare`; what makes a case honest is the
   no-op-fails property — and measurement shows only two of the four `prepare`
   steps are load-bearing in that way. `ToggleGroup` toggles collapse
   unconditionally (membership gates only `reset_cursor`), and `TransposeAxes`'s
   step restates axes `on_category_added` has already assigned.

2. selecting_the_page_already_selected_invalidates_no_cursor was vacuous against
   a non-pruning grid: `paged_model(false)` plus the deleted `moved &&` guard left
   it green, because with pruning off the reset never fires whatever `moved` says.
   Its two sibling arms both carry an anti-vacuity guard; this one had none.
   `assert!(app.view().prune_empty)` closes it.

3. The same test's page-selection assertion ran after the effect, which sets the
   page to Q1, so it could not detect a fixture that started elsewhere. The
   property was guarded — a Q2 fixture failed at the row-labels assertion — but
   not by the sentence naming it. Both preconditions now assert before the effect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:15:31 -07:00
Edward Langley f4d9d14fcb docs: a check adjacent to its question, and a threshold that stops pinning one machine (improvise-84wp, improvise-8ec3)
84wp's prose applied to §7 verbatim as filed. It names the shape three PR #59/#60
findings shared -- a vacuous assertion, a vacuous repair of it, and a clean
auto-merge -- and the remedy each needs: name the question you actually mean,
then find an input that separates it from its neighbour. It generalises
doctrine(fold_tests_must_test_order_not_the_axiom) rather than replacing it.

8ec3's second half. The 2s-per-binary rule kept a figure measured on one machine
("longest single binary 1.42s") as though it were a property of the suite; the
same commit measured 3.70-5.24s elsewhere. §7 now says re-measure, name your
machine if you cite a number, and read the RATIO -- one binary an order of
magnitude above the others -- because that is what reproduces. Measured here
over three runs: improvise-io lib 1.40-1.45s against a ~0.06s median, the same
binary the slow machine named.

The first half (constrain the .improv grammar generator's strategy, never the
case count) is improvise-8shu.

doctrine(suite_speed) and context/repo-map.md carry the same pinned figure and
are orchestrator-only; the replacement text is in the hand-back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:14:22 -07:00
Edward Langley 22245bc5ee docs: the ambiguous-date heuristic is a ruled limit, not an open question
improvise-l8rh carries the user's ruling of 2026-09-20: leave the first-match
heuristic as it is and track it as a known limit. The prose still read as a
filed-and-open question, and it did not record what the ruling rests on -- that
a day-first default only moves which half of the world imports silently wrong,
that the honest fix changes FieldProposal's shape, and that the limit is
tolerable chiefly because improvise-7yx made it deterministic.

No behaviour change; the two pinning tests are unchanged.

improvise-l8rh

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:13:55 -07:00
Edward Langley 3f8f7cb765 refactor(core): evaluate_aggregated takes Aggregation, not a list it cannot read
The parameter was none_cats: &[String] and the body consulted it exactly once,
for emptiness -- a boolean wearing a list's type, since the aggregation runs
over key.0 through matching_values. git log -S back to the commit that
introduced Axis::None shows it never examined an element, so the name promised a
filter the function has never performed.

Aggregation::{Exact, OverHidden} admits only the two states the body
distinguishes, and Aggregation::of reduces the list once at the boundary that
holds it. The regression guard is the signature: two lists with the same
emptiness and different contents is no longer a constructible call.

recompute_formulas and aggregate_raw keep &[String] -- they walk the elements.

improvise-5jm5

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:13:35 -07:00
Edward Langley 4552165a93 fix(ci): pages-deploy needs every check job, and README names them all (improvise-x7cq)
kb was the one job absent from the needs list, on the unstated ground that
kb.pl is not shipped by the site. harness-lint's crate is not shipped either and
was in the list, so the ground proved too much. A failing knowledge-base
consistency check means main is broken, and publishing from a broken main is
what the list exists to stop. The rule that does not go stale is "every check
job", so that is what the comment says.

README named three of the seven, which read as a promise the other four were
ungated -- and it is the shape gotcha 48 is about: a fact stated in two places
where changing one is an edit rather than a grep. It now names the rule and the
list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:13:07 -07:00
Edward Langley 1c3e1bd968 feat(doc-site): the published set is derived, and the generator must run last (improvise-res9)
The manifest only ever grows, so a doc added without a line in it published
nothing and said nothing: build green, link check green, doc simply absent.
The generator now walks context/**/*.md plus docs/design-notes.md, subtracts the
manifest, and fails unless the remainder is exactly the OMITTED map -- checked
in BOTH directions, because an entry naming a file nobody can find asserts a
reason for nothing.

Filed as latent; it was live. context/code/kb-tool.md had been unpublished,
unargued and unnoticed since it was added, while doc-site.md's own table says
context/code/*.md is published. It is published now, with context/code/tooling.md
and context/test-evidence/tooling.md: 23 pages to 26.

Second gate, same file: the generator reads web/assemble.sh and refuses unless
its own invocation is that script's last command. Its outward-link check resolves
../index.html and ../app/ against the filesystem, so a line appended below it
would make that check pass against a half-built tree.

Both run at module scope, before any page is generated, so a manifest error is
answered before the 48 outward-link failures a bare run reports -- a missing doc
is actionable wherever you ran the generator, and the two are unrelated
diagnoses.

Three faults injected and observed; evidence in context/test-evidence/tooling.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:12:46 -07:00
Edward Langley 2afbf37b67 docs: correct two stale gotchas, and record the N-1-guarded-edits shape
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
repo-map gotcha 24 named TransposeAxes and CycleAxis as the only effects
calling reset_cursor, and listed nine others as renumbering the grid without
resetting. improvise-9mo9 closed that the day before: all of them reset,
thirteen in total, SetPageSelection conditionally. The paragraph named as
absent the exact set that was by then present, so it read as a survey of
outstanding work. Re-derived by brace-matching impl Effect blocks, and the
derivation is published beside the list so the next reader can re-run it
rather than trust it.

gotcha 44's group_of sentence said "nearest preceding header ... changing
that means changing AxisEntry and is filed, not smuggled in". improvise-gd83
landed that change in PR #77: AxisEntry::GroupEnd exists and group_of answers
membership. Names the two that are still open beside it, h0iv and 1wl0.

Three doctrines added. The one worth the space is
name_the_test_that_fails_when_only_this_edit_is_reverted: three independent
reviews in one wave found the same shape, a repair of N edits where N-1 are
guarded and the last carries a careful comment instead of a test — and the
unguarded edit is reliably the one the author understood best.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:11:56 -07:00
Edward Langley 0889166fc7 Merge branch 'main' into worktree-agent-a471cf08030a9af4c 2026-09-20 21:11:31 -07:00
Edward Langley 8af37d44a1 feat(ci): two conventions become gates (improvise-w8ih, improvise-4ktt)
The stamp gate is scripts/assemble-stamp.sh, taking FILE PATTERN pairs, and
scripts/assemble-stamp-test.sh is its check_the_checker. Deleting the final
`test -z "$missing"` makes four of its eight cases fail; before the extraction
that same deletion was invisible, both misses printing while assemble.sh exited
0. It is a separate script because exercising it inside assemble.sh would need a
fabricated web/pkg, which is a second answer to "what does the site assemble
from" (improvise-vvl3). Wired into the `build` job, first, because it needs
nothing and guards a step in that job.

web/tests/bundle_freedom.test.js refuses a flat web/tests/ file that names the
bundle directory -- the constraint keeping the `js` job at seconds rather than
~4m30s, stated in prose and checked by nothing. It scans its own filename too,
so it cannot become the place a violation hides, and its companion asserts
web/tests/bundle/ still holds a test that DOES use the bundle: a scoping rule is
uninteresting if nothing falls outside it.

Both faults were injected and watched to fail; evidence in
context/test-evidence/tooling.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:11:08 -07:00
Ed Langley 6978a4e899 Merge pull request #76 from fiddlerwoaroof/wave/command-effect-gaps
command: seven edges — dirtiness, aborts, the cursor and three unguarded mappings
2026-09-20 21:10:22 -07:00
Edward Langley 35adf66757 docs: the duplicate-section merge and the WHERE boundary, with their evidence
context/code/formula-and-io.md records why the category merge happens in pass 1
rather than pass 2, and why the WHERE boundary is sp+ rather than a lookahead --
the grammar-walking generator cannot honour a predicate, which is the part that
generalises.

context/test-evidence records the fault injected for each test, including the
one that made the WHERE test carry two negative shapes: compound-atomicity alone
already refuses the ticket's own reproducer, so a test built only on it passes
over a rule still spelled sp*.

improvise-349m improvise-r08u

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:09:49 -07:00
Edward Langley 7d2e489c62 fix(formula): WHERE takes the same word boundary IS has
where_clause and inline_where were ordinary rules, so pest auto-inserted
WHITESPACE after the WHERE keyword and SUM(Rev WHEREAS x = y) parsed as a WHERE
on a category AS -- a silently wrong filter where it should have been a syntax
error. Both rules are now compound-atomic with sp+, exactly as is_expr.

A !bare_char lookahead with sp* would have preserved one more spelling, a
quoted category abutting the keyword, but the grammar-walking generator cannot
honour a predicate: it emits a bare identifier straight after the keyword and
generated_formula_parses goes red. sp+ is generatable and costs that one
spelling.

improvise-r08u

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:09:44 -07:00
Edward Langley 0ea74c2091 fix(io): two Category sections naming one category merge before they build
format_md -> parse_md -> format_md was not idempotent when a .improv file
carried two `## Category:` sections with the same name: the two outputs
differed in the order of the `>` group lines. A group is created either by the
item that names it or by a `>` line, and build_category creates its own
section's item-groups before its own `>` lines -- so the first section's `>`
groups landed ahead of the second section's item-groups, while format_md writes
one section with all the items before all the `>` lines.

ParsedFile::push_category concatenates in pass 1, which makes the built order
the written order by construction.

improvise-349m

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:09:31 -07:00
Edward Langley 7945221eed fix(kb): two comments claimed things the code does not do (improvise-9d5k)
kb-test case 10 said it was where same_key is discriminated. Reverting same_key
to unification leaves case 10 PASSING and fails the wildcard rm and case 17
instead -- a rule keys on its whole clause, so the `:-` term cannot unify with
probe_fact(a) and conflicts never fires there. Re-demonstrated by injection
before rewriting the comment. The site is guarded; the explanation was wrong.

replace's refusal said it would add the rules "beside the ones already there",
which is false when the predicate has none. The behaviour was always right and
fail-safe; only that clause of the sentence was wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:09:10 -07:00
Edward Langley f3b775dc88 Answer AxisEntry::GroupEnd in view_frame, after merging #77
PR #77 added `AxisEntry::GroupEnd` — the entry that ENDS a group's run, so
membership is not inherited (improvise-gd83). `App::view_frame` matches
`AxisEntry` exhaustively, so the merge compiled nowhere until it was
answered, which is the compiler doing the job an exhaustive match is for.

Hashed as a third discriminant rather than dropped into a `=> {}`. Where a
`GroupEnd` sits is what a row's group attribution depends on, and group
attribution is drawn frame — the thing `view_frame` exists to observe. A
silent arm would have made that class of reshape invisible to the cursor,
which is the failure mode improvise-ni6k replaced a per-effect table to
avoid.

It is not a spurious-reset risk of the kind the `page_selections` component
was: a `GroupEnd` moves when a group's extent changes, and that moves the
`DataItem` sequence in the same breath in every reachable case.

MERGE NOTES, since a clean merge is the dangerous kind here:
- Zero conflicts. #77's ground (`protocol/src/render.rs`, `improvise-core`,
  `ui/viewport.rs`, `src/draw.rs`) and mine are disjoint — my frame
  fingerprint reads `GridLayout` from improvise-core directly and never
  touches `AxisFrame`, so the only collision was the enum arm above.
- Test function names diffed by NAME across the whole workspace, not by
  count. Nothing lost from either side. The five names present at 6c70dba
  and absent after are all deliberate: two renames from improvise-j7v6 and
  improvise-8uj8, one from improvise-1xf4, and the two hand-written list
  helpers improvise-g9jb replaced with `AppMode::all`/`AppMode::text_entry`.

1422 tests, slowest binary 1.48s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-20 21:08:44 -07:00
Edward Langley 61473ef4d3 fix(tooling): evidence headings are diffed against cargo, not scraped (improvise-rm4s, improvise-9za9)
Four shapes let a heading name something that never runs: a test inside a raw
string literal, one behind an unenabled cfg feature, one behind
cfg(target_arch = "wasm32"), and one behind #[ignore]. All four cleared the
scrape; injecting them together made the previous checker print "543 entries,
all name a test that exists", exit 0.

--check-evidence now takes the Rust names from `cargo test --workspace -- --list`
minus its `--ignored` twin. All 539 live headings resolve against a real listing,
so the stricter source costs nothing today. rust_tests is rust_citations: the
scrape survives only for the reverse direction, where a false positive is loud.

Prose headings become a third kind, counted and printed, so retiring a claim by
rewriting it as prose moves a number instead of passing in silence
(improvise-9za9). No gate can read prose for truth; the reviewer is the
mechanism, and the diff is what to read.

context/code/tooling.md is new and is the file four docstrings in
check-comment-runs.py already pointed at. design-principles §7 stopped saying
the check runs in one direction, which improvise-lyyc had already made false.

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