Skip to content

feat(mention-review): Phase 0 + Phase 1 of the PR-review TUI, plus a third gate tier - #1698

Merged
ZacxDev merged 4 commits into
mainfrom
feat/mention-review-phase1
Sep 15, 2026
Merged

ZacxDev merged 4 commits into
mainfrom
feat/mention-review-phase1

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Sep 14, 2026

Copy link
Copy Markdown
Member

Implements claudedocs/proposal-pr-review-tui.md Phases 0 and 1. Read-only, and nvim-octo is untouched — Phase 4 is out of scope.


Phase 0 — three kill criteria, none tripped

# question result
1 The query 1 round trip. Measured in Go against the public innovation-upstream/devrc with a counting RoundTripper: median 663 ms (23-file PR), 536 ms (small PR), 402 ms (issue) over 5 runs each. viewer { login } present. Every panel's data present — no field was ABSENT; the only null was reviewDecision, a legitimate "nobody has reviewed it" state now rendered as the WORD NONE.
2 Auth ✅ go-gh v2.16.0 (floor v2.12.1 — CVE-2024-53859, CVE-2025-48938). OK / NO TOKEN / TOKEN REJECTED measured as a triple, all distinguishable. §10.1's rung 4 reproduced exactly: ("", "default") with no error.
3 The renderer viewport.View() with SoftWrap=false + SetContentLines is flat in buffer size: 318 µs @1k, 347 µs @4k (the p99), 342 µs @10k.

Probe 3's instrument control. A flat number means nothing unless the benchmark can move. SoftWrap = true — §6.2 hazard 1 — gives 2.45 ms / 8.46 ms / 21.55 ms at the same sizes: 63× slower at 10,000 lines, and past a 60 fps budget on its own. §6.2 hazard 1 is reproduced and quantified.

347 µs sits inside bubbletea#1724's reported 300–800 µs band, i.e. the issue's numbers are consistent with what I measured — and that is ~2% of a 60 fps frame. End-to-end, scrolling is frame-clock bound, not compute bound: wall latency is exactly 1/fps (16.7 ms at 60, 8.2 ms at 120) and identical at 4,000 and 10,000 lines.


🔴 The Phase-1 re-measurement found a real defect Phase 0 could not see

The proposal insisted this be re-measured on the real panel, not Phase 0's bare buffer. It was right to.

My first implementation styled the whole buffer on every cursor move — §6.2 hazard 2 — while carrying a comment claiming it styled only the visible window.

diff lines CPU/frame (before) 60 fps budget CPU/frame (after)
200 4.1 ms 24.6 % 3.4 ms
4,000 (p99) 20.5 ms 123.1 % — MISSES 3.5 ms
10,000 (max) 46.1 ms 276.3 % — MISSES 3.1 ms

Phase 0's bare viewport was flat across those same sizes because it styled nothing. The defect lived in the seam between two things that were each fine alone. Fixed with the viewport's own StyleLineFunc, which is called on the slice it is about to render — work now bounded by pane height (~36 rows), not buffer size. 14.6× at 10,000 lines.

TestScrollingTheRealPanelMeetsTheFrameBudget is the one genuine regression test here: red at the pre-fix implementation, green at HEAD, and it guards the frame budget itself rather than a number copied off a passing run.


What Phase 1 ships

argv contract + exit codes 64/65/66 ported from nvim-octo.sh case for case · one GraphQL fetch · four panels with focus cycling · a generated help footer · API-only unified diff (go-gitdiff) · the issue card · gruvbox pinned two-way against the Alacritty config · o browser, q quit · nix packaging with the version read out of the Go source.

The command seam (§3.3) is in from day one. Step(msg) (App, []Intent) is pure, intents are data, and one ~40-line runner converts them to tea.Cmds. No golden frames anywhere — §5.0's four reasons all hold under v2.

§10.2 is honoured. I independently confirmed this host's gh config carries two github.com users. viewer { login } rides the same round trip and is rendered in the Overview panel and the footer, with mutants for both.


A third gate tier (§5.6), wired rather than mentioned

scripts/run-go-tests.sh + gate.sh --tier go + checks.gotests + pkgs.go in gateTools. Same guard shape as the other two runners, plus two of its own: exit 3 on a missing toolchain (never a pass) and a SKIP budget of 0.

--tier both is now an alias for all — a caller who typed it before Go existed wanted the whole gate, and silently excluding a language is the "list that only grows when somebody remembers" defect.

doCheck = false on the deploy derivation is a safety property, verified in the pinned nixpkgs rather than assumed: module.nix defaults it true, so a red Go test would fail a home-manager switch — the skipped-host failure mode.

Three defects the floors caught, each of which would otherwise have printed RESULT: PASS over zero tests:

  1. a JSON field-order grep that matched nothing ("Package" sits between Action and Test);
  2. a second that matched on the dev host's go 1.25 and not on the sandbox's go 1.26;
  3. four packages failing to build for want of a C compiler, with the runner printing not one word of why.

Verification

  • Go: 119 tests, 0 skips — green on BOTH tiers. Dev host (go 1.25.14) and nix sandbox (go 1.26.7, nix build .#checks.x86_64-linux.gotests).
  • Mutation sweep: 13 mutants + a no-op control → 13 KILLED BY THEIR OWN GUARD'S ERROR, control SURVIVED. Two rounds were needed:
    • a guard that could not see its own mutant — every rejection test compared err.Code against the constant ExitBadRepo, so changing 65 → 64 moved the expectation with the behaviour. Now pinned as literals.
    • a colour-only encoding nothing covered — deleting the + marker from a diff row makes an add indistinguishable from context once colour is stripped, and it survived the whole suite. MeaningBearingStates() covered the Overview and not one row of the pane the tool is for.
    • Two mutants were also badly built and are recorded as such: one died on a compile error rather than the guard (not isolated), one had a wrong whitespace anchor and never applied.
  • Packaging: version mechanism mutation-tested — 0 matches → NULL, 2 matches → NULL, control → 0.1.0. Packaged binary verified: --version equals the store-path label; 64/65/66 hold.
  • gate.sh's new tier validated in both directions: FAIL→1, SCOPED→91, silent→90, real runner→PASS 0.
  • 766 existing pytest tests pass against the flake/gate/tools changes. test_no_real_launchers.py's PATH-clobber ledger caught my new test and now carries its justification — the system working.

Honest notes

  • Almost nothing here is regression coverage. This is a new feature: the tests are invariant guards for new code, and the mutation sweep is the evidence they work. The one real red-at-base/green-at-HEAD is the scroll budget guard.
  • The click path is NOT flipped. REVIEW_EXE still spawns nvim-octo. test_mention_open.py's two-way wrapper ledger already enforces this — adding pkgs.mention-review to the Alacritty PATH before flipping would fail as "dead weight in the closure".
  • Nothing was deployed and nothing merged.

Full detail, including where reality diverged from the proposal, is in the commit message.

🤖 Generated with Claude Code

ZacxDev and others added 4 commits September 15, 2026 00:45
…third gate tier

Implements `claudedocs/proposal-pr-review-tui.md` Phases 0 and 1. Read-only, and
`nvim-octo` is untouched — `REVIEW_EXE` still spawns it, so the click path is
unchanged and the rollback is one line that does not exist yet.

PHASE 0 — three kill criteria, none tripped
  1. THE QUERY. One `issueOrPullRequest` GraphQL read returns every field the
     four panels need plus `viewer { login }`. MEASURED in Go against the public
     `innovation-upstream/devrc` with a counting RoundTripper: 1 round trip,
     median 663 ms on a 23-file PR, 536 ms small, 402 ms on an issue. No field
     was ABSENT; the only null was `reviewDecision`, a legitimate state rendered
     as the WORD `NONE`. ⚠ `files(first:100)` caps at one page — surfaced as
     `TRUNCATED`, never a silently short list.
  2. AUTH. go-gh v2.16.0 (floor v2.12.1 for CVE-2024-53859 / CVE-2025-48938).
     OK / NO TOKEN / TOKEN REJECTED all distinguishable, measured as a triple.
     §10.1's rung 4 reproduced exactly: `("", "default")` with NO error.
  3. THE RENDERER. `viewport.View()` with `SoftWrap=false` + `SetContentLines`
     is FLAT in buffer size — 318 µs @1k, 347 µs @4k (the p99), 342 µs @10k.
     The SoftWrap control proves the benchmark reads the buffer: 2.45 ms / 8.46
     ms / 21.55 ms, i.e. 63x slower at 10k and past a 60 fps budget on its own.
     bubbletea#1724's 300-800 µs band is consistent with what was measured and
     is ~2% of the frame budget. No kill.

PHASE 1 — what shipped
  argv 64/65/66 ported from `nvim-octo.sh` case for case · one GraphQL fetch ·
  four panels with focus cycling · a GENERATED footer (`bubbles/help` over the
  same `key.Binding` values the dispatcher matches on) · API-only unified diff
  via go-gitdiff · the issue card · gruvbox pinned two-way against the Alacritty
  config · `o`/`q` · nix packaging with the version read OUT OF the Go source.

  The command seam (§3.3) is in from day one: `Step(msg) (App, []Intent)` is
  pure, intents are data, and one ~40-line runner turns them into `tea.Cmd`s.
  That is what makes "zero network intents" a mechanical assertion. No golden
  frames anywhere — §5.0's four reasons all hold under v2.

🔴 THE PHASE-1 SCROLL RE-MEASUREMENT FOUND A REAL DEFECT PHASE 0 COULD NOT SEE
  The first implementation styled the WHOLE buffer on every cursor move while
  carrying a comment claiming it styled only the visible window — §6.2 hazard 2,
  with a comment the code contradicted. On the real four-panel screen:

      diff lines   CPU/frame   60fps budget        after the fix
             200      4.1 ms        24.6 %              3.4 ms
            4000     20.5 ms       123.1 %  MISSES      3.5 ms
           10000     46.1 ms       276.3 %  MISSES      3.1 ms

  Phase 0's bare viewport was flat across the same sizes because it styled
  nothing — the defect lived in the SEAM. `TestScrollingTheRealPanel...` is a
  genuine regression test: RED at the pre-fix implementation, GREEN at HEAD, and
  it guards the 60 fps budget itself rather than a number copied off a pass.

A THIRD GATE TIER (§5.6), wired rather than mentioned
  `scripts/run-go-tests.sh` + `gate.sh --tier go` + `checks.gotests` + `pkgs.go`
  in `gateTools`. Same guard shape as the other two runners, plus two of its
  own: exit 3 on a missing toolchain (never a pass) and a SKIP budget of 0.
  `--tier both` is now an ALIAS for `all` — a caller who typed it before Go
  existed wanted the whole gate.

  `doCheck = false` on the deploy derivation is a safety property: verified in
  the pinned nixpkgs, `module.nix` defaults it TRUE, so a red Go test would fail
  a `home-manager switch` — the skipped-host failure mode.

  Three defects the floors caught, each of which would otherwise have printed
  `RESULT: PASS` over zero tests: a JSON field-order grep that matched nothing
  on go 1.25; a second that matched on 1.25 and NOT on the sandbox's 1.26; and
  four packages failing to build for want of a C compiler while the runner
  printed not one word of why.

VERIFICATION
  · Go: 119 tests, 0 skips — GREEN on BOTH tiers (dev host go 1.25.14; nix
    sandbox go 1.26.7 via `nix build .#checks.x86_64-linux.gotests`).
  · Mutation sweep, 13 mutants + a no-op control: 13 KILLED BY THEIR OWN GUARD'S
    ERROR, control SURVIVED. Two rounds — the first found a guard that could not
    see its own mutant (every rejection test compared against the CONSTANT
    `ExitBadRepo`, so moving it moved the expectation too) and a colour-only
    encoding in the diff pane that nothing covered. Both closed.
  · Packaging: version mechanism mutation-tested — 0 matches -> NULL,
    2 matches -> NULL, control -> 0.1.0. Packaged binary verified: `--version`
    equals the store-path label, exit codes 64/65/66 hold.
  · gate.sh's new tier validated in both directions: FAIL->1, SCOPED->91,
    silent->90, real runner->PASS 0.
  · 499 + 142 + 80 + 45 existing pytest tests pass against the flake/gate/tools
    changes. `test_no_real_launchers.py`'s PATH-clobber ledger caught the new
    test's `PATH` replacement and now carries its justification.

NOT DONE, deliberately: local git, caching, write actions, syntax highlighting
(§6.3 — lazygit does none either), and the click-path flip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session-Id: 09f2cb96-627d-425c-b6c1-7999c68c48ad
…test — plus the go tier's two missing registrations, and delete a provably-vacuous ledger

Four review findings from running this PR through `the-algorithm`, in the order
they were found. The fourth is the only one that was a real defect.

1. CI-RED, ONE LINE. `scripts/run-go-tests.sh` declares the
   `DEVRC_GIT_REPO_POINTERS` array but was never added to
   `test_git_repo_isolation.py`'s ledger, so `POINTER_CLEARERS` disagreed with
   the files that actually declare it:

     declares it but NOT pinned: ['scripts/run-go-tests.sh']

   Registered in `RUNNERS`, not in `POINTER_CLEARERS` directly — it resolves a
   ROOT with `git rev-parse --show-toplevel` and already places its `unset`
   before that block, so the ordering pin (`COMMIT_SH`/`CLAIM_WORK` are held out
   of `RUNNERS` precisely because it can assert nothing about them) validates it
   for free.

   🔴 AND REGISTERING IT SURFACED A SECOND, REAL GAP. Two more arms of the
   ledger then went red: `run-go-tests.sh` spelled
   `DEVRC_GITENV_CONTROL_VARS=(A B)` on ONE LINE, and the pins parse the
   multi-line form the other four runners use. An unparseable declaration is an
   UNCHECKED one — the CONTROL ledger and the duplicate check could say nothing
   about this runner at all. Reformatted to match its siblings, so all 120 tests
   in that file now actually cover it.

   The prose counts moved with it: `RUNNERS` is five, and the SPELLINGS count in
   both `test_git_repo_isolation.py` and `testlib/gitenv.py` was already stale by
   one (it said FIVE/SIX when `claim-work.sh` made it six) and is now SEVEN.

2. THE GO TIER WAS INVISIBLE TO THE DEADMAN. `main-green-check.sh` — the
   4-hourly reproduce-before-alerting guard that exists because branch
   protection is off — looped `for tier in pytests nodetests`. A break in
   `internal/ui` on `main` would never have been looked at by it. The tier list
   is pinned two-way, so: the loop, the behavioural pin (`_calls(world)`), the
   static pin (the regex over the source), and the three call-COUNT assertions
   that encode "2 tiers x 2 attempts" (now 3 x 2 = 6, and +3 for a moved memo /
   `--force`). `nix/home.nix`'s `TimeoutStartSec` comment said two tiers too.

3. DELETE THE WRITE-INTENT LEDGER (-184 lines net).
   `LedgerViolations(KnownIntents())` was a PROVABLE CONSTANT `nil`: all three
   registered intents return `Write() == false`, and `Confirmed`/`NotConfirmed`
   are empty map literals. The test's own log said so —
   `checked 3 registered intents, 0 of them writes`.

   ⚠ THIS IS A PHASING JUDGEMENT, NOT A DEFECT REMOVAL, AND IT WAS APPROVED AS
   ONE. The emptiness was documented deliberately as Phase-2 scaffolding. The
   argument for deleting it anyway: the Intent SEAM must exist from day one
   because retrofitting it means rewriting every handler — the LEDGER over that
   seam is purely additive and can be added in Phase 2 with no handler touched.
   🔴 PHASE 2 MUST REINTRODUCE IT, with the positive control the deleted version
   carried (a test-local write intent in neither set, reported with the guard's
   own error string). Both surviving comments say so.

   Gone: `Confirmed`, `NotConfirmed`, `LedgerViolations`, `Write()` from the
   interface and its three implementations, the five ledger tests, and
   `countWrites`/`containsString`. `IntentTypeName` went with them — exported,
   with ZERO call sites anywhere in the module.

   🔴 KEPT: `TestEveryRegisteredIntentIsHandledByRun` and
   `TestEveryIntentStepCanEmitIsRegistered`. Neither was ever vacuous — the
   dispatch walk emits 2 distinct intents and logs which. The test-local intent
   survives as `unregisteredIntent`, still the positive control for `Run`'s
   panic. While in there: `run.go` cited a `run_test.go` that has never existed;
   the guarantee is real and lives in `intents_test.go`, so the citation now
   names the test rather than an absent file.

4. 🔴 THE REAL DEFECT — FIVE ACTIONS HAD NO BEHAVIOURAL TEST AT ALL.
   PageUp, PageDown, Top, Bottom and NextFile were never pressed. `keys_test.go`
   walks `Dispatch()` and compares it to `FullHelp()`/`ShortHelp()` — both built
   from the SAME literal — so it asserts the keymap agrees with ITSELF and never
   looks at where the cursor lands.

   MEASURED at 4a6c6f8: inverting all four arms of `moveIn`'s switch
   (`-= page`/`+= page`, `= 0`/`= n-1` swapped pairwise) left ALL FIVE Go
   packages GREEN under `-count=1`. The suite is not uniformly weak — mutating
   `udiff.go:309`'s `h.LineIndex > from` to `>= from` kills three tests across
   two packages — the hole was exactly these five actions.

   `movement_test.go` presses them through `Step` and asserts literal cursor
   positions. Re-applying the same 4-arm inversion now fails three tests with
   nine assertions, each arm killed by its own:

     one ctrl+d from the top -> diffCur = 0, want 10
     ctrl+u at the top of the buffer -> diffCur = 10, want 0
     ctrl+d on the last line -> diffCur = 191, want 201
     g from line 100 -> diffCur = 201, want 0
     G from line 100 -> diffCur = 0, want 201

   `FileStart(f+1)` -> `FileStart(f)` kills the NextFile test on its own.

   🔴 NO EXPECTATION IS DERIVED FROM `moveIn`. The test PINS the viewport height
   (`a.vp.SetHeight(20)`), so "half a page" is 10 because the test made the page
   20 — the help text's own words (`C-u` "half page up") are the contract. The
   boundary cases carry the inversions: at the top of the buffer a correct
   `ctrl+u` is INERT, while an arm that added would move ten lines the wrong way
   and still look like a page. Fixture sizes are pinned as literals first, so a
   fixture that changed size cannot quietly move every expectation with it.

   ⚠ THE SECOND MOVEMENT SWITCH IS COVERED TOO. `move()` has a separate switch
   for the Overview panel's body viewport (`HalfPageUp`/`HalfPageDown`/
   `GotoTop`/`GotoBottom`) that no mutation run had touched.
   `TestTheOverviewBodyPagesAndJumpsToItsEnds` presses the same five keys there;
   inverting that switch fails it with six assertions.

