Adversarial review of the previous commit: scan() survived 16 literal-shape
attacks, and both HIGH findings were in the code that did NOT get rewritten.
The attribute handling still asked the RAW LINE the questions this commit's
whole argument says a raw line cannot answer.
- Bracket depth was counted over the raw line, so `#[cfg(feature = "[")]`
set attr_depth=1 with no reset path and exempted EVERY REMAINING LINE IN THE
FILE. The raw-string spelling of it is the exact defect class this rewrite
was for: scan() knew those bytes were string content and line 150 asked the
line instead. The tree has 14 `#[arg(...)]` and 3 `#[command(...)]` clap
attributes whose help strings are free text.
- `s.startswith("#[doc")` is an UNANCHORED PREFIX, so `#[doc(hidden)]` counted
as a comment and flagged correct code. Fourth instance of that class in this
campaign, eleven lines above the comment naming the previous three.
Also from the same review: `#![doc = "..."]` is the inner spelling of a doc
comment and was never counted (unlimited free crate doc); an inner attribute
between two `//!` blocks split one crate doc in half, where the rule exists to
separate a crate doc from an ITEM doc — so it now ends a run only where the doc
KIND changes; and a block comment closing on a line with code got one free line.
Attributes are now asked of the scanner: `scan()` returns the code-only text,
and depth is counted over that. `_is_attribute` is deleted — dead since the
logic moved inline, and an orphaned explanation is what --check-evidence exists
to catch one directory over.
The coverage finding was the important one. Two of scan()'s four literal
mechanisms had NO self-test case: reverting char-literal or plain-string
handling left the suite green. Both are load-bearing in opposite directions —
without char literals, `'"'` opens a string that eats the file (false negative);
without plain strings, a multi-line string holding `//` lines is a false
positive, the CI-breaking direction. Both now have cases, and so does every
shape above. The attribute-depth mechanism needed a fixture with code AFTER the
attribute, because comments inside one are counted either way: the first
fixture I wrote passed under its own injection.
Self-test: 17 violations caught, 9 legitimate shapes allowed. Every mechanism
proved discriminating by reverting it alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adversarial review demonstrated that the previous fix closed one class and
left three, then reintroduced the bug this file's own history is about.
- A ``` fence in a PLAIN `//` comment earned the doctest exemption. rustdoc
runs no test there ("running 0 tests"), so two lines exempted anything after
them. The cheapest bypass yet, and `--list-doctests` in the same file already
had it right: one of the two was wrong and it was this one.
- A block comment opened after code on the same line was invisible, since only
a line STARTING with /* was seen. That is the most natural way to write one.
- Nested block comments closed at the first */ and the rest read as code.
- And the false positive: `in_block` had no reset path, so a /* inside a raw
string swallowed the rest of the file. Latent — no .rs file starts a line
with /* today, so the block fix had zero effect on this repo — but 8 files
hold raw strings and this repo's habit is Rust fixtures inside them. The
first such fixture breaks CI with a diagnosis pointing at a string.
A line-based scanner cannot decide any of these, so `scan()` now walks
characters, tracking nested block comments, strings, raw strings (with hashes)
and char literals. `#![...]` ends a run where `#[...]` stays transparent,
because a crate doc and an item doc separated by an inner attribute are two
blocks a reader does not meet as one. `#[doc = "..."]` counts: it is the one
attribute that is a comment.
The self-test carries all of it: 12 bypasses, 6 shapes that must stay legal.
Each new fix was proved discriminating by injecting it alone.
One of those injections caught a VACUOUS test of my own: with raw-string
handling disabled, a plain-string fallback reached the same verdict, so the
fixture proved nothing. It now contains r#"x"y, which a non-raw parse closes
early — so the fault is visible. That is the defect this repo is named for,
found in the test written to prevent it.
Closes improvise-mbmf, which PR #45 claimed to close and did not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adversarial review of PR #44 constructed all three, each compiling clean with
no warnings, each passing the checker.
Block comments were invisible: the scanner only ever tested startswith("//"),
so a six-line /* */ passed — and /** */ is a DOC comment that rustdoc renders
exactly as /// does, making `/**` the cheapest way to write the long argument
the rule exists to forbid. There are zero block comments in the tree today,
which is the only reason this was unexploited; #44 is the change that gives
anyone a reason to look.
Fence state was per-FILE, so one unclosed ``` exempted every later comment in
the file. A dangling doctest would fail the build in a lib target, but doctests
do not run for bins, tests/, examples/ or harness/ — all default scan roots.
Fence state now belongs to the comment block: any non-comment line clears it.
Attributes terminated a run, so `#[inline]` between two pairs of doc lines
split one argument of four into two legal runs of two. The module docstring
had always promised attributes are "ignored"; the code disagreed with it, and
disagreed in the direction that passes.
Closing the last of those found two real violations the old checker could not
see. crates/improvise-command/src/command/cmd/text_buffer.rs carried TWO doc
comments straddling #[derive(Debug)] — both attach to the struct, so rustdoc
rendered both; a leftover from the comment wave. transport.rs had a three-line
run split by #[allow].
A checker with ten silent successes and no test of its own is the thing it
warns about, so --self-test runs every bypass this file has been fixed for
plus three shapes that must stay legal. Proved discriminating by reverting the
attribute fix: it fails and names that case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by two wave agents inside an hour, and all three are the same class as
the bd-audit exclusion list this repo already has a doctrine about: a filter
that over-matches reports success.
1. A FILE path walked nothing and printed 'none longer than 2 lines'. roots was
filtered by os.path.isdir, so 'check-comment-runs.py src/foo.rs' silently
scanned an empty set. One agent nearly shipped a file believing it done, and
every 'my unit reports none' claim taken with a file argument was worthless.
2. A NONEXISTENT path did the same thing, so a typo'd directory read as a pass.
Both now exit 2 and name the path. Defaults are unaffected.
3. --check-evidence accepted ANY fn, so a '### heading' naming a fixture or a
helper was reported as satisfied. One unit's '68 entries, all name a test'
was passing on that accident; re-checked strictly it was 64. A heading must
now name a fn carrying #[test] (or one inside a proptest! block), which is
what the convention always claimed.
Each fix fault-injected before keeping: a file path and a missing path now
error, and a heading naming a non-test fn is reported as an orphan while a real
test passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Grooming after wave 1. The tracker had 62 open beads, one label between them
and 36 dependency edges that nearly all pointed at closed work — so 'bd ready'
was empty, 'bd blocked' saw one issue, and the next wave's partition existed
only in the orchestrator's head.
AREA LABELS on every open bead (area:protocol, :command, :projection,
:shared-frontend, :browser, :active-view, :tui, :core-io, :gp, :release,
:epic, :docs), so a wave partitions with 'bd list --label' instead of from
memory. needs:user marks the three nobody else can move.
ORDERING that was real but unrecorded:
k69y <- jytm the draw_status fix is a no-op until the split lands
bck <- fdt6 'add a formula' is the milestone's one unmet criterion
1ey <- plcx criterion (5) needs a document big enough to scroll
jq8l <- plcx same reason
rw3a <- 2ud needs CmdContext::file_path, and should not add a 21st
copied field ahead of that refactor
gd28 <- 7b8v the addressed drill form is ADR-001 layer 2
Ready 48, blocked 16, no cycles.
Note for the next person: 'bd dep add' REFUSES to overwrite an existing
relates-to edge with a blocks edge — it errors rather than converting. Several
of these had been filed as relations when the relationship was directional, so
they were silently blocking nothing. 'bd dep unrelate' then 'bd dep add'.
validation.on-create = warn, because this session filed 19 beads with no
acceptance criteria and only found out from 'bd lint' afterwards. --validate
refuses outright and names the missing sections, and it works on --dry-run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The user's instruction (2026-09-20): reasoning moves out of code comments into
context/ prose, and a comment run is at most two consecutive lines — `///` and
`//!` included, and test doc-comments too.
scripts/check-comment-runs.py makes the rule checkable rather than remembered,
which is the difference between a convention and a guard. It reports 1321 runs
over the limit today; the move itself is improvise-9c8c.
A ``` fence in a doc comment is exempt: a doctest is a running test rather than
prose, and capping around one would delete it. `--list-doctests` finds the six
blocks that need individual decisions.
design-principles §7 said to record a bug in the test's doc-comment, which the
cap contradicts — that evidence now goes to context/test-evidence/ keyed by test
name, with the orphaning cost stated rather than discovered.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
improvise-anr9 and improvise-1125 closed by PR #23; improvise-s1xw closed as
absorbed, verified fact by fact rather than assumed.
anr9's close records the user's P1 and why it was larger than it looked: the
migration did not break a check, it OUTGREW one — the pre-anr9 code compared
slash-joined strings and never had cats to compare. And it was wrong end to end,
because grid_updates diffs against the framed mirror, so an apply that kept the
cells made the producer send nothing at all.
1125's close records that the ticket's stated home was unreachable:
improvise-wasm-client cannot depend on improvise-command, so putting the width
rule in viewport would have guaranteed the third copy the ticket exists to
prevent.
Wave 1 complete: 8 PRs merged, 1189 -> 1309 passing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
improvise-368h, gv9r, 955n and 7v62 closed by PR #26; improvise-08wb closed as
a duplicate of 955n.
955n's close records the finding that mattered most, and it was a regression the
work introduced: hoisting the basis check to the frame hoisted ALL THREE TIERS.
The document and layout tiers are frame-level facts; the record tier is not and
cannot be, because which records are at stake is a property of the command. An
independent reviewer confirmed the lost update by executing a real Host.
7v62 closed won't-fix with the compiler output on the type, which is what the
ticket asked for: a proof discharged before a mutation cannot be represented as
a borrow held across it.
improvise-yqpd filed — clause 4, transactional rollback — carrying the costed
design note, the two non-obvious acceptance criteria the P1 produced, and the
trade nobody had named: interleaving the record check with the applies would
close clause 4's silence for free, but it buys the legitimate double-write and
sells the loud failure on a broken chain.
improvise-748f filed — a drill exited with no edits, and view-back, mark the
document dirty. Unlike the LoadModel case, that is a wrong answer to no
divergence at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
improvise-0o6x P2 — body_from(idx) starts at the first ITEM, so a leading
FrameEntry::Group is skipped at offset zero. Pre-existing and byte-identical to
main, so the TUI has always done it and the characterization is honest — but a
COLLAPSED group first on its axis therefore has no header anywhere on screen in
either front end, and no way back.
It sits exactly where improvise-anr9's windowing rule says it must not: that
rule carries all headers between two in-window items precisely because a
collapsed group's header is the only affordance for getting its rows back.
Position zero is the one place the rule does not reach — not because the frame
omits the header, but because body_from drops it on the way to the screen.
The author knew: the agreement fixture orders Carol before Family to dodge the
case and says so in a comment. That knowledge lived in one comment in one test
and is now a ticket, because the moment web/app.js grows a click handler for
group headers, a leading collapsed group becomes a trap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
improvise-gd83 P3 — AxisEntry emits a group header on ENTERING a group and no
boundary on leaving one, so an ungrouped item after a grouped run
inherits it. Copilot found it, PR #23 declined it: it is
GridLayout's pre-existing rule, the TUI paints the same span, and
fixing it would destroy the exact-buffer characterization that
was that PR's only evidence the migration changed nothing. The
test already asserts the wrong answer with the corrected
expectation written beside it.
improvise-jq8l P3 — web/app.js calls scrollIntoView nowhere, so a selection can
sit outside the rendered window. Pre-existing, surfaced by the
viewport-reservation argument, and not group-specific.
improvise-plcx P2 — the browser harness renders web/app.js in three browsers but
web/demo.improv has no groups and one category per axis, so the
multi-level, group-span and collapse paths PR #23 added are the
one part of gridTable no test sees. Would be the first test here
able to see that code in a browser.
improvise-4zl7 retitled and re-scoped to the user's ruling: a fitted tree must
route per record and aggregate the results. The question was never about
categorical splits — aggregate-then-route is not route-then-aggregate, so every
fitted tree means one thing on a record row and another one row up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
improvise-vvl3, improvise-vhcz and improvise-hmws closed by PR #24.
vvl3's close records the finding that outlives the harness: a contract nobody
can execute is prose. storage_contract had been cited in gotcha 28 for months
as the spec OpfsFs must satisfy, and the first time OpfsFs was handed to it, it
failed on its FIRST assertion — on a semantic disagreement about what list
means, sitting in a trait doc everyone had read.
hmws needed neither of its two proposed fixes: an orphan-rule refusal is a fact
about where you are standing, not about the pair of types. The impl is simply
allowed in the crate that owns the trait.
improvise-xf1 narrowed rather than closed, and raised to P2. Chromium and
Firefox now run the full battery; Playwright's WebKit provides no OPFS at all,
so Safari is untested and the ticket's own 16.4+ claim is unverified. Parked as
a runtime probe-skip so it starts running when that changes.
improvise-xps8 filed: the handshake watchdog overwrites a genuine mid-session
disconnect eight seconds later. A third bug in the same family, and it survives
improvise-taxx's fix — confirmed by simulating that fix rather than assuming.
Not a CI guard: its trigger is the host's network stack, and an expected-failure
whose trigger is platform-dependent inverts CI's meaning rather than flaking.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
improvise-odbc, improvise-7yx, improvise-g6s1 and improvise-q0vb closed by
PR #22, with the reasoning on each.
improvise-odbc's close records that the ticket's premise was FALSE and that
testing it is what made the ticket cheap: quote_name mirrors bare_name
character for character, so the round-trip property was complete and the
generator needed nothing. The file had been hand-damaged through the
v2025-04-09 migration.
improvise-1ey's design field named bank-info.improv, which PR #22 deleted.
roadmap.org:194 is generated from that field, so the fix belongs at the bd
source and the file is regenerated — Copilot found the stale line and WU-L
correctly declined to hand-edit a generated file.
improvise-l8rh filed: the ambiguous-date heuristic reads American-first, so a
European CSV imports with days and months swapped. Pinned rather than changed
on the user's ruling — but it is now wrong DETERMINISTICALLY, which PR #22's
HashSet fix is what bought.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
improvise-oai9 closed on evidence with no code written — the report() helper
was already in the tree, and the fault was injected ({e:#} → {e}) before
closing rather than trusting the description.
improvise-r4lo, improvise-taxx and improvise-o8fa closed by PR #21. Two of the
three record that the TICKET was wrong rather than merely that the bug is
fixed: r4lo's offered escape hatch does not exist, and taxx's proposed
readyState mechanism cannot work because both cases arrive as code 1006 with
wasClean:false.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
From WU-I (PR #21) and WU-G (PR #27) hand-backs. Searched the open backlog
first; none of these duplicates an existing bead.
improvise-kc7n P2 — improvise-ws-server never checks Origin. Verified
independently: `grep -rni origin crates/improvise-ws-server/src/`
returns nothing. A WebSocket handshake is exempt from the
same-origin policy, so any page an allowed user visits can
drive their session from their own allowed address — the
attack an IP allow-list structurally cannot see, because the
request genuinely is from an allowed client. Loopback is not
exempt and has no proxy in front of it, so the local path is
the least protected. Related to improvise-kpnr rather than
blocked by it: authentication and CSWSH are different
questions and an answer to either leaves the other open.
improvise-sihr P2 — every minibuffer mode entered from records mode exits to
plain Normal while the layout is still records. Affects `:`,
Ctrl-E and Ctrl-W; only the last is new. Declined in PR #27
because fixing one arm of a three-arm rule is not a fix, and
characterized there instead.
improvise-vvl3 also gains the four assertions improvise-taxx's fix owes, so
that ticket need not re-derive them — including the one that must fail against
the previous unconditional close handler, which is the guard and not the
decoration. Recorded there because vvl3 now owns the JS test-infrastructure
decision that PR #21 declined.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All from PR #27's hand-back, searched against the open backlog first and
none of them duplicates.
improvise-mai9 P2 — a status message written while a minibuffer is open is
never drawn. draw_bottom_bar renders the minibuffer INSTEAD
of the status line, so improvise-6t3 carefully ordered a
failure message to win a race to a surface nobody can see.
Copilot then found the regression this causes, in the same
PR that documented the mechanism.
improvise-z05r P3 — CommitExport does not bail on an empty path and
ExportCsv does not abort the batch: the export arms of what
PR #27 fixed for save. Third instance of the same asymmetry,
so the structural fix may beat the third patch.
improvise-g9jb P3 — the text-entry mode list is hand-written in two crates
and checked against AppMode in neither, which now decides
whether a browser refuses to open a prompt it cannot commit.
improvise-zx8t P4 — draw.rs::mode_style matches AppMode with a wildcard, so
the repo map's "the compiler names every site" promise holds
in five places and not the sixth.
improvise-rw3a P3 — the open half of improvise-lae: whether ZZ routes into
the new save prompt, and what ZZ means once it is open.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closed by PR #20 (WU-K), with the reasoning on each bead rather than in a
commit nobody will read again.
Filed from WU-I's hand-back:
improvise-kpnr improvise-ws-server has no authentication, and the
reverse-proxy path the README now documents removes the
loopback binding that was its only control.
improvise-j7v6 the typed-string classification lives in two crates, and
its guard test states the obligation in its name while
asserting three outcomes by hand.
Filed from WU-K's hand-back:
improvise-44ad ColWidths is redundant under the label fold — tried and
reverted, because the test it would rewrite is gotcha
33(c)'s standing evidence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first pass of the whole-backlog campaign, applying the user's rulings
before any agent starts, so the tracker reflects reality while the campaign
runs rather than after it.
Deferred with the reasoning recorded on each bead:
improvise-e0u the in-process client/server split wants a campaign of its
own; it would conflict with every other work unit. Its cheap
cluster members (2ud, drg, s17o, i5tk) are being done now.
improvise-ltq epic; closes when e0u does, and every other child is closed.
improvise-uq7 blocked by e0u.
improvise-m91 stretch, and its natural home is the e0u restructuring.
improvise-a5q no concrete interop request; sits behind the ADR 003 direction.
improvise-zavn deferral re-affirmed; status set so it stops reading as ready.
Rewritten to context/adr/003-live-backed-models.md and deferred as a unit:
improvise-rucf the original text framed import-once vs live-backed as an
open choice and "read-only for now" as a scoping convenience.
ADR 003 decided both, the other way: live-backed, and
read-only as a feature. Now an epic over three filed children
— the .improv source specification and its grammar change,
the native-only SQLite source, and projection freshness for a
source an external writer can change.
improvise-qddo carries ADR 003's two new constraints: shape the trait around
the aggregate rather than the cells, or pushdown is
foreclosed; and keep writes out of the trait, so a read-only
source is unrepresentable rather than failing at runtime.
improvise-mjy7 the interop claim is untouched by the ADR and still good, but
it carries the cluster's rusqlite dependency for a capability
nobody has asked for concretely.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I inserted report() between start()'s fifteen-line doc comment and start
itself, so rustdoc attached the whole of it -- 'Boot the worker over OPFS',
the out-of-band naming argument, the reason the document is an argument
rather than a fourth ClientFrame -- to a two-line JsError shim, and left the
exported entry point undocumented.
Compiles either way, which is why nothing caught it: a doc comment is legal
on any item, so the only symptom is rustdoc nobody regenerated. Found by the
PR agent reviewing PR #17.
Documentation placement only, no behaviour change, so no test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
User ruling 2026-09-20: minimise the amount of new code a new UI framework
needs. Centralise logic in a redux-like store so a second or third front end
is a function from state to pixels plus a wire for events, not a second
implementation of how the application behaves.
Names the pieces that already exist as what they are -- Command is the
action, reduce_view and WidgetCache::apply are the reducer, ViewState plus
the caches are the store, Sessions::apply is dispatch -- and gives the
question to ask of any front-end code: would a third front end have to write
this again?
Three things make it concrete: the measure is lines of framework-specific
code and should trend to 'paint and wire'; a derivation done twice is the
defect even when both copies are correct today (improvise-edp is the worked
example, gotcha 44 is the same defect still open); and sharing a table beats
testing two copies of it, which is why the keymap is shared rather than
mirrored. Also says what legitimately stays local: painting, event wiring,
and a semantic-value-to-framework-type lookup like AxisColor -> axis_fg.
Sections 5-7 renumbered; the §6 testing references in repo-map.md and the
code-review skill updated with them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
The first real browser run of the deployed page produced 'Cannot open
demo.improv' and nothing else. That string is the CONTEXT vfs::open attaches;
anyhow's Display prints only the outermost context, so mapping through
e.to_string() kept the label and discarded the Storage or parse failure
underneath.
The worst possible place for it: bindings.rs is wasm-gated and is the one
module no test here can reach, so a devtools console is the only instrument
pointed at it -- and it was the code path discarding its own diagnostics.
report() renders with {:#}, which walks the chain. It lives in lib.rs rather
than bindings.rs so it can be tested at all. The test is written against the
old rendering and pins anyhow's Display as the trap, so the helper stops
being needed if that ever changes.
This does not fix the underlying failure; it makes the next reload name it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
The page and the README told the reader that a ws:// address is blocked from
an https origin, and then gave them a ws:// address. improvise-ws-server has
no TLS at all -- no rustls, no native-tls, nothing -- so wss:// is not
obtainable from it and the documented pairing could not work from the
deployed site.
Both now say the address must be wss:// from the deployed page, that the
server speaks plain ws:// only so TLS has to be terminated in front of it,
and that the ws:// form works when the page is served over http://localhost.
The missing capability is filed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
Gotcha 43 and ADR 001 both described apply_command/reduce_full as a
deliberate exemption with a hole in it. improvise-ondj closed the hole with
a type: Checked<'_>, whose only constructor runs the check. The principle is
unchanged -- a host holding the App is the truth, not a claimant -- but the
unchecked path is now unconstructible rather than discouraged.
Records why visibility was the weaker option (reduce_full has no production
caller, so pub(crate) would have made it dead code), that the regression
guard is the signature rather than a test, and the residual hole: a Checked
does not name which App it was checked against.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
improvise-xks6 consolidated three envelope vocabularies into one pair, so
the 'socket.rs is not wire.rs' half of gotcha 42 is now history. Replaced
with the question that had to be answered first -- do the worker and the
socket want the same vocabulary -- and the test applied to each difference:
whether the ARGUMENT for it names the thing they differ in. None did.
Also records why deleting both mirror tests was the measure of success: what
they checked is now type identity, and a surviving compatibility test after
a merge is evidence the merge did not happen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
Six improvise-* IDs cited in repo-map and ADR 001 never existed -- filed
with output suppressed and then written into the docs from memory. A dead ID
is worse than no ID, because it reads as a promise the follow-up exists. All
six corrected to the real tickets, and AGENTS.md gains the audit loop.
AGENTS.md also gains what PR #12 learned: requestReviews takes botIds and not
userIds, gh is shell-aliased to a different tool here, a Copilot review takes
about six minutes, and a finding can be CORRECT and still be wrong to fix in
that PR -- #12 applied a truncate panic (byte-identical on ASCII, so it cost
no evidence) and declined a padding bug in the same file that would have
rewritten the characterization tests which were the PR's only evidence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
From the wave review. Gotcha 36 gains why every relay out of apply_model is
ToAll and why copying apply_switch's ToOthers is invisible from the
bystander's side -- a test asserting through a 'what do the others hear'
helper looks away from the asker by construction, which is how the bug
shipped. Gotcha 24 gains the incompleteness of the reset_cursor calls the
epoch observes. Gotcha 43 gains what Basis does and does not buy, and that
apply_command/reduce_full are a second DOOR rather than an exemption: the
struct variant made the basis impossible to omit from the message, not
impossible to ignore on arrival, and '..' is the syntax for ignoring it.
ADR 001 gains a 'what the check is not' section saying the same plainly, so
the unforgeability claim is not relied on later.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
The important thing layer 1 found. ADR 001 says a batch carries the basis it
was composed against and never says how a client comes to KNOW one -- nothing
in the projection stream carried any of the three tiers, so the check as
written was satisfiable only by forging. Projection::Basis + BasisCache close
it, emitted after the grid updates because the counters describe the cells
those updates carry.
Also: a fourth force class (RecordUnstated), because 'any record the batch
touches' had to name whose list and the answer is the server's -- reading it
off the client's claim is a force by omission. And the two places the check
deliberately does not reach, with why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
Gotcha 18 gains matching_cells/matching_values and the reason the old hash
walk was not a tidiness issue -- the drill snapshot froze its records in that
order while pending_edits are keyed by row index, so a second process
addressed different records.
Gotcha 37 gains (d): a fit is storable only when the document can name its
features, the check asks the document rather than the name's shape, and this
is an interim because remedy 3 needs a scalar coordinate predicate the
formula language does not have.
Also corrects improvise-gp's dependency line, stale since improvise-4s8.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
Most work here is done by waves of agents in worktrees, and every rule in
this section was learned by breaking it: the three orchestrator-only files
and why, that kb.pl additions must be batched because loadProgram replaces a
whole predicate, that tickets sharing a file belong to one agent, that a
worktree does not reliably branch from the orchestrator's HEAD, that direnv
status reports 0 for allowed, and that the code review's characteristic find
here is a test that passes while the thing it names is broken.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
`a_frame_named_the_other_envelopes_way_does_not_parse` exists to prove that
`the_socket_framing_matches_the_server` is discriminating: mirroring
`improvise-ws-server`'s two wire enums is only safe if a *renamed* variant
fails a test. The injected fault was not a rename.
ours.replacen("Commands\":[", "Command\":", 1)
rewrites `{"Commands":[{"SetStatus":"saved"}]}` to
`{"Command":{"SetStatus":"saved"}]}` — the opening bracket is deleted with
the variant name and the trailing `]` is left behind, so the string is not
JSON at all. `is_err()` then held for a *syntax* error, and would have held
identically against a `ServerFrame` that really did carry a `Command`
variant. Proven by adding one: with `ServerFrame::Command(Vec<Command>)` in
the enum the old assertion still passed, because malformed JSON fails to
parse whatever variants exist. The checker was checking serde_json's
tokenizer, which is exactly `doctrine(check_the_checker)`'s failure mode
wearing the name of its cure.
The rename is now asserted well-formed before it is fed in, and the failure
is asserted to be `unknown variant` rather than merely a failure — the same
correction applied to the `Bootstrap` case, which was right by luck. Under
the injected variant the corrected test fails.
Second gap, same test: the round trip enumerates variants **by hand**, so a
variant added on the server side and never mirrored here fails nothing.
`every_server_variant_is_one_this_crate_mirrors` matches exhaustively over
`ClientMsg` and `ServerMsg`, so that is a compile error in this crate rather
than a runtime surprise in a browser.
1091 passing, clippy and fmt clean, and `cargo check -p improvise-wasm-client
--target wasm32-unknown-unknown` still clean — the guard lives in a test
module and `improvise-ws-server` stays a dev-dependency.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
User ruling: for many use cases editing capabilities would be a downside. Two
simplifications follow. It dissolves the ADR 001 tension by scoping rather than
amendment — ADR 001 governs concurrent edits and a read-only model has none, so
what remains is projection freshness, a smaller and different question. And the
machinery already exists: ADR 002 makes SetCell take a CellId::Record, so a
source projecting none makes a write unconstructible with no runtime check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
Accepted but explicitly not a priority: the goal is the OPTION of using
improvise as a DB client. Written down now so the option stays open, since
several decisions in flight could foreclose it by accident.
The consequence worth the ADR: a live-backed document splits into our
configuration and their data, which .improv does not distinguish — and read
through the DB-client framing that is the natural shape, a saved query plus a
pivot configuration, i.e. a report definition. Also flags that an external
writer voids a guarantee ADR 001 currently states.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
improvise-9ix declined on measurements: 22% larger than .improv, 4.7x gzipped,
160x at the floor, and still rewritten whole on every save. The generalisable
half is that a second serialization format duplicates the grammar and its own
round-trip test cannot detect drift from the first — the missing cross-format
equality test is what made the ticket look cheap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
An aggregate is not the absence of an identity but a different kind of one, so
Option<CellId> became CellId::{Record, Aggregate}. That makes an invalid Drill
unconstructible too, not just an invalid SetCell, and it closes the drill
addressing question the Option shape had left open. Hover previews deferred.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
Decided: hash the record (coord tuple minus _Measure), not the cell — it is
already ADR 001's atomicity unit and what record_versions is keyed by, so one
identity serves addressing and staleness. Resolution is a maintained index in
DataStore, which already interns names and already keeps a (Symbol,Symbol)
secondary index that makes invalidation tractable.
Records the three ways this breaks silently: a per-process-seeded hasher, a
derived Hash with no stability guarantee, and a width chosen for convenience
rather than for the consequence of a collision.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH