ctate/code-diff
340 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9f832c765b | Fix code diff metadata and selection layering | ||
|
|
904bafeb16 |
Merge origin/main into code-diff
- Bring in the disabled-button theme updates from main. - Regenerate the docs WASM bundle with both the theme fixes and code-diff scenes. |
||
|
|
0d0cf1e65c |
Add diff annotations to code component
- Add Geist-style added and removed line annotations across the Zig and markup APIs. - Preserve diff rendering through layout, editing, scrolling, and retained invalidation with focused tests. - Ship dedicated light/dark docs previews, rebuilt live WASM, documentation, and a changelog fragment. |
||
|
|
a14d225f0f |
fix(canvas): match disabled button themes (#265)
* fix(canvas): match disabled button themes - Match shadcn and Geist disabled colors across button variants. - Add regression coverage and refresh the docs WASM preview. * fix(canvas): remove disabled primary outline - Keep the implicit shadcn primary border transparent when disabled. - Refresh regression coverage and the docs WASM preview. * fix(canvas): honor disabled button border overrides * docs: refresh component preview wasm * fix(canvas): preserve disabled button overrides |
||
|
|
8fc933b9db |
fix(windows): accelerate gpu surface presentation (#258)
* fix(windows): accelerate gpu surface presentation - Render retained binary canvas packets through Direct2D and DirectWrite. - Preserve dirty-region updates and GPU effects with a safe software fallback. - Wire and validate the renderer across supported Windows build paths. Co-authored-by: Omer Shatzberg <131801941+oshtz@users.noreply.github.com> * fix(windows): harden Direct2D presentation * fix(sdk): separate GPU backend request types * fix(windows): honor GPU surface fallback contracts * fix(windows): scale transformed blur kernels * fix: harden Windows GPU surface presentation * fix(windows): harden gpu blur and caption sampling * fix(runtime): bypass packets for software surfaces * fix(windows): preserve precise gpu surface updates * fix(windows): reconcile GPU presenter state * fix: preserve retained canvas resources * fix: preserve Windows canvas packet fidelity --------- Co-authored-by: Omer Shatzberg <131801941+oshtz@users.noreply.github.com> |
||
|
|
8600d7e5d5 |
fix: align Geist tabs with design system (#259)
* fix: align Geist tabs with design system - Match Geist primary-tab sizing, spacing, icon treatment, and full-width rails while preserving default-theme pills. - Add a scrollable component explorer with focused specimen views and a content-hugging default theme picker. - Expand layout, rendering, semantics, interaction, and pixel-regression coverage for both themes. * build: refresh docs WASM preview - Recompile the checked-in component preview module against the updated Geist tab renderer. - Verify the production docs build and live WASM scene instantiation. * fix: extend Geist tab rails in flow layouts - Let primary Geist tab lists claim available row and column width while preserving default-theme flex behavior. - Add flow-layout regression coverage and refresh the docs WASM preview. * docs: correct tabs markup example - Compare the tab enum against quoted tag literals and show content for every tab. - Use the cross-theme row and spacer composition for compact house tabs and full-width Geist rails. * fix(canvas): correct tree, scroll, and tab layout * fix(canvas): keep tree indentation author-owned * fix(canvas): align Geist tab underlines with content * fix(canvas): preserve wide Geist tab rails * fix: preserve tab layout intent across themes * fix(canvas): bound indented Geist tab rails * fix: preserve Geist tab layout contracts |
||
|
|
bdfce1ea7d |
ci: switch to vercel setup-zig (#255)
- Replace Zig setup across CI and release workflows. - Keep generated scaffolds, tests, docs, and changelog aligned. |
||
|
|
8f1da1831b |
fix: keep Windows effect spawns hidden (#253)
* fix: keep Windows effect spawns hidden - Pass CREATE_NO_WINDOW for background Effects.spawn children on Windows. - Verify redirected child output remains available without an attached console. - Document the user-visible Windows fix in a changelog fragment. Co-authored-by: Omer Shatzberg <131801941+oshtz@users.noreply.github.com> * test: run Windows effect spawn probe in CI --------- Co-authored-by: Omer Shatzberg <131801941+oshtz@users.noreply.github.com> |
||
|
|
476173b6b5 |
docs: add canonical machine-readable routes (#248)
* docs: add canonical machine-readable routes - Move documentation under /docs with permanent legacy redirects and explicit canonical metadata. - Serve synchronized .md siblings and llms.txt from the canonical MDX sources. - Gate redirects, metadata, sitemap entries, and internal links against SEO regressions. * fix(docs): harden canonical route migration * fix(docs): preserve query strings in legacy redirects * fix(docs): preserve MDX content in markdown routes * fix(docs): decode MDX string expressions |
||
|
|
19519dd5ea |
chore: prepare v0.7.1 (#247)
- Synchronize the CLI, core, platform, and example package versions. - Merge pending changelog fragments into the marked v0.7.1 release notes. - Credit release contributors and retire the v0.7.0 release markers.v0.7.1 |
||
|
|
04b97cc2b7 |
feat: add native code editor example (#245)
* feat: add native code editor example - Add an editable syntax-highlighted code surface with robust selection, large-file rendering, and expanded language coverage. - Add the declarative Code Editor example with folder navigation, previews, permanent tabs, inline rename, save, and multi-window support. - Extend tree interactions, macOS folder picking, component docs, tests, and live WASM previews. * fix: address code editor review findings * fix code editor large-file stability * fix: harden large code editor state * fix: polish code editor interactions * fix: title empty code explorer windows * fix: stabilize code editor rendering * fix: harden code editor edge cases * fix: address code editor review findings * fix: resolve remaining code editor review findings * fix: harden code editing and click handling * fix: balance code editor tree spacing * fix: load code editor folders on demand |
||
|
|
8014e796a4 |
CI: the compiled-core parity battery gates every PR, with determinism fences proven to fire (#246)
* compiled-core builds pin the external toolchain release - tests/compiled-core/core_compiler_pin is the one place the release pin lives - build_core.sh verifies the supplied command reports that release and refuses any other, because the profile's fence table is release-pinned data * determinism fences get a pinned negative control - fence_check.sh compiles the pristine markup fixture and asserts its co-emitted sidecar attests deterministic: true - the same fixture with one injected ambient read (Date.now() in update) must refuse, naming the fenced surface id, with no archive or sidecar emitted - skip-clean when no external toolchain is supplied, like the parity battery * CI: compiled-core parity battery runs on every PR - new ubuntu job installs the pinned external core compiler, stages the contract artifacts, and builds all five fixture cores through build_core.sh - runs the determinism-fence negative control, then the full test-compiled-core-parity battery over the compiled archives - the release pin is read from tests/compiled-core/core_compiler_pin, so a bump stays a one-line change * comments carry prose unwrapped - one line per comment paragraph across the new CI job, the pin check, and the fence-check script * ci: isolate the dispatch-budget measurement from runner contention - The parity battery runs its test binaries serially in CI: five suites racing on a two-core runner turn scheduler contention into dispatch-budget failures the budgets were never meant to catch. - A budget failure now prints the measured whole-pipeline and core-only nanoseconds, so a real regression and a noisy runner are distinguishable from the log alone. * test: the paired lane budgets a paired dispatch - The paired-core module declares paired_lanes, the staged root re-exports it, and the soundboard dispatch pin reads it: a dispatch that runs both lanes and byte-compares every seam gets half the frame budget, while the single-lane suite keeps the strict microseconds-class pin. - The measured CI shape that set the number: core-only paired dispatch at just over the single-core budget on a shared runner, with the whole pipeline well inside its frame. |
||
|
|
a59015a246 |
corewire --facade emits the complete compiled-core entry; hand adapters deleted (#244)
* Contract sidecar: carry authored payload member names and type origins - The transpiler emits payload_members on each union (the authored member name of every single-payload arm) and a module-level type_origins table (the declaring module of every named contract-table type). - The sidecar extractor reads both tables into additive fields: "member" on message and union arms, "origin" on type-table entries. - The sidecar reader parses both as optional facts, so older sidecars keep reading clean. * corewire --facade emits the complete compiled-core entry module - The facade is now the generated twin of a hand-written adapter: it imports the author's core module, re-exports every named contract type from its declaring module, wraps init/update/subscriptions and the model helpers, and implements the full ABI dispatch surface (boot_cmd, the nine dispatch entries, the wired channel entries, subscriptions, model_snapshot, helper_call) over one committed model, with the inline wire codec and the guard-and-trunc wholeness proof at every i64-classed ingress. - The profile designates the facade's own entries (init/coreUpdate, coreSubscriptions only when the contract subscribes), maps plain export names to prefixed symbols, and carries the contract's integer_slots through; --f64-slot demotes a named record slot to f64 across the whole invocation for values that reach the f64-exact boundary. - The sidecar reader accepts the external compiler's additive synthesized marker on struct entries. * Compiled-core lane builds from generated facades and profiles - build_core.sh stages the generated entry module and compiler profile from zig-out/core-contracts (stage-core-contracts now installs core_profile.json beside the sidecar and facade, with the host fixture's Model.pastBytes carried as f64 — it holds 2^53 by design, past the honest i64 window). - The five hand adapters, the shared wire codec, and the hand profiles are deleted: the generated facade carries the whole surface, and the parity batteries hold every byte to the transpiler lane. - The conformance suite keeps the mirror axes (fingerprints, contract artifacts, envelope unpacking); the compiled side's encodings are proven at full behavioral depth by the parity batteries, whose entry the generated facade now is. The hand markup contract gains the member and origin facts. * fix(corewire): harden generated facade contracts * fix(corewire): harden facade dispatch projection * fix(corewire): harden generated facade contracts * fix: accept legacy sidecars in parity tests * fix: close corewire facade gaps * fix(corewire): avoid false subscription inference * fix(corewire): preserve unbound name precedence * fix(corewire): align effective facade contracts |
||
|
|
ad6fa36f3e |
Compiled-core parity: fixture profiles declare and prove their integer slots (#241)
* test: declare i64 slots in the markup and host-fixture core profiles - The shared wire codec, the markup adapter, and the markup frame channel prove decoded integer slots in place: bind the value, range-guard it with ordered comparisons, and state wholeness with Math.trunc at the write. - Fixture counter bumps saturate at the i64 class's provable ceiling, ±(2^53 − 1), so range discharges. - Model.pastBytes stays f64-classed: it holds 2^53 by design, past the provable window. * test: declare i64 slots in the ai-chat core profile - The text SDK gains one provable selection constructor: every editor-produced selection is range-guarded and stated whole with Math.trunc, keeping the proof local to the construction site. - The ai-chat composer binds composition bounds and proves them at the write; -1 stays the no-composition sentinel. - The chat_response status and nextId bumps prove in place with the same guard-and-trunc idiom. * test: declare i64 slots in the system-monitor core profile - Samples, probe results, and ps rows bind, range-guard, and Math.trunc their counts at each classed write; unprovable values keep the previous sample or skip the line like any other malformed row. - The adapter proves dispatch values and classed helper returns in place with the same guard-and-trunc idiom. - Structurally identical union arms lower to one record shape, so one code slot and one pid slot carry the class for their siblings (SC4009 keeps colliding declarations out). * test: declare i64 slots in the soundboard core profile - Catalog ids, playback clocks, and queue counters bind, range-guard, and Math.trunc at each classed write; the clock-tick cap comparison replaces Math.min so the proof stays in view. - The adapter proves its ten classed helper returns in place with the same guard-and-trunc idiom. - The id-carrying Msg arms share one lowered record shape with QueueEntry, which carries the class for all of them (SC4009 keeps colliding declarations out). * test: carry i64-classed slots on the i64 wire encoding - Snapshot, helper-result, record, and channel encoders write classed slots with wI64 (optionals via wOptionalI64), matching the contract each adapter now attests. - Text-input decoding reads selection offsets and composition cursors as i64; selection offsets ride a saturating reader because select-all sends the maxInt to-the-end sentinel, which every consumer snaps to the text's length. - Classed helper results route through their proven wrappers and encode with wI64. * test: declare the coalesced arm slots and prove the decode ingress - The ten same-shaped Msg arm slots join the soundboard and system-monitor profiles, so the declared set covers every provable reference slot and only the deliberate 2^53 boundary probe stays f64. - Decode ingress proves in place where synthesized record slots carry write obligations: soundboard's id dispatches and audio clocks, and the shared composition-cursor decode, each range-guarded with wholeness stated by Math.trunc. - The ai-chat and markup profiles drop a subscriptions_export naming a function their entry modules never exported; a dangling name refuses instead of resolving silently. * test: preserve compiled-core numeric dispatch classes |
||
|
|
7636ec3686 |
chore: prepare v0.7.0 release (#239)
- Sync the CLI, core, examples, and platform packages to 0.7.0. - Merge release notes and mark the v0.7.0 changelog entry.v0.7.0 |
||
|
|
bd3aab4b48 |
feat(canvas): add reusable code component (#235)
* feat(canvas): add reusable code component - Add highlighted code surfaces with optional line numbers and horizontal scrolling. - Route Markdown fences through Code while preserving indentation and first-line list alignment. - Cover the markup API with tests, documentation, previews, and changelog fragments. * fix(canvas): preserve multiline code rendering * fix(canvas): bound code layout capacity * fix(canvas): bound code span retention * fix(canvas): address code component review feedback * fix(canvas): polish code block rendering * fix(canvas): address remaining code review feedback * fix(canvas): preserve long code rendering * fix(canvas): preserve numbered code selection * fix(canvas): fold long span selections * fix(canvas): preserve empty code and paged selection * fix(canvas): address code rendering review findings * fix(canvas): resolve remaining code review issues * fix: bound transformed code rendering |
||
|
|
6a871356b4 |
ci: harden Windows canvas snapshot reads (#234)
- Retry structural snapshot reads that race frame publication. - Preserve Wine caption-offset geometry for reliable textbox input. |
||
|
|
c1bad63c5f |
chore: prepare v0.6.3 release (#233)
- Sync all CLI, core, platform, and example version references to 0.6.3. - Merge the textarea fixes into the marked v0.6.3 changelog entry.v0.6.3 |
||
|
|
3636af4b45 |
fix: improve textarea editing shortcuts (#231)
* fix: improve textarea editing shortcuts - Align Command and vertical arrow navigation with native multiline behavior. - Add bounded undo and redo while keeping controlled text buffers synchronized. - Cover retained runtime and markdown-viewer editing paths. * fix: harden textarea editing behavior * fix: harden textarea editor lifetimes * fix(canvas): harden textarea history and navigation * fix textarea wrap navigation and macOS undo menu * fix textarea navigation and history edge cases * fix textarea reconciliation failure atomicity * fix(runtime): make canvas text history replay transactional * fix: preserve textarea indentation and shift-click selection * fix: harden textarea navigation and selection * fix: harden textarea caret boundaries * fix: harden textarea history and CRLF editing * fix(canvas): keep carets outside CRLF boundaries * fix(core): keep CRLF edits atomic * fix(canvas): preserve CRLF editing boundaries * fix(canvas): match native selection navigation * fix(runtime): reroute compound textarea history events * fix(runtime): normalize reconciled CRLF selections * fix textarea left navigation across soft wraps * Fix IME history across CRLF boundaries |
||
|
|
ef1f8d9cdd |
chore: prepare v0.6.2 release (#230)
- Bump CLI, core, and platform package versions to 0.6.2. - Merge overlay and container background notes into the release changelog. - Credit release contributors and rotate release markers.v0.6.2 |
||
|
|
868d0116b0 |
feat: support reliable desktop overlay windows (#228)
* feat: support reliable desktop overlay windows - Add transparent, always-on-top, click-through, and passive-show window options across manifests and runtime APIs. - Apply overlay state before first visibility on macOS, Windows, and Linux, including first-present reveal behavior. - Cover the contract with runtime tests, SDK typings, documentation, and a changelog fragment. Co-authored-by: Jason Kneen <502002+jasonkneen@users.noreply.github.com> * fix: honor desktop overlay window options * fix: harden desktop overlay window presentation * fix: preserve overlay activation and alpha * Fix overlay window presentation * fix: correct overlay focus and transparency * fix: keep overlay windows sourceless * fix: use current GTK CSS loader * fix: make transparent overlays honest * fix(macos): preserve transparent canvas fallback * fix(windows): preserve transparent resize hit testing * fix(macos): activate before making window key * fix(linux): own transparent window CSS provider * fix: let transparent overlays idle * fix: keep transparent overlays canvas-only * fix(macos): activate app before focusing window --------- Co-authored-by: Jason Kneen <502002+jasonkneen@users.noreply.github.com> |
||
|
|
d866d922eb |
Fix layout container background rendering (#227)
- Paint explicit stack, row, and column backgrounds before their children. - Preserve configured corner radii in tree and retained-layout emission. - Cover all three container kinds with regression tests. Co-authored-by: Sepehr Safari <25853688+sepehr-safari@users.noreply.github.com> |
||
|
|
a7509a7fa6 |
chore: prepare v0.6.1 release (#214)
- Sync CLI, core, platform, and example package versions to 0.6.1. - Merge pending fixes into the marked v0.6.1 release notes and credit contributors.v0.6.1 |
||
|
|
ea98365a2d |
fix: sync pointer text selections to model (#213)
- Route pointer caret and selection changes through on-input. - Cover workbench address deletion with an end-to-end regression. - Update session replay expectations and changelog. |
||
|
|
7b5b226fb2 |
ci: parallelize native example coverage (#212)
- Split native example tests across four build-graph-backed CI shards. - Run the Windows web-layer audit in parallel while preserving the aggregate required check. |
||
|
|
5f48ec3f67 |
fix: respect layered webview cursors (#211)
- Yield macOS GPU cursor regions to higher-layer embedded webviews. - Prevent canvas cursor updates from overriding WebKit link and text cursors, with regression coverage. |
||
|
|
514ce820da |
chore: prepare v0.6.0 release (#210)
- Sync CLI, core, platform, and example package versions to 0.6.0. - Merge pending fragments into the marked v0.6.0 release notes. - Credit release contributors and retire the v0.5.4 markers.v0.6.0 |
||
|
|
db34c23ea5 |
fix: sync webview focus with canvas panes (#209)
* fix: sync webview focus with canvas panes - Report native webview focus changes across macOS, Linux, and Windows. - Preserve and journal canvas focus state with a workbench pane regression. * fix: sync CEF webview focus - Report actual Chromium browser focus through the shared runtime event. - Guard stale child generations and cover the CEF host seam. |
||
|
|
daefe82106 |
fix terminal macOS shortcuts (#208)
* fix terminal macOS shortcuts - Map Option/Cmd navigation and Cmd+Delete to shell-native sequences. - Route Cmd+V through the bracketed-paste-aware terminal input path. - Add runtime and example regressions for modified keys and paste. * fix terminal key release handling on macOS |
||
|
|
4c3fb0bc30 |
fix: add terminal copy/paste context menu (#207)
* fix: add terminal copy/paste context menu - Present standard Copy and Paste actions for terminal widgets. - Route emulator selections to the clipboard and pasted text to PTY input. - Add regression coverage and a changelog fragment. * Fix terminal context menu paste handling |
||
|
|
a43855001a |
fix: hide workbench terminal focus rule (#206)
* fix: hide workbench terminal focus rule - Blend the full-bleed terminal focus ring into the pane while preserving autofocus. - Pin the styling with a workbench regression and document the user-visible fix. * fix: reflect terminal focus in cursor * fix: complete terminal focus handling * fix terminal cursor keyboard ownership * fix: preserve terminal keyboard focus on Windows |
||
|
|
a0947a227d |
fix: keep terminal Tab input in the PTY (#204)
* fix: keep terminal Tab input in the PTY - Keep focus on active terminal components while routing Tab and Shift+Tab to the PTY. - Prevent focus-entry keys from reaching the shell and add router plus live-session regressions. * fix: preserve terminal Tab traversal boundaries * docs: clarify terminal Tab gesture handling |
||
|
|
d84536d04a |
Fix terminal text selection (#203)
- Add drag, word, and line selection backed by Ghostty's tracked cell model. - Copy active terminal selections without forwarding the clipboard chord to the child. |
||
|
|
beb1d8712a |
Compiled-core parity: every core fixture built externally and proven against the transpiler (#197)
* Add per-fixture compiled-core parity batteries and contract staging - PairedCore lockstep module: a fixture's e2e battery runs over both lanes, byte-comparing commands, snapshots, subscriptions, channels, and helpers per cycle - build.zig gains env-gated test-compiled-core-parity (per-fixture archive/sidecar pairs) and stage-core-contracts for external toolchains - paired roots are generated from each transpiled module's own export surface, so channel detection never goes stale * Compile the ai-chat core with an external toolchain and pass its battery - adds a hand-authored adapter entry, core profile, shared wire codec, and a static restatement of the core module for library-mode compiles - build_core.sh stages author sources with specifier resolution, readonly-array erasure, and byte-alias folding, then records cold and warm compile times - ai-chat update always returns the [model, cmd] tuple and api.ts iterates bytes by index * Compile the host fixture's core externally and pass its full battery - hand-authored host-fixture adapter and profile: 53 arms, four record payloads, the boot command, and the model-gated timer subscription - the fixture's update returns the [model, cmd] tuple on every arm - staging drops any SDK type alias the author's own sources declare; the mirror conversion walk raises its comptime branch budget * Corpus cores return the [model, cmd] pair on every update path - soundboard and system-monitor update signatures drop the bare-model union arm; every return carries an explicit Cmd - behavior is unchanged: the paths that produced no command now say Cmd.none * Compile the soundboard core externally and pass its battery - hand-authored soundboard adapter and profile: 19 arms, 35 model helpers, the chrome and env channels, and the frame/key channel ABI entries - channel ABI entries take flat parameters and answer the contract's channel flags through their export suffixes, listed after the unconditional exports - staging spells the SDK's event and text records as object-literal aliases so host-constructed channel arms carry value-stored records * Compile the system-monitor core externally and pass its battery - hand-authored system-monitor adapter and profile: 25 arms, 23 model helpers, the boot probe command, and the chrome channel - the sample sorts are explicit stable insertion sorts and the byte scans index instead of iterating, both lanes over one source * Compile the markup fixture's core externally and pass its battery - Add the markup adapter, profile, and build_core.sh case: the corpus's first three-function-channel core (frame, key, and pinch). - Add the markup compiled-core battery entry, the mirror image of the host one: the markup core pairs, the host fixture rides transpiler-only. - Return the [model, cmd] pair on every markup update path so the contract's shape flags read from a declared return type. |
||
|
|
2209f022ee |
Live <terminal> sessions, and examples/workbench: a terminal beside a browser (#198)
* Bring libghostty-vt back behind a consumer-safe terminal_vt seam
- The framework module imports terminal_vt everywhere: a ghostty-vt wrapper where the dependency is safe to traverse, a stub (enabled=false) elsewhere
- Root builds resolve a lazy ghostty pin gated on being the build root, so consumers running this build script as a dependency never touch ghostty's graph
- Apps opt in via addAppArtifacts .ghostty_vt with their own pin; scaffolded builds keep the stub and stay free of harfbuzz/translate_c
* Live <terminal> sessions: the runtime-owned emulator behind a bound pty key
- terminal_session.zig: per-pty-key libghostty-vt sessions publishing resolved TerminalGrid snapshots, with the example tier's lossless outbound ring, query-answer write-back, key encoding, and theme-derived palette promoted into the runtime
- The effects engine grows a pty delivery tap (live drain and replay feed alike), so emulator state derives from exactly the journaled stream; outbound bytes ride the journaled ptyWrite verdict path
- The app loop installs the builder's terminal grid lookup, reconciles cols/rows from each element's laid-out frame into ptyResize, applies the scrollback source-wins echo, routes focused keys/IME text and wheel scrollback to the session, and dispatches on-terminal states
* Terminal session store tests: the element contract at the store seam
- Lookup-driven session creation, fed output as resolved cells, stdin-ordered write-back (keys, text, DSR answers), retained-ring flush, resize reconcile, scrollback source-wins, respawn reset, and wide-cell spacer backgrounds
- Fix the snapshot sizing multiply: @min against a comptime bound refines its result type, so the product needed explicit widening
* The terminal emulator is an app-owned pin, opted into per app
- Replace AppOptions.ghostty_vt with terminal_sessions: bool; the framework resolves the app's own lazy ghostty pin with the app module's target/optimize and the safe flags, so an app asks for live <terminal> sessions with one field instead of threading a module
- Drop the ghostty pin from this package's build.zig.zon: a pin here materializes into every consumer's package directory even when lazy and unused (measured 27 packages / 585 MB in a scaffolded app's package dir, now zero), so the toolkit's own builds always carry the terminal_vt stub
- Add src/terminal_session_tests_root.zig so an emulator-wired app build can run the session store's tests, which skip in this package's own suite
* examples/workbench: a live terminal beside a browser in one split
- Add the workbench example: a <split> with a <terminal pty={key}> pane and a browser pane (back, forward, reload, address bar), app-owned navigation history, and a hidden-inset titlebar with per-pane drag bands
- The app spawns a shell with fx.ptySpawn and binds the key in markup; no emulator wiring, key encoding, or grid plumbing lives in the app
- Register test-example-workbench, whose build also runs the runtime session store's tests since this build wires the emulator
* Document live terminal sessions and how a build opts in
- Terminal docs: the runtime owns the emulator behind a bound pty key, plus an "Enabling live sessions" section covering the app-owned lazy pin, terminal_sessions = true, and the terminal_sessions_enabled flag apps can gate tests on
- Add the changelog fragment for the live sessions, the opt-in emulator, and the workbench example
* Workbench: the shell owns the keyboard when the window opens
- Mark the terminal element autofocus, so typing lands in the live session without a click first — a terminal window's keyboard belongs to its shell
* Terminal runs measure the face they actually ink with
- Derive the cell width from a 16-glyph mono probe divided by its length — the advance a merged run walks — and leave it unrounded, so glyphs stay on the cells the painter's backgrounds, cursor, and selection draw from
- Carry the measurement seam on each run: a command's raster extent is its own declared bounds, and the estimator's 0.6 em mono pitch falls short of a wider host face (macOS resolves the mono id to the system monospaced face at 0.618 em when Geist Mono is absent), which sheared a full-width row's last cell to a two-pixel sliver
- Cover both with a painter test that paints a full-width row against a wide-pitch provider and asserts every run's bounds hold its ink
* Workbench README: what the divider drag does to focus
- `stty size` named as the visible proof that the pty re-grids with the pane
- the divider keeps the keyboard after a drag; clicking the terminal returns it
|
||
|
|
cce5359d5b |
corewire --profile: the library-mode compiler profile beside the facade (#196)
* Facade channel entries take the wire shape; the packer becomes nsc_core_pack_msg
- Each wired channel now exports a wire-shaped entry mirroring the ABI
header's C declaration (bytes as buffers, u8 modifier booleans, the
pinch phase as its declaration-order member index) that builds the
event record, runs the channel-function gate, and packs the result.
- The produced-message-or-null packer moves to one export,
nsc_core_pack_msg, freeing the nsc_core_<channel>_msg names for the
wire shapes; conformance drives both surfaces.
- Wired channels fence their new facade declarations (event records,
channel-function names, PinchPhase) against type-table collisions.
* corewire --profile emits the library-mode compiler profile
- emit_profile.zig projects the sidecar into the profile JSON: the abi
block's mode symbols, an export map binding every attested facade
export to its marshalled signature (wire-shaped channel entries
included), and the contract-sidecar section with echoed generations
and identity-getter symbols.
- The determinism block carries the SDK policy as release-pinned data:
deny-fences over the ambient surfaces with teachings naming the
sanctioned Cmd/Sub routes, the shared async teaching, and trap
remediations; emission is deterministic byte-for-byte.
- main.zig grows --profile beside --out/--facade with the same staging,
alias nets, and check-runs-everything discipline.
* Facade slices spell plain arrays; flattened records stay undeclared
- Sequence types spell T[] (the declaration site's readonly already pins
immutability; the readonly-array operator has no contract projection).
- Single-use synthesized records flatten into their one arm literal and
no longer take an interface or a named encoder, keeping the
synthesized names free for consumers that re-derive them.
- The pinch wire entry's export-map signature drops a stray f64: six
parameters, six marshalling classes.
* Name the string constraints by their enforcer in the profile test comment
- The teachings/remediations comment points at the loader's rules
instead of restating provenance.
* Profile and facade carry the whole contract a library-mode consumer reads
- Export-map names keep the facade's fixed nsc_core_ spellings while
symbols take the contract's prefix; the profile's entry is the facade
path made relative to the profile's own directory, refusing pairs
with no relative spelling; --profile alone runs the facade emitter's
refusal checks.
- Facade entry stubs carry the contract's declared shapes (cmd-returning
init/update return the [state, effect] tuple, subscribing contracts
declare the subscriptions stub), the unbound facts also ride the split
modelUnbound/msgUnbound consts, and the appearance/chrome/env channel
conventions ride exported consts; the transpiler passes the split
unbound pair through unemitted like viewUnbound.
- The determinism fence table pins to the 0.0.11 surface manifest: the
wall clock (stdlib.date., perf_hooks.) and the process family join
with teachings naming the journaled clock and host-delivered inputs.
* Harden the split-const, entry-path, and profile-string edges
- The transpiler refuses a modelUnbound/msgUnbound constant that is not
a string-literal list: the names are reserved contract vocabulary, and
a data const under them would emit references without a declaration.
- The facade fences the split unbound names only when their consts
actually declare, so a contract type under either spelling projects
whenever no collision exists.
- The profile's relative-entry resolution carries the environment map
(drive-relative spellings resolve against the drive's own working
directory), and a non-UTF-8 entry spelling refuses with a teaching
before it can corrupt the JSON, with an emitter-side backstop.
* Declaration forms spell record storage; path and slot edges pinned
- The facade declares node-stored records as interfaces and value-stored
records as object-literal type aliases, and the transpiler's type
table learns the alias form (a struct exactly like an interface, with
storage still decided by the promotion walk) — a contract emitter
re-deriving storage from declaration form now lands on the contract's
own classes.
- Profile-relative entry spellings convert separators only on Windows
(a POSIX backslash is a filename byte), and the emitter's
release-pinned note records why integer-slot declarations stay
absent: the pinned prover demands inline wholeness proofs at every
construction of a slot-declared shape and does not trust classed-slot
reads, refusing spread updates, structural twins, and the facade's
own constructors alike.
* Unbound vocabulary, singleton unions, and storage edges hold both ways
- The checker refuses references to viewUnbound/modelUnbound/msgUnbound
as module data (the build reads them without emitting, so a reference
would name a missing declaration), symbol-resolved to the module
consts.
- A bare kind-tagged object literal classifies as a one-arm tagged
union, never a struct holding a textual kind.
- The facade fences viewUnbound only when the const declares, refuses a
record referenced by node and value at once (one declaration cannot
state both storages), and refuses a host-constructed channel arm
whose named record cannot flatten into the arm the host fills.
* Reserved-list references resolve by symbol; alias records stay whole
- The reserved-const reference check resolves through import aliases and
shorthand value symbols, catching a renamed binding or `{ modelUnbound }`
by what it declares rather than its spelling; the split pair joins the
entry-only export set and the rename guard.
- Object-literal aliases classify as structs only when every member is a
plain record property (identifier-named, annotated, non-optional);
quoted or optional properties refuse as unsupported aliases instead of
registering a struct with silently missing or wrongly required fields.
- A kind-tagged shape with an optional discriminator is no union: the
source permits the untagged value, so the shape refuses instead of
projecting a mandatory tag.
* Alias form pins value storage in promotion; false attestations refuse a profile
- markPromotions reads declaration form: an object-literal alias keeps
its by-value layout however the Model reaches it (the alias spelling
IS the value-storage form), while interfaces stay with the
reachability walk; pinned with the value/pointer pair test and the
emitted Zig compiles with a by-value model record.
- The profile is enforcement data, so a contract attesting
deterministic: false or async_free: false refuses emission with a
teaching naming the attestation — a compile under the fences attests
true by construction, and emitting would move the contradiction
downstream.
- The transpiler test suite gains pins for the alias/singleton/split-
const behaviors landed this branch.
* Value-record aliases refuse the shapes value storage cannot carry
- NS1061 teaches the whole family at check time: the model root stays
an interface, a model-kept alias holds scalar fields only (heap-backed
fields would dangle across frame resets under the shallow commit),
model arrays carry reference-stored records, identity comparison over
a value record refuses (no reference to compare), and an alias that
reaches itself by value refuses (no finite layout).
- Scalar aliases the model keeps directly and heap-carrying aliases
outside the model tree stay clean; pinned across all six shapes in
the checker suite.
* Entry roots keep their contract shapes; by-value recursion and wrapped identity refuse
- NS1062 teaches at check time when Model does not declare a record or
Msg does not declare a kind-tagged union, whatever declaration form
produced the wrong shape (a plain object alias, an interface Msg, or
a tagged singleton Model), instead of failing inside the emitted
module's dispatch and commit machinery.
- The by-value recursion walk covers every by-value node — value
records and tagged unions alike — so a singleton union reaching
itself refuses; arrays still break cycles by indirection.
- The identity-comparison guard unwraps union operand types and follows
plain type aliases, so a nullable or alias-wrapped value record
refuses the same as a bare one.
* Presence checks pass the identity guard; optional arms and class roots refuse
- The value-record identity guard fires only when BOTH comparison sides
can carry a record at once: a nullable presence check compares the
option and stays exact, so generated optional encoders and idiomatic
null gates compile.
- An optional payload property keeps a shape out of the kind-tagged
union classification (the source permits the absent member while the
emitted arm would demand it; absence spells | null), so the Msg root
teaches instead of requiring an unrepresentable payload.
- The entry-root shape rule covers class declarations: a class named
Msg is a struct and refuses with the union teaching.
* Flattening stays a value move; the root seeds node storage; assertions cannot shed identity
- Synthesized single-use records inline only when referenced by VALUE:
a node-stored payload keeps its named declaration and pointer in both
projections, however its name is spelled.
- The model root seeds the node-stored set (reference storage by
contract), so a value reference to it refuses as mixed storage
instead of re-deriving as node downstream.
- The identity guard's other-side test is structural: an
assertion-erased operand still counts as a record, so shedding the
alias name cannot slip a value-record comparison past the teaching.
* Integer-fixture envelopes pack through nsc_core_pack_msg
- The mixed-class envelope test drives the packer under its own name
and adds the wire-shaped key entry's nothing-produced route, so the
integer fixture exercises both channel surfaces.
* Identity peels assertions; record writes, inert lists, and skewed splits refuse
- The value-record identity guard types the peeled operand expressions
(assertions erase at emission), so a structural respelling on both
sides cannot shed the record's name.
- Record fields have no in-place write in the emitted layout: mutation
through a mutable interface or alias property refuses with the
reconstruction teaching instead of emitting an unassignable store.
- The facade refuses model-kept value records with non-scalar fields
and model sequences of value records, so corewire --check agrees
with what the facade's compilers accept.
- The split unbound consts must restate viewUnbound's resolved facts
exactly (unresolvable and missing entries both teach), and a
reserved-list const declared in an imported module refuses whether
exported or not.
* Identity stops at emission too; singleton unions claim syntactic literal tags only
- Equality emission gains the NS1061 re-derivation: a by-value struct
operand stops the build with the identity teaching, so comparisons
reaching emission through generic instantiation or erased assertions
stop the same way the checker teaches directly (pointer records keep
their identity ==).
- The checker guard reads each operand through both the peeled and the
spelled view, so an assertion that NAMES the record refuses at check
time as well.
- The singleton-union reading claims only a syntactic literal tag: a
record whose kind field resolves through a named literal union stays
the attested struct.
|
||
|
|
dbe15a7472 |
The <terminal> markup built-in: snapshot painter, widget kind, and element surface (#195)
* Canvas terminal grid: the resolved cell model and painter - TerminalGrid snapshots carry resolved cells (cluster bytes, final colors, wide/spacer occupancy) so the painter needs no emulator: real text runs, geometric box drawing, selection wash, cursor register, keyboard caret, and the scrollback thumb, with row-atomic command/text/glyph budgets - terminal_box.zig promotes the example's exact-cell-bounds box geometry (U+2500-259F) into the canvas - TerminalState is the app-visible view-state record (scrollback, history, cols, rows) the terminal element's event delivers * The .terminal widget kind: grid rendering, focus, and the editable-text register - Stable code 62; renders the bound TerminalBinding's grid snapshot through the terminal grid painter (full-bleed background, inset text region, shared per-view budget reserves) and the honest empty surface unbound; the house focus ring when focused - Joins the editable-text register everywhere but the TextBuffer pipeline: hit target, press claimer, focusable, I-beam cursor, textbox role — the emulator, not an editor state, owns the input model - Markup-inexpressible for now (documented exclusion); the <terminal> element entry lifts it * The <terminal> element: registry, both engines, contract, and teachings - Element 69 (widget kind terminal, a11y control) with pty (88, binding-only, required — dead markup otherwise) and scrollback (89, the scroll value source-wins echo); event terminal (13) delivers the new terminal_state payload (canvas.TerminalState) on the element only - Validator, interpreter, and compiled engine stay in lockstep: pty scoping/grammar, scrollback and on-terminal scoping, leaf rule, and the declared-record mirror (declaredTerminalStateRecord) for transpiled cores; contract classifies terminal_state arms and checks on-terminal payloads - Ui.terminal sugar, terminalMsg/translatedTerminalMsg constructors, msgForTerminal, and the TerminalGridLookup seam the app loop installs to resolve bound pty keys into published grid snapshots; re-pinned registry fingerprints via print-pins * Wire libghostty-vt into native framework builds - Adds the ghostty pin to build.zig.zon and imports the ghostty-vt module into every native instantiation of the framework module (desktop, print-pins, and the user-app graph in build/app.zig); the wasm docs preview and pty-unsupported targets leave the comptime-gated import unresolved - Two terminal_grid.zig comments now state the concrete degrade mechanism they describe * Document the <terminal> element and its state contract - Adds the declarative <terminal> section to the terminal docs page (both authoring tiers), covering the pty-key binding, the TerminalState echo, and layout-derived sizing - Changelog fragment for the element, the state contract, and the teachings * Painter lifetime, id/budget correctness, a11y, and saturation fixes - Rounded box corners allocate path elements from the builder's own store (strokePath retains the slice by reference; a stack local dangled for the retained renderer) - Box-cell command ids use an 8-command stride and the per-row reserve counts nine per column (eight box segments plus a background run): a 4-stride collided adjacent double-cross ids and the reserve under-counted, risking a frame-failing overflow instead of row-atomic degrade - Text preflight compares against the builder's live cumulative counter, so a grid sharing the store with earlier widgets degrades against the remaining space - Terminal widget floors its command budget to a positive value (0 means unbounded to the painter); a positive budget below the fixed prologue overhead paints nothing, checked before the prologue emits - A torn row (builder store exhausted mid-row) does not count as painted and stops further rows, so the cursor never draws over dropped content - Screen text rides the widget's text VALUE (semanticTextValue), not the accessible name, so an author label stays the stable control name - translatedTerminalMsg saturates instead of trapping on narrow declared fields - screen_text doc states its true scope (plain-text coverage, not colors/cursor/selection) - Regression tests for path lifetime, adjacent double-cross id uniqueness, and the shared-store text preflight * Terminal painter budget accuracy, id disjointness, and contract completeness - Command budget is checked per row against that row's actual upper-bound cost (rowCommandCost) instead of a flat worst-case-per-column reserve, so a wide cheap terminal paints its rows instead of reserving them all away - Per-row path-element preflight for rounded corners, plus a row-atomic rollback that drops a torn row's partial commands whole so nothing half a row reaches the glass - Box/block cells keep SGR underline (geometry replaces the glyph, not its decoration) - The focus ring on a bound terminal takes its id from the painter's reserved offset, disjoint from every grid command id - Contract validates the pty binding (undefined field caught, valid one marked used) and requires a bare on-terminal tag (an authored payload is dead data); contract format bumped to 3 so stale artifacts regenerate - TerminalBinding.grid documents its borrowed-pointer lifetime contract - Regression tests: wide-terminal painting, focus-ring id disjointness, on-terminal bare-tag * Terminal id-arithmetic wrapping, focus reachability, and audit registration - The prologue budget check accounts for commands already in the builder, so a full builder degrades to nothing instead of overrunning its absolute ceiling - All command-id arithmetic wraps (paintIdBase spans the full u64 space, so a base near maxInt no longer traps on a checked add) in both the grid painter and the box geometry - Horizontal box/block merging breaks on an underline mismatch, so a run keeps each cell's decoration instead of spreading the first cell's - Pointer, autofocus, and automation focus reveal the terminal's ring (a new canvasWidgetShowsPointerFocusRing predicate covers editable text plus terminal), so the ring is reachable by click, not only Tab - The focus-paint dirty-bounds register and the minimum hit-target audit both include the terminal, so a focus enter/leave damages the ring region and an undersized terminal is flagged - Regression tests for the builder-aware prologue guard and near-maxInt id wrapping * Terminal budgets count view-global text and path totals; merge-aware cost - rowCommandCost mirrors horizontal box merging: a long mergeable run (a border of identical box glyphs) costs one geometry command plus an underline, not nine per column, so a wide box row paints instead of being skipped on a bogus estimate - Text and path-element preflights degrade against the whole display list (every draw_text and fill/stroke_path, referenced sibling and static content included), matching the runtime's per-view CanvasResourceCounts limits, so a terminal sharing a view with text or icon widgets cannot push the frame over CanvasTextTooLarge/CanvasPathElementLimitReached - A builder smaller than the widget command reserve gets the whole capacity as its budget, so a small buffer paints what fits instead of flooring to a below-prologue budget that painted nothing - Regression tests: merged wide box row, referenced-sibling text preflight * LSP doc tables cover the terminal vocabulary - Hover/completion docs for the terminal element, its pty and scrollback attributes, and the on-terminal event, satisfying the doc-table conformance test that holds the tables equal to the registry * Terminal id namespaces by stride, nested double corners, and name honesty - Command ids follow the widget part-id convention scaled to a 24-bit slot space (id * 2^24 + offset, every painter offset below the stride), so two terminals' commands are disjoint by construction — the multiplicative spread let one widget's offsets land in another's namespace; an anonymous grid (id 0) now emits every command with id 0, the unkeyed convention, through the box geometry included - Pure-double corners (2554/2557/255A/255D) draw as two nested L joins — the double frame a terminal face draws — instead of both bars riding through the joint as hash crossings with inward stubs - The merged-run cost charges a double piece's two bars plus its underline, so a row of unmergeable underlined doubles stays under the hard ceiling - text= on terminal is a teaching error in the validator and both engines: the text channel is runtime-owned (the live screen rides it), so an authored value would rename the control every frame and falsely satisfy the accessible-name lint — the name comes from label - elementHoldsChildren classifies terminal as a leaf, so container roles that promise child structure are refused on it - Regression tests: budget ceiling under unmergeable underlined doubles, the anonymous-id convention, cross-grid id disjointness with the adversarial id pair, nested-corner geometry, and the text/role teachings * Glyph preflight charges atlas entries, not code points - The runtime atlas keys on (font, glyph, size, subpixel_x, subpixel_y) with four x buckets, so one distinct scalar can occupy four entries across a row's columns under a fractional cell width; the preflight now charges every new code point at that variant multiplier, so a grid within its budget can never undercount its own atlas usage and fail the frame with GlyphAtlasListFull - The glyph budget option and the widget mirror are documented in atlas-entry units; the budget regression test asserts the 4x charge * Box cost per glyph and zero-add rows pass the shared ceilings - rowCommandCost charges each non-merged box glyph its own worst case (box.maxCommands, kept lockstep beside the paint switch): a wide row of one-bar pieces costs what it paints instead of a flat joint worst case that rejected it whole - A row adding no text or path elements paints even when earlier widgets already spent past the grid's reserved share — the ceilings bound what the grid adds, never what siblings consumed - Regression tests: a 198-column row of one-bar pieces paints under the widget budget; an all-box row paints beside a text widget that exhausted the share * Terminal build-time leaf enforcement, a path reserve, and a scoped id claim - Both markup engines refuse a pty-less terminal and terminal children at BUILD time (the image-leaf policy), so hot reload — which skips the validation pass — fails the build instead of silently rendering an unbound surface or dropping the child; interpreter regression test added - The painter holds back a path-element reserve (widget_path_reserve, 512) for the widgets emitted after the grid, so rounded-corner rows degrade instead of starving a following chart into ChartPathElementListFull - paintIdBase's doc states its guarantee precisely: the widget part-id convention scaled to a 24-bit slot space, with the identical accepted residual (ids agreeing modulo the shifted range share a namespace, as they do for widgetPartId) * Cluster-isolated runs, engine-scoped scrollback, terminal-aware widget diff - Multi-codepoint clusters (combining marks) paint as their own single-cell run at their exact cell origin, so a text layout that advances a mark by a full glyph can never shift later cells off the grid; plain single-scalar runs keep the merged fast path - Both markup engines refuse scrollback off a terminal at build (validator parity for unvalidated paths), instead of storing an inert generic option - The widget-tree diff reports paint damage for terminal bindings: pty/scrollback changes compare, and a BOUND grid is never reported clean (its snapshot may be republished or refreshed in place), so partial-invalidation consumers never leave stale terminal pixels - Regression tests: cluster neighbor keeps its cell origin, misplaced scrollback fails the engine build, changed bound grids diff paint-dirty * Scope the libghostty-vt dependency to the terminal example - Drops the framework-wide ghostty wiring (root dependency, module imports in the repo and user-app build graphs): no framework code imports the module yet, and resolving ghostty at configure time runs its full build script — whose bench tooling pulls SharedDeps regardless of the lib-vt emit option, walking lazy dependencies (wuffs, translate_c) whose build scripts fail in consumer package stores that carry the whole transitive closure - Scaffolded and consumer builds no longer traverse ghostty at all; the terminal example keeps its own pin (the merged slice-A wiring), and the runtime session slice will bring the dependency back behind a consumer-safe seam when framework code actually imports it * Derive the terminal path reserve from the chart series ceiling - widget_path_reserve now derives from max_chart_points_per_series (stroke polyline plus area fill with closure slack): a flat 512 fell three elements short of one maximal filled-line series, so rounded-corner rows could still starve a following chart into ChartPathElementListFull - A lockstep test pins the reserve to the derivation |
||
|
|
19f2933e08 |
Validate integer-class attestations and decode boundary slots by class (#194)
* Validate integer_slots structurally with the u64 class in the checker
- Resolve every leftover integer_slots entry against the sidecar's own tables, teaching unresolvable paths, wrong-spelling slots, and the grammar's slice-element gap distinctly (V10)
- Accept the closed integer-class set {i64, u64}; f64 and unknown classes refuse with their own teachings
- Message-side slot paths spell the union's authored name in both the reader's bijection and the extractor's emission
* Carry the u64 wire class through the canonical codec
- Encode/decode accept u64 beside i64: 8-byte unsigned LE, total over the full wire range in both classes
- exactF64Unsigned holds the one-sided 2^53 exactness line for u64-attested dispatch payloads
- Golden vectors pin the +-(2^53 - 1) extremes, sign handling, and full-range round trips byte-for-byte
* Decode integer slots per their attested class in the generated mirror
- The spelling authority consults integer_slots by slot path: a u64 attestation refines the i64 spelling to the unsigned twin (8-byte unsigned LE decode), every non-attested slot stays f64
- Dispatch narrows u64-attested payloads through exactF64Unsigned; i64-attested slots keep the signed guard
- Tests pin every attestable path form with mixed classes and the empty list generating no integer slot anywhere
* Exercise attested integer classes through generated mirrors in the harness
- New hand-written integer fixture with mixed i64/u64 attestations; its generated mirror decodes envelope, dispatch, and snapshot bytes over the +-(2^53 - 1) extremes and the full 8-byte wire range, byte-exact against hand-computed vectors
- The external-core parity suite drives the i64-classed arms through real dispatch at the provable extremes and reads the exact integers back from the decoded mirror
- The parity build runs corewire --check over the supplied external sidecar before the tests; everything still skips cleanly when no archive is supplied
* Host adapters accept the u64 number class beside i64
- Event-record shape checks, the number/number_bytes routing entries, and the channel-record validation take u64 fields exactly where i64 stands; widening assignments were already class-agnostic
- The mini core grows u64-classed now and fetch arms proving both routing paths deliver the exact unsigned values
* Facade routes u64-attested slots through an unsigned encoder
- nscfU64 (emitted only when a slot attests the u64 class) refuses negative, fractional, and past-2^53 values instead of encoding two's-complement bytes a mirror would read as huge unsigned values
- Slot paths thread through the facade's encoders and value builders; u64-attested sample values stay non-negative so the deterministic sample model encodes
- The integer fixture's compiled facade joins the conformance parity axis: snapshot and envelope bytes match the canonical encoder for mixed classes
* Parity boundary script follows each slot's attested class
- The integer-extremes script includes negative crossings only for signed-attested arms, so a supplied sidecar attesting u64 on those slots still compiles and runs
- The decoded-mirror comparison widens both classes into one comparison type
* Refuse u64 attestations on host-supplied signed slots
- The checker refuses the u64 class on scroll-state axes and text-selection bounds: the host supplies negative values there, which the unsigned class cannot carry
- The pty event shape keeps code and signal signed (non-exited terminals deliver -1 sentinels); droppedWrites still takes either integer class
- The parity boundary script and its comparison handle f64-classed slots, so a sidecar predating integer attestation still compiles and runs
* Narrow the signed-slot refusals to values the host actually signs
- Two distinct slots spelling one path refuse as unaddressable (a message union named helpers can collide with a helper's return slot), so one attestation can never silently govern two slots
- Scroll refusal covers only offsets and velocities; extents, selection bounds, and pty signal take the unsigned class (the host supplies them non-negative), while pty code and chrome geometry stay signed
- Chrome geometry refuses u64 at check time and in the channel-record validation (embedders report signed content coordinates)
* Pin the signed-slot rules to the exact host domains
- The scroll refusal applies only to records a message arm routes (a model-only scroll-shaped record keeps its attestation), and the chrome refusal narrows to the control cluster's x/y position — insets and sizes are non-negative extents and take the unsigned class
- Channel-record validation takes a per-field signed list instead of a blanket switch
- A negative host number reaching a u64-classed arm or field (a pre-epoch clock, a -1 exit sentinel) teaches at the crossing instead of faulting in the cast
* Clamp audio scalars into the exact-integer delivery window
- Audio positions and durations clamp below 2^53 at every delivery entry (live platform events, fed fake/replay events, spectrum reports) — the video scalar clamp's twin, so integer-classed Msg fields and the mirrors never see an out-of-window host value
- The parity script's selection sample follows the supplied sidecar's classes: signed keeps the negative-value coverage, unsigned exercises a backward selection instead
* Refuse out-of-window audio records at replay instead of reshaping them
- The audio twin of the video scalar damage gate: a journaled position or duration at or past 2^53 can only be a damaged or hand-edited journal now that the delivery boundary clamps, so replay refuses it before the feed
- Predicate pinned at the window edges in the existing damage-gate test style
* Bump the journal semantic epoch for the audio scalar window
- Epoch 5: audio scalars now clamp below 2^53 at delivery and replay refuses wider journaled values as damage, so an older recording carrying one refuses as a different generation instead of being misreported as damaged
|
||
|
|
3a188de6b7 |
Windows pty: ConPTY behind the shared transport seam (#193)
* Windows pty transport: ConPTY behind the shared seam - pty_windows.zig: CreatePseudoConsole over an overlapped named-pipe pair, flags 0 (no inherit-cursor handshake), UTF-8/VT both directions per the pseudoconsole pipe contract; EOF manufactured by closing the console at the first quiet moment after the process handle signals - pty.zig becomes the dispatch seam (one Pty shape, wait takes the transport); exit semantics are exit-code-only on Windows (signal always 0), kill is TerminateProcess plus console teardown, and handle-close discipline replaces the posix zombie table - effects wiring: the io loop is unchanged, the env policy snapshots the live Windows environment through the same flatten seam, and the live suite gains ConPTY twins (containment assertions where conhost renders, byte-exact stays posix-gated) * Terminal example: deterministic shell pick covers Windows - cmd.exe joins the per-platform default-shell table (interactive by default, so the posix shells' -i rides a per-platform argv const) * Docs: Windows pty moves from staged to supported - platform matrix names the ConPTY backend and a Windows-semantics section states the differences plainly: exit codes only (signaled never occurs), rendered VT stream rather than raw child bytes, kill reach via console teardown, case-insensitive env names, no-CWD PATH resolution - changelog fragment for the Windows transport; the terminal fragment stops calling Windows staged * ConPTY: null std handles and an exit-quiet grace - STARTF_USESTDHANDLES with null handles blocks CreateProcess's std-handle duplication from a console-less parent (sshd/CI pipes), so the child's stdio binds to the pseudoconsole instead of bypassing it - the console close now waits out a 50ms quiet grace past the child's exit (short poll beat while it pends), so conhost's asynchronous final render is not cut off the frame it was painting - transport tests: mode.com by full path (the minimal test env carries no PATH) and the quoting expectation pins the lone-backslash no-quote rule * Fix the image-cache probe test's fake handle for Windows natives - std.Io.File.Handle is pointer-shaped on Windows; the fake the noop close never dereferences now matches per platform, so the runtime-core test shard compiles on a Windows host * ConPTY hardening: teardown reach, drain-before-close, env fidelity - reapEnding now tears the console down on every path behind a bounded discard-drain, so a kill fells attached descendants at the reap (not a retire the host may never drain to) and ClosePseudoConsole can never meet the full pipe that wedges pre-rework conhosts - State moves to process-lifetime backing (an abandoned io thread must outlive the caller's allocator), the exit quiet grace measures from the later of exit and last output, hidden =X:= drive-directory entries ride the env snapshot verbatim, and the no-PATH fallback derives the real Windows root - docs state the 32767 console-geometry clamp, the Windows 10 1809 floor, and the direct-child session-lifetime policy (measured: the pipe never breaks on its own, so the manufactured EOF is the only ending) * ConPTY: off-thread console close, batch-argument refusal, exact resolution - ClosePseudoConsole moves to a detached helper so the reader thread keeps draining while conhost tears down (a client's CTRL_CLOSE handler can write past the pipe capacity; closing inline on the reader is the documented deadlock), and the teardown drain now runs behind the initiated close until EOF or its bound - batch targets refuse argv bytes cmd.exe would reinterpret (a .cmd command line is reparsed by cmd's own grammar, which no CRT quoting neutralizes), extension probing appends suffixes only to extension-less names (a missing tool.cmd can never start a co-located tool.cmd.exe), and direct executables pass lpApplicationName so long paths start (batch keeps the null-application form that reroutes through cmd.exe) - synchronous read completions stamp the exit quiet grace like event-delivered ones, and the environment block sorts names case-insensitively by code point through the OS BMP uppercase fold (raw WTF-8 bytes misorder folded non-ASCII names) * ConPTY: safer failure-path closes, cmd-convention resolution, OS-order env block - spawn-failure cleanup breaks the pipe before ClosePseudoConsole (a terminated child may have filled it; a blocked conhost would block the close), and the teardown drain runs quiet-first so the close's inline OOM fallback can only meet an emptied pipe - extension-less names probe runnable suffixes before the bare spelling (a plain file named tool cannot mask tool.exe/tool.cmd), the probe's wide copy sizes to the candidate (long \\?\ PATH components resolve), direct executables keep the caller's argv[0] spelling in the command line (the module rides lpApplicationName), and a batch target's resolved path faces the same hostile-byte refusal as its arguments with plain space-guarding quotes (cmd reads backslashes literally, so CRT trailing-run doubling would corrupt them) - the environment block sorts by uppercase-folded UTF-16 code units (surrogate halves order below private-use BMP, the OS's own comparison), pinned by test * ConPTY: explicit cmd.exe for batch, deadlock-free close fallbacks, tighter quiet close - batch targets launch through an explicitly resolved cmd.exe with /d /s /c (AutoRun cannot splice around the script), every batch argument is quoted (a bare closing parenthesis could join cmd's block grammar), and the refusal policy is stated in the docs - the console-close fallback breaks the output pipe before closing inline (a reader that cannot spawn the detached closer never meets the blocked-conhost wait), spawn-failure cleanup closes on the same detached form, and retire guards the broken read handle - the quiet close re-probes the read event after the pipe peek, so a completion landing between the two restarts the grace instead of racing the close * ConPTY: pin the batch interpreter to the system cmd.exe - the batch command processor now resolves as <windows root>\System32\cmd.exe (GetWindowsDirectoryW first, env root then literal as fallbacks) and is probed for existence - a PATH-planted cmd.exe can never answer for a trusted script, and a PATH without System32 no longer breaks batch spawns - the no-PATH resolution fallback derives from the same shared windowsRoot helper |
||
|
|
72ed3644c6 |
Channel entries return the bytes envelope through corewire's ABI seam (#192)
* Rebind channel entries to the bytes envelope - The out-record channel shape is superseded: entries return [produced u8][tag u8][payload...] on the ordinary out-pointer pair, keeping the one-return-slot rule intact - shim_rt.channelEnvelope splits the header and panics with a teaching on malformed framing (short buffer, produced byte past 1, payload behind a nothing-produced header); the generated msgFromEnvelope unpacks and refuses out-of-range tags - The stub core's channel entries now hand back real envelopes (nothing-produced by default, test-settable), so generated shims' unpack paths execute without a compiled core * Emit channel envelope packing in the TypeScript facade - Wired channel entries export as nsc_core_<channel>(msg: Msg | null): the facade packs [produced u8][tag u8][payload...] onto one bytes return - The shared packer encodes each arm's payload with the sidecar's classes and flattened field orders, so the envelope tail matches the arm's mirror decoding - Unwired sidecars carry no envelope surface * Prove the channel envelope round-trip in the conformance suite - Facade-packed envelopes must equal [1] ++ the canonical union encoding of the produced message, across the corpus's payload families (bare, i64 number, bytes, flattened records, enum members); null packs exactly [0, 0] - The generated shims' channel entries execute against the stub core's test-settable envelope: produced gating, bare-arm and payload-arm decoding - All wired entries route one packer; unwired channels stay out of the facade surface * Drive channel entries through the envelope in the parity suite - Both lanes' channel functions run over gating and producing events: null gates must agree, and produced messages compare by canonical bytes in the mirror layout - Every produced message dispatches through both lanes as a full cycle (command bytes and committed snapshot byte-identical), and the frame channel re-gates after its resize lands - The suite still builds only when a compiled-core archive is supplied * Give channel wrappers a defined pre-call out state - The generated wrappers point the out pair at shim_rt.channel_out_guard with length zero before calling, so an entry that returns without writing yields the zero-length envelope - That envelope lands on channelEnvelope's short-buffer teaching instead of slicing undefined memory * Refuse the length-only channel out-pair violation with a teaching - shim_rt.channelEnvelopeBytes assembles the returned envelope: a nonzero length on the guard address panics with a teaching before any slice forms - The generated wrappers route the out pair through it, so every non-writing shape (neither slot, pointer-only, length-only) reaches a teaching panic, never memory outside a written buffer * Name the exact refusal and reporting mechanisms in test and doc text - Envelope framing faults panic with a teaching; a short envelope is refused by the envelope reader; the tag-range refusal belongs to the generated unpacker - The queue-overflow test name states the mechanism: dropped lines ride a carried drop count on the next delivery and the exit report |
||
|
|
f8c14c59c7 |
Terminal: pty effects, libghostty-vt, and a replayable terminal example (#191)
* Add the POSIX pty primitive: openpty, controlling-terminal fork/exec, resize, reap - One transport for terminal children: parent-built argv/envp (clean env plus TERM), async-signal-safe child path via login_tty/execve, group kill and waitpid decode - macOS and Linux-with-libc report supported; every other target compiles to loud PtyUnsupported stubs - Live tests prove the controlling terminal (test -t 0), exit codes, signal decode, and the env policy * Pin libghostty-vt and stage the terminal example skeleton - examples/terminal owns its build: ghostty pinned at 7aa9591 (the first upstream commit that builds under Zig 0.16.0; v1.3.1 still targets 0.15) with simd off so the vt module stays pure Zig - The pin is proven in-tree: the example's test round-trips VT parsing through the pinned module - zig-pkg/ (the project-local package store) joins .gitignore * Add the pty effect vocabulary: spawn, coalesced output, write, resize, kill, replayable exits - fx.ptySpawn/ptyWrite/ptyResize/ptyKill ride the keyed families' seams: one key space, the spawn argv budgets and env policy, exactly one exit per spawn, rejections staged loud and regenerating - Output coalesces through a per-pty staging ring paced by the drain (lossless back-pressure: a full ring parks the reader, never drops), delivered as bounded batches journaled via the content-addressed blob store; replay never spawns a process - The fake pty scripts the whole vocabulary headless (request/write/resize/kill mirrors plus output/exit feeds), and live POSIX tests pin coalescing, losslessness, controlling-terminal wiring, and teardown convergence * Render the terminal example: libghostty-vt grid on the canvas, keyboard-first, replayable - grid.zig wraps one emulator session (cell state, damage, scrollback, keyboard selection) and paints the viewport as real text: per-row background/text runs, theme-mapped ANSI-16 with exact 256-color and truecolor, wide CJK cells, a scrollback thumb - main.zig drives it on the pty vocabulary: typing rides the IME-correct text channel and the emulator key encoder, selection/scroll/copy chords, a variable-length chrome prefix for the grid, and the frame pump resizes the grid and pty together - UiApp gains on_text (the target-less committed-text seam) and a variable_prefix chrome mode; the gpu-surface layer routes unclaimed text_input to the app, the key_down fallback's twin - Tests pin the emulator round trip, CJK width, scrollback, line/block selection, palette honesty, and a fake-pty session that replays byte-identical offline * Document the terminal recipe, platform matrix, and replay story - docs/terminal: the pty vocabulary, coalesced output, the exit taxonomy, grid rendering, and the byte-identical offline replay story, with the honest macOS/Linux/Windows-staged/null matrix - Registered in the Core Concepts nav beside Dynamic Images - Changelog fragments for the pty vocabulary, the terminal example, on_text, and the TS Cmd family * Harden the pty transport: exec-failure detection, teardown, and write accounting - pty spawn distinguishes a failed exec from a real 127 exit via a close-on-exec self-pipe (a bad shebang interpreter now reports spawn_failed, not exited), and rejects argv entries with embedded NULs instead of silently truncating them at the C boundary - The io loop observes shutdown so a reader parked on a full staging ring exits promptly at teardown instead of forcing the abandon path - dropped_writes now counts outbound bytes lost to a write failure or a child that exits with input still staged; a kill racing a natural exit rewrites the terminal to cancelled with the -1 sentinel code - Bounded per-drain output: a delivered batch re-stamps the remaining backlog past the pass boundary, so a continuously writing child delivers one batch per drain instead of starving other events * Harden pty fd hygiene, the kill/reap races, replay provenance, and grid fidelity - pty spawn aborts if CLOEXEC on the exec self-pipe cannot be set (a leaked write end would hang the parent), and the wake pipe is now CLOEXEC so no descriptor leaks into the child; empty PATH components resolve as the current directory (POSIX) - ptyKill no longer signals a reaped pid (the OS may have reused it) and the io loop winds down on kill even when an escaped descendant holds the pty open, so the exit always delivers; dropped_writes counts each lost payload, not one lumped event - Platform-unsupported rejections are executor truth and feed under replay (a journal recorded where ptys are unsupported replays its rejection verbatim); the regenerating marker rides the record's truncated bit so a pty exit reason stays honest, with a replay damage gate for the pairing - feedPtyOutput refuses an over-bound batch instead of truncating, and every fake pty (not just replay parks) refuses a second feed after its exit is queued - Example: inverse-video cells paint text in the background color (not foreground-on-foreground), the grid self-limits to its chrome command budget so a pathological screen degrades to fewer rows, and the Linux default shell is /bin/sh (present on every install, unlike /bin/bash) * Wire the pty command family into the TypeScript tier - Cmd.ptySpawn/ptyWrite/ptyResize/ptyKill (wire opcodes 0x19-0x1C, additive over the current cmd format) expose the pty vocabulary to transpiled cores, with an event arm matched by field name (key/kind/bytes/code/reason/signal/droppedWrites) - The rt kernel encodes the four ops; the emitter lowers and validates them (argv and grid bounds) with teaching diagnostics; the ts-core host decodes them into the runtime Effects pty calls and dispatches events back to the core's event arm - Posting stays native-only (transpiled cores are single-threaded): the TS tier spawns, writes, resizes, kills, and receives. Covered by packages/core effects/conformance tests and the ts_core_host bridge tests, plus a cmdview decoder arm so the evals harness names the new commands * Harden pty close-on-exec fds, the pid-reuse guard, exact write accounting, and NUL-safe TERM - The pty parent end/child end (and the exec self-pipe read end) are close-on-exec, so a concurrent process or pty spawn on another thread cannot inherit another session's descriptors and hold its pty open; pipePair now aborts if either non-blocking fcntl fails instead of returning a pipe that could hang the UI thread in nudge - Closed the pid-reuse window: the io thread publishes a reaping flag under the mutex before waitpid, and ptyKill and teardown decline to signal once it (or exit_staged/io_done) is set, so a reaped child's reused pid is never signalled - dropped_writes now tracks per-payload boundaries: a fully-sent write pops as its bytes flush, so a discard counts only the writes that never reached the child, not already-delivered ones - ptySpawn rejects argv or TERM containing an embedded NUL (validated at the effect boundary so the fake executor and replay agree), never a silently truncated value * Bound the pty exec probe and reap, make kill signalling atomic, and cap the grid command budget - The exec-status probe polls with a timeout instead of a blocking read: a real exec failure writes its byte instantly (always detected), success is EOF, and the timeout is the net for the residual CLOEXEC window so a concurrent fork inheriting the pipe writer can never hang the spawn. Pipes are created close-on-exec atomically on Linux (pipe2); Darwin keeps the immediate fcntl behind the same timeout net - reapEnding replaces the unbounded reapBlocking after the stream ends: the normal case (child already exited) returns at once with no signal, and a child that closed its terminal but kept running is hung up and escalated to SIGKILL within a bounded window, so the exit always arrives and no kill is stranded - ptyKill and teardown signal the child UNDER the shared mutex, atomic against the io thread's pre-waitpid reaping publish, so a reaped pid's reuse is never signalled - dropped_writes stays exact: the outbound record ring is an admission bound (a write past it is refused and counted once, like a full byte buffer), so every accepted write owns a length record and none is folded - Example: the grid's per-row command reserve accounts for background + text + underline per cell (3/column) so a pathological screen truncates safely under the chrome budget; a resize allocation failure leaves the model dimensions uncommitted so the emulator and pty never disagree and the frame pump retries * Open the pty pair with an atomically close-on-exec child end, reset reused header flags - Replace openpty with posix_openpt + grantpt + unlockpt and open the child end O_CLOEXEC atomically: the child end is the descriptor whose inheritance across a concurrent fork's exec would hold the pty open and starve the exit event, and an atomic open closes that window on both platforms (the parent end's Darwin sub-syscall window is benign — an inherited parent end does not hold the pty open) - Reset every reused PtyShared flag on a new spawn (reaping, the outbound write records) so a prior session's state never steers the next: a stale reaping would make ptyKill skip its immediate SIGKILL - Declare ioctl variadic to match the C ABI — a fixed-arg declaration mis-passed the winsize pointer on arm64, silently dropping the initial grid and every resize; the resize test now verifies the applied size, not just the call * Handle already-reaped children, low child end fds, post-exit writes, and pre-output input - reap reports ECHILD (a child reaped by an embedder's own SIGCHLD handling) as a gone child, never as still-running, so the escalation path cannot signal a reused pid - Relocate a child end that lands on fd 0/1/2 to a high descriptor before login_tty: a same-fd dup2 does not clear close-on-exec, which would make the child's stdio vanish at execve when the host started with standard descriptors closed - ptyWrite drops a write once the reaper has staged the exit: there is no io thread left to send it, and mutating the finalized exit's dropped_writes would race the drain - The terminal example accepts input from spawn onward, not only after the first output batch — a shell with an empty prompt and no banner never flips to live, and gating input on it would strand every keystroke * Relocate low pty fds, wind down orphaned ptys, commit-only text, IME preedit, guarded restart - Relocate the child end AND the exec self-pipe write end above the standard descriptors before the fork: login_tty dup2's the child end onto 0/1/2, and a write end left on fd 1/2 would be clobbered, making an exec failure masquerade as a normal exit when the host began with stdio closed - Bind-time services-snapshot failure now winds down already-running ptys (bounded), not just channels, so their exit still delivers instead of stranding with no wake services - on_text delivers COMMITTED text only: an IME preedit (set_composition) or cancel routed through a focused non-text widget no longer types provisional bytes into a terminal - The targetless text path tracks the IME preedit and delivers it on commit — hosts emit an empty commit for unchanged marked text, so the composed bytes come from the buffered preedit; only committed UTF-8 reaches on_text - The terminal example restarts only a genuinely ended or failed session, never during starting/live, so a quiet shell is not duplicated onto its own occupied key * Non-blocking pty parent end, detach at retire, UTF-8-safe preedit truncation - The pty parent end is opened non-blocking and read/write surface WouldBlock: the sole io thread paces both directions through poll and never blocks inside a write when the child stops reading stdin while producing output, so stdout keeps draining and neither side deadlocks (ptyFlushOutbound writes at most one chunk per POLLOUT, leaving the rest staged) - Retirement detaches the io thread instead of joining it: retirement runs at exit delivery, inside the very dispatch a synchronous-marshal wake hook may be waiting on, and the thread's last act is that host wake — joining would recreate the deadlock ChannelWake forbids. By retirement the thread has published io_done and is past all transport/staging/pipe access, so detaching and reclaiming its fds is safe (conforming enqueue-only wakes have already returned; a violating one lingers against the process-lived header) - A truncated targetless IME preedit now cuts on a UTF-8 boundary, so an empty commit never forwards a split code point as committed text * Quiesce every pty wake at teardown, route IME to editable widgets only, chunk input - Teardown now quiesces the wake header of EVERY pty slot, idle included: the header is process-lifetime and reused, so a retired session's slow wake_fn can still be in flight when the services snapshot and platform are freed. A call still executing at the deadline is abandoned, counted, and the platform signalled to outlive it — the abandoned io-thread path accounts for its quiesce failure the same way, no longer silently - Text and IME route to a focused widget only when it is an editable text-entry widget; a focused non-text widget (a button, a list row) lets committed text fall through to the target-less on_text seam instead of dropping it - The targetless IME preedit buffer holds a full phrase-sized composition and the child's exec-failure report retries EINTR, so an interrupted one-byte report is never mistaken for a successful exec - The terminal example chunks committed text into per-write-bound pieces, so a long paste or IME commit types every byte instead of being refused whole * Free the pty header on clean teardown, honor emulator colors, bound the grid text store - A cleanly quiesced pty header is now freed at teardown: unlike a channel header (which an app thread may post to forever), the pty io thread is the sole toucher and is gone once quiesce confirms no in-flight wake, so repeatedly creating and destroying runtimes that used a pty no longer leaks ~64 KiB each; an abandoned header still leaks deliberately - The grid reads the emulator's resolved foreground, background, and cursor: the theme colors are pushed into the emulator's DEFAULTS so ghostty composes OSC 10/11/12 overrides and DECSCNM reverse-video itself, and the renderer honors an application's requested colors instead of forcing the theme - The grid degrades by whole rows when the display-list text store nears its budget (an emoji-dense screen), never blanking cells mid-frame with a swallowed allocation failure - Documented that an .exited pty terminal carries -1 only when the child was reaped outside the toolkit (an embedder installing SA_NOCLDWAIT / ignoring SIGCHLD / running its own reaper) — an already-broken parent/child contract where the real code is unrecoverable * Keep the pty header process-lived, serialize ptsname, honor OSC 4 by mask - Revert the teardown free of PtyShared: quiescing proves in_flight is zero, not that the io thread has RETURNED (it publishes its exit before calling requestHostWake, and a detached mid-life thread's late or violating wake still locks the header's mutex), so freeing risked a use-after-free. The header is now permanently retained like ChannelShared — a bounded per-slot leak that never grows during a runtime's life - ptsname's shared static buffer is resolved and copied under a process-wide spinlock, so concurrent pty spawns from independent runtimes on different threads never open each other's child end - The grid decides an ANSI slot is untouched by the emulator's override MASK, not RGB equality: a program that OSC-4-sets a slot to exactly the default RGB is honored instead of being replaced by the theme color * Stop macOS Option double-input and stage heavy graphemes whole - On macOS, Option is a compose key: Option+F commits the composed character through the text channel, so the key encoder no longer also treats Alt+printable as a chord there (which sent both the composed character and an Alt-escape to the child). Elsewhere Alt stays Meta - The grid's run staging buffer holds a full row plus a heavy grapheme cluster (8 KiB), so a cell with hundreds of combining marks stages whole; the run-break splits long runs across draw commands rather than overflowing, and any residual cut lands between code points (valid UTF-8) * Block for the exec verdict on Linux, refuse empty output records, make grid rows atomic - The exec-status probe blocks for a reliable verdict on Linux, where the report pipe is atomically close-on-exec (pipe2): a slow execve (an interpreter on a sluggish filesystem) is now awaited and correctly reported as spawn_failed rather than assumed successful. Darwin keeps the bounded poll — it has no atomic-CLOEXEC pipe, so blocking could hang on the fork-inheritance window (the documented residual every macOS terminal shares) - Replay refuses a .output pty record with a zero-length blob: the recorder journals output only for a non-empty batch, so a zero-length one is damage that would otherwise replay a synthetic empty event and diverge the fingerprint - The grid measures each row's exact text bytes and skips the row WHOLE when the display-list text store cannot hold them, and breaks a run before a cell whose grapheme would overflow the run scratch — so a heavy-grapheme row is never torn mid-way and a large cluster landing near the buffer's end keeps all its marks - The terminal-response staging buffer holds a large pipelined query burst (16 KiB); an overflowing reply is still dropped whole (never cut) and counted * Zero the signal on a cancelled pty, chunk query replies, grow the IME preedit to fit - A cancelled pty exit reports signal 0: the toolkit's own SIGKILL is what felled it, and the contract is "signal is nonzero only for a .signaled end", so the raw SIGKILL value no longer leaks into a cancelled terminal - Terminal query replies (DSR/DA/XTVERSION) flush through the chunking write path, so a batch that pipelined more than one write's worth of replies delivers every byte instead of being refused whole and leaving the child waiting - The targetless IME preedit buffer grows to fit the composition instead of truncating at a fixed size: each set_composition replaces it with the host's full string, reallocated only when a larger one arrives and freed at runtime deinit, so an unchanged commit of any length forwards the whole composition * Ignore empty pty output, clear stale preedit on OOM, reset on restart, map function keys - feedPtyOutput ignores an empty batch: the live drain only delivers non-empty output, so journaling a zero-length one would write a blob record replay refuses as damage - A failed IME preedit grow clears the buffer instead of leaving the superseded composition, so a later empty commit never inserts stale text the user has replaced - Restarting the terminal hard-resets the emulator (RIS + a fresh parser), so a shell that exited mid-escape or in a non-default mode does not corrupt the next session's key encoding or first output - The key map covers Insert and F1-F12, which carry no committed text and would otherwise be dropped before reaching the child * Bound the exec probe on both platforms, gate impossible exit records, clear selection on restart - The exec-status probe blocks up to a generous bound on every platform rather than forever on Linux: O_CLOEXEC closes an inherited pipe copy on the concurrent forker's exec, not its fork, so a forker slow to exec would delay EOF and an unbounded wait could hang the spawn thread. The bound caps that pathological wait; a timeout means our own child exec'd (an inherited writer merely delayed EOF) and is success. A slow-failing exec under the bound is still awaited and reported; only one that both blocks and fails past it misclassifies (an extreme filesystem pathology) — a self-pipe cannot distinguish that from a delayed EOF without the bound - Replay refuses a pty exit record whose code/signal contradict the delivered contract (signal nonzero iff .signaled; code -1 for every reason but .exited), so a hand-edited .cancelled with code 0 or signal 9 cannot dispatch a contract-violating event - Restarting the terminal leaves selection mode, so a restart while a selection caret is armed does not reject the new shell's typed input * Name the two ends of the pty pair parent and child - The controlling side the toolkit keeps is the parent end; the process side the spawned program adopts as its controlling terminal is the child end. Rename the transport field, the spawn locals, and the poll helper to parent/child, and rephrase every comment to match. - POSIX's own API names (posix_openpt, ptsname, grantpt/unlockpt) keep their C spellings at the call site; only the words the toolkit owns change. * Carry the session key on pty events, normalize fake exits, keep query replies lossless, and shrink the pty leak - The transpiled pty event arm now carries the app's own session key: two sessions routing one event arm were previously indistinguishable, so the arm gains a `key` field the host fills from the wire key (emitter, host bridge, and SDK type updated together). - `feedPtyExit` clamps its tuple to the event contract the live io loop guarantees — a signal only after a signaled end, a code only after an exited one — so a fake or replay can never stage an exit the recorder journals but replay's damage gate then refuses. - The terminal example feeds output in sub-slices and drains the emulator's query answers between them, so a burst of pipelined replies cannot outrun the write-back buffer and a child blocked on a DSR answer never hangs; a residual overflow surfaces on the status line instead of vanishing. - A pty session's ~64 KiB outbound ring and its per-write length ring move into a heap block freed at retire, so only a small header joins the process-lifetime leak (the channel header invariant), not the buffers; a teardown that abandons a stuck io thread still leaks the block with the thread that can reach it. * Make the pty rejection key self-contained and enforce the signaled-exit contract - A staged spawn-rejection Msg is delivered a frame after it is issued, past a frame-arena reset, so it can no longer copy the wire key into that arena: it carries the empty key (the self-contained-Msg rule the bytes field already follows), leaving the durable engine-key routing for live events untouched. - feedPtyExit now enforces the full exit contract as a biconditional — a signaled end REQUIRES a nonzero signal, not just a signal only on a signaled end — so a fake or replay feeding .signaled with signal 0 is refused loudly instead of journaling a record replay's damage gate would later reject. * Give replayed pty output the -1 code, keep the rejection key, and bound the exec probe by deadline - A replay-fed output batch now carries the -1 code sentinel the live drain delivers (the Entry default was 0), so an app that folds the event code into its model no longer diverges between a live run and its replay. - A staged spawn-rejection keeps the app's requested key: it is copied into a process-static ring (durable across the frame reset the staged Msg outlives) and referenced directly, so a duplicate-key or table-full refusal is correlated with the command that caused it instead of arriving keyless. - The exec-status probe bounds itself by a monotonic DEADLINE rather than a per-poll timeout, so signals interrupting poll (EINTR) can no longer each restart the full wait and defer the timeout indefinitely — the synchronous spawn stays bounded under a signal storm. * Make staged rejection keys grow with the batch, pace pty input to the FIFO, and reserve grid widget text - Staged spawn-rejection keys move from a fixed host ring to a growable engine store (stageLoopKey): each key gets its own stable heap buffer reclaimed when the staged stage next empties, so a Cmd.batch staging more rejections than any fixed ring holds can no longer overwrite a key still awaiting delivery and mis-correlate an exit. - The terminal example buffers typed and pasted input and drains it against the pty stdin FIFO's free space (new ptyOutboundFree query), so a paste larger than the 64 KiB FIFO no longer loses its tail when the child is not reading; it paces out as the child reads, with a per-frame flush covering a child that reads without echoing. Query replies stay direct and the flush reserves FIFO headroom for them, so bulk input never starves an answer the child blocks on. - Grid painting reserves display-list text bytes for the header and status widgets that share the per-view text store, so a grapheme-heavy viewport degrades to a few fewer rows instead of pushing the combined frame past the runtime limit and failing the whole update. - Any residual dropped input surfaces on the status line beside the reply-drop count. * Report pty write acceptance so the terminal never loses input or query replies - ptyWrite now returns whether the whole payload was accepted, since it alone knows the byte-FIFO and 256-record admission limits; a byte-capacity query could report room the record ring would still refuse, misleading a caller into treating a rejected write as sent. The misleading ptyOutboundFree query is removed. - The terminal example drains one stream-ordered outbound ring holding typed keys, pastes, AND emulator query replies, retrying any chunk ptyWrite refuses instead of dropping it. A paste larger than the FIFO paces out as the child reads; a reply refused by a full FIFO stays queued rather than being cleared, so a child blocking on a DSR answer is never stranded. A per-frame flush covers a child that reads without echoing, and a full-ring overflow is counted and shown, never silent. * Journal pty write verdicts for replay and free pty buffers through their allocating seam - A ptyWrite admission verdict is executor truth — whether the outbound FIFO and record ring had room depends on how fast the child was reading — so each one now journals (a .pty/.write record) and a replayed write returns the RECORDED verdict instead of recomputing optimistically against a fake with no child: an app that retains refused bytes takes the identical retain/remove path in both runs, pinned by a recorded session whose refused and accepted writes replay fingerprint-identical. The journal format fingerprint moves with the new record kind, refusing pre-verdict journals honestly. - Pty staging rings and outbound blocks now free through the channel_storage_allocator seam that allocated them, so a swapped-in tracking or arena allocator sees its own frees instead of a mismatched destroy against the default backing. * Scope target-less IME preedit per surface and gate command chords out of target-less text - The target-less preedit buffer now tracks its originating surface (window + view label), matching how a focused widget's editor scopes composition to the widget: only the owning surface's commit consumes the buffered bytes and only its events clear them, so a second surface's empty commit can never insert a composition typed into the first. - Target-less text_input now applies the same command-chord gate focused text widgets use (primary/command/control means shortcut, not typing; Alt stays out — Option and AltGr compose text), so Ctrl+C delivers the encoded chord alone and never a stray literal character; pinned at the runtime layer and against the terminal example's pty input. * Deliver key-carried typing target-less, count post-exit write refusals, and complete the terminal key map - Committed text carried on a key_down (hosts whose plain typing rides the key event, with no separate text event) now reaches the target-less on_text seam under the same command-chord gate the focused-widget path applies — ordinary typing was silently lost there, while specials carry no text on any host so nothing doubles with the key fallback. - A ptyWrite refused in the staged-exit window now counts into dropped_writes (the exit reads the count under the mutex at drain delivery, which has not happened yet), closing the one silent-refusal gap; only a write after the exit delivers finds no slot, the documented cancel race. - Every desktop platform now reports delete/home/end/pageup/pagedown/insert and f1-f12 as named keys (previously private-use strings or nothing), shortcuts and menu accelerators accept them, and the terminal example maps chorded punctuation and supplies the pressed character to the emulator's encoder — Ctrl+\ reaches the child as its C0 byte, Ctrl+[ as the encoder's fixterms CSI-u form, F1 as ESC O P. * Reset OSC colors on shell restart and honor widget precedence in target-less text - Session.reset now clears the palette and dynamic color overrides (OSC 4/10/11/12): the emulator's full reset leaves its color state alone, so a shell that tinted the palette and exited would otherwise color the next session; overrides drop while theme defaults stay. - The target-less committed-text fallback now honors the widget-precedence contract's structural-claim step: a key the focused widget answers as a control intent (Space pressing a focused button) no longer ALSO types its literal character through on_text, on either committed-text carrier, while unclaimed characters keep flowing to a terminal that happens to share focus with a button. * Wire the new named keys through every platform's menu accelerators - GTK translates them to accelerator names (Page_Up, F1, ...) so gtk_application_set_accels_for_action actually binds them; AppKit converts named keys to their NSMenuItem key-equivalent characters (function-key unichars, control characters) instead of passing the canonical string through, which also repairs the pre-existing arrow/escape menu bindings. - The Windows keydown path now dispatches menu-item accelerators alongside registered shortcuts — a menu item's key+modifiers emits its menu command exactly as if clicked; AppKit and GTK get this from their menu systems, and on Win32 the message loop is that system. * Settle replay feeds at the journal's end and make pty record accounting exact - Replay now verifies at the journal's end record that every journaled ptyWrite verdict was consumed and no write ran past them (the new .settle replay control): a write-count divergence changes no state when the caller is fire-and-forget, so fingerprint checkpoints alone could pass a diverged run — the settle check fails it loudly in both directions. - Output records journal the canonical scalar shape from BOTH drain sites (-1 code sentinel, no signal, no drops) and the replay damage gate refuses records claiming anything else — previously replay silently delivered defaults that differed from a damaged record's journaled fields. - A fed exit's dropped_writes now adds the refusals the fake itself counted (an oversized write against a scripted pty), so the counted-refusal contract holds without every script re-deriving the tally; under replay the slot count is always zero and the journaled count delivers verbatim. * Keep an IME sequence with the consumer it started over - A composition buffered by the target-less consumer continues target-less even when a text widget takes focus mid-sequence: routing its empty commit to the newly focused editor would resolve a composition that editor never saw and lose the composed text. Ownership ends with the sequence — the next composition belongs to the focused editor as usual. * Admit outbound terminal payloads whole or not at all - The pending-outbound ring's admission is now all-or-nothing: a payload it cannot hold whole is dropped whole and counted, never cut at the ring edge — a torn query reply or encoded key would feed the child a malformed escape sequence, which is worse than a counted loss. Reaching the drop at all means the child ignored 256 KiB of pending input, and the count stays on the status line. * Key the replay write verdicts by pty and grow the queue with the recording - The verdict queue now spills to the heap past its inline window (the pending-stage growth story): every verdict a dispatch recorded feeds before that dispatch replays, so a fixed bound would reject a valid recording whose one update wrote more times than the bound — a giant paste drained in per-write chunks is exactly that. - Each verdict carries the pty key it was recorded against, and a replayed write consumes only a matching-key verdict: the same call count and acceptance results against a DIFFERENT session is still divergent input, which a global boolean sequence would silently pass; a mismatch counts as divergence, strands its verdict, and fails the end-of-journal settle on both signals. * Canonicalize menu key case, teach the Chromium host the named keys, and state the reaping contract - Menu-item keys canonicalize to lowercase at each host's storage boundary, matching the shortcut stores that already did: key names validate case-insensitively, so a mixed-case spelling must still translate to a valid GTK accelerator name and a correct AppKit key equivalent (single characters lowercase there too — uppercase implies Shift, which the modifier mask already expresses). - The macOS Chromium host's key normalizer gains pageup/pagedown/insert and f1-f12, so shortcuts on those keys fire under that host exactly as under AppKit. - The kill fence's ownership premise is now stated at the signal site and in the recipe: the toolkit forks each pty child, is its sole reaper, and never touches SIGCHLD disposition — an embedder must not either, because POSIX offers no portable way to signal a pid once a third party (including a kernel auto-reap) can free it first; inside the contract the reaping fence is exact. * Settle the clock feed too, and retain query replies a full ring refuses - settleReplayFeeds now holds the clock feed to the write-verdict rule: a wallMs read past the journaled values (answered 0 optimistically) or a journaled value never consumed is divergence no checkpoint need see, and either fails the end-of-journal settle loudly. - A query reply the pending ring cannot take right now stays IN the emulator's buffer — uncleared, uncounted — and retries on the next output, resize, or frame nudge, so a child blocked on a DSR answer behind a full ring is never stranded by a discarded reply. Transient payloads (typed text, encoded keys) keep the counted-drop disposal since their bytes cannot outlive the dispatch; only a payload larger than the whole ring is impossible and counts immediately. * Align start-failure write verdicts, grow the reply buffer, and strip the primary alias from encoder chords - A write against a spawn whose transport failed synchronously now journals its refused verdict: the staged executor-truth terminal keeps the key occupied on the live side exactly as the parked slot does under replay, so the two verdict streams stay aligned where they previously diverged (live refused silently, replay consumed a verdict that was never recorded). - The emulator's query-reply buffer is heap-grown to fit (up to the outbound ring's own capacity): replies retained behind a full ring keep accumulating while further output feeds, instead of overflowing a fixed buffer into counted drops that could strand a blocked child; past the ceiling a reply still drops whole and counted, never cut. - The terminal encoder strips the runtime's primary-into-super fold when Ctrl raises it, so a bare Ctrl chord reaches the emulator clean and Ctrl+C delivers ETX — a stray super demoted it to a CSI-u chord no foreground shell treats as an interrupt. The GUI+Ctrl double chord encodes as plain Ctrl, the convention terminals follow. * Hold stdin order across a retained reply - A query reply retained behind a full outbound ring is older than any later keystroke, so transient input now gives the reply its retry first and refuses to jump the queue while it remains stuck (dropped counted, never reordered) — the child's stdin keeps the order a real terminal delivers: the answer it is parsing toward, then the typing. - The output arm retries retained replies right after its flush frees ring room, so the oldest bytes take that room before anything a later dispatch could enqueue. * Refuse and count fake writes after the staged exit, folding drops at delivery - A fake pty now mirrors the live admission path's staged-exit rule: a write after feedPtyExit staged the terminal refuses and counts as dropped, never a silent acceptance into a session that is already over. - The dropped tally folds the slot's count into the delivered exit AT DELIVERY (both the fed-exit queue drain and the start-failure stage), the same read-at-delivery rule the live staged-exit drain follows — so refusals landing between the feed and the drain still reach the delivered count; under replay the slot count is provably zero and the journaled count delivers verbatim. * Land start-failure write refusals in the staged exit's tally - A write refused in the synchronous start-failure window now counts onto the staged terminal entry itself: that terminal's slot was already released, so no slot tally could carry the drop to delivery, and the exit reported zero despite the refusal. The counting walk is the occupies-key predicate with the bump folded in, so the verdict journal and the delivered tally move together — the exit reports every refusal, never silence. * Settle undelivered pty feeds, serialize the spawn's descriptor window, and state the process-group kill limit - settleReplayFeeds now refuses fed pty results still queued at the journal's end: every result was journaled at its live delivery, inside an event that follows it in the stream, so a leftover means the journal was truncated past its consuming event — succeeding would silently omit a recorded delivery. Regenerating staged rejections stay exempt (the live run could end with one staged too). - The pty spawn's whole descriptor-opening window — posix_openpt through fork — now runs under one process-wide lock, and the parent end's close-on-exec fcntl is verified: Darwin ignores O_CLOEXEC on posix_openpt, so a concurrent toolkit fork landing inside another spawn's flag gap would gift its exec'd child a copy of that pty's parent end for the child's whole life. Our own forks can no longer land there; an embedder fork keeps the documented residual window the exec probe's timeout nets, and the probe still polls outside the lock. - The kill contract now states its real reach at every site: SIGKILL lands on the child's process group (the whole foreground job), and a descendant that re-grouped itself escapes — POSIX has no kill-whole-session primitive, the spawn family's documented limit. * Cover every descriptor window with the spawn lock, bound the surrendered reap, and carry an unresolved exec probe to the reap - Every toolkit fork and every Darwin flag gap now shares one lock: the wake pipe's creation and the job-spawn family's process start hold the pty spawn lock, so no toolkit child can inherit another spawn's not-yet-CLOEXEC parent end, pipe writer, or wake pipe across its exec — only embedder forks on threads the toolkit does not own keep the documented residual window. - reapEnding's final wait is bounded: a SIGKILL'd child the kernel holds in uninterruptible I/O cannot be waited out by any signal, so past a further deadline the reap is surrendered and the kill reported as the ending — the exit always reaches the app, and the eventual zombie is the bounded, documented cost that beats an io thread wedged forever. - An exec probe that times out no longer guesses success: the status pipe rides the transport (non-blocking) and is read at reap time, when a failing exec's byte — written before its _exit — is present by construction, so an executable that blocks past the bound and then fails still delivers spawn_failed, never a masqueraded normal exit. * Intern staged rejection keys for the model's life, bound failure-path reaps, and free the spawn lock from job starts - Staged rejection keys are now INTERNED and instance-lived: the TS commit walker shares non-frame pointers into the committed model rather than copying them, so a reducer that stored a rejection's key held memory the old reclaim could free mid-run. Interning bounds the storage by the app's distinct key vocabulary (repeat rejections of one key cost nothing) and the buffers outlive every model that references them. - The pty thread-start and wake-snapshot failure paths use the escalate-then-surrender reap: a child wedged inside an exec on a stalled mount cannot be waited out, and an unbounded waitpid there froze the UI loop instead of delivering spawn_failed. - The job-spawn process start no longer holds the pty spawn lock — it blocks on its own exec-status pipe, so a stalled execve would have wedged the lock and frozen the UI the moment a pty needed it. A job fork landing in a pty flag gap joins the documented bounded residual (inherited copies die at the job child's exec; a delayed exec-pipe EOF resolves through the carried reap-time verdict, never a guess). * Pin compositions to their starting editor, shorten the exec probe, and re-poll surrendered reaps - An IME sequence now routes its continuations to the text entry it STARTED in: a per-view owner is pinned when a set_composition routes and released when its commit, cancel, or a direct insertion closes the sequence — so a focus move mid-composition no longer strands the starting editor's marked text or leaks the commit through the target-less fallback, the mirror of the target-less ownership rule. - The exec probe's deadline drops to half a second: the unresolved verdict carries to the reap where a late failure still reports spawn_failed exactly, so the synchronous wait buys only the instant-failure nicety and no longer holds the loop for seconds; the PATH walk's access() probes remain the documented stalled-mount residual of the spawn-position verdict the replay contract requires. - A surrendered reap now parks its pid on a process-wide list re-polled (WNOHANG) at later spawns and at teardown, so a child that dies after its device or mount recovers is reaped then instead of zombieing for the process's life. * Decide the text claim before app rebuilds, swallow orphaned compositions, and state the Darwin pipe residual - The target-less committed-text claim is decided against the tree the input actually routed through, BEFORE the app dispatches that may rebuild it: a Space that pressed a focused button whose command removes that button still counts as claimed, so one physical keystroke can never double into a command and a literal space. - A widget-owned composition whose editor vanished mid-sequence resolves nowhere: its commit or cancel is swallowed (the newly focused editor never saw the composition, and the target-less consumer never composed it) and the stale pin clears, so a fresh composition reopens cleanly at the focused editor. - The Darwin pipe-then-fcntl window is stated fully at its site: pty forks are locked out, and the residual — a job or embedder fork landing inside it — is bounded on every axis (the copy dies at that child's exec, a wedged exec costs at most the half-second probe with the carried verdict keeping the outcome correct, a held wake pipe merely outlives its session's close until that exec). * Route converted commits to the composing editor, latch orphaned sequences whole, and keep EINTR'd surrendered pids - Every host encodes a converted commit (the composed result differs from the marked text) as cancel-then-text_input, so a cancel now holds the owner pin one event and arms a one-shot grace: the trailing text_input lands in the editor that composed it, never the newly focused one — while a plain cancel's own key_down disarms the grace before ordinary typing could inherit it. - An orphaned sequence swallows EVERY continuation: the pin holds through preedit updates (the sequence is still open, and the focused editor must not inherit them) and releases only at its commit or cancel, with an orphaned cancel arming the swallow grace so the converted commit's trailing text vanishes with the sequence instead of typing into a stranger. - reapSurrendered keeps a pid whose non-blocking poll was interrupted, rather than abandoning a live child to zombie unreaped; only a reap or ECHILD settles the entry. * Give target-less compositions the converted-commit grace too - A target-less composition's cancel now arms the same one-shot grace its widget twin got: the hosts encode a converted commit as cancel-then-text_input, so the trailing text still belongs to the surface that composed it — delivered target-less on that surface, never inserted into whichever text widget took focus mid-sequence. A plain cancel's own key_down disarms the grace before ordinary typing could inherit it. * Retry the late-failure read, orphan-check the commit grace, and die with the view - lateExecFailure retries an interrupted read instead of mistaking EINTR for success: the failure byte may be sitting right there, and reading past the signal is what keeps a delayed exec failure reporting spawn_failed. - The route-to-owner grace re-checks its owner at consumption: the cancel's own app dispatch can rebuild the tree away from the composing editor, and a dead owner converts the grace to a swallow — the converted commit's text resolves nowhere, never in whichever editor holds focus by then. - A target-less composition dies with its view: removing a surface clears any sequence it owned, so a later view reusing the same window and label never mistakes its first composition for a stale continuation that would bypass its own focused editor. * Disarm IME graces on blur and view removal, and re-anchor selection across a resize - A view losing focus disarms its cancel-to-commit grace (and the target-less twin) and releases the owner pin: hosts emit a standalone cancel when a composing view blurs, and after refocus an IM-consumed keystroke's text_input arrives before its key_down echo — a stale grace would route that fresh commit to the old editor or swallow it. - Removing a view clears the target-less grace even when the preedit length is already zero (cancellation zeroes it before arming), so a converted commit's trailing text can never leak target-less into a surface recreated under the same label. - Resizing the terminal re-anchors an armed selection at the clamped caret: reflow moves every cell, so coordinates into the old grid are dropped rather than copied, and Shift+Arrow keeps operating inside the new grid. * Share the blur-side IME hygiene across every focus path, fix the backspace key equivalent, and range-gate exit records - Every focus-mutation entry point — the pointer-driven focus move, the programmatic focusView, and the window-level clearFocusedView blur — now routes through one shared blur helper that disarms the cancel grace and releases its owner pin, closing the lifecycle class structurally instead of per path; audited all view-focus writers to exactly these three. - The macOS "backspace" menu key equivalent maps to 0x7f, the byte the physical Delete/backspace key actually emits (and the key-event normalizer maps back to "backspace") — the nominal BS control 0x08 never fired; every other named key in the table was audited against the normalizer and matches. - Pty exit records are range-gated to what waitpid's status word can produce — codes 0..255 plus the documented -1 externally-reaped sentinel, signals 1..127 — at both the replay damage gate and the feed boundary, refused as damaged rather than replayed into an event no live run can emit; pinned with hand-damaged records on both axes and both directions. * Honor the requested TERM, AltGr text, IME resolutions past claims, flush-first admission, and a clipped grid - The pty environment now replaces an inherited TERM with the spawn's requested value (the spawn declared what terminal the child is attached to; a stale TERM=dumb from the host would misdeclare it), pinned live; and the restart resets the previous session's copy feedback. - AltGr input survives both layers: the text-chord gate exempts Ctrl+Alt together (hosts represent AltGr that way, so a text event carrying both is composed text, not a shortcut), and the terminal's key mapper stops encoding Ctrl+Alt printables as chords for the same reason — the composed character rides the text channel alone. An IME resolution (an owned empty commit, or a converted commit's grace-ridden text) also bypasses the structural-claim gate: it ends a sequence the focused widget never participated in, so a button's Space claim cannot eat it. - Outbound admission flushes before refusing (stale occupancy must not drop a keystroke the drain would have made room for), with a fake full-FIFO test seam standing in for a child that stopped reading; and the grid paints under a clip to its frame with an exact bottom-row guard, so the one stale pre-resize frame degrades to a cropped grid instead of painting over the status bar and past the right edge. * Deadline the reap graces, carry claims across split events, and finish the exit accounting - reapEnding's grace windows are monotonic deadlines: usleep returns early on EINTR, so iteration counting let a signal-heavy embedder collapse the 500 ms hangup grace into an almost-immediate SIGKILL and shrink the surrender window; the surrendered list is also re-polled at every session end, so a recovered child is reaped at the next pty activity of any kind. - The committed-text claim carries across the event split: hosts that deliver a claimed key_down and its committed character as separate events (unlike the key-with-text shape) would recompute the claim against a tree the activation already rebuilt — a one-shot per-view carry keeps one physical keystroke from doubling into a command and a literal character. View blur now clears the composition owner unconditionally too: an active sequence's pin surviving a blur would redirect post-refocus typing to the stale editor. - The example's AltGr exemption narrows to the host that actually represents AltGr as Ctrl+Alt — elsewhere that combination is a genuine chord that must encode — and session end is accounted honestly: cleared outbound bytes count as dropped, retained replies drop with the dead key instead of retrying against it every frame, transport write refusals reach the status tally, and a signaled or cancelled end says so instead of rendering as exited (-1). * Never strand a pre-bind pty exit, and die the claim carry at blur - A failed bind-site snapshot publication now stages a live pty's cancelled terminal from the loop past the wind-down deadline, so the exit delivers instead of waiting on a wake that can never fire - The split-event claim carry clears at blur with the IME grace: a claimed activation that moves focus no longer swallows the refocused surface's first commit - A restarted shell starts its refused-write tally at zero instead of inheriting the dead session's drops * Give the input method its keys, and make the grid preflight measure what paints - A target-less composition now owns its surface's key_downs (candidate navigation, the trailing resolver key on hosts that run the IM filter first), so confirming a candidate with Enter never also sends CR; the buffered preedit and its graces die at the surface's blur - The split-event claim carry is keyed to the armed activation key's own literal, so a different key's committed text arriving next flows instead of feeding a stale latch - The grid's row preflight now counts only bytes painting emits (invisible cells suppress, clusters cap at the run scratch), a session exit counts retained reply bytes as loss, and each fix is pinned in both tiers * Free pty staging only on the io thread's own completion proof - Retirement defers the staging/outbound frees to the thread's io_done publish (same critical section as its staged exit) and leaks them past an abandon, so a detached thread can never observe freed blocks - The outbound flush re-checks open/generation after its unlocked write before touching the block or folding drop counts, closing the one ungated deref - A superseded thread (slot reused after an abandon) goes silent at its next loop head and never publishes reaping/io_done into the successor; pinned with a deadline-miss abandon carrying an in-flight write and a free-counting seam * Suppress IM-consumed composition keys at the GTK source, and bound the grid by what the renderer can hold - Replace the one-shot resolved-key grace with host-side suppression: GTK surfaces no key_down for a key its input method consumed while composing, so a mouse-committed candidate never costs the next genuine Enter or arrow - A styled wide character's spacer tail extends the primary cell's background run, covering both cells for SGR backgrounds and inverse video - Painting stops row-atomically before crossing a distinct-code-point budget (the glyph-atlas proxy), and the run scratch grows to the full text store so any cluster the emulator holds paints whole; each fix pinned * Let compositions own their keys before widget routing, and put real cells in the terminal's semantic surface - A live target-less composition suppresses unchorded key routing ahead of every widget pass (dismissal, focus moves, activation), so a confirming Enter never presses a freshly focused button; chorded shortcuts stay live - hostRequest's inline pre-flight now rejects keys held by live or staged ptys, matching the shared keyed-effect namespace every other issuer enforces - The grid's accessibility label is the viewport text, carrying real cell state into the session fingerprint (equal byte counters with different screens no longer verify), and a failed selection re-pin clears the emulator selection instead of leaving a stale copyable range * Settle every fed family, refuse fed overflow loudly, and keep the semantic screen honest - The end-of-journal settle now reports ANY fed result left undelivered (every family plus journaled env records), and a fed line against a full queue answers EffectQueueFull for the replay pump's drain-and-retry instead of silently converting a recorded delivery into a drop - GTK swallows a suppressed composition key's release too (no orphan key_up), and Windows lets AltGr-composed WM_CHAR text through the chord gate it raises Ctrl+Alt for - The terminal's semantic screen text is heap-exact (never truncated or cut mid-scalar), refreshes when a scroll moves the viewport, clears to unknown on a failed render instead of going stale, and a failed selection serialization surfaces as a failed copy with the selection kept * Close the key-suppression edges and keep selection, scrollback, and copy coherent - GTK dedupes suppressed composition keys under autorepeat and clears them at focus loss; Windows discards the WM_CHAR a dispatched shortcut already consumed (AltGr chords type only when no accelerator matched) - The surrendered-reap table evicts round-robin on overflow, so every wedged child keeps a retry seat instead of all forgetting behind slot zero - Scrollback chords pause while a keyboard selection is armed (viewport-relative caret vs absolute range), and a copy over a vanished emulator range reports failure instead of quietly keeping stale clipboard content * Track suppressed keys by keycode, disarm graces the suppression starved, and confirm before clearing - GTK tracks composition-suppressed keys by physical keycode (a lifted Shift cannot rename the release) and a plain cancel whose resolving key_down is suppressed emits a synthetic duplicate cancel, so the runtime's cancel-to-commit grace disarms instead of eating the next ordinary commit - The Windows shortcut latch holds across the keystroke's whole translated burst (ligature layouts post several WM_CHARs), disarmed by the message sequence at the next key transition - A terminal selection now anchors at the live cursor (never the last painted snapshot) and survives until the clipboard write CONFIRMS - a failed write keeps it standing for the retry the status promises * Paint the terminal where the user can see it - The grid region spacer is a stack, not a panel: the panel's surface chrome (fill, border, shadow, rounded corners) painted OVER the chrome-prefix grid, blanking the whole terminal - Grid text anchors its BASELINE (origin is not the glyph top), so rows land in their cells instead of one line high with row zero swallowed by the clip - The session-state indicator is a quiet muted label instead of a pill that read as a half-painted toggle, and a painted-output oracle now renders the retained frame to pixels headless and asserts the prompt's ink and the caret's cell * Build the terminal example on hosts with only the command-line tools - Disable ghostty's macOS app and xcframework artifacts in the dependency options: only the vt module is consumed, and their configure step resolves the iOS libc, which aborts without a full Xcode install * Fall back to FIONBIO when Darwin's pty parent end rejects F_SETFL - Some macOS releases return ENOTTY from fcntl(F_SETFL, O_NONBLOCK) on the posix_openpt fd while the FIONBIO ioctl succeeds; the shared setNonblock helper tries fcntl first and falls back, so a spawn no longer fails whole on those kernels * Set the pty parent non-blocking only after the replica opens - Some Darwin kernels answer ENOTTY to both fcntl(F_SETFL) and FIONBIO on a pty parent whose replica has never been opened, and accept the identical call once it has; the spawn now orders the non-blocking step after the child end opens * Scope the AltGr chord exemption to Windows and keep an armed selection on its text - Ctrl+Alt is a genuine command chord on Linux and macOS (AltGr rides its own level-3 shift there), so the text gate exempts the pair on Windows alone - a chorded key mid-composition now reaches the app instead of being swallowed - Output that scrolls the live screen rebases the keyboard selection from the emulator's absolute pins: the caret follows the selected text, and a range that leaves the viewport clears selection mode instead of desynchronizing copy from the caret * Arm the cancel-to-commit grace only while a composition owner is pinned - A duplicate cancel (the hosts' synthetic disarm for a consumed cancelling key) arrives after the grace probe already released the owner; re-arming ownerless converted the next ordinary character into a dead-owner swallow, so the arm now requires a live owner * Honor negotiated kitty modes for committed text and key releases, and admit one copy at a time - Committed single-scalar text routes through the emulator's key encoder: byte-identical raw text under legacy modes, CSI-u under a TUI's report-all; multi-scalar IME commits stay raw per the protocol - Key releases reach the encoder through an opt-in on_key phase (key_release_events), emitting kitty release events when negotiated and nothing under legacy; repeats stay presses, the one event type hosts do not distinguish - A copy while the clipboard write is in flight is a no-op, so a duplicate-key rejection can never overwrite the first copy's success; the non-Latin Ctrl-chord gap is recorded as an accepted tail pending base-layout key data in the platform vocabulary * Wheel scrollback, geometric box drawing, and a window that is just the terminal - Trackpad and wheel scrolls over the grid ride a new on_wheel app channel (the pinch channel's sibling) into whole-row scrollback with fractional accumulation; inert while a selection is armed - U+2500-259F render as geometry at exact cell bounds - lines, tees, crosses, doubles, quarter-arc rounded corners, diagonals, blocks, shades, and quadrants - with identical-piece runs merged into single bars, so borders join seamlessly where font glyphs showed seams - The window is a standard titlebar plus the grid: header and status bar removed, size readout gone, keyboard hints moved to the README * One seamless surface: hidden-inset chrome with the terminal running under the traffic lights - The window drops its title and titlebar strip (hidden_inset): the theme background fills edge-to-edge including the titlebar band, the grid's text starts below the reported chrome inset, and only the traffic lights float over the terminal * Shift the video tests' journal byte offsets past the appended pty trailer - The pty record fields append 33 bytes after the video fields in every effect payload, so the hand-patching damage helpers now offset from the pty trailer * Stitch the pty conformance fixtures after the video fixtures - Two fixture-list merge points kept both families' cases in sequence; the video entries close before the pty entries begin |
||
|
|
501b59e490 |
Two-axis canvas scrolling with axis-aware routing (#190)
CI / Zig Core (push) Has been cancelled
CI / Linux Canvas Smoke (push) Has been cancelled
CI / Linux Dev Smoke (Debug scaffold) (push) Has been cancelled
CI / Windows Canvas Smoke (Wine) (push) Has been cancelled
CI / Windows Effects Smoke (Wine) (push) Has been cancelled
CI / Frontend Examples (push) Has been cancelled
CI / Mobile Examples (push) Has been cancelled
CI / Generated App Scaffolds (push) Has been cancelled
CI / macOS WebView (push) Has been cancelled
CI / macOS GPU Perf (push) Has been cancelled
CI / Linux WebKitGTK (push) Has been cancelled
CI / Windows WebView (push) Has been cancelled
CI / CEF Platform Tooling (push) Has been cancelled
CI / npm Package (push) Has been cancelled
CI / Native Examples (push) Has been cancelled
CI / Evals Typecheck (push) Has been cancelled
CI / Docs (push) Has been cancelled
Release / Check for new version (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
Release / Publish CLI to npm (push) Has been cancelled
* Make canvas scroll state and routing two-axis - Widen ScrollState to eight per-axis fields (ScrollAxisState carries the physics), route each wheel axis independently to the nearest ancestor scrollable on that axis, and step kinetic motion per axis. - Add the scroll axis declaration (attr 86 axis, attr 87 value-x, Widget.scroll_axes/value_x) through schema, markup validation, both engines, layout, clamping, reconcile, semantics, and the two-bar scrollbar renderer. - Break the one-axis on-scroll record consciously: the reflect vocabulary, contract class, and both engines now teach the per-axis migration by field name, and the scroll-driver ABI/journal carry offset_x. * Carry both scroll axes through hosts, docs, and the TS SDK - Widen the macOS native scroll driver ABI to offset_x with a horizontal overlay scroller; GTK and Windows wheel handlers already forward delta_x. - Update the TS SDK ScrollState to the eight per-axis fields and regenerate the markup vocabulary and scroll reference for axis/value-x. - State the ScrollState break and its one-sentence migration in the changelog fragment. * Pin two-axis scrolling with routing, driver, replay, and markup tests - Cover the nested independent-axis routing case (dy to the vertical child, dx to the horizontal ancestor), the horizontal wheel/keymap/scrollbar paths, and per-axis driver sync with pinned ranges on ungranted axes. - Ride a diagonal wheel through the reference record/replay session so a journaled two-axis scroll replays to identical per-axis offsets. - Migrate the example apps to the two-axis ScrollState fields. * Give the soundboard detail page a horizontal collection shelf - Rail every other album under the track list on an axis="horizontal" scroll region: the grid's quiet-hover cover tiles, one press from record to record. - Echo the shelf's offset_x into value_x (the controlled-scroll shape on the sideways axis) and reset the rail when an album opens. - Teach the widget-wheel automation verb an optional delta-x token so drivers can scroll both axes like a real trackpad gesture. * Harden two-axis scrolling per external review - Keep the vertical scrollbar's thickness formula byte-identical (per-axis derivation), exclude anchored/clip-scope/concealed-disclosure subtrees from horizontal extents, and stop region-anchored surfaces riding scrolled content. - Route macOS native wheel gestures by dominant-axis capability with an engine-wire fallback, and rewrite a queued driver report when a programmatic offset lands. - Pick scroll semantics by live range on both-axes regions, page every granted axis on assistive steps, carry fling velocity per axis only while its offset survived, refine the axis/virtualized validation to the horizontal grant, and migrate bench_render. * Split residual wheel axes on macOS and arm driver chrome per grant - Forward the axis a locked native driver cannot travel to the wire, so one diagonal gesture scrolls a vertical list natively while its delta_x reaches the horizontal ancestor through per-axis routing. - Carry the axis grants on the driver spec: elasticity and scroller chrome arm only on granted axes, so a horizontal-only region can never bounce vertically or report an offset the runtime would fight. - Accept the optional delta-x token in the automation CLI, and warn at the builder seam when a DYNAMIC value pairs a horizontal grant with virtualization or value_x with a vertical-only region (both engines share the seam, so the diagnostic stays engine-uniform where validation cannot see the resolved value). * Route every macOS wheel through the axis splitter and split set_offset per axis - Stop the driver view claiming wheel events at hit test: the surface's dominant-axis selection, gesture lock, and residual-axis split now own all wheel routing, so a diagonal gesture over a nested vertical list cannot swallow the horizontal component its ancestor owns. - Make the driver set-offset flag per axis end to end, so a programmatic vertical write can never push a stale horizontal offset over native motion whose coalesced report is still in flight. - Teach the a11y focus-reachability audit the axis grants (offscreen-right tiles on a horizontal shelf are reachable, below-viewport content there is not) and bump the model-contract format to 2 - a format-1 artifact classified the retired one-axis scroll record as a scroll_state payload, which would be a false pass today. * Pin the axis-aware focus-reachability audit - A horizontal shelf's offscreen-right tile is reachable by design while a below-viewport button inside it stays a finding. * Match native wheel routing to the engine walk and step the live axis - Route each gesture axis to the deepest native driver that can consume its delta right now (direction-aware, so a saturated inner region hands an outward swipe to its ancestor), falling back to the outermost elastic region — rubber-band regions bounce even with short content — and reconcile the driver array in layout pre-order so keyed reorders keep the walk honest. - Forward a residual axis only when the locked driver can neither move nor bounce on it. - Assistive increment/decrement on a both-axes region whose only live axis is horizontal now step sideways instead of paging a zero-range vertical axis; pinned through the accessibility action path. * Resolve wheel owners per axis with measured residuals and axis-keyed reach latches - Replace the single-winner gesture lock with per-axis owner resolution at the gesture's anchor point, re-evaluated per event against live scroller offsets: saturation hands an axis to its ancestor (elastic-take never outranks a consumer), cross-owner diagonals whose native recipient would eat the other axis ride the wire whole, and the residual is MEASURED (delta minus what the scroller actually absorbed) so a nearly saturated region can never double-spend. - Flush the coalesced driver report before any residual or cross-owner wire emission (offsets first, one clock), and route wheels over a visible overlay scroller through the same splitter instead of the knob. - Key reach-end/reach-start hysteresis latches by (id, axis) so a region whose primary axis changes re-arms honestly. * Flush driver offsets before the no-owner wire hand-off too - Every wire emission now rides the offsets-first clock, including wheel events no native region owns. * Size the collection shelf to actually overflow at desktop widths - 168-point tiles keep seven sibling covers wider than the regular shell's content row, so the rail scrolls where it ships. * Close the native-seam races and honesty gaps from the cycle-2 review - Restrict wheel-owner resolution to the hit region's ancestor chain (parent driver ids through the ABI), anchor wire hand-offs at the gesture point, carry sub-half-point residuals across events, and wire-bind any region the engine scrolled this gesture so a later absolute native report can never erase relative wire motion. - Skip subtrees anchored directly to the region in every VERTICAL extent walker (a stationary surface counted against a moving offset grew the range without bound), decide the assistive step axis from the semantics metrics (concealed-disclosure and anchored exclusions included), and page exactly one axis on widget-level semantic steps. - Flag content stranded before a horizontal region's origin in both audits (offsets clamp at zero), double the reach-latch capacity for the (id, axis) key space, and fire reach signals only when the latch stores. * Treat each discrete wheel as its own gesture - Legacy (phase-less) events reset the wire bindings and residual carries per event, so a binding can never demote a region to the wire forever. * Scope phase-less wheel gesture state to bursts, not events - A quiet gap (250ms, well past the input queue's one-frame coalescing) is the gesture boundary for discrete streams: residual carries keep accumulating sub-half-point deltas and wire bindings outlive any in-flight relative hand-off, while a fresh burst still resets both so no binding lives forever. * Honor overlays, phases, and revoked axes on the native wheel seam - Push OCCLUDER rects (anchored floating surfaces at their frames, modal catchers as the whole view) with per-driver masks through the driver sync, so the host's geometric wheel routing declines exactly the points the engine's hit test would give to an overlay's branch; drivers inside the overlay stay exempt. Pinned through the null platform. - Forward zero-delta phase events (begins, the terminal Ended/Cancelled) to the gesture's last native recipient so the scroller's overscroll bookkeeping always terminates, and scope residual carries to the hit region so fractional motion never leaks onto whatever chain the pointer wanders to next. - Pin REVOKED axes home on native-driven regions too (the range clamp stays the OS scroller's), so an axis flip behaves identically on every host instead of resurrecting a stale echoed offset on re-grant. * Make native wheel routing engine-exact: one owner or the wire - One owner takes the WHOLE event natively and clamps at the edge exactly like the engine's consume-and-drop rule; split owners, dead axes, and wire-scrolled owners ride the wire whole — the residual measurement, fractional carries, and partial-clamp forwarding are gone, and consumption uses the engine's exact bounds. - Zero-delta phase events forward only the gesture bookkeeping to the last native recipient; nonzero no-owner deltas go wire-only, so an overlay opening mid-momentum can no longer keep moving the obscured region. - Fix the occluder model on every reported edge: view-local modal rects, tooltip passthrough, scrim=false previews, hidden/concealed surfaces, render transforms, self- and paint-order exemptions for anchored scroll regions, and a fail-safe whole-view occluder when surfaces outnumber the budget; both-axes semantic steps read child-frame range so the stepped axis matches the exposed semantics. * Carry the two-axis scroll state through corewire's ABI seam - dispatch_scroll_state now carries the eight per-axis scalars in declaration order; the retired one-axis quartet rides the generic record entry - pin the new routing and the quartet's retirement in emit tests, and match the stub core's export to the widened symbol |
||
|
|
7563a4e61a |
Letterbox video to its stream aspect and anchor the example's status bar (#189)
* Letterbox the video surface: contain is its one fit mode - The builder stamps contain fit and the LOADED report's stream dimensions on every media surface the video channel feeds; the emit computes the centered aspect-fitted quad over a black backdrop, so every host composites the same geometry with no fit math of its own - Unknown dimensions pre-LOADED keep the full-frame placeholder draw (no guessed geometry, no divide-by-zero); a source replacement re-fits from the new report - Paint-level pins: reference-rendered pillarbox/letterbox/exact-fit pixels and quads, the Ui stamp, and a null-platform end-to-end letterbox + re-fit test; the video doc states contain-by-default * Anchor the video-player status bar full-bleed to the window edges - The page padding wraps only the content column; the status bar is the root column's last child - flush left/right/bottom with its own padding and the token hairline separator, on both screens - A layout test pins the full-bleed frame at the window edges * Add the video contain-fit changelog fragment - One fix fragment: contain-by-default letterboxing on the video surface, placeholder pre-LOADED, re-fit on replacement * Scope stream geometry to the fed surface and keep the placeholder under the bars - Stream dimensions ride a dedicated Widget.stream_size stamped only on the surface the active playback feeds - a source-less <video> beside a custom-surface load keeps its placeholder, and image_src keeps its source-crop meaning on every widget - The letterbox paints black over exactly the remainder bars, leaving the deterministic id-derived placeholder under the picture quad for goldens and replay screenshots - A bar-inset quad drops the radius mask (its corners sit in the bars) while each bar rounds only its outer corners, keeping a rounded surface's silhouette * Harden the contain fit: diff coverage, seam-free quads, thin-bar masks, purge on video claims - stream_size joins the widget diff's content comparison so a LOADED report alone invalidates retained paint, and the known-geometry draw stretches into the engine's quad - a decoder whose true dimensions round off the report can never open a host-side contain seam - Bars thinner than the corner radius keep the frame's mask on the quad (a hairline bar cannot cover a rounded corner), pinned alongside the drop-the-mask case - A video-channel claim purges the previous playback's retained texture (entry, host copy, repaint) so a replacement's new fit never composites the old stream's stale frame; generic producer re-claims keep the adoption-boundary dedup contract * Draw the letterbox as a rounded black field with a tangent-masked quad - The fitted shape is now three commands: black across the whole frame at the frame's radius (the silhouette, exact by construction - no per-bar corner approximation), the placeholder confined to the quad, and the picture quad - The quad's mask is the tight inset radius max(0, corner - bar): internally tangent to the frame's corner circle, so hairline bars neither leak picture past the silhouette nor notch more corner than the silhouette demands - A declared image_src crop drives the fitted quad's aspect - the crop is what draws, never flattened into the full stream's proportions * Clamp the fit radius, clip crops to the stream, and end the picture on video release - The tangent-mask math starts from the renderer-effective corner radius (clamped to half the frame's short side), so an unbounded style radius can no longer round a fitted quad into a circle - A declared crop clips against the stream bounds before driving the quad's aspect, and the draw samples exactly the clipped region - an out-of-range crop never stretches its visible remainder - The video-channel release purges the adopted frame: stop/replace/failure drop the surface's contain stamp on the next rebuild, and a retained freeze-frame would composite distorted under "no playback" chrome; pause and natural completion keep their claim and picture * Keep crops off the fitted video draw and pin the binding's thread contract - The fitted draw always samples the whole texture at the stream's aspect: DrawImage.src is adopted-texture pixel coordinates and the video texture's size is unknowable at emit (macOS budget-fits large decodes), so a stream-coordinate crop cannot be translated - crops stay a generic-producer facility in texture coordinates - MediaSurfaceBinding is documented loop-thread-only on both halves: release purges loop-thread runtime texture state, matching its only callers (the video channel's update-dispatch paths); producer threads hold the sink, never the binding |
||
|
|
dd9307656a |
Hover Msg bindings: on-hover-enter and on-hover-leave (#188)
* Add the hover-enter/hover-leave event pair to the markup vocabulary - Registry events 11/12 (on-hover-enter / on-hover-leave): payloadless Msg bindings, legal on any element like the press family. - Binding stamps Widget.hover_msgs — hover-hittable via the chart hover-details rule (no press claim, no wash, no a11y action) — and widgetHoverMsgChainFromNode collects nested listeners outermost-first for containment tracking. - Both markup engines, the builder handler table (UiHandlerEvent.hover_enter/hover_leave), and parity/chain tests; the markdown arena canary's linear factor absorbs the larger Ui.Node. * Track hover-Msg containment in the runtime and dispatch enter/leave - Each view keeps a standing containment chain recomputed at exactly the wash-resolution seams (pointer phases, scroll re-hit-tests, layout adoption, rebuild and dismissal prunes), so hover Msgs and the wash never disagree; cancel is the window-leave edge and unbound apps keep an empty chain. - UiApp diffs the chain against its delivered mirror at the tail of every runtime event — leaves innermost-first, enters outermost-first, leave Msgs captured at enter time so unmounted elements still deliver the pair; a pass cap bounds flapping apps. - Tests pin nested containment, cancel, enter-that-unmounts, scroll-under-stationary-pointer, and record/replay determinism through the reference session's raw pointer moves. * Prove the hover pair end to end over a transpiled TS core - The markup fixture's task rows bind on-hover-enter/on-hover-leave with for-each payloads into a hoveredId mirror; payloadless events need no TS SDK type surface. - The e2e test drives raw pointer moves through the null platform: enter with the row payload, row-to-row handoff, and the paired leave clearing the mirror. * Document the hover pair and give notes a status-bar hover preview - Markup reference, LSP/vocab doc tables, and the native-ui skill cover on-hover-enter/on-hover-leave: containment semantics, the pairing guarantee, wash separation, and the touch-honesty note. - examples/notes: hovering a note row previews its title, age, and word count in the status bar without committing the selection, with a real-pointer test. - Changelog fragment for the feature. * Harden hover-Msg capture, delivery, and the touch-honesty gate - Captured leave Msgs deep-copy their payload slices into slot-owned bytes (a standing hover outlives the build-arena pair), with live-tree fallback for payloads the budget cannot own; enters resolve from the live tree per edge. - Containment advances only while a hover-capable pointer is live (a hover-phase move, which touch contact cannot produce), so taps, scrubs, and post-fling re-hit-tests never synthesize hover while mice keep full click/drag fidelity. - Delivery degrades per edge (one failed dispatch no longer swallows sibling edges), the drain also runs on a handler's error path, and a transiently missing handler tree defers the transition instead of consuming enters as silence. * Close the hover delivery seams: direct dispatches, pointer identity, and unbounded captures - Public dispatch and drainEffects settle hover edges at their own tails (re-entrancy guarded), so direct dispatches deliver an unmounted element's leave without waiting for a platform event; drain passes degrade per pass and the flap cap is sized past any honest cascade. - The hover-capable proof is scoped to the pointer identity that earned it, and a consumed secondary-stream cancel retires containment like the tooltip machine's pointer-left-view reading — a touch contact can never ride a mouse's proof on hosts that distinguish pointers. - Leave captures are arena-backed (any payload size) and the standing view label is copied out of runtime storage before dispatches that can compact the view array. * Anchor hover containment to the proven pointer and reserve a touch id bit - A proven pointer's wheel refreshes the chain's re-hit anchor before the scroll reconcile, so a wheel arriving ahead of its coalesced motion event derives containment from where the pointer really is. - platform.touch_pointer_id_bit rides the existing pointer_id field: hosts stamp touch-sourced events and the runtime refuses hover proof to stamped ids, so an OS-synthesized mouse-shaped float for a tap can never make touch hoverable; host-side stamping call sites are documented at the bit. - Dismiss events drain hover edges at their tail (the automation dismiss verb dispatches one standalone), and the leave-capture copier handles error sets, error unions, and vectors. * Make hover containment per-listener and its captures slot-owned - The mirror diff is an id-set diff with capture slots decoupled from chain position: an outer listener unbinding (or binding) while an inner one stands dispatches edges for the changed id only — the retained entry never flickers and keeps its captured leave. - Point-blind scroll re-hit-tests use the proven pointer's own anchor, so containment hands off correctly even after another device cleared the shared pointer position. - Allocation failure while capturing a leave defers the enter (and the entering tail) to the next drain instead of dispatching an enter whose paired leave is already lost; the copier's non-transient refusal narrows to single-item pointers. * Close the enter/leave pairing seams: platform window-leave and tree currency - GTK connects the motion controller's leave signal and Windows arms TrackMouseEvent per hover session, both emitting the pointer cancel macOS already sends from mouseExited — window-leave retires hover state on all three desktop hosts (press-in-flight streams settle through their own release/capture change). - Rebuild and slot-rebuild wrap in one currency invariant: a failed build or publication marks the handler trees stale, entering edges defer until a rebuild lands (never resolving through a tree the runtime refused, never consumed as absent), and captured leaves dispatch regardless — a broken destination cannot withhold them. - Standing captures refresh whenever the build generation moves — a leave handler added mid-hover is captured before an unmount needs it, payloads deliver their latest binding, and unbinding retains the last capture; pinned by late-bind, failed-publication, and recovery tests. * Refine hover currency to per-tree families and harden capture copies - Tree currency splits per family: a main-only rebuild can never restore currency for a secondary-window tree whose publication failed — only a clean pass over the slots does — and every drain gate resolves the flag for its own destination. - Capture refresh is copy-then-swap into a spare slot, so a failed allocation keeps the still-valid capture it was replacing. - The slice copy preserves the payload type's own alignment and sentinel, pinned end to end by a builder-only app binding a 64-byte-aligned leave payload through capture, unmount, and delivery. * Carry slot-rebuild bookkeeping everywhere and honor host pointer hand-offs - Every slot rebuild path — the full pass and the direct resize/install sites — stamps the slot family stale on failure and ticks the build generation on success; only the clean full pass restores currency. - The GTK click gesture wires cancel: a transferred or broken grab rolls a pipeline-visible press back with a pointer cancel (a drag-claimed press rolls back silently), so a stale pressed flag can never suppress later leaves. - Windows WM_MOUSELEAVE skips the cancel while the cursor is still inside the client rect — the HTTRANSPARENT window-drag hand-off, not a real departure — so entering a hidden-titlebar drag header freezes hover instead of dispatching false leaves. * Stamp hover currency at the install window itself, per tree - Staleness marks exactly the window between publication and handler-tree adoption: a build or layout failure keeps the old, still-matching pair current (edges flow even when an idle app performs no further rebuild), and post-install follow-up failures never defer. - Each window slot carries its own currency, so one window's failed publication never defers hover into its siblings, and that window's own next successful rebuild restores it wherever it was driven from. - Capture-refresh allocation failures surface through the dispatch-error machinery instead of being swallowed; tests pin the enter-only deferral, leaves-never-wait, and build-failure-keeps-pair-live paths. * Open the hover staleness window at adoption and bound capture indirection - The currency stamps sit immediately after each setCanvasWidgetLayout: publication rejection is validated-then-atomic, so a refused chrome build, oversized-text rebuild, or slot budget overflow keeps the old still-matching pair current instead of deferring hover enters indefinitely. - The leave-Msg copier bounds slice indirection (64 hops): a cyclic value graph refuses as unsupported instead of recursing toward allocator exhaustion or stack overflow. * Track adoption exactly, drain standalone edits, and refresh captures per transition - The currency stamp keys on a per-view adoption counter incremented the moment copyWidgetLayoutTree replaces the retained tree, so a failure in the publication pipeline's post-adoption steps still marks the pair stale while a validated-then-atomic rejection never does. - Standalone keyboard events (accessibility selection edits, context-menu cut/paste/select-all) carry a flag the hover drain honors at their own tail — an edit that unmounts the hovered listener no longer waits for an input cycle that never comes. - Leave captures refresh at every rebuild commit, not just at drain time, so two dispatches in one cycle (payload moved, then unmounted) deliver the latest binding; a proven pointer's consumed secondary release outside the view retires containment — the leave the frozen right-drag stream suppressed. * Keep capture slots leak-free and refuse unownable hover pairs whole - The enter loop and the unwind release any slot a mid-batch capture refresh installed before assigning or dropping a position, so repeated re-entry with rebuilding edge handlers can never exhaust the slot budget; pinned by a 40-cycle churn test asserting zero slots held after exit. - An unownable leave payload (a single-item pointer) now refuses the PAIR: the enter never dispatches, the exit owes nothing, and the refusal settles with one warning instead of retrying every drain — no enter without a deliverable leave. - Capture-refresh failures at rebuild commits land in the dispatch-error ring, so a stale payload delivered after a swallowed allocation failure can no longer hide behind rebuilds that reported success. * Size the capture pool for both transition populations and stamp adoption at the tear - The slot pool covers the departing chain's held captures plus a fully refreshed standing mirror plus one swap slot, and claiming degrades like allocation pressure instead of trapping if the accounting is ever wrong. - The adoption witness moves inside copyWidgetLayoutTree at its destructive boundary, so a per-node failure mid-copy (an invalid command name escapes the pre-validation) counts as adopted-and-torn while up-front rejections leave it unmoved. - A leave rebind no copy can own on a STANDING element warns once and degrades to live-tree resolution (a later ownable rebind upgrades), and sentinel-terminated array payloads capture with their sentinel stamped; both pinned by probe-app tests. * Give hover containment its own hit-test policy and a half-open outside test - Hover-Msg listeners are invisible to the interactive hit test (wash, cursor, press routing, text selection) and resolvable only through the new hover-containment policy, so binding hover provably never paints a wash or steals a click — even from an overlapping sibling; pinned by a probe asserting the interactive hit test finds nothing where the containment chain stands. - The consumed-release outside test uses the engine's own half-open rectangle containment, so a release at exactly the right or bottom edge retires the chain like every hit test already treats that point. * Resolve hover state by view identity and prune with the hover predicate - Hover capture refresh and drain lookups key on window id plus canvas label, so a replacement window reusing a closed window's label (even with identical structural ids) can never answer for its predecessor's captures. - Chain pruning uses a hover-specific survival predicate — a hover-only listener is never evicted into a false leave, and a widget whose hover bindings a rebuild removed stops standing — and a torn mid-copy failure prunes containment against the partial tree so owed leaves dispatch at that failure's own drain. - Windows WM_MOUSELEAVE suppression requires the point's owner to be one of the child's own ancestors (the HTTRANSPARENT hand-off signature): an overlapping sibling pane taking the cursor is a genuine departure and cancels. * Own the dispatch-error event name and bump the journal semantic epoch - DispatchError keeps its event name in inline storage with an accessor (the detail pattern, 64-byte cap): records are copied by value into a ring that outlives every caller's buffer, so a name recorded through reusable storage can never dangle; pinned by a clobbered-buffer test and every consumer swept to the accessor. - Reserving pointer-id bit 63 as the touch-source stamp changes a journaled field's meaning: the session journal's semantic epoch bumps 3 to 4, so older recordings refuse with the standard re-record teaching; the changelog states the conscious break. * State the secondary-stream scope honestly in the input comments - Only the secondary down/up/cancel stream is consumed: hosts report drag motion without a button, so containment follows a right-drag on the primary path (the mouseenter/mouseleave convention), exactly as the wash does — the comments now say so instead of claiming a freeze. - The GTK leave handler's suppression comment scopes itself to click-gesture presses and names the settling paths (release, gesture cancel, the runtime's outside-release check). * Teach the markup fixture sidecar the hover pair - The hand-written contract sidecar (the schema's independent ground truth) gains the hoveredId model field and the hover_row/hover_off f64 arms in declaration order, so the corewire mirror stays fingerprint- and contract-byte-identical to the transpiled lane. - The facade wire-tag pins move with the union: zoomed sits at tag 11 behind the hover pair. * Retire containment when a proven primary release lands outside the view - A captured drag's release beyond the surface clears the chain and the hover proof instead of re-hit-testing: hosts held a grab through the drag (no motion-leave fired) and send no later cancel, so an overflowing listener could stay entered off-view and a parked off-view anchor could re-enter one on a later rebuild. - The primary rule mirrors the consumed secondary release; pinned by a down/drag-out/release-out test asserting the delivered leaves, the empty chain, and the retired proof. |
||
|
|
b57224d0d1 |
Pair compiled cores with their own sidecars and pin the export attestation order (#187)
* Let the parity suite pair a compiled core with its own contract sidecar - NATIVE_SDK_EXTERNAL_CORE_SIDECAR points the generated mirror at the caller-supplied archive's own emitted sidecar, so the boot-time build_id fence checks the pairing the ABI ratified instead of always reading the committed fixture sidecar - the committed fixture sidecar stays the default for stub-shaped callers that restate its identity * Pin the normative canonical order for the sidecar's export attestation - The reader's expected-order array, the extractor's emitted list, and the fixture sidecar all state one order: version/identity entries, init, the mode-provided entries, the remaining unconditional suffixes, then wired channel entries - V11's teaching names the expected suffix at the diverging position |
||
|
|
ac06f6c25e |
Add an env-gated behavior-parity suite for compiled core archives (#186)
Release / Check for new version (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
Release / Publish CLI to npm (push) Has been cancelled
CI / Zig Core (push) Has been cancelled
CI / macOS WebView (push) Has been cancelled
CI / macOS GPU Perf (push) Has been cancelled
CI / Linux WebKitGTK (push) Has been cancelled
CI / Windows WebView (push) Has been cancelled
CI / CEF Platform Tooling (push) Has been cancelled
CI / npm Package (push) Has been cancelled
CI / Native Examples (push) Has been cancelled
CI / Linux Canvas Smoke (push) Has been cancelled
CI / Linux Dev Smoke (Debug scaffold) (push) Has been cancelled
CI / Windows Canvas Smoke (Wine) (push) Has been cancelled
CI / Windows Effects Smoke (Wine) (push) Has been cancelled
CI / Frontend Examples (push) Has been cancelled
CI / Mobile Examples (push) Has been cancelled
CI / Generated App Scaffolds (push) Has been cancelled
CI / Evals Typecheck (push) Has been cancelled
CI / Docs (push) Has been cancelled
* Extract the mirror-value converter into a shared sidecar test module - tests/sidecar/mirror_value.zig now carries convertValue; the conformance suite imports it unchanged - prepares the compiled-core behavior-parity suite, which converts scripted messages and reference models the same way * Add an env-gated behavior-parity suite for a caller-supplied compiled core - NATIVE_SDK_EXTERNAL_CORE_ARCHIVE (path-delimiter-separated link inputs) builds a test binary pairing the transpiler lane with a fresh corewire mirror over the real archive; unset, the step skips and the suite never builds - one scripted sequence through both lanes, byte-comparing command, snapshot, and subscription bytes per cycle, plus collect invariance and deterministic re-init on the archive side - new test-external-core-parity step; wired into zig build test only when the archive is supplied |
||
|
|
64ab5f205c |
corewire: generate core bindings from a machine-readable contract sidecar (#185)
* Add corewire: contract-sidecar reader, validator, and shim emitter - Parse core.contract.json (schema format 1) via std.json with teaching refusals naming exact field paths; unknown fields warn, unknown format/enum values refuse whole-file - Enforce the reader-checkable validation rules (V1-V11): hash encodings, one type namespace, reference resolution and reachability, acyclicity, tag bounds, descriptor consistency, unbound lists, channel/export biconditionals, and the integer-slot bijection - Emit core_shim.zig: mirror Model/Msg/channel types with the sidecar's names, order, and number classes, declaration-order tag table, descriptor-driven dispatch stubs, snapshot decoder, and helper forwarders; extern bindings isolated in core_abi.zig so an ABI revision is a one-file edit * Wire the sidecar-shim conformance harness into zig build - Pair every ts-core fixture (host, markup, soundboard, system-monitor, ai-chat) with a corewire-generated mirror: the markup fixture from a committed hand-written sidecar, the rest extracted from the transpiled modules at build time so corpus sidecars cannot go stale - Hold both lanes byte-identical by layout_fingerprint.describe of Model/Msg and by the serialized model-contract artifact (anonymous payload records mask only Zig's per-module instance counter) - Force full analysis of every generated shim against a stub core exporting the draft ABI's symbol set, so dispatch stubs, snapshot decoders, and channel forwarders compile and link; new sidecar-conformance step, wired into zig build test beside the ts-core e2e suites * Record sidecar schema gaps and observations from the conformance corpus - Four underdeterminations: number_bytes field order, integer-slot grammar coverage for slice elements and union arm payloads, and the synthesized-name container for anonymous helper returns - Observations on worked-example spellings, anonymous-type name counters, and the channel-entry payload encoding pin * Format corewire sources with zig fmt * Harden corewire emission edges and buffer lifetimes - Double-buffer the shim's model arenas so a previously returned root survives the decode that replaces it (the transpiler lane's one-generation grace at compaction), pinned by a test - Refuse sidecars whose type, message, or helper names collide with declarations the generated shim itself must make; keep authored types that merely spell like synthesized names as top-level declarations unless referenced exactly once - Escape every sidecar string landing in generated Zig literals and every reflected string the extractor writes into JSON; bound tabled unions and enums to the one-byte wire index; restate the deterministic/async_free attestations as shim constants for host policy gates * Keep corewire's checker as strict as generation and its errors honest - --check now runs the full pipeline and discards the text, so a sidecar can never pass the checker and refuse at generate time - JSON-parse memory pressure propagates as OutOfMemory instead of masquerading as malformed input; a diagnostic that cannot be recorded terminates loudly instead of vanishing - Record the synthesized-vs-authored table-entry ambiguity as a schema gap and document the fidelity-driven inlining bias at its site * Close corewire's remaining name and mode edges - model_unbound accepts exported helper names (the dead-state opt-out vocabulary spans fields and bindable model methods); schema wording gap recorded - --check and --out are mutually exclusive modes; combining them is a usage error instead of a checker that writes files - Channel forwarders and the wire-message decoder speak the sidecar's message union name instead of a hard-coded spelling; the void TypeRef is refused outside bare union arms and decodes as no value; optional glue names reserve only when their glue is emitted * Prove the attested export set at link time and tighten shape checks - boot() references every abi.exports symbol via doNotOptimizeAway, so an object missing an attested entry point fails at link instead of lurking behind lazy extern binding - Text-input routing applies the markup engines' full structural predicate (payload shapes, not just the eleven tag names); near-miss unions ride the record entry - Boolean and presence discriminants past 1 refuse as layout skew instead of decoding silently; UpdateResult reserves only when emitted; the view_unbound spelling is fenced against helpers, fields, and arms that would shadow the opt-out tuple * Alias the wiring's root spellings and reserve the attestation glue - A sidecar naming its roots something other than Model/Msg gets transparent aliases under the wiring's fixed spellings, so the staged main's re-exports and the reflecting seams keep compiling; the alias names reserve against colliding table entries - referenceAttestedExports joins the reserved emission names - State at the attestation site why the no-extra-exports direction is the producer's conformance obligation (a linker ignores unreferenced extras; the generator never opens the object) * Refuse un-buildable channel arms and the discard identifier at tool time - The appearance and chrome arms must carry the exact structural records the host builds by field name; a named-type payload of any other shape refuses with the shape teaching instead of a compile error inside generated wiring - A name spelled _ is the discard token, not a declarable identifier; it now takes the quoted form like keywords and primitives * Hold channel record checks to by-value storage and bare arms to empty payloads - The structural vocabularies the host constructs by field name (chrome insets/buttons, text-input payload records) accept by-value records only; a node reference mirrors as a pointer neither consumer's shape takes - A channel message on a bare arm must carry zero payload bytes; stale bytes refuse as layout skew instead of dispatching silently * Sharpen the number_bytes and helper-return gap records - State the wire consequence of the missing field-order fact and why the generated shim itself cannot skew (one mirror type feeds both encode and decode) - Record that anonymous helper returns lack a pinned mirror spelling as well as a container rule; the generator deliberately keeps them named until a producer ratifies an inline convention * Reserve channel detection names unconditionally - The host wires channels by probing declaration names, so subscriptions/commandMsg/frameMsg/keyMsg/pinchMsg/appearanceMsg/ chromeMsg/envMsgs are off limits even when the channel is absent — a type under one of those names would falsely activate the channel and fail as a non-function - Event-record names (FrameEvent/KeyEvent/PinchEvent/PinchPhase) stay conditional; they are plain declaration collisions, never probes * Fold the ratified core-ABI deltas into the binding layer - helper_call traps on an unknown index through the panic sink instead of returning a status; the shim glue drops its check - Lifetimes follow arena truth: boot_cmd bytes live until the next frame_reset, dispatch, or init; the snapshot buffer is transient and does not survive a frame reset; collect folds the transient-arena reset and joins the runtime-owned mode symbols - Narrow signature integers are documented as C plumbing for host-produced values, never payload crossings; the sink covers detected traps only (escaped exceptions arrive with an Uncaught prefix; hardware faults stay with the host's process-wide handler) * Emit core_facade.ts: the TypeScript projection of the contract sidecar - One sidecar, two projections: corewire --facade emits a subset-TS module carrying the mirror types (interfaces, literal-union enums, kind-tagged message union), per-arm wire-tag constants and typed constructors, the generator-carried viewUnbound list, identity constants, and deterministic zero/sample model builders - The canonical value encoding rides subset arithmetic: byte values stay integer-derived end to end (owned Uint8Array runs concatenated by copy), and the f64 bit pattern extracts exactly by halving scans and comparisons, covering denormals, signed zero, the infinities, and the canonical quiet NaN; nsc_core_probe_i64/f64 expose the scalar encoders for parity proofs - Emission is deterministic, TS-identifier and nsc-name-space fences refuse collisions with teachings, and the module compiles through the shipped checker as a validation proxy (boundary-fed scans stay out of shared helpers and sparse writers zero their buffers explicitly, per the emitted lane's allocation and number-model rules) * Prove facade byte parity in the conformance harness - Compile every fixture's generated core_facade.ts through the shipped transpiler in-graph (the compile is the subset-acceptance proof) and execute its encoders: scalar probes across f64 edge cases (signed zero, denormals, the infinities, the canonical quiet NaN, the 2^53 bounds) and i64 two's complement must match native bit patterns - Facade-encoded zero and sample model snapshots must byte-match the canonical encoder over the shim's sidecar-classed mirror, via a by-name value conversion that normalizes inference-decided classes and reference storage; constructor tags ride declaration order - Record the FACADE-GAPS section: each surface the projection cannot carry, pinned to the subset rule that excludes it * State the facade generator's input model in its module doc - The generator consumes contract facts (author-module declarations plus profile constants), which precede compilation in production; the conformance harness feeds those facts from corpus sidecars as its adapter * Reword the helper-return gap note to name the missing emitter convention * Close facade projection edges: paths, parens, fences, fallbacks - corewire refuses aliased generation paths (--out/--facade naming one file, or any output naming the sidecar itself) - Composite slice element spellings parenthesize (a bare number | null[] would type the null as the array) - The generated NscfContractError and NSCF_POW declarations join the facade's name fence, and field names may not take the reserved nsc space (constructor parameter fallbacks live there, collision-proof) * Normalize output-path checks and refuse valueless contract shapes - Generation-path aliasing compares lexically normalized paths, so ./x and x cannot slip past the overwrite fences - Empty message unions, tabled unions, and enums refuse at read time: a valueless union or enum has no declarable mirror form in either projection - number_bytes descriptor fields join the reserved-nsc-space fence (they become constructor parameters and record members without ever entering the type table) * Canonicalize NaN in both encoders and fence the remaining shadows - The canonical value encoding maps every NaN — negative or payload-bearing — to the one quiet pattern: payload bits are not values in either source language and engines may rewrite them at any store, so a bit-preserving encoder could never be deterministic across producers; both encoders and the parity suite pin it - A type named Uint8Array refuses (it would shadow the ambient byte type out from under every facade encoder) - Comment sanitizers also blank U+2028/U+2029, the line terminators a TypeScript scanner honors * Keep both projections declarable under renamed and exotic contracts - The Zig emitter fences every parameter and local name its generated bodies bind (a module-level type under any of them would shadow) - The facade declares its roots under the profile's designated Model and Msg exports whatever the contract calls them, aliasing the contract names — the root commit machinery and dispatch wiring key on the exact spellings (renamed-root facade transpiles end to end) - viewUnbound carries only names the facade declares (message arms and model fields); nested optionals refuse with a teaching (one null, one absence level); strict-mode reserved words join the identifier fence * Key synthesized flattening on the contract's message name and fence unbound homonyms - Constructor-side synthesized-record detection patterns on the contract's own message name, so renamed unions flatten consistently with their arm types (pinned end to end for an Event_loaded payload) - A name that is an unbound arm and a bound field (or the reverse) refuses: the projection's single name-resolved viewUnbound list would mark both declarations; names unbound on both sides ride once * Flatten synthesized union payloads and fold case in path aliasing - Synthesized inline records inside tabled unions flatten beside kind (the authored multi-field arm shape), in the arm type, the union encoder, and the value builders alike; single named payloads keep the erased-member value spelling - Output-path aliasing folds ASCII case: the default volumes on two of the three desktop platforms are case-insensitive, so differently cased spellings of one file count as aliases everywhere * Ask the filesystem about output aliases the spelling checks cannot see - Beyond lexical and ASCII-case checks, an output path that already resolves to the sidecar's own file (Unicode case folding, links) refuses via inode identity, and the facade write re-checks against the shim file the same invocation just wrote * Compare output aliases by canonical path, before any write - sameExistingFile resolves canonical paths instead of comparing bare inode numbers, which collide across volumes; canonical paths are unique per volume, so distinct files can never compare equal (hard links keep distinct canonical paths and stay noted as out of scope) - The out/facade pair check runs in the preflight loop too, so two existing links to one file refuse before the shim write, not after * Land outputs by rename so aliased entries can never truncate shared content - writeOutput writes a sibling temp file and renames it over the target: a rename replaces the destination's directory entry and never writes through it, so a hard link to the sidecar (invisible to canonical-path preflight) keeps the input intact under its other names — verified with a hard-linked output - The identity helper's note states why hard links passing it are harmless by construction * Claim staging spellings in preflight and refuse inexact integer crossings - The .corewire-tmp staging names join the alias checks, so a sidecar spelled as an output's staging path refuses before anything writes - Scalar dispatch entries route integer payloads through a checked narrowing: values at or past 2^53 alias on the f64 wire, so they trap with a teaching instead of corrupting silently (number, number_bytes, and scroll-state paths alike), matching the bridge's shipped bound * Stage outputs exclusively under unique names and widen synthesized identities - The staging file is created exclusively with a per-invocation nonce: exclusive creation can never truncate an existing entry whatever it links to, and concurrent invocations stay off each other's bytes (a hard-linked staging-prefix entry now survives generation intact) - The extractor's synthesized source_hash and build_id cover the complete reflected surface — unbound lists, channel wiring, entry-shape flags, and the export set included — so any contract-visible fixture edit moves both identities * Pin the two schema-underdetermination dispositions as tests - number_bytes mirrors number-first (the one order every producer of the shape declares; the descriptor carries no order fact), and a bytes-first record rides the record family with its order carried explicitly — both directions now executable - A single-use pattern-named record inlines to mirror the emitted module's anonymous records — the direction every corpus fixture requires while the schema carries no synthesized marker * Stage both projections before committing either - A combined --out/--facade run writes both staging files completely before the first rename, so a write failure can never leave a fresh shim beside a stale facade; a failure between the two renames names both files as a possibly skewed pair and exits nonzero so the caller regenerates - Concurrent invocations aimed at one output path remain the caller's serialization to provide, stated at the commit site * Canonicalize existing ancestors for future outputs and clean sibling staging - Alias comparison canonicalizes the deepest existing ancestor directory and carries the not-yet-existing tail verbatim, so two spellings of one future file through symlinked or case-folded parents refuse before generation (verified with a symlinked output directory) - A staging failure deletes the sibling projection's already-staged file instead of leaving it beside the exit * Canonicalize per component, recheck after the shim commit, keep samples linear - Path canonicalization walks components against the filesystem, so .. applies to the real parent instead of lexically across a symlink (verified: link/../core.contract.json refuses as the sidecar itself) - Once the shim rename lands, the facade target re-resolves against the now-existing file, catching aliases no spelling check can see — filesystem Unicode normalization included (verified with NFC/NFD spellings on the default macOS volume) - Sample sequences emit two elements at the outermost level and one per nested level, keeping generation linear in TypeRef nesting depth * Fence helper names against the generated glue they would shadow - Model methods shadow file-scope declarations inside the struct, so a helper named after generated glue (callHelper above all) would capture the forwarders' own call sites; such names refuse at tool time with a teaching * Walk paths with the native component parser and record the arm-storage gap - canonicalSpelling parses roots and separators with the platform's component iterator, so drive and UNC spellings normalize to one prefix instead of splitting on a single separator - Any facade-staging failure shape deletes the already-staged shim file; helpers may not shadow the mirror's own type names inside the model struct (field types resolve against methods first) - A record reused as a bare message arm rides by reference in the emitted lane, which the named-type payload family cannot say: the extractor refuses the construct with a teaching and SCHEMA-GAPS records the missing storage-kind fact and its closure * Keep drive-relative spellings lexical and refuse dishonest integer bytes - A drive-relative Windows path (C:foo) names a file under that drive's own working directory, which cannot resolve portably; canonicalization keeps it lexical instead of folding it under the drive root into false aliases - The facade's i64 encoder throws the kind-tagged teaching value for non-integer or out-of-range input instead of truncating into wrong canonical bytes (probes and misclassified slots alike) * Resume canonicalization after a dot-dot pop and reserve the field capture - A .. that unwinds a nonexistent tail re-probes the filesystem, so a symlink after the pop resolves instead of riding unresolved past the alias guard (verified with a nonexistent scratch segment) - The tag-table consistency block's field capture joins the reserved emission names * Refuse contract shapes the projected subset cannot author - Quoted (non-identifier) property names and one-member enums have no accepted spelling in the shipped subset (probed: both refuse at the emitter), so the facade refuses such contracts with teachings instead of emitting members the compile would drop or misread; the Zig mirror keeps expressing both - FACADE-GAPS records the pair * Fence flattened kind fields and escape TS line terminators in literals - A number_bytes field or synthesized inline record field spelled "kind" would declare the flattened arm's discriminator twice (message arms and tabled-union arms alike); such contracts refuse with a teaching — named record payloads ride a value member and stay unaffected - tsString escapes LS and PS, the scanner's remaining line terminators beside the already-escaped LF and CR (NEL is ordinary text), so exotic member names cannot end an emitted string literal early; both pins ride the unit suite * Refuse model-reachable records as message arm payloads in the mirror - The compiled core stores a model-graph record by reference when it doubles as an arm payload, a storage fact the record family cannot carry; mirroring by value would silently change the reflected layout, so the generator refuses the shape with a teaching until the schema says storage — message-only records stay by value, matching the emission and the conformance corpus * Reserve view_unbound as a type name and the intrinsic TS type keywords - The unbound-list tuples nest inside Model and Msg, where a same-named top-level type would be shadowed at every field-type resolution; such a type now refuses at tool time - any/unknown/never/bigint/symbol join the facade's reserved words: a declaration under an intrinsic type keyword would bind every reference to the built-in and erase the contract silently * Key the arm-storage refusal on node storage, not model reachability - The sidecar itself says which records ride by reference (node TypeRefs); only those keep reference storage when doubling as arm payloads, so the refusal now targets node-stored records exactly — value-promoted model records (a text-input union's caret and selection payloads) pass through by value as the emission keeps them, pinned in both directions * Note the facade-constructor consequence of the missing synthesized marker - Gap 4 now records that the inline bias is visible in constructor parameter shapes as well as in type-name artifacts, and why the compiled layouts agree either way (a collapsed named record and a flattened anonymous one carry the same fields in the same order) * Pin anonymous-name detection to digits and make codecs strictly positional - Anonymous container names carry the compiler's instance counter to the end of the name; an authored type merely containing the marker keeps its declared identity in extraction - The canonical codec's enum and union discriminants ride declaration-order positions on BOTH sides, never numeric values (explicit-valued types round-trip, pinned by test) - tsString and the comment sanitizers propagate allocation failure instead of returning unescaped or empty text * Hold facade names legal through the whole proxy pipeline - One authority validates every name category the facade declares (types, arms, fields, enum members) against BOTH languages: the compiled module takes identifiers verbatim, so its charset, keyword, primitive, and discard rules apply on top of TypeScript's — with the TypeScript reserved words scoped to declarations only (reserved words are legal members, as the corpus's own number field proves) - Anonymous-type detection keys on the FINAL compiler suffix, so an authored name containing the marker keeps its identity while an anonymous member nested under it is still recognized (pinned both ways); the string escaper is pinned directly as defense in depth * Anchor anonymity to the parent name and widen file-identity checks - Synthesized classification requires two fields (smaller inline arms collapse at emission and never table), so the two projections can never diverge on a one-field pattern-named record - Extraction recognizes an anonymous member only when its name prefix IS the enclosing parent's own name beside the final counter suffix, so authored marker-bearing names keep their identity at any nesting - Existing-file identity falls back to full metadata agreement (inode, kind, size, links, both timestamps) when canonical paths differ, the honest net for mounts that expose one directory twice * Reflect aliased root names, disambiguate slot paths, consume attestations - Extraction takes the root type names from the reflected types, so an aliased root export tables and names its real type consistently - Names containing dots refuse wherever they would join an i64 slot path (two different slots could spell one path), and the V10 bijection now consumes entries one-to-one so a single attestation can never cover two slots * Harden identity hashing, alias checks, and graph-walk bounds - Frame the synthesized identity surface with labeled NUL-separated sections covering both root type names, so renaming the message union or moving an unbound name between lists moves source_hash and build_id - Treat same-directory-entry aliases as conflicts while letting hard links pass: rename lands on the entry, so only the entry's identity matters - Bound TypeRef nesting at 256 levels in the reader and validate the named-type graph iteratively with a chain-depth ceiling, refusing pathological documents with a teaching instead of exhausting the stack * Bound expanded value depth, vet abi.prefix, gate all corewire suites - Depth validation now sums optional/slice wrapping with record chaining, so compound nesting refuses at 256 expanded levels instead of overflowing downstream walks - abi.prefix must be a linker-symbol-safe spelling (ASCII letters, digits, underscores, non-digit start); embedded NULs and spaces refuse with the offending byte and offset - Register sidecar.zig and emit_facade.zig test roots in zig build test - imported files' tests never ran under an importer's root |
||
|
|
87fa3f92d0 |
Video playback: AVFoundation through the media surface, replayable end to end (#184)
* Add the video playback tier: platform seam, effects channel, journal v9
- One video channel mirroring audio end to end: loadVideo with the local-then-URL cascade and surface claim, transport verbs (play/pause/stop/seek/volume/mute/loop), key-stamped events, honest failed/rejected degrades, and automation-snapshot mirrors
- Pixels never enter the core: the platform decoder pushes RGBA8 through a copyable VideoFrameSink into the media-surface texture channel the load claimed
- Journal format v9: the .video effect-result kind (code 13) and platform-event tag (code 25) append the video fields after the v8 channel fields and journal every event verbatim for byte-identical replay with no producer attached
* Cover the video tier: lifecycle, frames, teardown, replay identity
- Fake and real executor batteries mirror the audio suite: request capture, event round trips, transport mirrors, rejection classes, cascade order, staged-host degrade, straggler swallowing, and the quit-while-playing stop hook
- Sink coverage proves decoded frames reach the claimed surface, replace/stop release the claim, and a stale sink push lands inert
- A recorded playback replays byte-identical into a decoder-less host with no producer attached, fingerprint and model equal; journal codecs round-trip the video event and effect shapes
* Export the video effect types and decline video on the embed host
- native_sdk.EffectVideo/EffectVideoEventKind/EffectVideoSource ride the SDK roots like their audio twins
- The mobile embed host declines video_playback until a shim registers a real decoder, the audio honesty rule
* Decode video on macOS with AVFoundation; teach on Windows and Linux
- One AVPlayer in the AppKit host: AVPlayerItemVideoOutput frames fitted to the sink's pixel budget, BGRA-to-RGBA vImage swizzle, a 1/60s run-loop frame pump plus the audio tier's 0.5s position clock, loop wraps without a completion, and paused seeks still paint their frame
- The frame sink crosses the C ABI as a callconv(.c) trampoline over the runtime's VideoFrameSink; a released claim answers 1 and the host stops its pump
- Windows and Linux stage the capability honestly: video_playback reports false and the load verbs answer a named teaching plus error.UnsupportedService; the CEF host stubs the video C ABI like audio
* Add Cmd.videoLoad and videoCtl to the TypeScript tier at opcodes 0x17/0x18
- videoLoad claims the named media-surface, resolves the local-then-URL cascade, and bakes autoplay/loop/muted into one flags byte; videoCtl drives play/pause/stop/seek/volume/muted/loop by verb ordinal with an f64 value
- The event arm is the seven-field record matched by NAME with the exact five-member state union both directions, the audio arm convention; refusals teach with the NS1027/NS1029/NS1030 vocabulary
- The host routes events on the TSVI key namespace, parks loads under the fake executor like audio, and covers the wire byte-for-byte in the package, host, and e2e suites
* Declare video from markup: the <video> element, house chrome, reconciler
- Element code 68 with flag attrs controls/autoplay/loop/muted at codes 82-85 (src rides the existing attr name; registry law keeps names unique); a leaf on all three surfaces — validator, interpreter, compiled engine — rejecting children like image
- ui.video composes the playback surface on the framework-owned surface id plus runtime-consumed transport chrome (ghost play/pause, proportional scrub slider, clipped time readouts in the built-in bar's register); presence IS playback: the ui-app reconciler loads on src change, applies loop/muted deltas, and stops when the element leaves the view
- Handler-less playback stays honest under replay: platform video events steer the channel mirrors while journaled effect records remain the only Msg source, so house-chrome sessions replay with live readouts and identical fingerprints
* Scope declarative video ownership to the playback it started
- The reconciler stops or retunes the channel only while the active key is the declaration's own: an update handler that loaded its own playback owns the single player, and a departing <video> element must not kill it
* Add the video-player example: house chrome and custom controls
- Player screen is one declarative ui.video with the house transport chrome; Custom screen composes its own bar (transport, +/-10s, proportional scrub, volume, mute, loop) from the command vocabulary over a bare media surface
- No bundled media: the launch argument or the source field names a local clip or http(s) URL, so a live macOS check is one command away
- Headless tests drive the whole transport with the fake executor's synthetic events, the automation widget path, and the null decoder behind the declarative screen
* Document the video element across the docs surfaces
- Components page, catalog entry, native-ui element table row, and the LSP/vocab doc strings for src/controls/autoplay/loop/muted
- Deterministic preview scenes render the placeholder with the house chrome (regenerated video webps only; the untouched catalog keeps its committed renders)
- Changelog fragment states the new element, the command vocabulary, the staged platforms, and the journal v9 break
* Resolve video attribute docs in the markup LSP hover path
- attributeDoc consults the video scoped table so controls/autoplay/loop/muted hover like every registry attribute; the coverage pin now names the video composite
* Make the video-player example's docs and status line honest
- README run instructions use the real CLI verbs: native dev for build-and-run, or native build plus the zig-out binary with the clip argument (the argument passthrough shape)
- The status line now matches each screen's ownership model: the declarative Player screen shows a static teaching (its transport state lives in the runtime-owned chrome, so an event-fed status there could only lie or stall), while the Custom screen keeps the event-fed loading/dimensions/finished/failed line it owns; tests pin both across load, playback, pause, and completion
- Direct-SDK examples handle the runtime's video event in their exhaustive event switches
* Stage video events non-lossily and deliver fed terminals under replay
- Video events leave the lossy pending ring for their own non-lossy stage (the image/channel discipline): a loop-side .rejected or .failed is its load call's only terminal and a fed event is one recorded delivery, so a burst past the ring's capacity must never evict one — pinned by a 40-rejection burst test
- Fed events capture the handler at feed time and deliver their journaled values verbatim when the channel no longer resolves them: under replay the platform .failed event that follows the record applies the channel reset first, and delivery-time resolution silently dropped the Msg the recording dispatched — pinned by a recorded mid-playback failure replaying fingerprint-identical
- loadVideo's deterministic refusal classes extract to the pure videoLoadRejected, one source of truth a caller-side validator can consult
* Refuse an invalid video_load before it re-routes the bridge entry
- The engine keeps the current playback when it rejects a load, but the bridge re-keyed its single routing entry optimistically first: a refused replacement left the surviving stream's events and transport verbs answering to the refused key and arm
- The bridge now consults the engine's own videoLoadRejected gate before committing the entry and stages the rejection Msg to the refused arm directly (stageLoopMsg, the channel-admission precedent), leaving the entry and the engine untouched
- Pinned: a rejected replacement delivers its rejection while the live stream keeps its events and its wire-key gate
* Drive the house video chrome from keys and rebuild it in every window
- Keyboard activation (Enter/Space) of the transport's play/pause control now drives the video channel exactly like the pointer release: the control advertises Play/Pause to focus and accessibility, so the intent must act instead of being consumed silently — pinned beside the pointer-toggle test
- Runtime-consumed control paths and handler-less video events rebuild through one helper that follows dispatch's discipline: the main canvas against ITS window (a control event from a secondary window used to target that window with the main canvas label and error) and the window slots after it, so a <video controls> declared in a secondary window's tree repaints from the moved mirrors
* Fail a macOS video load whose conversion buffer cannot allocate
- The frame tap's reusable BGRA-to-RGBA conversion buffer failing to allocate used to degrade to a silent zero-frame pump while the load still acknowledged LOADED: playback reported positions forever and could never deliver a pixel
- videoAttachOutputForItem now reports the failure and the status hop answers with the FAILED terminal instead of the acknowledgment - the honest degrade; an audio-only item (no video geometry) keeps its documented honest-absence path
* Journal handler-less video terminals and quarantine stale fed events
- Loop-side video terminals now stage and journal with no Msg handler bound (the image arm's rule): a declarative playback binds no handler, but its synchronous .failed is executor truth — the record is what replays the channel reset, and the staged delivery's wake re-renders the chrome
- A fed event whose staged key no longer names the live playback (replaced before its drain, or a replayed platform .failed already applied the terminal) delivers its staged values verbatim and leaves the live channel alone — applying a replaced stream's terminal would reset the replacement
- A journal-fed video record claiming a millisecond or dimension scalar at or past 2^53 refuses replay as a damaged record at the gate (no recorder writes one, and the TS tier's exact-integer widening would trap on it) — pinned by a hand-patched journal
* Route every video event by the key of the load that produced it
- The bridge's engine key now carries the issuing load's event-arm tag in its low byte (videoKeyForTag): a staged synchronous .failed that delivers AFTER a replacing load re-keyed the single entry still routes the arm of the load it answers, instead of handing the old stream's failure to the replacement's arm
- videoEventMsg routes by the event's own key tag, never the mutable entry's; the wire-key gate on transport verbs is unchanged
- Pinned: a replaced load's straggling terminal routes its own arm while the replacement's stream keeps its own; the request-key pins move to the tagged shape
* Reconcile <video src> from every window's tree and repaint Msg-less failures
- Secondary-window builds now feed the video reconciler: Ui.video promises that declaring the element IS the playback in every window's tree, but slot builds discarded the declaration — the main canvas wins when both declare (one player, one owner), the first declaring slot keeps ownership until it stops declaring or its window closes (reconcile-close included)
- The main build stamps the video mirrors it rendered; drainEffects compares that stamp after a Msg-less drain and re-renders the chrome when they moved — a handler-less declarative playback's synchronous failure no longer leaves controls advertising a playback that is gone
* Paint the poster frame for paused macOS video loads
- A load acknowledged while paused (autoplay = false, the documented poster-frame shape) ran no frame timer and never pumped its first decoded frame: the surface held the placeholder until the user played, paused, or seeked
- LOADED now arms a bounded first-frame hunt: the frame timer polls until the first frame pushes (a paused load then stops the timer; a playing one keeps it), surrendering honestly after ~3s if the output never yields one
* Restart the macOS frame timer when buffered playback actually begins
- The poster hunt could stop the pixel clock while AVPlayer was still waiting to play (a remote autoplay stream yielding its poster mid-buffer, or a hunt surrendering past its bound), and nothing restarted it when the waiting phase ended: audio played and positions ticked with no frames flowing
- The timeControlStatus hop now arms the frame timer whenever playback reports rolling (idempotent beside videoPlay's own arm), for local and remote sources alike
* Deliver a staged video terminal past stop instead of panicking
- A Cmd.batch([videoLoad, videoStop]) on a host whose load fails at once stages the .failed before stop retires the bridge entry; the drain then hit the entry gate's panic even though the terminal is the load call's only answer
- videoEventMsg drops the entry gate: every event reaching it was produced by a bridge-issued load and carries its arm tag in its own key (the engine swallows its post-stop stragglers itself), so routing needs no entry at all - pinned by the load-then-stop batch shape
* Retain every window's video declaration and promote on the owner's close
- The reconciler kept only the owning window's <video src>: closing that window (or its declaration vanishing) stopped playback and left another window's mounted video inactive until an unrelated rebuild
- Slot declarations now retain one entry per window (the table matches the window budget); when the owner closes or stops declaring, the next retained declaration promotes inside the same dispatch - pinned by a two-window close-promotes test with no on_close Msg and no rebuild
* Retry the frame wake for identical pushes still awaiting adoption
- A refused frame-wake request leaves pending clear so a retry can land, but the push-boundary fingerprint short-circuit returned before the wake: a static frame pushed again after a transient request_frame_fn refusal short-circuited forever and the staged bytes were never adopted
- An identical push now falls through to the wake while bytes are still staged; only an adopted frame's repeat stays a pure no-op
* Expose videoPlayback through the bridge alias and the TS feature type
- platformFeatureFromString gains the videoPlayback camel-case alias every other feature carries, so window.zero.platform.supports("videoPlayback") answers the platform truth instead of InvalidPlatformFeature
- NativeSdkPlatformFeature adds video_playback/videoPlayback so TS clients can compile the support query
* Stamp every platform video event with the load that produced it
- The engine mints a token per loadVideo, passes it through the load seam, and every host echoes it in each event: takeVideoMsg swallows an event whose token is not the current load's, so a replaced playback's queued terminal can neither reset the replacement, release its claim, nor route through its handler - pinned by a stale-failed-after-replace regression
- macOS hardening on the same theme: the item end/failure notification blocks re-check note.object against the current item (a removeObserver cannot recall a block already enqueued on the main queue), and the host carries the token across its terminal emits' teardown
- The journaled video platform event carries the token, so replay's mirror steering swallows stale recorded events exactly as live (the re-run loads mint the same deterministic sequence); the null platform echoes tokens like the real hosts and its fake position advance saturates instead of trapping on hostile deltas
* Pair replayed video deliveries with their events; token-gate fed entries; stop abandoned players
- A recorded video Msg dispatched synchronously inside its platform event's dispatch, but replay delivered the fed record at the NEXT drain: an update loading the next clip from its completion handler ran too late and the new clip's .loaded event was swallowed against the old load's token - takeVideoMsg under replay now pops the fed head (the journal's contiguity puts each event's record immediately before it) and dispatches it during the same event, pinned by a chained-load playlist session replaying byte-identical
- Pending video entries carry the load token that staged them, and delivery resolves against the live channel only while it still IS that load: the public key alone cannot tell two loads under one app key apart, and a stale fed terminal resolving against a same-key replacement would have reset it
- failVideoChannel silences the platform player it abandons (best effort): a load that succeeded before a later step refused kept its player decoding while the reset channel forgot it, and the inactive channel skipped it at teardown too
* Keep refused and uninstalled declarations out of the video reconciler
- A declared src the engine's own gates refuse (a malformed URL, say) no longer commits the reconciler's tracked ownership: the running playback keeps its identity - so removing the element later stops IT, instead of hashing the refused source and stranding the playback forever; the refused src is remembered separately and taught once, never re-attempted every rebuild - pinned by a refused-declaration ownership test
- A secondary window's declaration is captured only after its build pass and layout succeed and the tree installs: a build whose layout errors never displays, so its declaration never steers the playback either
* Link CoreVideo wherever appkit_host.m builds standalone
- The frame pump's CVPixelBuffer calls are real CoreVideo symbols: the central build gained the framework, but the generated-app template and the standalone example builds still linked only AVFoundation - a generated macOS project failed to link
- The template and every example build.zig that compiles the AppKit host now link CoreVideo beside AVFoundation
* Gate the poster hunt on a real push and clamp millisecond CMTimes
- videoPumpFrame initialized its push result to the success code, so a NULL base address or failed vImage permutation ended the poster hunt and stopped the frame timer with nothing on the surface; a sentinel now keeps the hunt alive until a push actually returns success
- NativeSdkCMTimeFromMs narrowed the u64 millisecond position straight into the signed CMTimeValue, turning absurd seeks negative; the value now clamps at INT64_MAX so AVFoundation clamps to the duration as documented
* Retire the null video player when a non-looping playback completes
- advanceVideo left the fake player loaded after its completion, so post-completion transport calls succeeded where a live host's torn-down player refuses them (and a replayed play could emit a second completion)
- the completion now unloads before the event returns, matching the live hosts' retire-before-emit order; tests pin the unload and the failed-play degrade path
* Stop the custom playback when an empty source commits
- loadCustom returned early on an empty committed source, leaving the previous video rolling under a status line that said "no source"
- an empty commit now stops the playback and resets the transport mirrors, with a test pinning the stopped channel and the honest status line
* Route replayed video records by the journaled load identity
- Every .video effect record now carries the producing load's token; replay feeds through feedVideoRecord, which resolves the token against the live channel or a retired-load park instead of binding the record to whatever the channel holds at feed time.
- loadVideo and stopVideo park the outgoing load's identity under replay, so a synchronous terminal staged inside the very dispatch that then replaced or stopped its playback still delivers the recorded Msg with the recorded identity - never EffectNotFound, never a reset of a same-key replacement the recording kept playing.
- The replay pairing in takeVideoMsg now token-gates fed entries against the platform event, and two session tests pin the batch shapes: load-then-stop and load-then-replace under one app key.
* Check the Core Video lock result before touching the pixel buffer
- A failed CVPixelBufferLockBaseAddress never maps the buffer, so reading the base address was undefined and the unconditional unlock unbalanced the lock count; the frame now drops with the buffer released.
- The poster hunt stays latched across the dropped frame - only an actual push verdict may end it, the converted-but-unpushed rule.
* Document why rate > 0 is transport intent at the playing derivations
- Per the AVPlayer.h contract, a playback waiting in AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate keeps rate at the requested value ("not currently effective but instead indicates the rate at which playback will start or resume"), so a stalled-but-unpaused stream already reports playing=1 + buffering=1 and the transport control offers Pause.
- AVPlayer resets rate to 0.0 on its own only with waits-to-minimize-stalling disabled - the local-file configuration, where playback would not self-resume and offering Play is the honest affordance.
* Keep the controls-bearing video element zero-intrinsic
- The <video controls> wrap column now adopts the media sizing contract (WidgetLayoutStyle.zero_intrinsic): the transport bar's intrinsic size never leaks into the element, so an unsized element in a hug container measures zero like the bare surface instead of rendering a controls-only strip.
- The wrap clips its content, so chrome cannot paint past a box the layout granted nothing; declared width/height stay definite through the frame and min/max bounds.
- A ui test pins both shapes: zero in a hug container, surface-above-bar inside a declared 320x180 box.
* Bound video scalars at delivery, sweep replay parks, republish flag deltas
- Platform video events clamp position, duration, and dimensions into the exact-integer delivery window (max_effect_video_scalar_exclusive, 2^53) at takeVideoMsg, whatever a host or embedder reports - the engine-side guarantee behind replay's damage gate, so an honest recording can never be refused as damage; a past-window readout now records and replays clamped, pinned by test.
- Replay-side retired video identities release at the first drain-pass boundary after parking (any journaled record for them feeds before that pass's event dispatches), so a long replayed playlist parks and releases one entry per clip instead of accumulating them; pinned at the channel level.
- Same-src declarative loop/muted deltas republish the runtime mirror in the same reconcile, so an automation snapshot taken after the flip reports the new value instead of the one published before the rebuild.
* Journal the video cascade's resolved source for replay
- loadVideo now journals a Msg-less .video_load record (the .clock/.env discipline) carrying which source the recording host's cascade resolved - a missing local file that fell through to the url is filesystem truth the replayed fake load cannot re-probe.
- Replay queues each record and the replayed load consumes it by its deterministic token, so videoSnapshot() and the automation mirror report .stream with the optimistic buffering flag exactly as the recording did, handler-less declarative playbacks included.
- Pinned by a record/replay test on an assets-absent host; the handler-less house-chrome pin now expects exactly the one Msg-less record.
* Keep buffering honest across paused streams and widen the fake's loop wrap
- Buffering means an un-paused stream waiting for bytes: a fresh URL load now starts the flag from the autoplay intent (engine mirror and replayed cascade resolution alike), the macOS host derives it purely from timeControlStatus instead of presetting it, and pauseVideo clears it - a conforming host emits no pause acknowledgment that could.
- The macOS time-control observer stays silent when the transition lands on paused: pause emits nothing by contract and position reports are for playback in motion; waiting and playing transitions still emit.
- The null platform's advanceVideo runs its loop wrap in widened arithmetic so a past-u64 delta lands on the exact residue instead of a saturated one; pinned along with the paused-stream and pause-clears-buffering shapes.
* Spill the replayed cascade-resolution queue past its inline capacity
- Loads per dispatch are unbounded by contract and every .video_load record lands before the dispatch's event, so the replay-side queue now grows geometrically past its inline 64 (freed when it empties and at deinit) instead of refusing the 65th record as a false divergence - the pending stages' non-lossy discipline.
- Pinned by a record/replay burst one past the inline capacity on an assets-absent host.
* Cross-check the journaled key on every replayed video pairing
- Reminted tokens pair records with replayed loads by position; the journaled key now proves the load at that position is the one the recording issued - feedVideoRecord refuses a mismatch as divergence, and a cascade resolution consumed under a different key latches one for the finish check.
- Replay now ends with a consistency check (the .finish replay control): a structurally valid journal whose queued cascade resolutions the replayed timeline never claimed fails as ReplayEffectDivergence instead of reporting success.
- Pinned at the channel level: wrong-key feeds refuse, an unclaimed or misclaimed resolution fails finishReplay, and the honest pairing stays silent.
* Copy the captured video declaration and verify the load bijection under replay
- The main-canvas <video src> capture now copies the source out of the build arena into app-owned storage (the slot captures' rule): a later failed rebuild resets the arena the old capture borrowed, and a window-close reconcile could hash or load overwritten bytes.
- Every non-rejected real load now journals exactly one .video_load record - refused cascades included (failVideoLoad) - so the replayed loads and the journaled records form a bijection: a replayed load with no record at its position, and a record whose position was never issued, both latch divergence for the finish check.
- The journal semantic epoch bumps to 2: identical bytes, changed replay meaning - an earlier recording's failed loads would replay as false divergence, so old journals refuse with the re-record teaching. Pinned: the extra-load shape fails finishReplay and the captured src aliases the app-owned buffer.
* Run the replay finish check even when no records were fed
- A latched video-load divergence can exist with zero fed effect records (the recording journaled nothing; the replayed timeline loaded anyway), so the end-of-journal consistency check now runs unconditionally; hookless apps answer ReplayUnsupported, which is honestly nothing-to-check.
- Pinned: a zero-record recording replayed into a build that declares a video fails as ReplayEffectDivergence instead of reporting success.
* Refuse non-http(s) schemes at the videoLoadUrl seam
- The seam documents streaming-only and promises to reject bad arguments before the platform is asked, but forwarded any scheme - a direct caller could hand file:///... to a host whose media stack opens it (AVPlayer does); the scheme gate now holds at the seam for every caller, matching the engine's own load validation.
- Pinned: file:, ftp:, and unparseable URLs refuse as InvalidVideoOptions without reaching the platform; https passes.
* Retire the null platform's video player before the load probes refuse
- The macOS host stops the previous player before the file-existence probe, so a refused load leaves no playback behind; the null platform now models the same ordering in videoLoad and videoLoadUrl instead of returning early with the replaced player still emitting frames and events under its old token.
- Pinned: a VideoSourceNotFound load leaves nothing loaded, no pending acknowledgment, and no position ticks.
* Fail frame-less video loads and keep the seek mirror on platform truth
- An asset with no video geometry (an audio-only file loaded as video) now fails the load instead of acknowledging a playback that can never paint - sound over a permanently blank surface is the same broken promise as the conversion-buffer failure the attach path already refuses.
- seekVideo applies the platform seek before moving the mirror: a player the host already retired (a completed non-looping playback) refuses the call, and the snapshot and house slider must stay on the frame actually on the glass; pinned in the post-completion transport test.
* Refuse post-completion seeks on the fake video channel too
- A non-looping natural end retires the platform player (retire-before-emit), so a later seek refuses live and the mirror keeps the terminal position; the fake executor now latches the completion and refuses identically, so replayed chrome and snapshots land exactly where the recording left them instead of scrubbing a player that no longer exists.
- Pinned by a record/replay parity test: a scrub after the natural end keeps position at the duration on both sides, model and fingerprints identical.
* Pair replayed video deliveries from any stage position; stop cancels the TS stream
- The replay pairing in takeVideoMsg now scans the pending stage for the event's fed entry instead of checking only the head: a regenerated loop-side rejection staged earlier in the same dispatch keeps its own drain-time order and no longer reverses the recorded Msg order; pinned by a record/replay ordering test.
- VideoMsgFn may return null - the adapter tier's cancel gate - and the TS bridge uses it to honor Cmd.videoStop's wire contract (stop CLOSES the stream, no events for the key after this): a staged synchronous terminal from the very batch that stopped the stream drains through the engine but never reaches the app, and reopening the tag lifts the latch.
- The house videoMsg constructor never returns null, so Zig-native apps keep the one-terminal-per-load delivery whole; the bridge pin now asserts the swallow and the reopen.
* Cancel the key's staged video answers inside the engine at TS stop
- Cmd.videoStop's cancel now runs where the answers live: stopVideoCancel removes every staged-but-undrained entry for the key before the channel goes idle, so nothing for a stopped stream can reach the app - even the synchronous terminal of a load-fail-stop batch, and even when a later load reuses the same event tag before the drain (a bridge-side tag latch could not tell those generations apart).
- A cancelled answer never journals, so replay regenerates and cancels the same entries and the timelines stay identical with no extra machinery; the VideoMsgFn seam returns to plain Msg and the Zig-native stopVideo keeps its one-terminal-per-load delivery.
- The bridge pin now covers the same-tag reopen-before-drain shape.
* Mirror synchronous load refusals under replay; token-scope ownership and cancel
- The .video_load record's video_kind now carries the load's outcome: a refusal reset the live channel before loadVideo returned, so the replayed fake load resets at the same instant (parking its identity for the journaled terminal) and a snapshot an update reads inside the very dispatch matches the recording's; semantic epoch bumps to 3 (same bytes, the field gained meaning). Pinned by a record/replay probe test.
- Declarative video ownership now rides the load token the reconciler captured at its own load, never the derived key alone - the key is a pure function of the source string, and a manual load carrying it must survive declaration removal and flag deltas untouched; pinned.
- stopVideoCancel is token-scoped: the stopped stream is the latest accepted load, so a replaced predecessor sharing the arm's public key keeps its owed terminal - only stop cancels; pinned at the channel level.
* Refuse post-completion play on the fake video channel too
- A non-looping natural end retires the platform player, so play meets an absent player live: one .failed terminal and the channel resets before playVideo returns; the fake executor now models the same refusal (failVideoChannel on plain fake; under replay just the identity park and reset, because the journaled terminal delivers itself), so a snapshot an update reads right after its own play answers the same on every executor.
- Pinned by a record/replay probe test: the mid-dispatch snapshot reads inactive on both sides, the terminal delivers at its recorded wake, and models and fingerprints match.
* Break the video timer retain cycles and gate video-load outcomes at replay
- Both video timers (the 1/60 pixel clock and the 500ms readout) are now weak-host block timers that self-invalidate when the host is gone: a target-selector repeating timer retains its target through the run loop, so a host destroyed mid-playback without an orderly stop could never dealloc - leaving AVPlayer, the timers, and the conversion buffer permanently retained.
- Replay refuses a .video_load record whose video_kind is neither .loaded nor .failed as a damaged journal: the recorder writes exactly those two outcomes, and anything else would steer the replayed fake load into a state the recording never had; pinned by a hand-patched-journal test.
* Commit the video declaration at install; provenance-gate video rejections
- The main-canvas <video src> capture now stages during the build and commits only when the rebuild installs: a build that fails downstream never mounted, and the retained tree on the glass still shows the old declaration - a later reconcile acting on the unmounted capture would stop or replace a playback the presented tree still declares.
- Replay refuses a .video record whose kind and token are recorder-impossible: rejections stamp token 0 (a refused load never minted one) and every delivery carries its minted token, so a .rejected re-stamped onto a delivered record can no longer slip through the regeneration skip and silently omit the recorded Msg; pinned by a hand-patched-journal test.
* Fail the replay finish check on undelivered fed video results
- A fed record is one recorded delivery, and the event that consumed it live follows it in every honest journal - so a fed entry still staged when the journal ends means truncation or hand-editing, and finishReplay now refuses instead of reporting success; regenerated loop-side answers may honestly outlive the last drain on both timelines and stay exempt.
- Pinned: a fed-but-undelivered result fails the finish check and the same result delivered makes it silent again.
* Restart a finished playback from Play instead of failing it
- A non-looping natural end retires the player, so Play answered with one failed event and the seek was guaranteed to spring back - a broken response to a valid finished state. Effects.restartVideo is the resume path: a fresh load of the channel's own remembered source with autoplay, keeping key, surface, handler, and the loop and mute flags, journaling like any load.
- The house transport's toggle restarts when the completion latch is set and its scrub disables (Play is the live affordance); the snapshot and chrome state expose completed so custom players can do the same - the video-player example's Play now restarts its finished clip.
- Pinned: clicking the house toggle after the natural end issues a fresh platform load and the playback runs again.
* Validate replayed video payloads and source shapes; retire the example's dead seeks
- The replay pairing now requires the fed record's payload to equal what the platform event resolves to: the recorder journals every delivery verbatim from that event, so an altered kind or scalar around an intact identity refuses as divergence instead of handing the app a Msg the mirrors contradict; pinned by a hand-patched-journal test.
- A journaled cascade resolution must be one the load's request shape could select (.local needs a path, .stream needs a url); an impossible pairing latches divergence for the finish check, pinned at the channel level.
- The video-player example's seek-family controls (slider, back, forward) disable after a natural completion - a retired player refuses seeks and the thumb would spring back; Play stays live and restarts.
* Document the video output's vended-size contract at the frame tap
- The pixel-buffer width/height attributes are client requirements AVPlayerItemVideoOutput satisfies by scaling every vended frame, not hints: a 3840x2160 H.264 source tapped with fitted 1866x1050 attributes vends 1866x1050 buffers, so a 4K playback never hits the oversized-frame drop - that guard is defense in depth against a hypothetical non-conforming host, and the comment at the attributes site now says so.
* Keep declared ownership across restart; saturate past-window seeks
- The house transport's restart mints a fresh load identity, so the declarative reconciler re-captures its ownership token when the finished playback was its own - removal and same-source flag deltas keep working after a replay-from-end; pinned in the restart test.
- The TS bridge no longer rewinds a finite seek past the exact-integer window to zero: it saturates just below the window, where the engine's duration clamp lands it at the end - an oversized forward seek means the end, and only NaN and negatives (not millisecond offsets at all) seek to 0; pinned at the bridge level.
* Class non-finite seeks as invalid offsets, never oversized forward seeks
- Infinity reached the past-window saturation branch and sought an active video to its end, though the literal validation rejects non-finite offsets: the saturation now requires a FINITE value, so Infinity seeks to 0 like NaN and negatives - not millisecond offsets at all.
- Pinned at the bridge level beside the finite past-window seek.
* Keep the reserved video surface id inside the f64 exact window
- Surface ids ride the TS wire as f64, and a source-less <video> with custom controls is fed by the app's own Cmd.videoLoad naming exactly the reserved id - the old value sat near 2^63, so the bridge's exact-integer validation rejected it and the element could only ever show its placeholder from TypeScript.
- The id moves below 2^53 (mnemonic preserved, bit 63 clear, still a valid producer id reserved by convention); pinned: in the window, outside the derived-texture namespace, and round-trips the f64 wire unchanged.
* Deterministic seek mirror, journaled handler presence, and baked rotation
- The seek mirror now gates on the deterministic completion latch on every executor: a completed playback keeps its terminal position live and replayed alike (the platform is not even asked - the retired player could only refuse), and residual platform verdicts are fire-and-forget so an exotic host's answer can never diverge replay's mirrors from the recording's.
- Every .video record journals whether its delivery dispatched a Msg, and feedVideoRecord requires the replayed handler presence to agree - a record whose Msg silently vanished, or a Msg live never dispatched, refuses as divergence instead of a silent consume.
- Rotated media renders upright: a track with a non-identity preferredTransform gets a properties-of-asset video composition baked into the vended frames (verified against a portrait-flagged H.264 asset: the raw tap vends encoded orientation, the composition vends display orientation); identity-transform assets skip the render pass and track-less streams have no transform to bake.
* Repaint the chrome when the reconcile moves playback; seek on slider key steps
- A src change loading an autoplaying replacement reconciles after the build installs, so the just-installed chrome advertised the OLD transport state while its control acted on the new one - Play on the label, pause in effect. The rebuild now runs one guarded repass when the reconcile moved the mirrors the build rendered; the repass reconciles an unchanged declaration, so the mirrors are a fixed point. Pinned: the replacement's pause glyph shows in the same build.
- Keyboard and assistive steps on the house seek slider arrive as set_value intents with no widget change event behind them; they now map their fraction onto the duration and drive the channel like the pointer scrub instead of being consumed silently for the next tick to snap back. Pinned with a focused arrowright step.
* Repaint secondary-window chrome when its own reconcile moves playback
- The slot rebuild reconciles its captured declaration after the slot tree installs, so a first mount of an autoplaying <video controls> in a secondary window rendered its chrome from the still-inactive snapshot - disabled Play over a playing video - until some later platform event, seconds away on a slow stream. The slot path now runs the main rebuild's guarded repass when the reconcile moved the mirrors the build rendered; the repass reconciles an unchanged declaration, so the mirrors are a fixed point.
- Pinned: the slot's first installed build shows an enabled pause toggle.
* Latch missing video records; scope bridge verbs to their own stream
- A recorded video event arriving under replay with a handler bound and no fed record before it now latches divergence for the finish check: the recorder journals every handled delivery immediately before its event, so absence is truncation or hand-editing - never a Msg to drop silently; pinned at the channel level.
- Bridge video verbs now prove ownership with the load identity their own accepted load minted (Effects.videoMintedToken): a wire key whose stream was since replaced by a load the bridge never issued - a declarative element's - no-ops its transport verbs, and its stop cancels only its own stream's staged answers while the playback on the channel survives untouched; pinned at the bridge level.
* State the replace-is-not-stop rule at the videoLoad contract
- A replaced load still delivers the terminal it owes, routed to its own event arm - the never-silent promise applies to the replaced stream's failure exactly as to any other, and only Cmd.videoStop cancels undelivered answers. The wire contract now says so explicitly where the open-or-replace semantics are defined; the bridge's routing and the arm-separation pin already enforce it.
* Remember volume across a failed load; gate impossible terminal payloads
- Volume is a remembered preference the next load re-applies, so the bridge's ownership gate now lets videoSetVolume through when the channel is IDLE - a failed load's handler routinely sets it before retrying, and with nobody's playback on the channel there is nothing to protect; a foreign live playback still gates it. Pinned both ways.
- Replay refuses a .video record whose payload shape the recorder never writes for its kind: terminals deliver with playing and buffering false and no dimensions, and a completion pins position to the duration - a synchronously failed load's record has no platform event behind it to cross-check, so the gate is where a hand-set width refuses; pinned by a hand-patched-journal test.
|
||
|
|
2d23d6f449 |
Cover anti-aliasing bleed and grid misalignment in incremental canvas damage (#183)
* Cover anti-aliasing bleed in incremental canvas damage - Reflowed content left stale fringes: rasterizers ink up to one device pixel past a command's bounds (antialiased edges, glyph overshoot — the packet host's raster cache carries a one-pixel apron for exactly this), but incremental dirty bounds were cut to the exact bounds, so pixels vacated by removed, shrunk, moved, or keyed-replaced commands kept their old edge ink. - Inflate every finalized incremental dirty rect — the refined union, each refined cluster, and the summary fallback, in both frame planners — by one device pixel before surface clipping; full repaints already cover the surface. - Regression test pins removed, shrunk, moved, and keyed-replacement reflows: damage must cover the vacated extent plus the bleed, on the scissor and on the refined rect list a retained host clears. * Prove incremental reflow presents match full renders pixel-for-pixel - Widget-tree oracles swap keyed subtrees (pills removed, shrunk, moved) and byte-compare the incrementally maintained pixel buffer against a fresh full render, on both the summary and refined dirty paths. - A markup-driven oracle covers the ui-app loop end to end: model dispatch, keyed for/if reconciliation, and incremental pixel presents against a retained baseline, for an if-arm swap inside a stable key and a whole keyed-subtree replacement. * Adopt the AA-bleed allowance in canvas frame-plan dirty expectations - Incremental dirty bounds, scissors, and profile dirty areas now carry the one-device-pixel bleed inflation; the pinned rects and JSON move with it. * Add the reflow stale-fringe fix to the changelog - One fragment: incremental damage now carries the one-device-pixel AA bleed allowance. * Hoist the frame-helpers import in the ui app tests - The reflow oracle's pixel-size lookup now rides a file-level import like its peers. * Widen presented damage when the effective scale coarsens the device pixel - Presentation-scale overrides land after planning, so a present at a coarser scale than the plan carried less than one of ITS device pixels of AA bleed allowance - the stale-fringe hole reopened for scale-overriding hosts. - Both override sites now widen the frame's dirty bounds and refined rect list by the allowance difference before the packet build, so the scissor-culled command subset rides the widened scissor. - Regression test plans at scale 2, presents at scale 1, and pins that a removed pill's damage carries the full presented-scale device pixel. * Snap incremental damage outward to the device-pixel grid - Clears land on whole pixels while command culling tests the float dirty rect, so a fractional dirty edge erased the boundary pixel's antialiased coverage without redrawing the unchanged neighbor that painted it - a missing fringe beside every reflow. - Finalized dirty rects (refined union, refined clusters, summary fallback, both planners, and the presentation-scale widening) now snap outward to the device-pixel grid before surface clipping, making the cull region identical to the cleared pixels. - Pixel regression: a mid-pixel dirty edge beside an unchanged antialiased neighbor must leave the incremental buffer byte-identical to a full render. * Re-align presented damage on any differing presentation-scale grid - Pixel grids need not nest across scales: a plan-grid-aligned dirty edge sits mid-pixel at a fractional finer scale (plan 1, present 1.5), so the present's clear wiped a boundary pixel whose antialiased coverage came from an unchanged neighbor the plan-aligned scissor culled. - The presentation-scale rework now re-snaps the dirty bounds and refined rect list outward on the presentation grid whenever the scales differ, keeping the coarser-scale bleed growth. - Pixel regression plans at scale 1, presents at 1.5, and byte-compares the incremental buffer against a full render. * Keep snapped dirty edges on their device boundary through the f32 round trip - ceil(v*s)/s can land on an f32 whose product with the scale rounds back past the device boundary (65 points at 1.75x becomes 65.14286, product 114.00001), so consumers re-deriving pixels cleared one more pixel than culling against the rect admits - erasing an unchanged neighbor's boundary coverage. - The device-grid snap now nudges each edge ulp by ulp until its product lands on the intended boundary, never moving inside the original rect. - Pixel regression pins the 65-point/1.75x round trip: the dirty edge's product must ceil to its own boundary and the incremental buffer must match a full render byte for byte. * Decide damage pixels on integer device boundaries, not rounded logical insets - The AA bleed moved by a rounded 1/scale in logical points, which can multiply back to the original boundary and fall a whole device pixel short (a pill at x=170 removed at scale 1.3 never dirtied column 219); the bleed is now whole device pixels applied to the floor/ceil boundaries directly. - Stored rects re-derive their max edge as x + width, and re-encoding a nudged edge as a width could push the reconstructed product past its boundary; spans are now chosen by testing the reconstructed edge itself, so stored fields round-trip onto exactly the intended pixels in both directions. - A property sweep pins the stored-field round trip across fractional scales, and a fractional-scale removal pins the whole-pixel bleed reach. * Clamp damage boundaries before the device-grid edge walk - A far off-screen coordinate can floor/ceil to infinity once scaled, and a nextAfter walk from infinity never terminates - frame planning hung instead of clipping the damage away. - Boundaries clamp into f32's exact-integer range (2^24) before the edge search, and NaN degenerates to zero; surface clipping owns everything past the clamp. - Regression pins finite results for damage rects near f32's range on both sides. * Walk the damage edge value, never the span, when re-encoding widths - With both boundaries collapsed onto the off-screen clamp, the span walk started at zero and stepped denormal ulps that never moved the sum - over a billion iterations before incremental frame planning could continue. - The span derivation now walks the reconstructed EDGE value (ulp proportional to magnitude, a few steps), re-encodes it as the stored span, and gives up the sub-ulp sliver when a step cannot move the sum - an empty rect surface clipping discards. - Regression pins the collapsed case: a small far off-screen rect must settle immediately into a finite, discardable result. * Judge damage boundaries in double precision and keep far damage a superset - Retained hosts reconstruct and scale the wire rect in double precision: an f32 edge whose product merely rounds onto its boundary can sit past it exactly (64 wide at 1.75x lands 115.0000019), so the host cleared a pixel the engine culled for. The edge and span walks now judge products and reconstructed sums in f64 - exact for f32 operands and precisely what hosts compute - which implies the f32 behavior too. - The off-screen clamp collapsed legitimate unclipped damage to empty (a change near x=20M with the frame planner's default empty surface reported nothing to repaint). Out-of-walk-range boundaries now keep a finite, non-collapsed superset instead; exact cull/clear equivalence only matters within presentable surfaces, far below the walk range. - The round-trip property sweep now pins the double-precision boundaries as well, plus the known 1.75x seam case and the far-off-screen superset. * Clip damage on device boundaries and keep the far-damage superset in place - Surface clipping ran as a rect intersection AFTER the exact snap, and even a no-op intersection reconstructs the width - the stored edges' round trip could cross their device boundary again (a 5.89-wide change at 2.5x re-encoded from a ceil of 20 to 21, clearing a column the packet's culled command list never repaints). Clipping now happens on the integer device boundaries inside the snap, and the finished rect is never re-encoded. - The far-coordinate fallback clamped edges to 3e37, parking damage away from far-but-representable content; only NaN and the infinities are replaced now, and the reconstructed max edge is shaved back to finite at the f32 range edge. - The property sweep pins the surface-clamped path's double-precision boundaries, and the superset tests pin damage staying AT far content. * Canonicalize signed zeroes out of snapped damage edges - A zero boundary's edge walk steps below zero and back, settling on negative zero; serialized scissors then printed a signed zero. Snapped edges and spans normalize to plain zero. * Say list-detail for the selection-pane pattern - Wording only: the changelog fragment and two test comments; no behavior. |
||
|
|
e48af75fcc |
Replace journal and protocol version counters with layout fingerprints (#182)
* Replace the automation protocol version with a comptime layout fingerprint - The CLI/app handshake identity is now a Wyhash over a canonical comptime description of the protocol surface (the Action enum reflected with names and values, the Command shape, dropbox and queue naming, budgets), so shape changes move it automatically - no counter to remember, no next integer for parallel branches to contend over; a manual semantic_epoch stays for changes the description cannot see. - Snapshots stamp protocol=0x<fingerprint>, the CLI parses hex or the retired decimal counter (a stale app gets a NAMED mismatch, never .missing), and skew teachings, the journal header, replay's refusal, and native version all name fingerprints instead of version numbers. - The +1-skew handshake fixtures perturb the fingerprint; new tests pin that an added field, a rename, a reorder, and an epoch bump each move the identity. * Replace the journal format version with a comptime layout fingerprint - The preamble now carries a u64 fingerprint hashed from a canonical description of the on-disk layout: record kinds, event tags, every record struct and the effect record reflected field-by-field (journaled enums with names AND values), each event payload type, and deliberate constants for the hand-written codec facts reflection cannot see (framing, string/optional/enum encodings, modifier bits, the accessibility verb's i32, the gpu_surface_frame journaled subset - each commented at its codec). - JournalUnsupportedVersion becomes JournalFormatMismatch and the teaching now says what is true: the journal was recorded by a build whose journal format differs from this one - re-record with this build; a format_semantic_epoch covers the rare meaning-only change with identical bytes. - Skew tests perturb the fingerprint (including a retired-counter-era preamble refusing as mismatch, never corruption), and a new test pins that an epoch bump moves the combined fingerprint. * Print both layout fingerprints from print-pins and note the conscious break - zig build print-pins now leads with the session journal format and automation protocol fingerprints (with their semantic epochs), the same values native version and the skew teachings name, so coordination and debugging can quote a build's wire identities exactly. - print-pins builds against a HOST clone of the framework module for the journal constant (one compilation cannot hold a source file in two modules, and the run step must stay executable when -Dtarget cross-compiles everything else); the fingerprints are layout identities, identical across targets. - Changelog fragment states the deliberate break: journals and automation sessions from earlier builds refuse with the re-record teaching; identity beats ordering because no compat path exists. |