FLOORS DID NOT MOVE. `internal/ui` is 54 tests against its pinned floor of 51
(five ledger tests out, five movement tests in), and `run-go-tests.sh` printed
no replacement number. `RESULT: PASS`, `TOTAL: pass=119 fail=0 skip=0
(global floor 111)`, `SCOPE: FULL`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session-Id: 09f2cb96-627d-425c-b6c1-7999c68c48ad
…nd adding them BLINDED the ledger to the two it was built to catch

🔴 NOT IN THE REVIEW BRIEF — found by running the authoritative `nix build`
sandbox tier, which is the only tier that had ever run this PR's full pytest
suite. `tekton/devrc-pytests` reported `failed=2` and its 140-char description
named only one of them.

THE DEFECT. `scripts/tests/test_gate_reexec.py::_env_vars_gate_sh_reads()`
derives every environment variable gate.sh reads, and a two-way pin asserts the
ambient-scrub tuple equals it. It harvests indirect reads out of the refusal
loop with a regex anchored on `^…for … in … ; do` — ONE line. This PR added
three more names to that loop (`DEVRC_GATE_GO_RUNNER`, `MIN_GO_TESTS`,
`MAX_GO_SKIPS`), which pushed it onto a second line with a backslash. The regex
then matched NOTHING, so the harvester stopped seeing the ENTIRE list —
including `DEVRC_TARGETS` and `MIN_TESTS`, the two the comment directly above it
says it was added to catch after they sat unscrubbed once before.

