[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay
5

Configure Feed

Select the types of activity you want to include in your feed.

Add virtualizer spec and test plan docs

Taras Mankovski (Apr 11, 2026, 6:41 AM EDT) 4ebaa33c fbc05cd1

+449
+233
specs/clayterm-virtualizer-spec.md
··· 1 + # `@clayterm/virtualizer` Current-State Specification 2 + 3 + **Status:** Current-state specification for the implementation on `feat/virtualizer-v1`. 4 + 5 + This document was imported from the draft virtualizer spec in `~/Downloads` and updated to match the current implementation in this repository. It is intended to describe the implemented package surface and behavior as it exists in-tree today. Where the implementation is known to be more provisional or implementation-shaped than the original draft contract, that is called out explicitly. 6 + 7 + ## 1. Scope 8 + 9 + `@clayterm/virtualizer` is a TypeScript package that provides viewport virtualization over large terminal text output. It owns: 10 + 11 + - ring-buffer storage of logical lines 12 + - per-line cached display width 13 + - per-line wrap-point caching at the current column width 14 + - anchor-based scrolling 15 + - viewport resolution 16 + - approximate scrollbar-estimation fields 17 + 18 + It does not own: 19 + 20 + - ANSI style parsing beyond CSI/OSC skipping for width and wrapping 21 + - rendering, layout, or terminal output 22 + - input normalization 23 + - input parsing 24 + - search, selection, filtering, or disk-backed scrollback 25 + 26 + ## 2. Package Boundary 27 + 28 + The package lives in [`virtualizer/`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer) and exports: 29 + 30 + - [`Virtualizer`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/virtualizer.ts) 31 + - [`VirtualizerOptions`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/types.ts) 32 + - [`ViewportEntry`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/types.ts) 33 + - [`ResolvedViewport`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/types.ts) 34 + 35 + The package has no runtime import of `@clayterm/clayterm`. Width measurement is injected by the caller. 36 + 37 + The intended width provider from the renderer package is `createDisplayWidth()` from root [`width.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/width.ts), which is an async factory returning a synchronous `(text: string) => number` function. 38 + 39 + ## 3. Public API 40 + 41 + The current implemented API is: 42 + 43 + ```ts 44 + class Virtualizer { 45 + constructor(options: VirtualizerOptions); 46 + 47 + readonly lineCount: number; 48 + readonly baseIndex: number; 49 + readonly columns: number; 50 + readonly rows: number; 51 + readonly totalEstimatedVisualRows: number; 52 + readonly currentEstimatedVisualRow: number; 53 + readonly isAtBottom: boolean; 54 + readonly anchorLineIndex: number; 55 + readonly anchorSubRow: number; 56 + 57 + appendLine(text: string): number; 58 + resize(columns: number, rows: number): void; 59 + scrollBy(deltaVisualRows: number): void; 60 + scrollToFraction(fraction: number): void; 61 + resolveViewport(): ResolvedViewport; 62 + 63 + getLineDisplayWidth(lineIndex: number): number | undefined; 64 + } 65 + 66 + interface VirtualizerOptions { 67 + measureWidth: (text: string) => number; 68 + maxLines?: number; 69 + columns: number; 70 + rows: number; 71 + } 72 + 73 + interface ViewportEntry { 74 + lineIndex: number; 75 + text: string; 76 + wrapPoints: number[]; 77 + totalSubRows: number; 78 + firstSubRow: number; 79 + visibleSubRows: number; 80 + } 81 + 82 + interface ResolvedViewport { 83 + entries: ViewportEntry[]; 84 + totalEstimatedVisualRows: number; 85 + currentEstimatedVisualRow: number; 86 + isAtBottom: boolean; 87 + } 88 + ``` 89 + 90 + Notes: 91 + 92 + - `appendLine()` returns the assigned monotonic `lineIndex`. 93 + - `resolveViewport()` reads `columns` and `rows` from internal state and takes no parameters. 94 + - `getLineDisplayWidth()` is a current implemented API for exposing cached per-line display width. It returns `undefined` for evicted or never-assigned indices. 95 + 96 + ## 4. Width Model And ANSI Handling 97 + 98 + The current implementation expects `measureWidth(text)` to be: 99 + 100 + - synchronous 101 + - ANSI-agnostic 102 + - additive across concatenation 103 + - based on per-codepoint `wcwidth` semantics 104 + 105 + The virtualizer owns ANSI CSI/OSC skipping internally. 106 + 107 + Recognized sequences: 108 + 109 + - CSI: `ESC [` ... final byte in `0x40..0x7E` 110 + - OSC: `ESC ]` ... terminated by BEL or `ESC \` 111 + 112 + Current behavior: 113 + 114 + - recognized CSI/OSC bytes are skipped for both cached display width and wrap computation 115 + - `measureWidth` is called only on visible codepoints 116 + - unrecognized escape families are not skipped 117 + - no ANSI style state is carried between lines 118 + 119 + Implementation note: 120 + 121 + - unterminated CSI/OSC sequences are currently consumed to the end of the string by the scanner in [`virtualizer/ansi-scanner.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/ansi-scanner.ts) 122 + 123 + ## 5. Data Model 124 + 125 + The virtualizer maintains: 126 + 127 + - a ring buffer of `{ text, displayWidth, lineIndex }` 128 + - `baseIndex` 129 + - a monotonic next-line counter 130 + - current `columns` and `rows` 131 + - anchor state: `(anchorLineIndex, anchorSubRow, isAtBottom)` 132 + - `totalEstimatedVisualRows` 133 + - `currentEstimatedVisualRow` 134 + - a wrap cache `Map<lineIndex, wrapPoints>` 135 + 136 + Current storage implementation lives in [`virtualizer/ring-buffer.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/ring-buffer.ts). 137 + 138 + Current empty-buffer behavior: 139 + 140 + - `lineCount === 0` 141 + - `totalEstimatedVisualRows === 0` 142 + - `currentEstimatedVisualRow === 0` 143 + - `isAtBottom === true` 144 + - `resolveViewport().entries` is empty 145 + - `scrollBy()` and `scrollToFraction()` are no-ops 146 + 147 + ## 6. Operations 148 + 149 + ### 6.1 `appendLine(text)` 150 + 151 + Current implementation flow: 152 + 153 + 1. Compute `displayWidth` with ANSI skipping. 154 + 2. If the buffer is at capacity, evict the oldest line before insertion. 155 + 3. Delete the evicted line’s wrap-cache entry. 156 + 4. Decrement `totalEstimatedVisualRows` by the evicted line’s estimated rows. 157 + 5. Adjust anchor and current estimate if eviction occurred before the anchor. 158 + 6. Store the new line with the next monotonic `lineIndex`. 159 + 7. Increment `totalEstimatedVisualRows` by `max(1, ceil(displayWidth / columns))`. 160 + 8. If `isAtBottom`, advance anchor to the new line at sub-row 0. 161 + 162 + ### 6.2 `scrollBy(deltaVisualRows)` 163 + 164 + Current implementation: 165 + 166 + - walks exact sub-rows using cached or freshly computed wrap points 167 + - clamps at top and bottom 168 + - clears `isAtBottom` when scrolling up from bottom 169 + - sets `isAtBottom` when clamped to the last exact sub-row of the last line 170 + - recomputes `currentEstimatedVisualRow` from estimated line prefixes plus the anchor sub-row 171 + 172 + ### 6.3 `scrollToFraction(fraction)` 173 + 174 + Current implementation: 175 + 176 + - maps the fraction into an estimated target row 177 + - walks from `baseIndex` using estimated rows per line 178 + - lands on an estimated sub-row within the selected line 179 + - recomputes `currentEstimatedVisualRow` after updating the anchor 180 + 181 + ### 6.4 `resize(columns, rows)` 182 + 183 + Current implementation: 184 + 185 + - clears the wrap cache on column change 186 + - recomputes `totalEstimatedVisualRows` by summing `ceil(displayWidth / columns)` 187 + - updates `columns` 188 + - recomputes `currentEstimatedVisualRow` 189 + - updates `rows` 190 + 191 + Current implementation detail: 192 + 193 + - anchor sub-row clamping is currently based on the estimated row count for the anchor line at the new width, not an exact re-wrap of that line 194 + 195 + ### 6.5 `resolveViewport()` 196 + 197 + Current implementation: 198 + 199 + 1. Walks forward from the anchor, filling as many rows as possible. 200 + 2. If the forward walk does not fill the viewport, it backfills upward: 201 + - first by expanding the anchor line upward when `anchorSubRow > 0` 202 + - then by including preceding lines 203 + 3. Returns ordered `ViewportEntry` records plus estimation metadata. 204 + 205 + Each `ViewportEntry` contains the original `text`, all wrap points for that line, and a visible sub-row window. 206 + 207 + ## 7. Output Invariants The Implementation Intends To Satisfy 208 + 209 + The current test suite checks the following output invariants through public API behavior: 210 + 211 + - `entries` ordered by ascending `lineIndex` 212 + - `text` preserved exactly 213 + - `wrapPoints` strictly increasing 214 + - no wrap point inside surrogate pairs 215 + - no wrap point inside recognized CSI/OSC 216 + - `totalSubRows === wrapPoints.length + 1` 217 + - `firstSubRow` and `visibleSubRows` stay within bounds 218 + - total visible sub-rows do not exceed the viewport row budget 219 + - every visible slice fits within `columns` 220 + - output can be reconstructed from `text + wrapPoints` 221 + 222 + ## 8. Current Implementation Notes And Known Gaps 223 + 224 + These notes document the current branch shape rather than defining desired future contract. 225 + 226 + - The renderer-side width provider is exposed as `createDisplayWidth(): Promise<(text: string) => number>`, not a directly callable synchronous top-level export. 227 + - `getLineDisplayWidth()` is currently public to support direct verification of cached display width. 228 + - The wrap golden fixture for `abc文d` is currently exercised at `columns = 4` in [`virtualizer/test/wrap-golden.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/wrap-golden.test.ts), reflecting the corrected boundary case used by the branch’s tests rather than the older draft wording from Downloads. 229 + - The implementation and tests should be treated as the source of truth for current behavior where they diverge from the older draft. 230 + 231 + ## 9. Source Alignment 232 + 233 + This file replaces the earlier draft-only location in `~/Downloads` with an in-repo copy under [`specs/`](/Users/tarasmankovski/Repositories/frontside/clayterm/specs). It is intended to track the implemented branch and should be updated together with public API or behavioral changes in [`virtualizer/`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer).
+216
specs/clayterm-virtualizer-test-plan.md
··· 1 + # `@clayterm/virtualizer` Current-State Test Plan 2 + 3 + **Status:** Current-state test plan for the implementation in this repository. 4 + 5 + This document was imported from the draft virtualizer test plan in `~/Downloads` and updated to reflect the test suites, filenames, and assertion strategy that exist on `feat/virtualizer-v1`. 6 + 7 + ## 1. Scope 8 + 9 + This plan covers: 10 + 11 + - root renderer width-provider tests for `createDisplayWidth()` 12 + - virtualizer conformance-style tests in [`virtualizer/test/`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test) 13 + - golden fixtures 14 + - real-world ANSI fixtures 15 + - property-style randomized tests 16 + - informational benchmark gates 17 + 18 + Current suite count: 19 + 20 + - root renderer width tests: 7 21 + - virtualizer tests: 113 22 + 23 + These counts reflect the current branch contents. 24 + 25 + ## 2. Test Inventory By File 26 + 27 + Root package: 28 + 29 + - [`test/width.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/test/width.test.ts): renderer width-provider tests for `createDisplayWidth()` 30 + 31 + Virtualizer package: 32 + 33 + - [`virtualizer/test/invariants.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/invariants.test.ts) 34 + - [`virtualizer/test/width.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/width.test.ts) 35 + - [`virtualizer/test/ansi.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/ansi.test.ts) 36 + - [`virtualizer/test/append.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/append.test.ts) 37 + - [`virtualizer/test/empty.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/empty.test.ts) 38 + - [`virtualizer/test/viewport.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/viewport.test.ts) 39 + - [`virtualizer/test/wrap-golden.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/wrap-golden.test.ts) 40 + - [`virtualizer/test/ansi-golden.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/ansi-golden.test.ts) 41 + - [`virtualizer/test/real-world-ansi.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/real-world-ansi.test.ts) 42 + - [`virtualizer/test/scroll.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/scroll.test.ts) 43 + - [`virtualizer/test/fraction.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/fraction.test.ts) 44 + - [`virtualizer/test/eviction.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/eviction.test.ts) 45 + - [`virtualizer/test/resize.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/resize.test.ts) 46 + - [`virtualizer/test/exactness.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/exactness.test.ts) 47 + - [`virtualizer/test/property.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/property.test.ts) 48 + - [`virtualizer/test/bench.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/bench.test.ts) 49 + 50 + ## 3. Assertion Strategy 51 + 52 + The current branch uses three broad test styles: 53 + 54 + - deterministic conformance-style tests 55 + - reference-behavior tests for exact formula/provider expectations 56 + - informational benchmarks 57 + 58 + Current observability surfaces used by tests: 59 + 60 + - `appendLine()` return value 61 + - read-only `Virtualizer` state getters 62 + - `resolveViewport()` output 63 + - `getLineDisplayWidth(lineIndex)` 64 + - instrumented `measureWidth` mocks 65 + 66 + The addition of `getLineDisplayWidth()` is important for current coverage because several width assertions now verify cached width directly rather than inferring it from row counts. 67 + 68 + ## 4. Traceability To Current Suites 69 + 70 + ### 4.1 Renderer width-provider 71 + 72 + Covered in [`test/width.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/test/width.test.ts): 73 + 74 + - `R.WIDTH.ascii` 75 + - `R.WIDTH.cjk` 76 + - `R.WIDTH.combining` 77 + - `R.WIDTH.zwj-emoji` 78 + - `R.WIDTH.additivity` 79 + - `R.WIDTH.empty-string` 80 + - `R.WIDTH.zero-width` 81 + 82 + These validate `createDisplayWidth()` as the current reference provider. 83 + 84 + ### 4.2 Core identity and append behavior 85 + 86 + Covered in: 87 + 88 + - [`virtualizer/test/invariants.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/invariants.test.ts) 89 + - [`virtualizer/test/append.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/append.test.ts) 90 + - [`virtualizer/test/empty.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/empty.test.ts) 91 + 92 + Current assertions include: 93 + 94 + - monotonic line indices 95 + - identity survival across eviction 96 + - identity non-reuse 97 + - empty-buffer semantics 98 + - bottom-follow behavior 99 + - append behavior while scrolled up 100 + - wrap-cache stability across append 101 + 102 + ### 4.3 Width and ANSI handling 103 + 104 + Covered in: 105 + 106 + - [`virtualizer/test/width.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/width.test.ts) 107 + - [`virtualizer/test/ansi.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/ansi.test.ts) 108 + - [`virtualizer/test/ansi-golden.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/ansi-golden.test.ts) 109 + - [`virtualizer/test/real-world-ansi.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/real-world-ansi.test.ts) 110 + 111 + Current assertions include: 112 + 113 + - `measureWidth` never sees recognized ANSI bytes 114 + - cached display width excludes recognized CSI/OSC 115 + - unrecognized escape forms are not skipped 116 + - wrap points never land inside recognized sequences 117 + - no ANSI state leaks across lines 118 + - captured ANSI fixtures behave consistently at multiple widths 119 + 120 + ### 4.4 Viewport invariants and wrapping fixtures 121 + 122 + Covered in: 123 + 124 + - [`virtualizer/test/viewport.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/viewport.test.ts) 125 + - [`virtualizer/test/wrap-golden.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/wrap-golden.test.ts) 126 + 127 + Current assertions include: 128 + 129 + - ordering and text preservation 130 + - wrap-point monotonicity and bounds 131 + - surrogate-pair safety 132 + - visible row budget 133 + - slice width within columns 134 + - self-contained reconstruction 135 + 136 + Current fixture note: 137 + 138 + - the branch’s `G.WRAP.cjk-boundary` fixture uses `abc文d` at `columns = 4`, matching the corrected test expectation in code 139 + 140 + ### 4.5 Scroll, fraction, eviction, and resize 141 + 142 + Covered in: 143 + 144 + - [`virtualizer/test/scroll.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/scroll.test.ts) 145 + - [`virtualizer/test/fraction.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/fraction.test.ts) 146 + - [`virtualizer/test/eviction.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/eviction.test.ts) 147 + - [`virtualizer/test/resize.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/resize.test.ts) 148 + 149 + Current assertions include: 150 + 151 + - top and bottom clamping 152 + - `isAtBottom` transitions 153 + - fractional jumps to top and bottom 154 + - eviction anchor recovery 155 + - viewport stability when eviction removes older lines 156 + - cache invalidation across resize 157 + - `displayWidth` stability across resize via `getLineDisplayWidth()` 158 + 159 + ## 5. Property And Benchmark Coverage 160 + 161 + Property-style randomized coverage lives in [`virtualizer/test/property.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/property.test.ts): 162 + 163 + - identity monotonicity and non-reuse 164 + - eviction stability 165 + - append independence from scroll position 166 + - viewport invariants after random operations 167 + - estimation-field constraints 168 + - resize invariant preservation 169 + 170 + Informational benchmark coverage lives in [`virtualizer/test/bench.test.ts`](/Users/tarasmankovski/Repositories/frontside/clayterm/virtualizer/test/bench.test.ts): 171 + 172 + - Gate 1: width-provider overhead 173 + - Gate 2: extended ANSI corpus 174 + - Gate 3: estimate vs exact wrap-count match rate 175 + - Gate 4: `scrollToFraction` performance 176 + - Gate 5: skip-optimization comparison 177 + - Gate 6: ANSI-heavy viewport resolution ratio 178 + - Gate 7: resize performance 179 + 180 + These are informational measurements, not conformance gates. 181 + 182 + ## 6. Commands 183 + 184 + Run root width-provider tests: 185 + 186 + ```sh 187 + deno test --allow-read test/width.test.ts 188 + ``` 189 + 190 + Run virtualizer tests: 191 + 192 + ```sh 193 + cd virtualizer && deno test 194 + ``` 195 + 196 + ## 7. Current-State Notes 197 + 198 + This plan is intentionally aligned to the branch implementation, not the older Downloads draft verbatim. 199 + 200 + Notable updates from the older draft: 201 + 202 + - width assertions now use `getLineDisplayWidth()` directly where the branch exposes it 203 + - the renderer width-provider surface is `createDisplayWidth()` rather than a direct synchronous export 204 + - the current suite count is lower than the draft’s projected count because the implementation consolidates multiple properties into fewer files and parameterized tests 205 + - the `wrap-golden` boundary fixture has been corrected to the current checked-in test case 206 + 207 + ## 8. Maintenance 208 + 209 + This file should be updated when: 210 + 211 + - the `Virtualizer` public API changes 212 + - test files are renamed or reorganized 213 + - current assertion strategy changes 214 + - additional real-world fixtures or benchmark gates are added 215 + 216 + The in-repo `specs/` directory is now the canonical location for these virtualizer docs.