diff --git a/CHANGELOG.md b/CHANGELOG.md index 550bb59a..8eb4366e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,14 +38,12 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Added -- Pre-push coverage gate — `make push-check` (and the - `.githooks/pre-push` hook that calls it) now runs - `ci/coverage_gate.sh`, which fails if total line coverage drops - below the `coverage_floor:` value in `pubspec.yaml`. The floor - starts at 34% (today's measured floor) and only ratchets up; the - end target is 95% (D-66). `ci/test.sh` now writes - `coverage/lcov.info` as a side effect of the unit/widget/golden - run so the gate adds no extra test invocation. +- Pre-push coverage gate — `make push-check` runs `ci/coverage_gate.sh`, + which fails if total line coverage drops below `coverage_floor:` in + `pubspec.yaml`. Floor ratchets up only; target 95% (D-66). +- Pre-push changelog gate — `ci/changelog_gate.sh` fails on any + `## [Unreleased]` bullet over 60 words; warns at 40. Enforces the + Keep-a-Changelog conciseness rule in the git-commit skill. - Test sweep — `keybindings`, `toolchain_paths`, and several `widgets/src/` primitives (tooltip, palette, multitab, markdown). - `tree_sitter_service` sweep — fake-FFI + real-library smoke, @@ -71,35 +69,21 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Fixed -- `TerminalView.onTapUp` callbacks now actually fire on a primary - tap. The parameter was wired to a code path that nothing in the - gesture-detector chain ever invoked — the documented - "Callback for when the user taps on the terminal" was a no-op - for every caller. Routes through `TerminalGestureHandler.onSingleTapUp` - now, with the resolved cell offset (T-93). Dead `onTapUp` surface - on `TerminalGestureHandler` and `TerminalGestureDetector` removed - in the same change. +- `TerminalView.onTapUp` now actually fires on primary tap — was + wired to a dead code path (T-93). Dead `onTapUp` surface on + `TerminalGestureHandler` / `TerminalGestureDetector` removed. - `BufferLine.eraseRange` no longer panics when called with `end == 0`. The right-side wide-char guard read `_data[-1]` via `getWidth(-1)`, which threw a `RangeError`. Real trigger path: `Terminal.eraseDisplayAbove` with the cursor at column 0 — common after `ESC[H\x1b[1J` (home + erase-above) sequences that many TUIs emit on redraw. -- Terminal selections no longer silently disappear when the terminal is - resized narrower. Reflow's tail-anchor handler used to reparent - anchors past the source's trimmed-content range onto a builder line - that was never emitted, leaving them detached from the visible - buffer; the selection controller's `extent.attached` check then - returned null and the highlight vanished. Common trigger paths: - Ctrl+A (select-all) followed by a width change, and mouse drag - selections that extended past the end of a partially-filled line - (T-92). +- Terminal selections no longer vanish when resizing narrower — + reflow's tail-anchor handler left anchors detached past the + trimmed range. Common triggers: Ctrl+A then resize, drag past a + partially-filled line (T-92). - `BufferLine.removeCells` / `insertCells` / `dispose` no longer skip anchors due to concurrent list modification during iteration — - surfaced by unit tests added under T-91. Anchors disposed inside - the loop were unhooking themselves from the same list the loop was - iterating, causing later anchors to be silently skipped (no - reposition, no dispose) and leaving the buffer in an inconsistent - state. Iteration now snapshots the list first. + iteration now snapshots the list first (T-91). ### Changed @@ -143,22 +127,18 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. - Terminal cell grid no longer drifts on bold text — bold rendering is suppressed at the painter level since synthetic bold (with no Bold.ttf registered) shifts glyph advance widths. -- PTY surfaces errno on `forkpty`, `write`, and `ioctl` failures - instead of swallowing them. `execve` failures in the spawned - child now write a diagnostic line to the slave PTY before - `_exit`, so the parent's reader sees the cause instead of an - immediate EOF that looked indistinguishable from clean exit. - PTY `write` loops on short writes; both `NativePty.write` and - `PtySession.write` now throw `PtyException` on hard errors. -- PTY teardown order fixed — kill the child first so the master - fd returns EOF, await the reader isolate exit, then close the - fd. Previously closing the fd while the isolate still polled it - could briefly target a reused fd. Reader isolate spawn errors - in both `NativePty` and `PtySession` are now surfaced via the - output stream instead of silently dropped. `_recvFdAsync` no - longer leaks the `ReceivePort` when `Isolate.spawn` throws, and - `PtySession.spawn` closes the master fd if any post-receive - step fails. +- PTY surfaces errno on `forkpty` / `write` / `ioctl` failures + instead of swallowing. `execve` failures write a diagnostic to + the slave before `_exit`. `NativePty.write` and `PtySession.write` + loop on short writes; both throw `PtyException` on hard errors. +- PTY teardown order fixed — kill child first so the master fd + returns EOF, await reader isolate exit, then close the fd. + Closing earlier could briefly target a reused fd. +- Reader isolate spawn errors in `NativePty` / `PtySession` are now + surfaced via the output stream instead of silently dropped. + `_recvFdAsync` no longer leaks the `ReceivePort` on spawn throw; + `PtySession.spawn` closes the master fd if any post-receive step + fails. - IPC server hardening: per-request 60s timeout (configurable), broadcast/response write failures logged instead of swallowed, client dropped on response-write failure, and the stale-socket diff --git a/Makefile b/Makefile index 15cc24b2..886ee5fc 100644 --- a/Makefile +++ b/Makefile @@ -106,6 +106,10 @@ test-all: test-core test test-a11y test-integration test-e2e ## Everything, sequ coverage-gate: ## Coverage gate — fails if total line % < pubspec.yaml `coverage_floor:` (D-66). Assumes `make test` ran first. ci/coverage_gate.sh +.PHONY: changelog-gate +changelog-gate: ## Changelog concision gate — fails on `## [Unreleased]` bullets over 60 words. + ci/changelog_gate.sh + .PHONY: smoke-bundle smoke-bundle: ## Build Linux release bundle and run it under xvfb for 5s. ci/smoke_bundle.sh @@ -247,7 +251,7 @@ decisions-validate: ## Parser dry-run over governance/{decisions,questions,rejec pql decisions validate .PHONY: push-check -push-check: decisions-validate test-core test test-a11y coverage-gate ## Pre-push gate. +push-check: decisions-validate test-core test test-a11y coverage-gate changelog-gate ## Pre-push gate. .PHONY: hooks hooks: ## Install the repo's git hooks. diff --git a/ci/changelog_gate.sh b/ci/changelog_gate.sh new file mode 100755 index 00000000..0a3252fb --- /dev/null +++ b/ci/changelog_gate.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# CHANGELOG concision gate — enforces the per-bullet word caps from the +# git-commit skill (40 soft, 60 hard) across the `## [Unreleased]` +# section. Released sections are frozen and skipped (don't penalize +# historical entries pre-dating the rule). +# +# A "bullet" is a markdown list item beginning with `- `, including any +# indented continuation lines until the next bullet, blank line, or +# heading. Word count is whitespace-tokenized. +# +# Soft cap 40 → warn (non-zero exit only if HARD is also breached). +# Hard cap 60 → fail. +# +# Bypass: never. If the rule rejects something genuinely user-visible +# that needs more context, the context belongs in the commit body or a +# D-record — see .claude/skills/git-commit/SKILL.md. +set -euo pipefail +cd "$(dirname "$0")/.." + +CHANGELOG=CHANGELOG.md +SOFT_CAP=40 +HARD_CAP=60 + +if [[ ! -f "$CHANGELOG" ]]; then + echo "==> changelog gate: $CHANGELOG missing" >&2 + exit 2 +fi + +awk -v soft="$SOFT_CAP" -v hard="$HARD_CAP" ' + BEGIN { in_unreleased = 0; bullet = ""; bullet_start = 0; fail = 0; warn = 0 } + + function check_bullet() { + if (bullet == "") return + n = split(bullet, _words, /[[:space:]]+/) + # split() counts a trailing empty token when the string starts/ends with + # whitespace; trim the leading "- " marker too. + gsub(/^- +/, "", bullet) + n = split(bullet, _words, /[[:space:]]+/) + if (n > hard) { + printf "FAIL line %d: bullet is %d words (hard cap %d)\n", bullet_start, n, hard + printf " %s\n\n", substr(bullet, 1, 120) (length(bullet) > 120 ? "..." : "") + fail++ + } else if (n > soft) { + printf "WARN line %d: bullet is %d words (soft cap %d)\n", bullet_start, n, soft + printf " %s\n\n", substr(bullet, 1, 120) (length(bullet) > 120 ? "..." : "") + warn++ + } + bullet = "" + bullet_start = 0 + } + + /^## \[Unreleased\]/ { in_unreleased = 1; next } + /^## \[/ && in_unreleased { check_bullet(); in_unreleased = 0; exit_loop = 1 } + !in_unreleased { next } + + # Within Unreleased. + /^### / { check_bullet(); next } # subsection header + /^[[:space:]]*$/ { check_bullet(); next } # blank line ends bullet + /^- / { # new bullet + check_bullet() + bullet = $0 + bullet_start = NR + next + } + /^[[:space:]]+/ && bullet != "" { # continuation of current bullet + bullet = bullet " " $0 + next + } + + END { + check_bullet() + if (fail > 0) { + printf "\n==> changelog gate FAIL: %d bullet(s) over %d words.\n", fail, hard + printf " Trim them. Rationale, probe results, behavior-change deep dives\n" + printf " belong in the commit body, a D-record, or the ticket — not here.\n" + printf " See .claude/skills/git-commit/SKILL.md \"Be concise\".\n" + exit 1 + } + if (warn > 0) { + printf "==> changelog gate OK with %d warning(s) over %d words.\n", warn, soft + } else { + printf "==> changelog gate OK\n" + } + } +' "$CHANGELOG"