🔴 IT IS THE SAME FAILURE THE FUNCTION'S OWN DOCSTRING WARNS ABOUT, ONE
INDIRECTION DEEPER, AND IT IS INVISIBLE BY CONSTRUCTION: "the regex found no
loop" and "the loop reads nothing" are the same empty set, so a blind harvester
reports FULL COVERAGE. The only reason this failed loudly rather than silently
is an accident — `DEVRC_GATE_GO_RUNNER` is read a second time as
`${DEVRC_GATE_GO_RUNNER:-…}`, so it stayed visible and made the two sides
disagree. `MIN_GO_TESTS` and `MAX_GO_SKIPS` appear NOWHERE else in gate.sh
except comments; had the go tier not happened to have a runner override, all
three would have been added to the refusal and none would have been pinned,
green.

THE FIX, IN THREE PARTS.

1. THE INSTRUMENT. Join backslash continuations before scanning, so the
   harvester sees what the SHELL sees. Reformatting the loop can no longer
   retire the ledger.

2. THE CONTROL THAT WOULD HAVE CAUGHT IT —
   `test_the_derivation_sees_a_refusal_list_WRAPPED_ACROSS_LINES`, pinned
   against a SYNTHETIC script, not against gate.sh, so gate.sh reformatting its
   own loop cannot quietly make the control vacuous. `WRAPPED_C` is reachable
   ONLY past the continuation; a harvester that stops at the backslash returns
   the first two and looks fine. It carries its own negative control: a name
   mentioned only in a COMMENT must not be derived.

   🔴 MUTATION-TESTED, AND THE EXISTING POSITIVE CONTROL SURVIVES THE MUTANT.
   With the join replaced by a regex that cannot match, the new control FAILS
   and the two-way pin FAILS (`scrubbed but not in gate.sh: ['DEVRC_TARGETS',
   'MAX_GO_SKIPS', 'MIN_GO_TESTS', 'MIN_TESTS']`) — while
   `test_the_env_var_derivation_can_actually_see_a_variable` PASSES. That is the
   measurement showing the old control could never have seen this.

