* refactor(runtime): extract pure path-candidate, review-branch, and folder-workspace helpers from orca-runtime.ts
Mechanical move of three closed, pure module-scope clusters out of
orca-runtime.ts (37,608 -> 37,207 lines) into domain-named siblings:
- terminal-output-path-candidates.ts: PTY output path harvesting and the
recent-candidate history bound (3 entry points + 15 private callees).
- selected-review-branch.ts: forge-agnostic selected-review predicates and
lookup hints (GitHub/GitLab/Bitbucket/Azure DevOps/Gitea).
- runtime-folder-workspace.ts: folder-workspace id math and the repo+meta
-> Worktree projection.
Bodies are token-identical to their previous form; the only production
changes are the moves, the new import statements, and `export` keywords.
The no-control-regex suppression travels with the path-candidate scanning
that needs it. No max-lines suppression was added and the ratchet is
unchanged.
Adds characterization tests for the two clusters that had no direct
coverage; the path-candidate cluster keeps its existing tests, repointed
at the new module.
* fix flaky timer on CI
* Add daily macOS dev build release channel
Publish once-daily signed macOS builds from main at a dedicated cadence,
separate from hourly (too noisy) and release branches (too infrequent).
Builds are notarized and installable via the updater, but unvetted —
published to stablyai/orca-daily rather than the main repo to avoid
evicting stable/RC entries from the releases feed.
* fix lint
* fix commit
* Add third token mint to daily macOS build workflow
The upload step's 2x45m retry budget can outlive the one-hour token, so a third
is minted after it for verify and cleanup operations. Release notes are moved to
a file to ensure consistency between draft creation and publish. Daily channel
description updated with specific UTC release time.
* Strip liveness gate from AI Vault session delete
Delete now requires only path validation + user confirmation — no process
roster, no liveness check, no quiescence, no ownership ledger.
Co-authored-by: Orca <help@stably.ai>
* Remove obsolete AI Vault liveness delete reliability gate
Session delete no longer checks process liveness, so drop the
manifest entry that still referenced the deleted test files.
* minor fix
---------
Co-authored-by: Orca <help@stably.ai>
* fix(ui): align toggle switch handle symmetrically in on state
The switch handle (translate-x-4 / 16px) left a 6px gap on the right
in the on state while the off state had only a 2px gap on the left.
Accounting for border-box sizing (1px border each side reduces content
width to 34px), the correct on-position is translate-x-4.5 (18px) so
both sides have a 2px inset.
* docs: add JSDoc to exported functions in changed files for docstring coverage
* fix(ui): align toggle switch handle symmetrically in on state
The `h-5 w-9` switch track is `border-box` with a 1px border, leaving 34px
of content for the 14px `size-3.5` handle. `translate-x-0.5` insets the off
state by 2px, so the on state needs 34 - 14 - 2 = 18px. It used
`translate-x-4` (16px), leaving 4px on the right against 2px on the left.
Adds a boundary test so the offset cannot drift again across the 26
hand-rolled switch call sites, and drops the docstrings the original patch
added to satisfy a coverage bot (the surrounding files carry none).
Original patch by @sei0.
Co-authored-by: Orca <help@stably.ai>
* test(ui): widen the switch-handle alignment guard to every h-5 w-9 track
Scan the renderer with the readdirSync walk and TypeScript AST already used by
no-top-level-translate.test.ts instead of shelling out to `git grep`, and find
each knob inside its own track rather than by scanning source text forward.
Why: gating on `role="switch"` silently skipped ClaudeUsageLoadingState.tsx (a
loading skeleton, so a div with no role) and HiddenExperimentalGroup.tsx, so
regressing the loading-state handle back to translate-x-4 left the guard green.
The guard now also fails when the premise behind 34 - 14 - 2 = 18px stops
holding: a track that loses its 1px border or gains padding, a conditional whose
two branches no longer pair off/on, and a track whose knob moved into a child
component where the scan would otherwise just stop seeing it.
---------
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(composer): autosize Create Worktree note on PR prefill
The Note textarea only grew on onInput, so programmatic PR title prefills
left the box at one row with overflow hidden. Resize whenever the note
value changes and allow scroll under max-height (fixes#10575).
* review: size the composer note with field-sizing instead of a measure pass
The prefill bug is the failure mode of imperative sizing: the height is only
recomputed at the events someone remembered to hook, so a programmatic setNote
(and a pane resize, and a font reflow) leaves it stale. Let the layout engine
own the height, matching NativeChatComposerField and LinearIssueTextEditor.
Drops the extracted helper and its unit test — that test asserted the two
assignments it wrote and stayed green with the bug present. The composer test
now pins the class contract and goes red on the pre-fix markup.
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(editor): map .jsp/.jspf to the html language id
* docs(editor): mark the jsp html mapping as a markup-only stopgap
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(browser): stop impersonating the source browser's UA on cookie import (STA-3514)
Delete the import-time UA synthesis entirely instead of patching its version
detection. The session now always keeps the engine-derived UA the registry
sets at startup (clean or native), for imported and non-imported profiles
alike.
Why deletion is the fix:
- The synthesis read CFBundleShortVersionString, which on forks is a
marketing version — Arc imports presented Chrome/1.x and sites rejected
the browser as ancient (STA-3514).
- Its stated purpose (keep Google-bound cookies valid) is handled by
isGoogleSourceBoundCookie excluding those cookies since #736, and #12884
measured that Google rejects re-transplants regardless of UA identity.
- #12608 measured the synthesized Chrome-shaped UA reaching
/v3/signin/rejected while the engine UA reached account lookup.
Persisted userAgent/userAgentByPartition meta is no longer read; legacy keys
drop off on the next meta write. The #12811 gate existed only to catch bad
synthesized values, so it leaves with the synthesis.
* test(browser): catch source UA impersonation regression
* fix(repo-icon): center emoji glyph within its sized icon box
The emoji glyph span in RepoIconGlyph received a fixed-size box via
iconClassName but had no self-centering, so the outer flex only
centered the box itself while the glyph sat top-left inside it —
most visible in the settings preview (size-10 box, size-5 icon).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ewCWxSox2WHN8T78AT7T4
* test(repo-icon): cover emoji centering in its sized icon box
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Prevent older paired hosts from discarding the entire taskResumeState
when they encounter the strict linearIssueView field, which was causing
silent loss of github and jira queries during remote pairing. Layout,
grouping, ordering, and per-workspace filters are now device-local.
* fix(terminal): show a preedit the IME resumes without a compositionstart
Typing 2-Set Korean shows committed syllables but not the in-progress jamo, so
the user composes each syllable blind. Long-standing hole in the vendored
terminal library, not a regression: the same test fails identically against the
bundle this branch starts from.
The `.active` class that CSS keys `display: block` off is added only in
`compositionstart` and dropped in `_finalizeComposition`. Some IMEs (observed on
Windows/WSL Korean) resume a composition with a bare `compositionupdate` and no
second `compositionstart`, by which point `compositionend` has already hidden the
overlay, so the resumed preedit is written into a hidden element and never
positioned. `updateCompositionElements` also early-returned on `!_isComposing`,
so it would not lay the overlay out either.
Re-show the overlay on an update that carries data, and key the layout guard on
the shown overlay instead. `_isComposing` is deliberately left alone, so no
commit bookkeeping changes and `onData` stays byte-identical. The two guards are
equivalent on every pre-existing path: `compositionstart` sets both,
`_finalizeComposition` clears both.
The bundle hunks are the same two edits applied to the shipped minified output;
the sourcemaps are carried through unchanged.
* test(terminal): prove the resumed-preedit fix against a recorded Windows capture
The synthetic test pins the shape; this replays events a real Microsoft Korean
IME emitted on Windows/WSL. The capture holds three compositionupdates that
resume a composition with no second compositionstart — the exact ordering that
wrote the preedit into a hidden overlay.
Without the fix all three report shown:false; with it all three are visible.
Fixture derived from the sealed 11919-windows-wsl-current capture, which is
read-only and unmodified.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): stop the recorded Hangul fixture pinning a derivation artifact
The capture logs each event twice — a dispatch record and a batched next-frame
re-log. Deriving from both replayed every event twice, which made three
compositionupdates appear to land after a session had ended. Filtered to
dispatch records the capture holds zero resumes and 11 balanced sessions, so
the previous toHaveLength(3) was pinning an artifact of the derivation.
Re-scoped to what the capture does prove: the preedit stays visible across all
37 real updates. Verified by reverting the patch that this passes either way,
so it is coverage and the synthetic test remains the discriminator. Both facts
are now stated in the file.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): restore the preedit visibility patch onto its own branch
The previous commit accidentally reverted it: checking main's patch and lockfile
into the worktree to test whether a test discriminates also stages them, so the
commit that followed swept them up.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): claim printable keydowns structurally so committed text survives
Co-authored-by: Orca <help@stably.ai>
* chore(reliability-gates): retarget the IME forwarding gate after the allowlist removal
The gate listed terminal-ime-input-source.test.ts, which went with the
input-source allowlist. Points at the substituted-text commit test instead,
which covers what the gate is actually protecting: text committed outside a
composition session reaching the pty exactly once.
Co-authored-by: Orca <help@stably.ai>
* docs(terminal): record why withholding a claimed keydown needs no timer
The predicate withholds a keydown's byte until the commit arrives, so a key the
IME eats without committing would be dropped. Measured across the recorded
corpus that case does not occur, and the browser marks IME-owned presses on the
keydown itself. Both facts belong next to the predicate rather than only in a
handoff note, since the obvious fix for the imagined gap is a timer, and a timer
here once wrote a newline the user never typed.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): pin the kitty all-keys-as-escape-codes hole explicitly
Flag 8 asks for every printable key as an escape code; this path sends the
committed text raw instead. That is a deliberate trade, not an oversight, but it
was untested — the suite only covered the disambiguate flag. Pinning it makes
the choice visible and records the gate to use if it ever needs closing.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): keep the kitty key-release report for presses that reached the pty
Claiming the keyup unconditionally suppressed xterm's release report. That was
sized for the old design, which claimed only a short punctuation list; the
structural claim takes every printable keydown, so on macOS an app that
negotiated kitty report_event_types stopped seeing releases for ordinary typing
and would treat every printable key as held down.
Suppress the release only when the press put nothing on the wire — swallowed by
the input source, or owned by a composition transaction. xterm emits nothing
from keyup unless kitty report_event_types (or win32 input mode) is on, so
letting it through is inert everywhere else.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): assert IME preedit geometry headlessly for Korean and CJK input
Both IME defects that shipped and were reverted walked through a suite of ~3000
passing assertions, because every one of them was about bytes reaching the PTY.
A preedit rendered into a hidden overlay satisfies all of them while the user
composes blind. The real-geometry coverage that would have caught it existed but
was headful, env-gated and macOS-only, so it never ran in CI.
Drives composition through CDP Input.imeSetComposition instead of a native input
source, which removes the accessibility grant and the system input source that
forced the headful gate. The suite runs in the normal headless project in about
55s serially, and asserts the composition overlay's real bounding rect — the one
property an overlay clipped to max-width:0 cannot fake and a DOM emulator cannot
produce.
Three tests are red on main and marked test.fail() so they stay visible in CI and
flip loud when their fix lands: the preedit resumed by a bare compositionupdate,
and full-width punctuation and digits committed from a keydown that still carries
the ASCII layout key.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): drop the known-broken markers now the stack closes all three
Validated on real macOS hardware: with the two fixes below this layer, all
three report "Expected to fail, but passed". Korean preedit renders at
non-zero geometry through every jamo, and an Apple pinyin source sends
ef bc 8c e3 80 82 to the pty where main sends ASCII.
Worth recording why the punctuation case looked green on main once: an input
source whose id happens to contain an allowlist term, as Sogou's does, satisfies
the old gate. Correctness there depended on which IME the user had selected.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): cover the Linux and Windows IME ownership shapes headlessly
The ten headless IME specs on this branch all decided ownership through a macOS
user-agent override, so the two platforms whose failure mode is a *dropped*
character rather than a downgraded one had no coverage at all, and the one
Windows-recorded trace already in the suite was replayed under whichever policy
the runner happened to report — macOS locally, Linux on the CI shards.
Adds four Linux specs and two Windows specs, every one of them replaying a
native capture rather than a hand-authored ordering:
- IBus/X11 Hangul mixed with literal ASCII. Its `compositionend` is EMPTY and
the syllable arrives afterwards as a bare `insertText`, so reading the commit
off `compositionend.data` — which the Windows capture rewards — drops every
syllable on this framework.
- fcitx5/Wayland Hangul. No keydown at all for a composing key, not even 229,
and physically wrong `code` values on the literal keys. Any ownership rule
reading 229 or `code` fails here.
- Numeric pinyin candidate selection under both frameworks, with the ordinary
digit kept as the negative control, so the two directions are pinned against
each other rather than separately.
- Windows Microsoft Korean captured with real scan codes, including the two
lines committed with Shift held.
Each asserts both sides of the boundary: the preedit's real geometry at every
frame the user would see, and the exact byte stream the native run put on the
PTY. The recorded `onData` the Windows/WSL fixture already carried is now
asserted instead of sitting unused.
Chinese moves up to first-class alongside Korean: pinyin preedit width is now
pinned the way the Japanese phrase already was, and full-width punctuation is
covered in the composition-session shape the Windows and Linux frameworks use,
not only the macOS insertText shape.
Two harness fixes fell out of the recorded traces and are why the IBus one
passes. The replay applied each event's recorded textarea state *after*
dispatch, one event too late for the handlers that read `textarea.value`; and
it left a task boundary between `compositionend` and the `input` carrying the
commit, which Chromium never inserts, letting xterm's deferred finalizer settle
against a textarea the committed text had not reached yet.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): replay the recorded macOS IME shapes instead of only synthesising them
The macOS coverage on this branch drives Chromium composition through CDP, which
is genuine but hand-ordered, so it could not assert the one property that
decides the macOS rule: a composing keydown arrives with keyCode 229 while `key`
is still the single translated character the input source produced — `ㅎ`, not
`Process` — which is indistinguishable by length from an ordinary printable key.
Three native captures were sitting unused in the evidence set.
Adds a recorded 2-Set Korean session, including the syllable boundary where one
composition closes and the next opens with no keydown between them, asserted
against its own recorded byte stream.
Adds the third failure mode, which had no coverage in any shape: an abandoned
preedit leaking to the shell. Pinyin and Cangjie both backspace a composition
away to nothing, and the assertion is not "the right bytes" but "no bytes".
Both cancellation captures continue with a literal `ordinary` typed as bare
keydowns, the recorder's own negative control. That tail carries no `input`
events because the build it was captured on produced the byte from the keydown
itself, so replaying it would measure the recorder rather than the product; the
specs cut at the `compositionend` and say so.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): promote the non-allowlist input-source punctuation spec to the suite
Qingg matches none of the terms the pre-structural build enumerated, so on that
build its bypass never installs. It is the one arm no headless spec can express,
and the only test here that the pre-structural build cannot pass.
Promoted from scratch with four changes, each forced by a measurement rather than
by taste:
- A non-attached input method is a refusal, not a negative result. macOS attaches
per app instance and the attach can simply fail — 3 of 6 instances under
exclusive host access, and re-selecting the source did not recover one of them
across 9 keystrokes. That is now `test.skip()` with a reason naming the rerun,
not a thrown error, and the suite does not gate on a fully green session.
- Attachment is probed with a LETTER. Punctuation substitution emits no
compositionstart and no keyCode 229 even under a fully attached source, so at
the keydown it is indistinguishable from having no source at all. The
punctuation arms are judged on PTY bytes alone.
- The ASCII-layout control is now part of the spec rather than a side experiment.
Without it a build that rewrote every `.` into `。` unconditionally would pass
the Qingg arm and be badly wrong.
- The assertion runs by default instead of behind a strict-mode flag, and gained
a non-vacuity check: the input source must have committed something. That is
the sharp end of the mechanism — on the old build the DOM carries only keydown
and keyup, so nothing is committed at all and the character is destroyed before
the source is asked.
The verdict stays an equality between two measurements, never a comparison
against a hardcoded glyph, so it holds whatever punctuation mode the operator's
input source happens to be in. It reads `beforeinput`, not `input`: the forwarder
consumes `input` in the capture phase on the pane element, so a probe on the
helper textarea never sees it and a strict run fails with correct bytes
underneath.
Co-authored-by: Orca <help@stably.ai>
* feat(terminal): encode IME commits as CSI-u under the all-keys kitty flag
A pane that negotiates `report_all_keys_as_escape_codes` (bit 3) asked for every
printable key as a CSI-u report. The commit path wrote IME-committed text raw,
so such a pane got a legacy byte stream it had declined. That predicate has no
IME-specific condition, so it affected every macOS user in such a pane, not just
CJK users.
Encode the press that produced the commit instead, reusing xterm's own kitty
encoder rather than hand-rolling CSI-u.
`claimKeyEvent` is untouched: still unconditional, still structural, still no
kitty read on the keydown. The flag read happens once per commit.
The gate is bit 3 alone. Flags 1/2/4/16 leave printable keys as text, so panes
negotiating only those keep receiving substituted characters; gating on "kitty
active" would strip the substitution from every pane that negotiates anything.
Known limit, pinned by test: the report carries the physical key's codepoint,
not the committed glyph. Bit 3 is the app declaring it does not want text, and
bit 4 is how it asks for text back — but xterm's encoder derives that text field
from the same `key` it derives the keycode from, so carrying the committed glyph
needs an encoder change, not a wider gate.
* fix(terminal): report a held key's repeats as REPEAT under the kitty flags
The commit encoder never passed an event type, so xterm's encoder applied its
PRESS default to every auto-repeat keydown. A pane negotiating report_event_types
alongside bit 3 saw one held key as N separate strikes.
Carry the keydown's `repeat` on the claimed press and map it to the protocol's
REPEAT. The event type only reaches the wire when report_event_types is
negotiated, so this is inert for panes that asked only for bit 3.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): cover a macOS system key remap reaching the terminal (#11170)
Co-authored-by: Orca <help@stably.ai>
* chore: drop non-mergeable IME e2e scratch files
---------
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): show a preedit the IME resumes without a compositionstart
Typing 2-Set Korean shows committed syllables but not the in-progress jamo, so
the user composes each syllable blind. Long-standing hole in the vendored
terminal library, not a regression: the same test fails identically against the
bundle this branch starts from.
The `.active` class that CSS keys `display: block` off is added only in
`compositionstart` and dropped in `_finalizeComposition`. Some IMEs (observed on
Windows/WSL Korean) resume a composition with a bare `compositionupdate` and no
second `compositionstart`, by which point `compositionend` has already hidden the
overlay, so the resumed preedit is written into a hidden element and never
positioned. `updateCompositionElements` also early-returned on `!_isComposing`,
so it would not lay the overlay out either.
Re-show the overlay on an update that carries data, and key the layout guard on
the shown overlay instead. `_isComposing` is deliberately left alone, so no
commit bookkeeping changes and `onData` stays byte-identical. The two guards are
equivalent on every pre-existing path: `compositionstart` sets both,
`_finalizeComposition` clears both.
The bundle hunks are the same two edits applied to the shipped minified output;
the sourcemaps are carried through unchanged.
* test(terminal): prove the resumed-preedit fix against a recorded Windows capture
The synthetic test pins the shape; this replays events a real Microsoft Korean
IME emitted on Windows/WSL. The capture holds three compositionupdates that
resume a composition with no second compositionstart — the exact ordering that
wrote the preedit into a hidden overlay.
Without the fix all three report shown:false; with it all three are visible.
Fixture derived from the sealed 11919-windows-wsl-current capture, which is
read-only and unmodified.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): stop the recorded Hangul fixture pinning a derivation artifact
The capture logs each event twice — a dispatch record and a batched next-frame
re-log. Deriving from both replayed every event twice, which made three
compositionupdates appear to land after a session had ended. Filtered to
dispatch records the capture holds zero resumes and 11 balanced sessions, so
the previous toHaveLength(3) was pinning an artifact of the derivation.
Re-scoped to what the capture does prove: the preedit stays visible across all
37 real updates. Verified by reverting the patch that this passes either way,
so it is coverage and the synthetic test remains the discriminator. Both facts
are now stated in the file.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): restore the preedit visibility patch onto its own branch
The previous commit accidentally reverted it: checking main's patch and lockfile
into the worktree to test whether a test discriminates also stages them, so the
commit that followed swept them up.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): claim printable keydowns structurally so committed text survives
Co-authored-by: Orca <help@stably.ai>
* chore(reliability-gates): retarget the IME forwarding gate after the allowlist removal
The gate listed terminal-ime-input-source.test.ts, which went with the
input-source allowlist. Points at the substituted-text commit test instead,
which covers what the gate is actually protecting: text committed outside a
composition session reaching the pty exactly once.
Co-authored-by: Orca <help@stably.ai>
* docs(terminal): record why withholding a claimed keydown needs no timer
The predicate withholds a keydown's byte until the commit arrives, so a key the
IME eats without committing would be dropped. Measured across the recorded
corpus that case does not occur, and the browser marks IME-owned presses on the
keydown itself. Both facts belong next to the predicate rather than only in a
handoff note, since the obvious fix for the imagined gap is a timer, and a timer
here once wrote a newline the user never typed.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): pin the kitty all-keys-as-escape-codes hole explicitly
Flag 8 asks for every printable key as an escape code; this path sends the
committed text raw instead. That is a deliberate trade, not an oversight, but it
was untested — the suite only covered the disambiguate flag. Pinning it makes
the choice visible and records the gate to use if it ever needs closing.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): keep the kitty key-release report for presses that reached the pty
Claiming the keyup unconditionally suppressed xterm's release report. That was
sized for the old design, which claimed only a short punctuation list; the
structural claim takes every printable keydown, so on macOS an app that
negotiated kitty report_event_types stopped seeing releases for ordinary typing
and would treat every printable key as held down.
Suppress the release only when the press put nothing on the wire — swallowed by
the input source, or owned by a composition transaction. xterm emits nothing
from keyup unless kitty report_event_types (or win32 input mode) is on, so
letting it through is inert everywhere else.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): assert IME preedit geometry headlessly for Korean and CJK input
Both IME defects that shipped and were reverted walked through a suite of ~3000
passing assertions, because every one of them was about bytes reaching the PTY.
A preedit rendered into a hidden overlay satisfies all of them while the user
composes blind. The real-geometry coverage that would have caught it existed but
was headful, env-gated and macOS-only, so it never ran in CI.
Drives composition through CDP Input.imeSetComposition instead of a native input
source, which removes the accessibility grant and the system input source that
forced the headful gate. The suite runs in the normal headless project in about
55s serially, and asserts the composition overlay's real bounding rect — the one
property an overlay clipped to max-width:0 cannot fake and a DOM emulator cannot
produce.
Three tests are red on main and marked test.fail() so they stay visible in CI and
flip loud when their fix lands: the preedit resumed by a bare compositionupdate,
and full-width punctuation and digits committed from a keydown that still carries
the ASCII layout key.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): drop the known-broken markers now the stack closes all three
Validated on real macOS hardware: with the two fixes below this layer, all
three report "Expected to fail, but passed". Korean preedit renders at
non-zero geometry through every jamo, and an Apple pinyin source sends
ef bc 8c e3 80 82 to the pty where main sends ASCII.
Worth recording why the punctuation case looked green on main once: an input
source whose id happens to contain an allowlist term, as Sogou's does, satisfies
the old gate. Correctness there depended on which IME the user had selected.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): cover the Linux and Windows IME ownership shapes headlessly
The ten headless IME specs on this branch all decided ownership through a macOS
user-agent override, so the two platforms whose failure mode is a *dropped*
character rather than a downgraded one had no coverage at all, and the one
Windows-recorded trace already in the suite was replayed under whichever policy
the runner happened to report — macOS locally, Linux on the CI shards.
Adds four Linux specs and two Windows specs, every one of them replaying a
native capture rather than a hand-authored ordering:
- IBus/X11 Hangul mixed with literal ASCII. Its `compositionend` is EMPTY and
the syllable arrives afterwards as a bare `insertText`, so reading the commit
off `compositionend.data` — which the Windows capture rewards — drops every
syllable on this framework.
- fcitx5/Wayland Hangul. No keydown at all for a composing key, not even 229,
and physically wrong `code` values on the literal keys. Any ownership rule
reading 229 or `code` fails here.
- Numeric pinyin candidate selection under both frameworks, with the ordinary
digit kept as the negative control, so the two directions are pinned against
each other rather than separately.
- Windows Microsoft Korean captured with real scan codes, including the two
lines committed with Shift held.
Each asserts both sides of the boundary: the preedit's real geometry at every
frame the user would see, and the exact byte stream the native run put on the
PTY. The recorded `onData` the Windows/WSL fixture already carried is now
asserted instead of sitting unused.
Chinese moves up to first-class alongside Korean: pinyin preedit width is now
pinned the way the Japanese phrase already was, and full-width punctuation is
covered in the composition-session shape the Windows and Linux frameworks use,
not only the macOS insertText shape.
Two harness fixes fell out of the recorded traces and are why the IBus one
passes. The replay applied each event's recorded textarea state *after*
dispatch, one event too late for the handlers that read `textarea.value`; and
it left a task boundary between `compositionend` and the `input` carrying the
commit, which Chromium never inserts, letting xterm's deferred finalizer settle
against a textarea the committed text had not reached yet.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): replay the recorded macOS IME shapes instead of only synthesising them
The macOS coverage on this branch drives Chromium composition through CDP, which
is genuine but hand-ordered, so it could not assert the one property that
decides the macOS rule: a composing keydown arrives with keyCode 229 while `key`
is still the single translated character the input source produced — `ㅎ`, not
`Process` — which is indistinguishable by length from an ordinary printable key.
Three native captures were sitting unused in the evidence set.
Adds a recorded 2-Set Korean session, including the syllable boundary where one
composition closes and the next opens with no keydown between them, asserted
against its own recorded byte stream.
Adds the third failure mode, which had no coverage in any shape: an abandoned
preedit leaking to the shell. Pinyin and Cangjie both backspace a composition
away to nothing, and the assertion is not "the right bytes" but "no bytes".
Both cancellation captures continue with a literal `ordinary` typed as bare
keydowns, the recorder's own negative control. That tail carries no `input`
events because the build it was captured on produced the byte from the keydown
itself, so replaying it would measure the recorder rather than the product; the
specs cut at the `compositionend` and say so.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): promote the non-allowlist input-source punctuation spec to the suite
Qingg matches none of the terms the pre-structural build enumerated, so on that
build its bypass never installs. It is the one arm no headless spec can express,
and the only test here that the pre-structural build cannot pass.
Promoted from scratch with four changes, each forced by a measurement rather than
by taste:
- A non-attached input method is a refusal, not a negative result. macOS attaches
per app instance and the attach can simply fail — 3 of 6 instances under
exclusive host access, and re-selecting the source did not recover one of them
across 9 keystrokes. That is now `test.skip()` with a reason naming the rerun,
not a thrown error, and the suite does not gate on a fully green session.
- Attachment is probed with a LETTER. Punctuation substitution emits no
compositionstart and no keyCode 229 even under a fully attached source, so at
the keydown it is indistinguishable from having no source at all. The
punctuation arms are judged on PTY bytes alone.
- The ASCII-layout control is now part of the spec rather than a side experiment.
Without it a build that rewrote every `.` into `。` unconditionally would pass
the Qingg arm and be badly wrong.
- The assertion runs by default instead of behind a strict-mode flag, and gained
a non-vacuity check: the input source must have committed something. That is
the sharp end of the mechanism — on the old build the DOM carries only keydown
and keyup, so nothing is committed at all and the character is destroyed before
the source is asked.
The verdict stays an equality between two measurements, never a comparison
against a hardcoded glyph, so it holds whatever punctuation mode the operator's
input source happens to be in. It reads `beforeinput`, not `input`: the forwarder
consumes `input` in the capture phase on the pane element, so a probe on the
helper textarea never sees it and a strict run fails with correct bytes
underneath.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): normalise the pty line terminator so IME specs can run on Windows
A Unix pty's line discipline turns the terminal's CR into a bare LF, but
Windows ConPTY hands the reading process CRLF. Every spec compares against a
recorded line ending in LF, so on Windows 16 of 21 failed on the terminator
alone while the IME payload bytes ahead of it were byte-for-byte correct.
Verified on real Windows: the renderer-to-pty boundary assertion passed there,
so the app writes a bare CR and the LF is added by the console downstream of
anything we control. Normalising in the reader keeps the specs asserting the
IME bytes unchanged rather than loosening them.
Co-authored-by: Orca <help@stably.ai>
* chore: drop non-mergeable IME e2e scratch files
---------
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): show a preedit the IME resumes without a compositionstart
Typing 2-Set Korean shows committed syllables but not the in-progress jamo, so
the user composes each syllable blind. Long-standing hole in the vendored
terminal library, not a regression: the same test fails identically against the
bundle this branch starts from.
The `.active` class that CSS keys `display: block` off is added only in
`compositionstart` and dropped in `_finalizeComposition`. Some IMEs (observed on
Windows/WSL Korean) resume a composition with a bare `compositionupdate` and no
second `compositionstart`, by which point `compositionend` has already hidden the
overlay, so the resumed preedit is written into a hidden element and never
positioned. `updateCompositionElements` also early-returned on `!_isComposing`,
so it would not lay the overlay out either.
Re-show the overlay on an update that carries data, and key the layout guard on
the shown overlay instead. `_isComposing` is deliberately left alone, so no
commit bookkeeping changes and `onData` stays byte-identical. The two guards are
equivalent on every pre-existing path: `compositionstart` sets both,
`_finalizeComposition` clears both.
The bundle hunks are the same two edits applied to the shipped minified output;
the sourcemaps are carried through unchanged.
* test(terminal): prove the resumed-preedit fix against a recorded Windows capture
The synthetic test pins the shape; this replays events a real Microsoft Korean
IME emitted on Windows/WSL. The capture holds three compositionupdates that
resume a composition with no second compositionstart — the exact ordering that
wrote the preedit into a hidden overlay.
Without the fix all three report shown:false; with it all three are visible.
Fixture derived from the sealed 11919-windows-wsl-current capture, which is
read-only and unmodified.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): stop the recorded Hangul fixture pinning a derivation artifact
The capture logs each event twice — a dispatch record and a batched next-frame
re-log. Deriving from both replayed every event twice, which made three
compositionupdates appear to land after a session had ended. Filtered to
dispatch records the capture holds zero resumes and 11 balanced sessions, so
the previous toHaveLength(3) was pinning an artifact of the derivation.
Re-scoped to what the capture does prove: the preedit stays visible across all
37 real updates. Verified by reverting the patch that this passes either way,
so it is coverage and the synthetic test remains the discriminator. Both facts
are now stated in the file.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): restore the preedit visibility patch onto its own branch
The previous commit accidentally reverted it: checking main's patch and lockfile
into the worktree to test whether a test discriminates also stages them, so the
commit that followed swept them up.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): claim printable keydowns structurally so committed text survives
Co-authored-by: Orca <help@stably.ai>
* chore(reliability-gates): retarget the IME forwarding gate after the allowlist removal
The gate listed terminal-ime-input-source.test.ts, which went with the
input-source allowlist. Points at the substituted-text commit test instead,
which covers what the gate is actually protecting: text committed outside a
composition session reaching the pty exactly once.
Co-authored-by: Orca <help@stably.ai>
* docs(terminal): record why withholding a claimed keydown needs no timer
The predicate withholds a keydown's byte until the commit arrives, so a key the
IME eats without committing would be dropped. Measured across the recorded
corpus that case does not occur, and the browser marks IME-owned presses on the
keydown itself. Both facts belong next to the predicate rather than only in a
handoff note, since the obvious fix for the imagined gap is a timer, and a timer
here once wrote a newline the user never typed.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): pin the kitty all-keys-as-escape-codes hole explicitly
Flag 8 asks for every printable key as an escape code; this path sends the
committed text raw instead. That is a deliberate trade, not an oversight, but it
was untested — the suite only covered the disambiguate flag. Pinning it makes
the choice visible and records the gate to use if it ever needs closing.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): keep the kitty key-release report for presses that reached the pty
Claiming the keyup unconditionally suppressed xterm's release report. That was
sized for the old design, which claimed only a short punctuation list; the
structural claim takes every printable keydown, so on macOS an app that
negotiated kitty report_event_types stopped seeing releases for ordinary typing
and would treat every printable key as held down.
Suppress the release only when the press put nothing on the wire — swallowed by
the input source, or owned by a composition transaction. xterm emits nothing
from keyup unless kitty report_event_types (or win32 input mode) is on, so
letting it through is inert everywhere else.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): assert IME preedit geometry headlessly for Korean and CJK input
Both IME defects that shipped and were reverted walked through a suite of ~3000
passing assertions, because every one of them was about bytes reaching the PTY.
A preedit rendered into a hidden overlay satisfies all of them while the user
composes blind. The real-geometry coverage that would have caught it existed but
was headful, env-gated and macOS-only, so it never ran in CI.
Drives composition through CDP Input.imeSetComposition instead of a native input
source, which removes the accessibility grant and the system input source that
forced the headful gate. The suite runs in the normal headless project in about
55s serially, and asserts the composition overlay's real bounding rect — the one
property an overlay clipped to max-width:0 cannot fake and a DOM emulator cannot
produce.
Three tests are red on main and marked test.fail() so they stay visible in CI and
flip loud when their fix lands: the preedit resumed by a bare compositionupdate,
and full-width punctuation and digits committed from a keydown that still carries
the ASCII layout key.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): drop the known-broken markers now the stack closes all three
Validated on real macOS hardware: with the two fixes below this layer, all
three report "Expected to fail, but passed". Korean preedit renders at
non-zero geometry through every jamo, and an Apple pinyin source sends
ef bc 8c e3 80 82 to the pty where main sends ASCII.
Worth recording why the punctuation case looked green on main once: an input
source whose id happens to contain an allowlist term, as Sogou's does, satisfies
the old gate. Correctness there depended on which IME the user had selected.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): cover the Linux and Windows IME ownership shapes headlessly
The ten headless IME specs on this branch all decided ownership through a macOS
user-agent override, so the two platforms whose failure mode is a *dropped*
character rather than a downgraded one had no coverage at all, and the one
Windows-recorded trace already in the suite was replayed under whichever policy
the runner happened to report — macOS locally, Linux on the CI shards.
Adds four Linux specs and two Windows specs, every one of them replaying a
native capture rather than a hand-authored ordering:
- IBus/X11 Hangul mixed with literal ASCII. Its `compositionend` is EMPTY and
the syllable arrives afterwards as a bare `insertText`, so reading the commit
off `compositionend.data` — which the Windows capture rewards — drops every
syllable on this framework.
- fcitx5/Wayland Hangul. No keydown at all for a composing key, not even 229,
and physically wrong `code` values on the literal keys. Any ownership rule
reading 229 or `code` fails here.
- Numeric pinyin candidate selection under both frameworks, with the ordinary
digit kept as the negative control, so the two directions are pinned against
each other rather than separately.
- Windows Microsoft Korean captured with real scan codes, including the two
lines committed with Shift held.
Each asserts both sides of the boundary: the preedit's real geometry at every
frame the user would see, and the exact byte stream the native run put on the
PTY. The recorded `onData` the Windows/WSL fixture already carried is now
asserted instead of sitting unused.
Chinese moves up to first-class alongside Korean: pinyin preedit width is now
pinned the way the Japanese phrase already was, and full-width punctuation is
covered in the composition-session shape the Windows and Linux frameworks use,
not only the macOS insertText shape.
Two harness fixes fell out of the recorded traces and are why the IBus one
passes. The replay applied each event's recorded textarea state *after*
dispatch, one event too late for the handlers that read `textarea.value`; and
it left a task boundary between `compositionend` and the `input` carrying the
commit, which Chromium never inserts, letting xterm's deferred finalizer settle
against a textarea the committed text had not reached yet.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): replay the recorded macOS IME shapes instead of only synthesising them
The macOS coverage on this branch drives Chromium composition through CDP, which
is genuine but hand-ordered, so it could not assert the one property that
decides the macOS rule: a composing keydown arrives with keyCode 229 while `key`
is still the single translated character the input source produced — `ㅎ`, not
`Process` — which is indistinguishable by length from an ordinary printable key.
Three native captures were sitting unused in the evidence set.
Adds a recorded 2-Set Korean session, including the syllable boundary where one
composition closes and the next opens with no keydown between them, asserted
against its own recorded byte stream.
Adds the third failure mode, which had no coverage in any shape: an abandoned
preedit leaking to the shell. Pinyin and Cangjie both backspace a composition
away to nothing, and the assertion is not "the right bytes" but "no bytes".
Both cancellation captures continue with a literal `ordinary` typed as bare
keydowns, the recorder's own negative control. That tail carries no `input`
events because the build it was captured on produced the byte from the keydown
itself, so replaying it would measure the recorder rather than the product; the
specs cut at the `compositionend` and say so.
Co-authored-by: Orca <help@stably.ai>
* test(e2e): promote the non-allowlist input-source punctuation spec to the suite
Qingg matches none of the terms the pre-structural build enumerated, so on that
build its bypass never installs. It is the one arm no headless spec can express,
and the only test here that the pre-structural build cannot pass.
Promoted from scratch with four changes, each forced by a measurement rather than
by taste:
- A non-attached input method is a refusal, not a negative result. macOS attaches
per app instance and the attach can simply fail — 3 of 6 instances under
exclusive host access, and re-selecting the source did not recover one of them
across 9 keystrokes. That is now `test.skip()` with a reason naming the rerun,
not a thrown error, and the suite does not gate on a fully green session.
- Attachment is probed with a LETTER. Punctuation substitution emits no
compositionstart and no keyCode 229 even under a fully attached source, so at
the keydown it is indistinguishable from having no source at all. The
punctuation arms are judged on PTY bytes alone.
- The ASCII-layout control is now part of the spec rather than a side experiment.
Without it a build that rewrote every `.` into `。` unconditionally would pass
the Qingg arm and be badly wrong.
- The assertion runs by default instead of behind a strict-mode flag, and gained
a non-vacuity check: the input source must have committed something. That is
the sharp end of the mechanism — on the old build the DOM carries only keydown
and keyup, so nothing is committed at all and the character is destroyed before
the source is asked.
The verdict stays an equality between two measurements, never a comparison
against a hardcoded glyph, so it holds whatever punctuation mode the operator's
input source happens to be in. It reads `beforeinput`, not `input`: the forwarder
consumes `input` in the capture phase on the pane element, so a probe on the
helper textarea never sees it and a strict run fails with correct bytes
underneath.
Co-authored-by: Orca <help@stably.ai>
* feat(terminal): encode IME commits as CSI-u under the all-keys kitty flag
A pane that negotiates `report_all_keys_as_escape_codes` (bit 3) asked for every
printable key as a CSI-u report. The commit path wrote IME-committed text raw,
so such a pane got a legacy byte stream it had declined. That predicate has no
IME-specific condition, so it affected every macOS user in such a pane, not just
CJK users.
Encode the press that produced the commit instead, reusing xterm's own kitty
encoder rather than hand-rolling CSI-u.
`claimKeyEvent` is untouched: still unconditional, still structural, still no
kitty read on the keydown. The flag read happens once per commit.
The gate is bit 3 alone. Flags 1/2/4/16 leave printable keys as text, so panes
negotiating only those keep receiving substituted characters; gating on "kitty
active" would strip the substitution from every pane that negotiates anything.
Known limit, pinned by test: the report carries the physical key's codepoint,
not the committed glyph. Bit 3 is the app declaring it does not want text, and
bit 4 is how it asks for text back — but xterm's encoder derives that text field
from the same `key` it derives the keycode from, so carrying the committed glyph
needs an encoder change, not a wider gate.
* fix(terminal): report a held key's repeats as REPEAT under the kitty flags
The commit encoder never passed an event type, so xterm's encoder applied its
PRESS default to every auto-repeat keydown. A pane negotiating report_event_types
alongside bit 3 saw one held key as N separate strikes.
Carry the keydown's `repeat` on the claimed press and map it to the protocol's
REPEAT. The event type only reaches the wire when report_event_types is
negotiated, so this is inert for panes that asked only for bit 3.
Co-authored-by: Orca <help@stably.ai>
* chore: drop non-mergeable IME e2e scratch files
---------
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): show a preedit the IME resumes without a compositionstart
Typing 2-Set Korean shows committed syllables but not the in-progress jamo, so
the user composes each syllable blind. Long-standing hole in the vendored
terminal library, not a regression: the same test fails identically against the
bundle this branch starts from.
The `.active` class that CSS keys `display: block` off is added only in
`compositionstart` and dropped in `_finalizeComposition`. Some IMEs (observed on
Windows/WSL Korean) resume a composition with a bare `compositionupdate` and no
second `compositionstart`, by which point `compositionend` has already hidden the
overlay, so the resumed preedit is written into a hidden element and never
positioned. `updateCompositionElements` also early-returned on `!_isComposing`,
so it would not lay the overlay out either.
Re-show the overlay on an update that carries data, and key the layout guard on
the shown overlay instead. `_isComposing` is deliberately left alone, so no
commit bookkeeping changes and `onData` stays byte-identical. The two guards are
equivalent on every pre-existing path: `compositionstart` sets both,
`_finalizeComposition` clears both.
The bundle hunks are the same two edits applied to the shipped minified output;
the sourcemaps are carried through unchanged.
* test(terminal): prove the resumed-preedit fix against a recorded Windows capture
The synthetic test pins the shape; this replays events a real Microsoft Korean
IME emitted on Windows/WSL. The capture holds three compositionupdates that
resume a composition with no second compositionstart — the exact ordering that
wrote the preedit into a hidden overlay.
Without the fix all three report shown:false; with it all three are visible.
Fixture derived from the sealed 11919-windows-wsl-current capture, which is
read-only and unmodified.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): stop the recorded Hangul fixture pinning a derivation artifact
The capture logs each event twice — a dispatch record and a batched next-frame
re-log. Deriving from both replayed every event twice, which made three
compositionupdates appear to land after a session had ended. Filtered to
dispatch records the capture holds zero resumes and 11 balanced sessions, so
the previous toHaveLength(3) was pinning an artifact of the derivation.
Re-scoped to what the capture does prove: the preedit stays visible across all
37 real updates. Verified by reverting the patch that this passes either way,
so it is coverage and the synthetic test remains the discriminator. Both facts
are now stated in the file.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): restore the preedit visibility patch onto its own branch
The previous commit accidentally reverted it: checking main's patch and lockfile
into the worktree to test whether a test discriminates also stages them, so the
commit that followed swept them up.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): claim printable keydowns structurally so committed text survives
Co-authored-by: Orca <help@stably.ai>
* chore(reliability-gates): retarget the IME forwarding gate after the allowlist removal
The gate listed terminal-ime-input-source.test.ts, which went with the
input-source allowlist. Points at the substituted-text commit test instead,
which covers what the gate is actually protecting: text committed outside a
composition session reaching the pty exactly once.
Co-authored-by: Orca <help@stably.ai>
* docs(terminal): record why withholding a claimed keydown needs no timer
The predicate withholds a keydown's byte until the commit arrives, so a key the
IME eats without committing would be dropped. Measured across the recorded
corpus that case does not occur, and the browser marks IME-owned presses on the
keydown itself. Both facts belong next to the predicate rather than only in a
handoff note, since the obvious fix for the imagined gap is a timer, and a timer
here once wrote a newline the user never typed.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): pin the kitty all-keys-as-escape-codes hole explicitly
Flag 8 asks for every printable key as an escape code; this path sends the
committed text raw instead. That is a deliberate trade, not an oversight, but it
was untested — the suite only covered the disambiguate flag. Pinning it makes
the choice visible and records the gate to use if it ever needs closing.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): keep the kitty key-release report for presses that reached the pty
Claiming the keyup unconditionally suppressed xterm's release report. That was
sized for the old design, which claimed only a short punctuation list; the
structural claim takes every printable keydown, so on macOS an app that
negotiated kitty report_event_types stopped seeing releases for ordinary typing
and would treat every printable key as held down.
Suppress the release only when the press put nothing on the wire — swallowed by
the input source, or owned by a composition transaction. xterm emits nothing
from keyup unless kitty report_event_types (or win32 input mode) is on, so
letting it through is inert everywhere else.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): pin the macOS key-binding substitution against #11170
An OS key-binding remap of the character a Korean layout puts on Backquote
is honoured everywhere on macOS except the terminal, which sent the raw
layout character to the PTY. The substitution is applied inside the system
text input path, so it exists only on keypress.charCode and the input
event's data; the keydown still carries the layout character. Nothing needs
to parse the binding file - Chromium has already applied it by the time
`input` fires.
The reported build sent the raw character. A later punctuation table
happened to list that one character, which closed the issue by enumeration
rather than by design, and the structural claim removes the table entirely.
Without a test the fix could regress silently on a change that never
mentions the issue.
Replays the reporter's captured event shape and pairs it with the same
physical key carrying no substitution, so a fix that rewrote the Backquote
position unconditionally would fail. Discrimination checked by mutation:
suppressing the structural claim, and separately removing the single table
character on a pre-rewrite tree, each make the replay send the raw layout
character while both negatives stay green.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): cover the other Korean layout on the remapped key
Korean layouts disagree about what the backquote position produces: two of
them give the currency sign the issue reports, one gives an asterisk. One
key-binding entry has to survive either, but honouring the substitution by
listing characters covers only the ones someone remembered to list - which
is why the reported character worked and this one did not.
This arm discriminates without a mutation: it fails on the pre-rewrite tree
and passes on the structural claim. The harness supplies no input-source
classification, modelling a source the older design did not recognise,
including the window before its async probe resolves. With the source
recognised the older design claimed all ASCII punctuation and covered this
too, so the gap was real but conditional; the header says so rather than
letting the failure read as unconditional.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): drop the Won-setting arm from the keybinding-dict replay
The Won-to-backquote feature was reverted, so the module this replay imported
no longer exists. The #11170 coverage is unaffected: the remaining arms pin the
substitution itself, which never depended on that setting.
Co-authored-by: Orca <help@stably.ai>
* test(terminal): correct the fixture's provenance count
The header said two derived cases when there are four, and counted the
second layout arm as a negative when it is a positive. Each case already
carries its own recorded flag and note; this stops the summary contradicting
them, which matters in the one field whose whole job is provenance.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>