Files
claude@clouddev1 e88fa79f09 docs: handoff-77, changelog-discipline rule, and #39 changelog entry
- docs/handoff/20260622-handoff-77.md — session handoff for the #39
  schema-cache-gate fix; records the four open issues (incl. the new #40).
- CLAUDE.md — add the changelog-discipline rule: update [Unreleased] in
  the same change as any user-facing behaviour change (scripted/pasted
  paths count), under the two copy rules; release-time sweep of
  commits/handoffs as the backstop.
- CHANGELOG.md — [Unreleased] → Fixed bullet for the #39 paste/scripted-
  input fix.
2026-06-22 17:13:12 +00:00

7.5 KiB

Session handoff — 2026-06-22 (77)

Continues from handoff-76. Per the user's direction (handoff-76 §7: clear the open Gitea issues before resuming feature work), this session took the bug of the four open issues — #39 (Form-B insert misparse against a stale schema cache after fast DDL) — and fixed it end-to-end. Three open issues remain (#36/#37/#38, all enhancements); see §3.

§1. State

Branch main. Commits this session (on main, not pushed — push is the user's step):

  • 07575da fix(app) — schema-refresh submission gate (#39) + Tier-1 and Tier-4 PTY regression tests + ADR-0022 Amendment 8 + README index.
  • this docs(handoff-77) commit — folds in the CHANGELOG.md [Unreleased] Fixed bullet for #39 and a new changelog-discipline rule in project CLAUDE.md (see §4).

Test baseline: 2521 passed / 0 failed / 1 ignored (was 2519; +1 lib test, +1 e2e_pty test → e2e_pty now 7 on Linux). clippy --all-targets -D warnings + fmt --check clean. The new PTY test was confirmed to time out without the fix (genuine guard), green with it.

Issue #39 is closed (resolution comment + diagnosis recorded on the issue).

§2. What shipped — issue #39

Root cause. App::update is pure-sync and validates submissions against App::schema_cache. The runtime refreshes that cache asynchronously after the worker applies a command — a SchemaCacheRefreshed event posted on the same FIFO channel as key events (runtime.rs:1591, applied app.rs SchemaCacheRefreshed arm). Under faster-than-human input (paste / script / unpaced PTY) the next Enter is processed before the refresh lands, so a simple-mode Form-B insert (insert into T values (…), columns derived from the cache) submitted right after add column sees the pre-DDL columns, its arity can't match, and the friendly layer tags it "trying to write SQL?". The worker itself is never wrong (it runs commands serially); the bug was purely in client-side pre-validation racing the refresh.

Fix — submission gate (src/app.rs). Two new private App fields: awaiting_schema_refresh: bool + held_submissions: VecDeque<(String, EffectiveMode)>. dispatch_dsl arms the flag on every ExecuteDsl dispatch; while armed, a new DSL submission is held (queued in submission order) rather than validated. The SchemaCacheRefreshed handler clears the flag and drains the queue against the now-fresh cache, stopping as soon as a drained command re-arms the gate (the rest then wait for its refresh — order preserved). App-lifecycle commands route through dispatch_app_command before dispatch_dsl, so quit/help/load/undo/rebuild are never held.

Why arm on every dispatch, not just DDL — it keeps at most one DSL command in flight, so refreshes are strictly one-per-dispatch and in order, making the boolean provably correct. Arming only on DDL would let a preceding non-DDL command's refresh clear the gate early and drain a held insert against a pre-DDL cache (a real corner case). Cost was one existing test (walking_skeleton::colon_escape_in_simple_mode_is_one_shot) updated to model the post-dispatch refresh — faithful, since it had been assuming it.

Scope = interactive only. The replay / history-log / startup rebuild-from-text batch path already re-snapshots the schema synchronously, inline, before every line (run_replaybuild_schema_cache, runtime.rs:2514), so it always had this ordering guarantee; the fix brings the interactive path in line with it. No interactive-user impact (the gate clears in ms); held input is never lost because the runtime sends a SchemaCacheRefreshed after every dispatch, success or failure (the unconditional post-match block in spawn_dsl_dispatch).

Tests (both verified red→green):

  • app::tests::form_b_insert_after_ddl_is_held_until_refresh_then_dispatched — Tier-1, deterministic: stale cache → submit DDL (arms) → submit insert (held, no error) → deliver fresh SchemaCacheRefreshed → insert dispatches.
  • e2e_pty::back_to_back_insert_after_ddl_still_succeeds — Tier-4 PTY, the unpaced inverse of flow 3; asserts the insert's own ('Alice') ✓ echo.

Docs. ADR-0022 Amendment 8 records the gate (the §9 schema-cache refresh-vs-validation timing contract); README index updated in the same edit (ADR-0000 rule).

§3. Open / follow-ups

Per the user's direction the open issues come before feature work. After closing #39, four issues are open. The hint/help trio (#36/#37/#38) are all enhancements on the pedagogy mission (read handoff-76 §7's table for fuller scope; read ADR-0053 first — it orients #37/#38). #40 is a CI/packaging follow-up filed this session.

# One-line read
#36 help <sql-form> shows no distinct content — the 7 advanced-mode SQL nodes share help_id: None (src/dsl/grammar/mod.rs:915-918) Contained. Good next pick; touches H3/help, maybe an ADR amendment.
#37 Clause-concept hints (cursor inside on delete …, with pk, 1:n/m:n, create-table constraint slots) — deferred ADR-0053 extension Medium scope, richest teaching value. Likely an ADR-0053 amendment.
#38 Pre-submit-diagnostic F1 route + ~33 diagnostic.* tier-3 blocks; needs a class/message_key threaded through every diagnostic site Broad mechanism, most marginal value. Get the user's do/defer/close call before investing.
#40 Wire CHANGELOG.md into the winget release notes (komac --release-notes-url); ADR-0056 area Filed this session (the changelog→winget thread, see §4). Small, deferred-by-decision originally.

Suggested order (confirm with the user): #36 (contained) → #37 (highest on-mission value) → #38 (decision first: do / defer / close). #40 is independent (release pipeline) and can slot in whenever. #38 in particular should be escalated for a do-or-close call rather than silently built.

§4. Changelog discipline — new rule + open winget thread

The session surfaced that no rule existed for keeping CHANGELOG.md current (it was created in handoff-76's plan but never given a maintenance process), so the #39 fix wasn't logged until the user asked. Decided with the user:

  • New CLAUDE.md rule (this commit): update [Unreleased] in the same change that alters user-facing behaviour (incl. scripted/pasted/power-user paths, not just the interactive happy path), under the two copy rules; no entry for refactor/test/CI-only changes; at release time rename [Unreleased] and sweep commits/handoffs since the last tag as a backstop.
  • #39 entry added under [Unreleased] → Fixed.
  • winget release notes → issue #40. komac supports --release-notes-url / --release-notes; the winget job (publish.yaml ≈ L276) passes neither. Tracked, not done (it's a release-pipeline / ADR-0056 change, kept out of this bug-fix session by the user's call).

§5. Process pins

  • Commit user-confirmed, no AI attribution, append-only, on main; push is the user's step (this commit is unpushed).
  • Test-first honored: both regression tests were confirmed RED before the fix (the PTY one via a git stash of src/app.rs), GREEN after. A written Devil's-Advocate pass on the implementation surfaced one comment imprecision (fixed) and no behavioral findings.
  • cargo sweep not run this session; the build grew modestly (one extra PTY test binary). Consider a sweep at the next milestone.