3. THE TUPLE. `DEVRC_GATE_GO_RUNNER`, `MIN_GO_TESTS` and `MAX_GO_SKIPS` added to
   `_AMBIENT_GATE_VARS`. Until now an exported value for any of them rode
   straight into every re-exec test.

🔴 AND THE REPAIRED INSTRUMENT IMMEDIATELY FOUND A SECOND CONSEQUENCE.
`test_the_help_text_documents_every_env_var_the_script_reads` is built on the
same derivation, so it was blind in the same way: `MIN_GO_TESTS` and
`MAX_GO_SKIPS` are read by gate.sh and were documented in `--help` NOWHERE. Both
now have an `Env:` entry. A flag documented nowhere the operator looks is a flag
that does not exist — and CLAUDE.md tells readers `--help` lists every variable
it reads, which was false for as long as the go tier had existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session-Id: 09f2cb96-627d-425c-b6c1-7999c68c48ad
… ANY, not just /usr/bin/env

c5570bb fixed a blind harvester and tripped a different guard doing it: its
synthetic script wrote `#!/usr/bin/env bash` at runtime, which
`test_runtime_shebangs.py` exists to forbid — `patchShebangs` fixes the source
tree and cannot reach a file a test writes while running, so such a shebang
breaks in the nix sandbox.

