diff --git a/docs/active-work.md b/docs/active-work.md index 287c5c2..bb0864b 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -56,8 +56,10 @@ If it does not, stop and repair the remote/fetch configuration. - Portable branch: `githubsucks/folding`; worktree `../pmacs-folding`. - Base: canonical `main` @ `cac4961` (post Vterm Stage 3 #135). -- Framing head: `ee6c77f` (`docs/folding-framing.md` draft rev 1). -- State: **framing only, awaiting review then approval.** No implementation. +- Framing head: revision 4 of `docs/folding-framing.md` (this commit; rev 1 + `ee6c77f` → rev 2 `7898b8f` → rev 3 `944b42b` → rev 4). +- State: **framing only; three review rounds absorbed (rev 4), awaiting + approval.** No implementation. Load-bearing decision (Q#FD1): the bundled grammars ship no fold query and no `folds.scm`, so the roadmap's "tree-sitter fold ranges" is not free; the draft recommends structural node folding (nearest enclosing block-like node @@ -68,8 +70,9 @@ If it does not, stop and repair the remote/fetch configuration. like vterm: Stage 1 engine (headless), Stage 2 TUI, Stage 3 GPU. - PR: none yet — framing is committed to the branch for review, not opened as a PR. Stage 1 implements on this same branch after approval. -- Next: user review rounds on `docs/folding-framing.md`; bindings (Q#FD4) and - the block-kind heuristic (Bet B1) are the two calls flagged for the user. +- Next: user approval of rev 4 (or a round-4 review). Bindings (Q#FD4) + remain the user's call; the block-kind heuristic stays Bet B1 with + curated Tier-1 queries as fallback. Recovery worktree: diff --git a/docs/folding-framing.md b/docs/folding-framing.md index 7e508ea..aa517ef 100644 --- a/docs/folding-framing.md +++ b/docs/folding-framing.md @@ -1,9 +1,10 @@ # Folding — framing (Arc 6) -**Revision 3 — 2026-07-22. Status: framing only, on branch `folding` +**Revision 4 — 2026-07-23. Status: framing only, on branch `folding` (off canonical `main` @ `cac4961`); no implementation.** Rev 1 passed a -ground-truth review; rev 2 fixed round 1's seven findings; rev 3 fixes round -2's five majors and four minors. See §0 for the per-round changelog. +ground-truth review; rev 2 fixed round 1's seven findings; rev 3 fixed round +2's five majors and four minors; rev 4 fixes round 3's one major, three +minors, and a nit. See §0 for the per-round changelog. ## 0. Revision history @@ -57,6 +58,36 @@ ground-truth review; rev 2 fixed round 1's seven findings; rev 3 fixes round three named interactions (§8): fold-aware `LineNumbers`, visible-line viewport/scroll accounting, and hidden-line signs/presence clamp-or-drop. +### Round 3 (rev 3 → rev 4) + +- **R3-1 (major).** Rev 3's head-selection ascend was **not** a no-op for + brace languages: rustfmt wraps long signatures (`fn foo(` / `a: u32,` / + `) -> bool {`) and puts `{` on its own line under a `where` clause, so + `block.start_line > parent.start_line`, the ascend fired, and the fold hid + the wrapped signature — the R2-5 defect class reintroduced one level up. + Replaced by a **derived head line**: the interior comes from the body node + alone (closer-aware tail unchanged) and the head is **the line immediately + above the first hidden line** (§3) — Emacs hideshow / LSP `foldingRange` + parity; wrapped introducer text now stays visible in both grammar shapes. + The introducer↔body association survives for **matching and `close-all` + only**. Acceptance 1 gains wrapped-signature cases. +- **R3-2 (minor).** "Innermost-first" on a shared head line made the outer + fold unreachable via `fold.toggle` (close inner, reopen inner, forever) + and allowed zero-visible-change presses. Replaced by **state-aware + ordering** with an org-TAB-style toggle cycle (§6); acceptance 9 updated. +- **R3-3 (minor).** Stage 1's "command path" is `dispatch_key` + self-insert/delete only; interactive Lua commands (yank, query-replace, + comment-toggle) mutate through the Lua mutator path and classify + programmatic, so their edits land inside a fold without unfolding. Now + stated as the intended Stage 1 line, and **widening the classifier to + interactive Lua command contexts is a named Stage 2 obligation** (Q#FD5, + §5, §8), beside Stage 3's CRDT-origin unfold. +- **R3-4 (minor).** The data API's normalization of an arbitrary range was + unstated; §6 now defines it (no node, so no introducer or closer + inference). **Nit:** stored-range containment pinned **start-exclusive, + end-inclusive** with the matching `View` boundary bias, so typing at the + end of a head line neither unfolds nor lands hidden (§5; acceptance 6). + ## 1. Problem and what ships Pmacs cannot fold. `FoldState` was declared in the M11.1 semantic-frontend @@ -74,7 +105,7 @@ has a decode arm); Arc 6 only starts *producing* it. **Git gutter markers are a SIBLING rider, not this arc** (§11). -## 2. Ground truth (scouted 2026-07-22, `main` @ `cac4961`; verified across two review rounds) +## 2. Ground truth (scouted 2026-07-22, `main` @ `cac4961`; verified across three review rounds) - **`FoldState { buffer_id, folds: Vec }`** — `pmacs-protocol/src/message.rs:886`, gated on `semantic_render`, @@ -84,8 +115,10 @@ has a decode arm); Arc 6 only starts *producing* it. - **No fold source exists** — the bundled grammars export `HIGHLIGHTS`/`INJECTIONS`/`LOCALS`/`TAGS` only, no fold query, no `folds.scm`. Fold source is Q#FD1. **tree-sitter-python's `block` node - starts on the first statement line, not the `def` line** (R2-1) — the reason - the head-selection rule is required. + starts on the first statement line, not the `def` line** (R2-1), and + **tree-sitter-rust's `block` starts at `{`, which rustfmt places below the + `fn` line for wrapped signatures and standalone under `where` clauses** + (R3-1) — the two facts the derived-head rule answers. - **Two frontend render paths (F1).** Grid TUI: daemon-rendered (`render_states` → `render_state.render_frame`, `src/daemon.rs:1106`), advertises `semantic_render: false` (`src/frontend.rs:385`), never receives @@ -102,7 +135,7 @@ has a decode arm); Arc 6 only starts *producing* it. (`:706`) is nonzero while edits await settle. - **Greenfield** Lua/commands. -## 3. Fold source (Q#FD1) — structural node folding with head selection and closer-aware tail +## 3. Fold source (Q#FD1) — structural node folding with derived head line and closer-aware tail The grammars ship no fold queries, so pmacs defines "what is foldable." v1 is **structural node folding for grammar-backed buffers** (reuses the parse trees @@ -112,19 +145,33 @@ Indentation folding (grammarless fallback) and curated per-language queries The source, at a point: -1. **Match** the nearest enclosing NAMED node `B` spanning **≥2 source lines** +1. **Match** the nearest enclosing NAMED node spanning **≥2 source lines** (source lines, not display rows — soft wrap is frontend-only and unknowable instance-side), biased to block-like kinds (`block`, `body`, `*_list`, `declaration_list`, `statement_block`, brace/bracket-delimited nodes). -2. **Head selection (R2-1).** Ascend: while `B`'s parent introduces `B` (a - `function_definition` / `if_statement` / … whose block child is `B`) **and** - `parent.start_line < B.start_line`, take the parent as the head node. This - makes the **introducer line the head** — `def foo():` on Python, where the - `block` starts a line lower. It is a **no-op for brace languages**, where - `{` shares the introducer's line (`parent.start_line == B.start_line`), so - the head node stays `B` and the result is identical. -3. **Tail selection (R2-5).** The hidden interior is a whole-line range. Its - first hidden line is `head_line + 1`. Its last hidden line is: +2. **Resolve introducer↔body (R2-1, R3-1).** If the matched node is an + introducer — a `function_definition` / `if_statement` / … matched from its + header lines, whose block-like body child (grammar field `body` / + `consequence`; feeds B1) starts at or below it — descend to that body + child. The interior-defining node `B` is the body; otherwise it is the + matched node itself. `B` is *introduced* when its parent is such an + introducer. The association exists for **matching and `close-all` + enumeration only** — `fold.toggle` on `def foo():` or on any + wrapped-signature line resolves to the body below; it no longer selects + the head line (rev 3's start-line ascend is removed, R3-1). +3. **Head — the line immediately above the first hidden line (R3-1).** The + head line is `B.start_line - 1` when `B` is an **introduced, + delimiter-less body** (a Python `block`: its introducer's header ends on + the line above — `def foo():`, or the `):` line when the signature + wraps). Otherwise it is `B.start_line` (a brace body's `{` line — + normally the introducer's own line; the `) -> bool {` line when rustfmt + wraps the signature; the standalone `{` under a `where` clause). The + first hidden line is `head_line + 1`. **Wrapped introducer text always + stays visible** — rev 3's ascend took the introducer's *start* line as + the head and so hid wrapped signatures and `where` clauses, the R2-5 + defect class one level up. This is Emacs hideshow / LSP `foldingRange` + parity: the fold hides the body, nothing else. +4. **Tail — closer-aware (R2-5).** The last hidden line is: - if `B`'s last line begins with `B`'s **closing-delimiter token** (`}`/`)`/`]`, and `end`-style closers later) — a brace/bracket node — then `B.last_line - 1`, **keeping the closer line visible**. This is what @@ -133,8 +180,12 @@ The source, at a point: hiding through the last body line. The stored range is the byte range `[end of head_line, end of last-hidden -line]` (§5). A node that yields **zero** hidden lines (e.g. `fn f() {}` on two -lines, empty body) is **not foldable**. +line]` (§5). A fold must have **≥1 hidden line**: the ≥2-source-line gate is +a *match* condition on the matched node, foldability is the ≥1-hidden-line +rule on the *normalized* interior. So `fn f() {` / `}` (empty body — the +closer-aware tail leaves nothing between head and closer) is **not +foldable**, while a two-line `def foo():` / `x = 1` **is**: the matched +`function_definition` spans two lines and its one-line body is the interior. **Stale-tree rule (Q#FD10, F4).** The source reads `ParseViewHandle::current()`; if it is `None` (no settle yet) or @@ -144,8 +195,9 @@ and must not be computed against stale coordinates. Settle is a main-thread pump, so the window is sub-frame. Translate-through-pending is a §11 refinement. -The block-kind heuristic (step 1) remains a taste bet (Bet B1); step 2 fixed -the *determinable* Python defect, which was not taste. +The block-kind heuristic (step 1) and the body-field bias (step 2) remain a +taste bet (Bet B1); steps 3–4 fixed the *determinable* defects (R2-1, R2-5, +R3-1), which were not taste. ## 4. Where fold state lives (Q#FD2) @@ -158,13 +210,23 @@ allowed; the store is **shared by every attached frontend** (Emacs parity). ## 5. Fold model semantics (Q#FD3, Q#FD5, Q#FD6, Q#FD8) **Stored range = line-aligned hidden interior (Q#FD3).** A fold is identified -by its **head line** (the introducer, §3 step 2), which stays visible with a -frontend-drawn ellipsis. The stored byte range is `[end of head line, end of -the last hidden line]`, where the last hidden line is chosen by §3 step 3 — -so a **closing-delimiter line stays visible** (fixing `} else {`), while a -delimiter-less node hides through its last body line. One normalized form is -computed by the source and seen identically by the store, the grid renderer, -the wire, and `folds()`. +by its **head line** (the line immediately above the hidden interior, §3 +step 3), which stays visible with a frontend-drawn ellipsis. The stored byte +range is `[end of head line, end of the last hidden line]`, where the last +hidden line is chosen by §3 step 4 — so a **closing-delimiter line stays +visible** (fixing `} else {`), while a delimiter-less node hides through its +last body line. One normalized form is computed by the source and seen +identically by the store, the grid renderer, the wire, and `folds()`. + +**Containment and boundary bias (R3-4 nit).** The stored range is +**start-exclusive, end-inclusive** — `(start, end]`. A point at +`range.start` (the end of the head line) is **outside** the fold: typing +there does not trigger the pre-edit unfold, and the store `View` translates +an insert at exactly `range.start` by shifting the fold right (the +`BufferStyleSpanTranslator` at-or-after bias), so the typed character lands +visible on the head line. A point at `range.end` (the end of the last hidden +line) is **inside**: typing there unfolds. One convention covers both the +containment test and the translation bias. **Point and folds (Q#FD3, F5).** - Folding a range containing the **invoking frontend's** point moves that @@ -189,9 +251,19 @@ the wire, and `folds()`. **authenticated source frontend's point, not the transport** (R2-3): a GPU user's CRDT-op insert at a point inside a fold is interactive and must unfold, even though it arrives as a CRDT op. **Stage 1 implements this for - the command path** (daemon `dispatch_key` self-insert/delete, which has the - frontend + point); **CRDT-origin unfold is a Stage 3 obligation**, wired - when the GPU renders folds and a GPU user can type into one. + the command path** — daemon `dispatch_key` self-insert/delete, the only + point-anchored edits the daemon applies directly with the frontend + point + in hand. Two widenings are named, each landing with the rendering that + makes it user-visible (R3-3): + - **Interactive Lua command edits — Stage 2 obligation.** Yank, + query-replace, and comment-toggle mutate through the Lua mutator path, + which this split classifies as programmatic: in Stage 1 such an edit + inside a fold translates without unfolding. Invisible while headless, + but a visible "the yank vanished into the fold" once the TUI collapses — + Stage 2 widens the classifier to interactive Lua command contexts, + keyed on the edit position. + - **CRDT-origin unfold — Stage 3 obligation**, wired when the GPU renders + folds and a GPU user can type into one. **Store lifecycle vs producer baseline (Q#FD8, F3, R2-4).** Three coupled resets, kept distinct: @@ -213,17 +285,35 @@ resets, kept distinct: ## 6. Lua command surface and validation (Q#FD4, Q#FD11) **Interactive commands** (resolve to the invoking frontend's active-window -buffer — command context, not ambient resolution). On a head line shared by -more than one fold, they act **innermost-first** (minor b): +buffer — command context, not ambient resolution): - `fold.toggle`, `fold.close`, `fold.open`, `fold.close-all`, `fold.open-all`. - `close-all` folds **top-level** foldable regions only (Emacs `hs-hide-all` parity — nested regions are not auto-folded; feeds B2). `open-all` clears. +- **Shared head lines — state-aware ordering (R3-2).** On a head line shared + by more than one fold (`foo(() => {`), plain "innermost-first" dead-loops: + toggle closes the inner fold, then acts on it again and *reopens* it, + forever — the outer fold is unreachable, and opening an inner fold while + the outer is closed changes nothing on screen. Ordering is therefore + keyed on fold **state** so every press has a visible effect: `fold.close` + closes the **innermost open** fold (repeated presses walk outward); + `fold.open` opens the **outermost closed** fold (repeated presses walk + inward); `fold.toggle` **cycles org-TAB-style** — it closes the innermost + open fold until every fold on the head is closed, then one more press + opens them all. **Data API (Q#FD4, F6): explicit buffer, no ambient resolution** (matching #127): `pmacs.fold.fold(buffer, range)`, `unfold(buffer, range)`, `folds(buffer)`, `toggle(buffer, pos)`. +**Arbitrary-range normalization (R3-4).** A data-API `fold(buffer, range)` +carries no node, so none of §3's introducer or closer inference applies — +the caller names exactly what to hide. The head line is the line containing +`range.start`; the hidden lines are the full lines strictly after it, +through the line containing `range.end` — or through the *previous* line +when `range.end` sits at a line start. The stored form is §5's; validation +then applies. + **Validation (Q#FD11, F6).** `fold(buffer, range)` rejects unless: the buffer is a normal document buffer; both endpoints are UTF-8 boundaries; and the range normalizes to **≥1 hidden line**. Q#FD9 (terminals never fold) follows @@ -249,7 +339,7 @@ is derived per path like the diagnostic sign bars — no new wire type. ## 8. Staging and scope - **Stage 1 — fold engine (instance), headless.** The per-buffer store + its - translating/dropping `View`; the structural source with head selection, + translating/dropping `View`; the structural source with derived head line, closer-aware tail, and the stale-tree rule; the Lua data API + interactive commands + validation; the **command-path pre-edit unfold**; `FoldState` production (authoritative-empty, diff-suppressed); headless acceptance. No @@ -259,8 +349,9 @@ is derived per path like the diagnostic sign bars — no new wire type. head placeholder; caret clamps to the head. **Must also make the daemon-computed `LineNumbers` family fold-aware** (skipped lines; relative distance measured across a fold), **count visible lines in viewport/scroll - accounting**, and **clamp-to-head-or-drop diagnostic signs on hidden lines** - (minor d). + accounting**, **clamp-to-head-or-drop diagnostic signs on hidden lines** + (minor d), and **widen the pre-edit interactive unfold to interactive Lua + command edits** (yank / query-replace / comment-toggle — R3-3). - **Stage 3 — GPU collapse + gutter marker.** The GPU consumes `FoldState`, excludes folded bytes, draws the glyph, makes caret/hit-test fold-aware at TUI parity, **clears the fold mirror on `BufferSnapshot`** (R2-4), wires @@ -273,22 +364,29 @@ lands. This framing asks approval for the architecture and Stage 1's detail. ## 9. Numbered decisions - **Q#FD1** Structural node folding: match block-like node ≥2 source lines → - **ascend to the introducer head** → **closer-aware tail** (closing-delimiter - line kept visible; delimiter-less nodes hide through the last body line); + resolve introducer↔body (matching + `close-all` only) → **head line = the + line immediately above the first hidden line** (wrapped introducer text + always visible) → **closer-aware tail** (closing-delimiter line kept + visible; delimiter-less nodes hide through the last body line); stale/absent tree refuses. Indentation and curated queries deferred. (§3) - **Q#FD2** Fold state is instance-side, per-buffer, a set of ranges, shared by all frontends; nested allowed. (§4) -- **Q#FD3** Stored range = line-aligned hidden interior; head line visible; - closer line visible for closer-terminated nodes; invoking point moves to the - head; no-cursor-inside is per-cursor render-time, creation-time-only in - Stage 1. (§5) -- **Q#FD4** Interactive commands (invoking frontend's buffer, innermost-first - on shared heads); data API takes an explicit buffer, no ambient resolution; - bindings decided by the user. (§6) +- **Q#FD3** Stored range = line-aligned hidden interior, **start-exclusive, + end-inclusive** with the matching `View` boundary bias; head line (the + line above the interior) visible; closer line visible for closer-terminated + nodes; invoking point moves to the head; no-cursor-inside is per-cursor + render-time, creation-time-only in Stage 1. (§5) +- **Q#FD4** Interactive commands (invoking frontend's buffer; shared head + lines use **state-aware ordering** — close innermost-open, open + outermost-closed, toggle cycles); data API takes an explicit buffer, no + ambient resolution, with the §6 arbitrary-range normalization; bindings + decided by the user. (§6) - **Q#FD5** The store `View` translates + drops only (provenance-blind); the **pre-edit interactive unfold** lives at the dispatch layer, keyed on the authenticated source frontend's point (not transport), unfolding **every** - fold containing it; Stage 1 = command path, CRDT-origin = Stage 3. (§5) + fold containing it; Stage 1 = command path (`dispatch_key` + self-insert/delete), interactive-Lua-command widening = Stage 2, + CRDT-origin = Stage 3. (§5) - **Q#FD6** A fold whose head/tail an edit destroys is dropped, not re-anchored. (§5) - **Q#FD7** Placeholder + gutter marker are frontend-local per path (TUI @@ -304,13 +402,14 @@ lands. This framing asks approval for the architecture and Stage 1's detail. from bounds. (§6) - **Q#FD10** Fold creation against a `None`/stale parse tree refuses. (§3) - **Q#FD11** `fold(buffer, range)` validates buffer kind, UTF-8 boundaries, - ≥1 hidden line; rejects otherwise. (§6) + and ≥1 hidden line after the §6 normalization; rejects otherwise. (§6) ## 10. Bets -- **B1** The block-kind heuristic (§3 step 1) picks a fold *target* users find - natural. FALSIFIABLE on real Rust/Python; fallback is curated Tier-1 - queries. (Head selection and closer-aware tail are now decided, not bet.) +- **B1** The block-kind heuristic (§3 step 1) and body-field bias (step 2) + pick a fold *target* users find natural. FALSIFIABLE on real Rust/Python; + fallback is curated Tier-1 queries. (The derived head line and the + closer-aware tail are decided, not bet.) - **B2** Whole-buffer `FoldState` is cheap: folds are a handful, `close-all` is top-level only, so the set is O(top-level blocks). No viewport scoping. - **B3** Both collapse paths reuse existing machinery (daemon cell painting; @@ -330,10 +429,14 @@ lands. This framing asks approval for the architecture and Stage 1's detail. ## 12. Acceptance — Stage 1 (engine) -1. **Head selection, both grammar shapes (R2-1).** In Rust `fn foo() { … }`, - a point in the body folds with head line `fn foo() {`. In Python - `def foo(): / body`, a point in the body folds with head line `def foo():` - — **not** a body line. `close-all` on each yields the introducer as head. +1. **Head line, both grammar shapes, wrapped headers (R2-1, R3-1).** In Rust + `fn foo() { … }`, a point in the body folds with head line `fn foo() {`; + with a rustfmt-wrapped signature (`fn foo(` / `a: u32,` / `) -> bool {`) + the head is the `) -> bool {` line and **every signature line stays + visible**. In Python `def foo(): / body`, a point in the body folds with + head line `def foo():` — **not** a body line; with a wrapped signature + the head is the `):` line and the signature stays visible. `close-all` + on each yields the same heads. 2. **Stale/absent tree (Q#FD10).** With `current() == None`, and with `pending_edit_count() > 0` after an edit before settle, `fold.toggle` refuses and stores nothing; after settle it succeeds. @@ -346,12 +449,16 @@ lands. This framing asks approval for the architecture and Stage 1's detail. the range (hidden). `folds(buffer)` returns exactly the normalized ranges. 5. **Point (Q#FD3).** Folding a range containing the invoking point moves it to the head; Stage 1 does not prevent later motion into a fold. -6. **Edits — separated mechanisms (Q#FD5/Q#FD6, R2-2/3).** The store `View` - translates a fold across a programmatic edit inside it and drops a fold - whose head an edit deletes — with no knowledge of source. A command-path - self-insert at a point inside a fold (or inside **nested** folds) unfolds - **all** of them before the edit applies. (CRDT-origin unfold is asserted in - Stage 3.) +6. **Edits — separated mechanisms (Q#FD5/Q#FD6, R2-2/3, R3-3/4).** The store + `View` translates a fold across a programmatic edit inside it and drops a + fold whose head an edit deletes — with no knowledge of source. A + command-path self-insert at a point inside a fold (or inside **nested** + folds) unfolds **all** of them before the edit applies. A self-insert at + the **end of the head line** (`point == range.start`) unfolds **nothing** + and the fold shifts right — the character lands visible on the head line. + An interactive Lua-command edit (e.g. a yank) inside a fold translates + without unfolding in Stage 1; the test documents this as the named + Stage 2 widening. (CRDT-origin unfold is asserted in Stage 3.) 7. **`FoldState` production (Q#FD8, F7, R2-4).** The flipped pin test asserts all three transitions to a semantic session — nothing until a fold exists, nothing when unchanged, exactly one empty frame after `open-all` — while @@ -359,15 +466,20 @@ lands. This framing asks approval for the architecture and Stage 1's detail. `BufferSnapshot`. The test documents that empty-after-snapshot suppression is correct only paired with the Stage 3 frontend-mirror clear. 8. **Store lifecycle.** Buffer content replacement (revert) drops the store. -9. **Nested folds.** Folding an inner then an outer region yields two ranges; - `open-all` clears both; a shared head line toggles innermost-first. +9. **Nested folds (R3-2).** Folding an inner then an outer region yields two + ranges; `open-all` clears both. On a shared head line: repeated + `fold.close` closes innermost-then-outer, repeated `fold.open` opens + outermost-then-inner, and `fold.toggle` cycles close-inner → close-outer + → open-all — the outer fold is reachable by every command. 10. **Injected layer.** A fold sourced inside an injected layer — a fenced code block in a markdown buffer — returns the inner block's range, proving the source walks injection layers, not just the root tree. -11. **Lua data API (Q#FD4/Q#FD11).** `pmacs.fold.*` with an explicit buffer - drives the above and round-trips `folds()`; an out-of-bounds, - non-boundary, or sub-one-line range is rejected; a fold on a terminal - identity buffer is rejected via the ≥1-hidden-line rule (Q#FD9). +11. **Lua data API (Q#FD4/Q#FD11, R3-4).** `pmacs.fold.*` with an explicit + buffer drives the above and round-trips `folds()`; an out-of-bounds, + non-boundary, or sub-one-line range is rejected — including a range + whose `end` sits at the start of the line after its head, which + normalizes to zero hidden lines; a fold on a terminal identity buffer + is rejected via the ≥1-hidden-line rule (Q#FD9). ## 13. Gates (Stage 1) @@ -380,7 +492,10 @@ acceptance is bite-verified with `scripts/bite`. ## 14. Branch and PR plan Branch `folding`, worktree `../pmacs-folding`, off canonical `main` @ -`cac4961`. This framing (rev 1 → rev 3) is its opening commits. After -approval, Stage 1 implements on this same branch and opens as the first -folding PR. Stages 2 and 3 are separate branches/PRs off the main resulting -from the prior stage, each with its own detailed framing. +`cac4961`. This framing (rev 1 → rev 4) is its opening commits. Canonical +`main` has since advanced past the base (documentation + tab-width #137); +Stage 1's instance-side scope does not overlap that work — rebase onto +current `main` when implementation starts. After approval, Stage 1 +implements on this same branch and opens as the first folding PR. Stages 2 +and 3 are separate branches/PRs off the main resulting from the prior +stage, each with its own detailed framing.