🔴 THE GUARD IS WIDER THAN ITS NAME. `test_no_test_writes_a_usr_bin_env_shebang_
at_runtime` rejects a self-written `#!/bin/sh` too — measured, after trying that
as the fix. The remedy it names is the only one: `testlib.mockbin.write_exec`,
which owns the shebang so a call site cannot reintroduce one.

The shebang could not simply be dropped: `_env_vars_gate_sh_reads` computes
`body_start` over `range(1, …)`, so line 0 is skipped by construction and a
script without one would lose its first real line.

RE-PROVED THE CONTROL AFTER CHANGING THE FIXTURE. Removing the continuation-join
still fails `test_the_derivation_sees_a_refusal_list_WRAPPED_ACROSS_LINES` (and
`test_the_ambient_scrub_list_covers_every_gate_variable`), so the guard c5570bb
added is still load-bearing through the rewrite — that it passes is not the same
claim as that it can still go red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session-Id: 09f2cb96-627d-425c-b6c1-7999c68c48ad
@ZacxDev
ZacxDev force-pushed the feat/mention-review-phase1 branch from c5570bb to f6103dc Compare September 15, 2026 05:47
@ZacxDev
ZacxDev merged commit 7b827d1 into main Sep 15, 2026
0 of 3 checks passed
@ZacxDev
ZacxDev deleted the feat/mention-review-phase1 branch September 15, 2026 06:01
ZacxDev added a commit that referenced this pull request Sep 15, 2026
…mation ledger that is no longer vacuous (#1723)

* feat(mention-review): Phase 2 — the five write verbs, behind a confirmation ledger that is no longer vacuous

Comment (PR-level) · approve · request changes · submit review · merge. Four of
the five prompt first; a PR-level comment does not, per proposal §3.7, and the
reason is stated per entry in NOT_CONFIRMED rather than left to be re-derived.

🔴 PR-LEVEL COMMENTS ONLY. The operator settled proposal §12.4: no inline
diff-line positioning. Nothing here computes a diff position, and the endpoint
comment in ghapi/write.go says why /issues/{n}/comments is the correct one.

🔴 THE WRITE-INTENT LEDGER IS BACK, AND NON-VACUOUS THIS TIME. #1698 deleted
Confirmed / NotConfirmed / LedgerViolations / Write() as a PHASING judgement —
in Phase 1 every intent returned a constant false, so the ledger compared two
empty maps and read as coverage while providing none. It is reinstated with the
assertion that makes it mean something: TestTheDispatchWalkEmitsWriteIntents
asserts the keyboard walk PRODUCES each of the five write verbs by name. That
test found its own gap on the first run — the walk reached four of five, and
SubmitReview was unreachable from any state it drove.

🔴 §10.2 IS THE REASON THE PROMPT IS SHAPED THE WAY IT IS. cli/cli#14370: the
OS keyring is not partitioned by account, so the resolved token may belong to a
different github.com user than the config calls active, and this host's
hosts.yml carries two. So `as <login>` is inside every pinned confirmation
string, the compose bar names it too (a comment is outward and does not prompt),
and a snapshot with NO login refuses every write verb rather than acting with
the clause omitted.

🔴 THE MERGE METHOD IS READ, NEVER GUESSED. internal/cfg splits ABSENT (the
declared default, spelled once) from PRESENT-AND-UNTRUSTWORTHY (refuse). An
unknown method is the MERGE_METHOD_UNKNOWN sentinel carried over from the Lua —
where a mutation replacing it with "squash" survived a fully green suite because
nothing reached the arm. Here one keypress reaches it, and a test presses it.

Modes are DISJOINT and the keymap ledger is now PER MODE. A ledger that walked
only the browse table would report "every binding is helped" while every key
that composes or confirms was invisible in the footer — the defect the
preceding arc existed to fix, reintroduced inside a mode.

Also closes one of the three over-claiming guard descriptions the handoff
batched: MeaningBearingStates' doc comment said the ledger covers every
constructor in words.go and nothing checked it. words_ledger_test.go now does,
by AST rather than by grep. Phase 2 added ModeWord to that file, so the gap was
about to grow rather than merely persist.

No test can reach the live API: App.runner is nil in every pure test, the one
end-to-end test installs a recording fake, and http.DefaultTransport is replaced
in both network-reaching packages by one that refuses any non-loopback host —
with a negative control proving it rejects api.github.com and a positive control
proving it allows an httptest server. A Python guard derives WHICH packages must
carry that from their imports, so a new one cannot be added without it.

The click path is untouched: REVIEW_EXE still spawns nvim-octo. Phase 4 is
gated on the operator using this for a real review.

Claude-Session-Id: 09f2cb96-627d-425c-b6c1-7999c68c48ad

* test(mention-review): pin the intent -> API-call mapping by RUNNING the tea.Cmd

Three different intents collapse onto ONE endpoint, distinguished only by an
event string. A swap there approves where the operator asked to BLOCK, and
every other test in the package passes: Step emitted the right intent, the
ledger agreed, the prompt named the right verb, and the wrong verdict went to
GitHub. A tea.Cmd is opaque, so the mapping is only observable by CALLING the
returned closure against a recording runner.

Claude-Session-Id: 09f2cb96-627d-425c-b6c1-7999c68c48ad

* test(mention-review): three findings from the mutation battery, fixed rather than recorded

1. The browse-mode sweep shared its fixture list with the MODAL states, which
   are built by pressing keys — so the very mutation it exists to catch (merge
   fires with no confirmation) ALSO broke the chain building the confirm-merge
   fixture, and the mutant died with the FIXTURE's error instead of the sweep's
   assertion. A kill by a different arm is green for the wrong reason and stays
   green with the arm deleted. The sweep now drives a list built without
   pressing anything.

2. Disabling proposeMerge's UNKNOWN-method check let a mutant SURVIVE: a second
   lock catches it (ConfirmPrompt cannot name an empty method), so no merge
   went out — but the notice changed to "no confirmation prompt could be built
   … this is a bug", which does not tell the operator what to fix. The test
   asserted only the REFUSED prefix; it now asserts THIS refusal.

3. The footer's mode was a field nobody branched on in any test: keys_test.go
   compares the dispatched and helped SETS per mode and says nothing about
   which mode renderFooter asks for. A footer pinned to ModeBrowse passed every
   ledger assertion while showing "q quit" over a merge prompt.
   TestTheFooterFollowsTheMode presses into each mode and asserts both
   directions — this mode's help present, the others' absent.

Floors re-measured by the runner and copied from what it printed.

Claude-Session-Id: 09f2cb96-627d-425c-b6c1-7999c68c48ad

* docs(flake): the mention-review overlay comment said PHASE 1 (read-only)

It can write now. Everything else in that block still holds: it is still not
in the Alacritty wrapper's makeBinPath, because REVIEW_EXE still spawns
nvim-octo and test_mention_open.py pins that list two-way.

Claude-Session-Id: 09f2cb96-627d-425c-b6c1-7999c68c48ad

* fix(mention-review): the write bar was not part of the layout, so the frame overflowed the terminal

Found by re-reading the diff, not by a test — which is the point of the guard
this adds. `render` subtracted the bar's height from the body; `relayout`,
which is what actually SIZES the diff viewport, did not. So opening a compose
buffer or raising a confirmation left the viewport sized for a bar-less frame
and pushed the bottom rows off the bottom of the terminal.

Every existing assertion passed through it. They are about STATE (the right
intent, the right mode, the right pending prompt) or about a SUBSTRING of the
frame — and the state was correct and the substring was present. Only counting
the frame's LINES finds it, which layout_test.go now does, at two terminal
heights because the layout branches on available space.

Three parts to the fix, each with its own mutant:
- relayout subtracts lipgloss.Height(renderBar()), the same call render makes,
  so the two cannot disagree;
- every mode or notice change goes through settled(), because both change the
  bar's height and neither previously triggered a relayout;
- the compose bar is a fixed composeBarLines-row window that FOLLOWS the
  cursor, so its height cannot move while the operator types — which is the
  property relayout depends on, since it runs when compose opens.

Also drops a duplicated COMPOSING word: ComposeHeader already leads with it,
and a doubled word reads as a rendering fault rather than as emphasis.

Claude-Session-Id: 09f2cb96-627d-425c-b6c1-7999c68c48ad
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant