Compare commits

...

352 Commits

Author SHA1 Message Date
Chris Tate edaa882337 fix(automation): escape menu snapshot catalogs 2026-08-16 22:46:59 -05:00
Chris Tate 6787f7adf4 test(app-runner): verify manifest menu registration 2026-08-16 22:14:59 -05:00
Chris Tate 65d4b028a3 fix(app-runner): load manifest menus and commands
- Resolve app.zon commands, menus, and shortcuts consistently across live and replay runners.
- Add zero-config TypeScript coverage, automation, and documentation.

Co-authored-by: MohakBajaj <77928693+MohakBajaj@users.noreply.github.com>
2026-08-16 21:30:02 -05:00
Chris Tate 8d0da34e62 chore(release): prepare v0.9.2 (#372)
- Synchronize CLI, core, platform package, and example versions to 0.9.2.
- Add release notes and contributors for all changes since v0.9.1.
2026-08-16 20:22:43 -05:00
Chris Tate 1c1fba0c0f fix(macos): place fresh windows correctly (#369)
* fix(macos): place fresh windows correctly

- Separate restored, explicit, and default window placement from persistence policy.
- Honor explicit origins and restore policy across platform seams and both macOS hosts.
- Add placement coverage, update docs, and remove obsolete example workarounds.

* fix(macos): honor window placement policies

* fix(macos): correct window placement policies

* fix(macos): preserve window placement contracts

* fix(runtime): restore secondary window placement
2026-08-16 19:06:29 -05:00
Chris Tate 86fecf6cee fix(macos): report content rects in window frame events (#370)
* fix(macos): report content rects in window frame events

- Emit content geometry consistently from the AppKit and CEF hosts.
- Preserve layout and restore round-trips without titlebar drift.

Co-authored-by: Sepehr Safari <25853688+sepehr-safari@users.noreply.github.com>

* fix(macos): restore CEF content geometry

---------

Co-authored-by: Sepehr Safari <25853688+sepehr-safari@users.noreply.github.com>
2026-08-16 17:18:48 -05:00
Chris Tate 23d0f5908a feat(images): decode photos to fit runtime budgets (#366)
* feat(images): decode photos to fit runtime budgets

- Decode encoded photos to fit the app's registered-pixel budget across platform codecs.
- Add a validated 1-8 MiB app.zon image budget and independent 8 MiB source bound.
- Cover deterministic pixels, 1080p loading, replay, ABI plumbing, and documentation.

Co-authored-by: Sepehr Safari <25853688+sepehr-safari@users.noreply.github.com>

* fix(images): honor raised budgets across hosts

* fix(images): cover generated and non-mac hosts

* fix(images): harden source and replay limits

* fix(images): fit Android panoramas before validation

---------

Co-authored-by: Sepehr Safari <25853688+sepehr-safari@users.noreply.github.com>
2026-08-16 16:26:36 -05:00
Chris Tate 919d0e6cdf fix(canvas): respect link span underline flag (#368)
- Render span underlines only when explicitly enabled.
- Preserve conventional underlines for Markdown-generated links.
- Cover linked spans with and without underline decoration.

Co-authored-by: Sepehr Safari <25853688+sepehr-safari@users.noreply.github.com>
2026-08-16 16:02:39 -05:00
Chris Tate 5d7fee8262 schema and rendering fixes (#367)
* canvas: allow checkbox and radio label content

* canvas: add interaction washes to actionable surfaces

* canvas: keep selection control geometry and fills consistent

* canvas: separate switch thumb and track colors

* canvas: preserve resolved colors through disabled washes

* canvas: skip zero-width badge and button strokes

* canvas: tint destructive alerts and align compact content

* docs: regenerate canvas control previews

* canvas: preserve text attributes on text-bearing controls

* canvas: fast-path idle layout container rendering

* canvas: keep authored row fills as the rest state

* bench: recalibrate full-view toggle budget

* canvas: complete schema and rendering fixes

- Enable checkbox and radio labels consistently across schema, markup engines, accessibility, docs, and previews.
- Unify actionable state, disabled-color, variant-accent, and zero-width stroke behavior across controls and surfaces.
- Correct compact control geometry and destructive alert styling/alignment with comprehensive golden and gate coverage.

* canvas: address schema review findings

* canvas: fix actionable state routing

* canvas: fix drag menus and disabled control tokens
2026-08-16 15:05:24 -05:00
Chris Tate 5c32accf12 fix(runtime): reveal autofocus targets and carets (#364)
* fix(runtime): reveal autofocus targets and carets

- Share scroll-then-verify focus reveal across keyboard, autofocus, and automation.
- Preserve selection while revealing collapsed end carets inside text editors.
- Cover scrolled focus flows and document the autofocus contract.

* fix(runtime): make focus reveals transactional

* fix(runtime): make widget focus reveal transactional
2026-08-16 08:02:28 -05:00
Chris Tate d575734635 Fix anchored surface correctness (#363)
* fix(canvas): dismiss anchored surfaces without focus

* fix(canvas): relayout anchors after scroll restore

* fix(canvas): place modal surfaces against root

* fix(canvas): address anchored surface review issues

* fix(canvas): unify window surface behavior
2026-08-15 22:26:04 -05:00
Chris Tate ee63266095 Implement logical canvas radio groups (#361)
* Implement logical canvas radio groups

- Scope nested radios as one roving-focus, single-selection group.
- Align radio keyboard, pointer, and handler dispatch behavior.
- Expose radiogroup accessibility semantics and document the contract.

* Fix radio group accessibility edge cases

* Fix radio group focus traversal edge cases

* Fix radio group keyboard and naming semantics

* fix: preserve radio selection semantics
2026-08-15 18:36:17 -05:00
Chris Tate e7c161970c Add configurable accessory startup (#358)
* Add configurable accessory startup

- Add manifest, runner, and macOS host support for flash-free Accessory startup.\n- Require a tray affordance and cover launch/runtime composition with tests.\n- Update the TypeScript menu-bar example and lifecycle documentation.

* Fix flash-free accessory startup

* Enforce tray requirement when packaging accessories
2026-08-15 00:52:34 -05:00
Chris Tate ddd975e4ea chore(ovation): checkpoint agent step (#357) 2026-08-14 18:03:46 -05:00
Chris Tate 474cb5e364 fix(core): pin scriptc tuple normalization fix (#356)
- Upgrade Native SDK compiler dependencies and generated references to scriptc 0.0.31.
- Add a real compiled-core ABI regression for bare-model and effect-tuple returns.
- Centralize test pin lookup and remove release numbers from comments and prose.

Co-authored-by: John Lindquist <36073+johnlindquist@users.noreply.github.com>
2026-08-14 17:27:26 -05:00
Chris Tate e6ac6ac4fc fix(linux): avoid GTK alert dialog crash (#354)
- Initialize GtkAlertDialog with a valid empty format string.
- Add regression coverage preventing a NULL constructor argument.

Co-authored-by: ElSebas41 <189925713+ElSebas41@users.noreply.github.com>
2026-08-14 09:53:19 -05:00
Chris Tate 0ecdc7d2e9 feat(core): expose model-declared windows to TypeScript (#351)
* feat(core): expose model-declared windows to TypeScript

- Add canonical TypeScript window descriptors with close-policy and close-command routing.
- Compile and hot-reload label-addressed secondary-window markup in generated launchers.
- Cover quit/hide behavior end to end and port the TypeScript system-monitor settings window.

* fix(core): harden TypeScript window declarations

* fix(core): validate TypeScript window views

* fix(core): validate returned window descriptors

* fix(core): harden TypeScript window contracts
2026-08-14 09:30:18 -05:00
Chris Tate e924d7fcac feat(examples): make the feed reader the end-to-end services showcase (#352)
* feat(examples): make the feed reader the end-to-end services showcase

* fix(examples): contain feed results in scroll pane

* fix(core): preserve service facade unbound metadata
2026-08-13 22:34:57 -05:00
Chris Tate 41c4cdc47a feat(files): add delete file effects (#350)
* feat(files): add delete file effects

- Expose Cmd.deleteFile as the primary TypeScript API with checked routing and documentation.
- Add real, fake, permission, journal, and replay support through the shared file-effect engine.
- Cover compiled cores across macOS, Linux, and Windows, including reliable Windows append behavior.

* fix(files): await Windows append completion

* fix(files): preserve final symlink on delete
2026-08-13 21:00:48 -05:00
Chris Tate 31d5b202bc feat(mobile): compile TypeScript cores and services for mobile targets (#346)
* feat(mobile): compile TypeScript cores and services for mobile targets

The external core and service compile drivers admit the pinned
compiler's three mobile triples — aarch64 iOS device and simulator
(macOS build host, iOS 15.0 floor) and aarch64 Android (any desktop
host, API 26 floor) — as library archives only, mapping the build
graph's Zig triples onto the compiler's own spellings and threading
the Android NDK location the way the graph already threads its zig.
The service executable lane refuses mobile targets with the
in-process pointer: no child process exists there, so the carrier
resolution turns "auto" into the in-process pool on iOS/Android and
teaches on an explicit "child", while desktop behavior is unchanged.

Mobile app builds stage a generated mobile entry beside the desktop
wiring: it satisfies the embed host's AppDef contract over the same
mirror, markup, and registry, drives the canonical mobile scene plus
the manifest's declared chrome, owns the service pool (markers and
stream relays live in the shim-installed app-data directory), and
delivers that directory through envMsgs. The compiled archives merge
into the embed static library the host tiers already link — flattened
to plain objects on Android, where Zig's ELF static-library emission
stores archive inputs as nested members the NDK link would skip.

A mobile execution lane (scripts/mobile-e2e.sh, NATIVE_SDK_MOBILE=1
in either gate tier) stages a battery over the service fixture's
compiled core and service archives, packages the fixture app for both
mobile targets and a services-free example for Android, then executes
the battery on a booted iPhone simulator and a headless arm64
emulator: typed pool results across update round trips, trap
isolation poisoning exactly one instance, and a journal replay that
reproduces the recorded model without initializing the archive. All
checks pass on both device classes. The TypeScript and services
chapters and the ts-core/ts-services skills state the mobile matrix;
persistence, boot images, and URL media caching remain unwired on
mobile.

* fix mobile TypeScript app wiring

* fix(mobile): validate TypeScript package outputs
2026-08-13 18:13:37 -05:00
Chris Tate 2bc942db46 feat: support close policies on secondary windows (#349)
- Expose UiApp.WindowDescriptor.close_policy and forward it through window creation.
- Preserve hide/reopen identity while retaining existing platform safety gates.
- Document and test quit, hide, and unsupported-host lifecycle behavior.
2026-08-13 17:37:27 -05:00
Chris Tate 199b89e06d feat: add actionable notifications (#347)
* feat: add actionable notifications

- Add stable replacement identifiers and paired notification actions across SDK APIs.
- Dispatch activations through the normal command path on macOS, Windows, and Linux.
- Advance the TypeScript wire generation and cover validation, test platforms, and docs.

* fix: secure notification action dispatch
2026-08-13 16:33:37 -05:00
Chris Tate 716eb27c53 feat(core): classify subset rules as guarantee or deferred (#345)
Every NS rule in the diagnostics catalogue carries class: "guarantee" |
"deferred". Guarantee rules protect a core invariant (determinism and
replay, fixed shapes, immutability of shared data, the one text
representation) and are permanent; deferred rules (NS1011, NS1019,
NS1040, NS1042, NS1044) wait on an easing decision and their
diagnostics say the capability is deliberately deferred, not
impossible.

Fix text names the concrete service alternative where one exists:
NS1002 (network via the generated client), NS1005 (clock reads),
NS1011 (Map/Set transforms), NS1040 (regex matching), NS1041 (JSON
parsing returning a typed record).

Docs and skills frame the core and services by role: the core is the
app's deterministic logic; services do the app's imperative work.
2026-08-13 14:15:06 -05:00
Chris Tate baef0d96d3 feat(storage): harden file effects (#339)
* feat(storage): harden file effects

- Add bounded streaming reads, atomic write sinks, stat, and append effects.
- Gate external paths with symlink-safe filesystem permission checks.
- Preserve deterministic record/replay through content-addressed stream blobs.

* fix(storage): harden file effect lifecycles

* fix(storage): harden stream replay and key ownership

* fix(storage): keep stream sink on rejected chunks

* fix(storage): address file stream review findings
2026-08-13 14:10:10 -05:00
Chris Tate f228e861d1 Upgrade scriptc to 0.0.29 (#344)
- Pin scriptc and its compiler/runtime lock data to 0.0.29.
- Refresh service contracts, compatibility fixtures, calibration, and documentation.
- Regenerate the service compile-surface reference; the 0.0.29 surface manifest carries no entry changes from 0.0.28 and adds a coverage statement for the mobile library targets (aarch64-apple-ios, aarch64-apple-ios-simulator, aarch64-linux-android).
2026-08-13 13:56:28 -05:00
Chris Tate 6ec836c491 feat: support multiple macOS status items (#343)
- Add keyed status-item lifecycle and independent live updates across the runtime and macOS hosts.
- Expose model-driven TypeScript and Zig collection APIs with status-item-aware events.
- Extend automation, journaling, documentation, and regression coverage.
2026-08-13 13:14:16 -05:00
Chris Tate 9ce0370181 chore(release): prepare v0.9.0 (#341)
- Add complete v0.9.0 release notes and contributor credits.
- Synchronize CLI, core, platform, tool, and example version pins.
- Discover every TypeScript example during version synchronization.
2026-08-13 02:00:28 -05:00
Chris Tate a7665807f3 feat(cores): compile TypeScript cores for cross desktop targets (#340)
The external core compile driver now enforces the same host/target
pairing matrix as the service compile lane: same-triple compiles keep
the native lane, Linux and Windows GNU targets cross-compile from any
macOS/Linux/Windows build host over the compiler's zig-cc lane, macOS
targets need a macOS build host, and every refused pairing teaches
before compiler work starts. The co-emitted contract sidecar is
target-independent: a macOS-native, x86_64-windows-gnu, and
x86_64-linux-musl compile of one staged tree emit byte-identical
documents with identical integer-class decisions, and the COFF and ELF
archives declare the same nsc_core_* symbol surface as the Mach-O one.

The SDK's fixture graph compiles corewire for the build host, so the
battery lanes configure under a cross -Dtarget, and a new
stage-cross-e2e step installs the host-fixture, markup, and in-process
service pool batteries under <prefix>/e2e for execution on the target
machine. scripts/cross-e2e.sh drives the lane end to end: it
cross-builds the batteries, the kanban example, and the service fixture
app (in-process carrier — the core and service archives linked into one
executable, with no defined-symbol overlap between the localized
service archive and the core's contract surface) for x86_64-windows-gnu
and x86_64-linux-musl, then executes the batteries on the Windows box
over ssh and in an amd64 Alpine container. gate.sh runs the lane in
either tier when NATIVE_SDK_CROSS=1 is set and skips it otherwise.

All three batteries pass on both targets: 29 host-fixture, 10 markup,
and 19 in-process pool tests on Windows (the three posix-spawn tests
skip there), and 32/10/19 in the musl container. The TypeScript chapter
and ts-core skill state the supported build matrix, including the Linux
glibc spelling and the unchanged desktop-only scope for mobile.
2026-08-13 01:28:42 -05:00
Chris Tate fe92cff10a feat(services): extend the in-process carrier opt-in to Windows and cross targets (#337)
* feat(services): extend the in-process carrier opt-in to Windows and cross targets

scriptc 0.0.28 localizes archive runtime symbols format-aware (ELF,
COFF, Mach-O) on every desktop host and for cross targets, so the
explicit `.service_carrier = "in_process"` opt-in no longer needs the
host-native macOS/Linux gate. Carrier resolution and the SDK fixture
lane's archive gate now admit the compiler's build matrix: Linux and
Windows targets from any macOS/Linux/Windows build host, and macOS
targets from a macOS host. Auto still resolves to the isolated child
carrier everywhere, and child stays selectable on every shape.

The service compile lane replaces its build-host-only refusal with the
same matrix. Same-triple compiles keep the native lane; admitted cross
pairings run the compiler's zig-cc lane (SCRIPTC_CC=zigcc,
SCRIPTC_TARGET=<triple>, and the build's own zig at the front of PATH
through the new --zig-exe argument); refused pairings keep a precise
teaching. The child executable's name follows the target OS instead of
the host's.

Cross Linux targets carry one encoded caveat: a bare `-gnu` spelling
lands on Zig's default glibc floor, which predates arc4random_buf — a
symbol the compiled service runtime references — so carrier resolution
teaches the `-gnu.2.36`+ (or `-musl`) spelling at configure time, and a
stated glibc version now rides the platform triple into the compile.
Windows targets link ws2_32, iphlpapi, and advapi32 beside the compiled
archives: the fixture modules get a shared helper, and the app lane's
Windows platform block adds advapi32 for the archive's CSPRNG.

Both carrier e2e suites pass natively on Windows (14 child-carrier
tests, 19 in-process pool tests: parallel keys, per-key FIFO,
cooperative cancellation and deadlines, trap isolation, streaming, and
replay), a macOS-built x86_64-windows-gnu service archive links and
initializes on Windows, and the linux-musl child executable and archive
cross-compile from macOS. The docs services chapter, packages page, and
ts-services skill state the widened opt-in and the cross-target scope.

* fix(services): align cross-target ScriptC archives

* fix(services): gate in-process archive architectures

* fix(build): keep ScriptC floor checks step-local

* fix(services): tighten cross-target toolchain gates
2026-08-12 23:51:19 -05:00
Railly Hugo 7d67158444 fix(docs): redirect /docs to the introduction page (#338)
/docs returned 404 while every page beneath it resolved and every legacy
URL form already redirected into /docs/*. docsSlugs() only yields nested
slugs, so the /docs segment itself had neither a route nor a generated
redirect, and /docs/ compounded it by 308ing into that dead end.

/docs is the parent of every documentation link on the site and the
likeliest hand-typed entry point, so open it on the Introduction. The
.md sibling keeps the Markdown surface whole for agents that reach for
it, matching the pairing legacyDocsRedirects applies to every slug.

Fixes #331

Co-authored-by: Cris <cristian.correa.cs@gmail.com>
2026-08-13 01:20:52 -03:00
Chris Tate c71a7b4638 Upgrade scriptc to 0.0.28 (#336)
* Upgrade scriptc to 0.0.28

- Pin scriptc and its compiler/runtime lock data to 0.0.28.
- Refresh service contracts, compatibility fixtures, calibration, and documentation.
- Regenerate the service compile-surface reference; the 0.0.28 surface manifest carries no entry changes from 0.0.27.

* docs: state the carrier default correctly on the packages page
2026-08-12 19:56:19 -05:00
Chris Tate d7aeea1ea9 Promote credentials to core effects (#335)
* Promote credentials to core effects

- Add app-scoped TypeScript and Zig credential effects with capability and permission gates plus platform-backed storage.

- Redact credential journal results, synthesize deterministic replay placeholders, and provide hermetic devhost and test stores.

- Update checker diagnostics, mobile plumbing, SDK surfaces, documentation, skills, and conformance coverage.

* fix(runtime): harden credential effect lifecycle

* fix: harden credential effect handling
2026-08-12 19:21:30 -05:00
Chris Tate 8b2a97ffe7 feat(services): add the in-process TypeScript service carrier (#334)
* feat(services): add the in-process TypeScript service carrier

Compile src/services into a thread-instanced, runtime-localized library
archive (scriptc 0.0.27 library mode) linked into the app binary, and run
it on a small worker-thread pool: one archive instance per pool thread,
same-key requests strictly FIFO, distinct keys in parallel across
instances. The carrier preserves the child-process seam exactly — the
same HostCallBinding, poll-based completion delivery, journaled results,
lazy start, and replay that never initializes the archive.

Cancellation and deadlines ride the same cooperative marker-file token
the child publishes, with the same grace; an operation that ignores its
token is abandoned (thread detached, timeout routed, pool refilled), and
a detected trap routes kind service_trap through the per-instance panic
sink while other instances keep answering. Streaming chunks relay live
through a per-request framed file the pool's supervisor thread tails, so
chunks keep preceding the typed terminal mid-operation.

In-process is the default carrier on host-native macOS/Linux builds;
app.zon .service_carrier (and -Dservice-carrier) select explicitly, with
.service_pool_size (-Dservice-pool-size) setting the pool width (default
min(4, cores)). Windows and cross builds keep the child carrier.

corewire grows the two in-process projections (library facade entry and
compiler profile), the service compile script grows an --out-archive
lane, and bench-service-host now measures both carriers. The new pool
e2e suite covers success/throw routing, duplicate and unkeyed keys,
live streaming with cancellation, queued deadlines, parallelism,
per-key FIFO, trap isolation, the registry/archive pairing fence, and
journal replay against the pool.

* test(services): absorb runner load in the pool parallelism proof

One retry of the parallel batch keeps the timing assertion meaningful
on saturated runners; a pool that serialized distinct keys fails both
attempts deterministically.

* fix(services): harden in-process carrier supervision

* fix(services): harden pool queue scheduling

Wake the supervisor when newly admitted work can move the next deadline earlier, and scan the full queue so large busy-key bursts cannot hide runnable independent work.

* fix(services): drain streams after grace race

Let completion-owned requests remain in the supervisor stream polling path when the grace poison CAS loses, and cover the boundary with a deterministic regression.
2026-08-12 16:25:50 -05:00
Chris Tate 473cad71ef feat(storage): ship checked relational SQLite (#326)
* feat(storage): ship checked relational SQLite

- Add capability-gated SQLite effects, migrations, transactions, live queries, and replay across desktop and mobile.
- Validate schemas and named SQL at build time, generating typed command and subscription APIs.
- Add the relational notes flagship, documentation, tests, skills, and SDK mirror support.

* fix relational SQLite correctness gaps

* fix(storage): harden relational SQLite checks

* fix relational runtime policy parity

* fix sqlite tooling on node 22

* fix(storage): harden relational sqlite boundaries

* fix(storage): address relational review findings

* fix(storage): address remaining relational review findings

* fix(storage): address relational review issues

* fix(sqlite): retire stale live queries before replacements
2026-08-12 14:26:00 -05:00
Chris Tate 06b6ccd53c Upgrade scriptc to 0.0.27 (#333)
- Pin scriptc and its compiler/runtime lock data to 0.0.27.
- Refresh service contracts, compatibility fixtures, calibration, and documentation.
- Regenerate the service compile-surface reference; the 0.0.27 surface manifest carries no entry changes from 0.0.26.
2026-08-12 12:41:59 -05:00
Chris Tate ed84e35975 tools: scriptc surface-manifest diff and service-capability gate (#328)
* feat(tooling): gate capability claims on the compiler surface manifest

- Add surface_manifest_diff.mjs: mechanical diff of two scriptc surface
  manifests keyed by stable entry ids — flips to static (with the
  staging-transform/caveat retirements each flip makes due), tier
  regressions, easings, added/removed entries, SC-code and semantic
  note changes; human-readable and --json output.
- Add gen_service_surface.mjs: generate the author-facing service
  compile-surface reference (skill-data/ts-services/references/
  service-surface.md) byte-derived from the pinned compiler's manifest,
  plus a --check mode that fails on a stale or hand-edited reference,
  a manifest/pin version mismatch, any hand-written SC-code claim in
  docs/skills/skill-data prose, or a compiler version literal that does
  not match the pin.
- Wire the check into scripts/gate.sh as the surface-claims step:
  fast tier when docs/, skills/, skill-data/, or packages/core/ change;
  always in full.
- Point skill-data/ts-services/SKILL.md at the generated reference and
  cover the tooling in packages/core/test/surface_tools.test.ts.

* fix(tooling): harden service surface checks

* fix(tooling): close service surface audit gaps

* fix(tooling): close manifest review gaps

* fix(tooling): harden surface claim auditing

* fix(tooling): keep surface audits layer-aware
2026-08-12 09:50:03 -05:00
Chris Tate 5fa8074f7c feat(bench): add service-host carrier benchmark (#329)
Measure the out-of-process TypeScript service carrier through its
production HostCallBinding against a bytes-echo service compiled by the
production service lane (frontend contract -> corewire host/registry ->
exact-pinned plain-scriptc executable). The echo operation returns its
request unchanged, so the numbers are the carrier's — lazy child spawn,
hello fence, framing, pipes, worker-thread queueing — not a workload's.

Scenarios: cold start (fresh host, first keyed request from admission to
polled completion, including the lazy spawn), warm round trips for 64 B
and 256 KiB payloads (p50/p90/p99), and queued throughput for N keyed
requests drained through the single worker thread.

Run: zig build bench-service-host -Doptimize=ReleaseFast
(requires node and npm ci in packages/core, like the services e2e lane).
2026-08-12 01:43:40 -05:00
Chris Tate 8d7946edbd docs: add the TypeScript Services chapter (#327)
* docs: add the TypeScript Services chapter

Give TypeScript services their own documentation page covering the
two-tier split, service authority and the sanitized environment,
operation authoring and kind-tagged errors, the generated typed client
with keys/timeouts/cancellation/streaming, exact vendored npm with the
calibrated coverage results, and the runtime behavior (lazy start,
supervision, replay without the service child, desktop scope).

Condense the inline services section on the TypeScript Cores page to a
summary that links the chapter, point the packages page at the new
route, wire navigation and page titles, and correct the keyed-effect
note: service requests reject duplicate live keys rather than
replacing them.

* docs: restyle the TypeScript Services chapter as reference documentation

Rewrite the chapter in a terse reference style: noun-phrase sections,
short declarative paragraphs, code examples and tables first. All
technical content is unchanged: the two tiers, service authority and
the environment allowlist, operation rules and boundary types,
kind-tagged errors, the generated client, keys/timeouts/cancellation,
streaming, exact vendored npm with the calibration results, runtime
behavior, the dev loop, and the NS1065-NS1067 teachings.

Replace vague failure-mode wording on the cores page with the precise
outcomes: dropped requests dispatch no message, stream cancellation
routes cancelled to err, and truncated streaming fetches end with
err: truncated.
2026-08-12 01:32:56 -05:00
Chris Tate 011caa9183 Upgrade scriptc to 0.0.26 (#325)
- Pin scriptc and its compiler/runtime lock data to 0.0.26.
- Refresh service contracts, compatibility fixtures, calibration, and documentation.
- Invalidate cached service frontends whenever the compiler manifest changes.
2026-08-11 22:38:24 -05:00
Chris Tate b7c493bab0 feat(ts-services): add typed boundary and ecosystem (#321)
* feat(ts-services): add typed boundary and ecosystem

- generate record-typed service clients/codecs and enforce hermetic 100% npm-static coverage

- run services in the devhost with native-format record/replay parity

- add typed streaming, cooperative cancellation, deadlines, fixtures, and guidance

* fix(ts-services): harden request admission

* fix(ts-services): align devhost runtime behavior

* fix(ts-services): enforce request deadlines
2026-08-11 21:25:48 -05:00
Chris Tate aa7ed9aa52 feat(storage): add SQLite-backed record store (#320)
* feat(storage): add SQLite-backed record store

- add capability-shed SQLite storage with deterministic effects, replay, and atomic record operations
- expose matching TypeScript and Zig APIs across desktop and mobile hosts
- add hermetic coverage, devhost support, documentation, and a worked example

Co-authored-by: carvalab <1446654+carvalab@users.noreply.github.com>

* fix(storage): restore pristine SQLite amalgamation

- restore the upstream byte removed during whitespace cleanup so the vendored source matches its documented checksum

Co-authored-by: carvalab <1446654+carvalab@users.noreply.github.com>

* fix(storage): harden store result delivery

* fix(storage): align devhost store semantics

* fix(storage): address review findings

---------

Co-authored-by: carvalab <1446654+carvalab@users.noreply.github.com>
2026-08-11 19:59:33 -05:00
Chris Tate 833e79e44a Implement the TypeScript service seam (#317)
* Implement the TypeScript service seam

- Classify src/services as ordinary static-tier TypeScript and generate a checked service contract.
- Compile, package, and supervise a pinned out-of-process service host behind Cmd.request.
- Cover service authority, failures, restart, timeout, replay, docs, skills, and showcase fixtures.

* fix service boundary validation and staging

* fix TypeScript service packaging and validation

* fix TypeScript service review findings
2026-08-11 14:23:22 -05:00
Chris Tate cafbf206e8 Implement model persistence (#316)
* Implement model persistence

- Add atomic engine-owned snapshots with generated codecs, restore and migration routes, backup recovery, debounce, and replay support.
- Gate persistence through app manifests and native check while keeping the TypeScript, Zig, and devhost surfaces in parity.
- Add store and end-to-end coverage, documentation, and a persisted TypeScript example.

Co-authored-by: carvalab <1446654+carvalab@users.noreply.github.com>

* Address model persistence review findings

* Address remaining model persistence review findings

* Fix persistence identity and restore route checks

* fix persistence replay and rollback safety

---------

Co-authored-by: carvalab <1446654+carvalab@users.noreply.github.com>
2026-08-11 13:01:01 -05:00
Chris Tate 0126d20f30 feat(ts): expose platform service commands (#315)
* feat(ts): expose platform service commands

- Add first-class Cmd APIs for credentials, external URLs, path reveal, and local-time formatting.

- Route commands through validated runtime services and native macOS, Linux, and Windows backends.

- Document effect semantics and cover TypeScript encoding, platform behavior, and runtime delivery.

* fix(windows): apply timestamp DST rules

* fix(platform): align local time formatting
2026-08-11 08:46:23 -05:00
Chris Tate 2fd7c4c3dc Add macOS lifecycle hooks for TypeScript apps (#314)
* Add macOS lifecycle hooks for TypeScript apps

- Expose hidden-window, fullscreen, Dock-presence, and launch-at-login controls through manifests and TypeScript effects.
- Implement AppKit and SMAppService behavior across both macOS hosts with safe cross-platform fallbacks.
- Cover the new wire/runtime paths with tests and document the menu-bar app migration workflow.

* fix: address platform lifecycle review findings

* fix: harden macOS lifecycle hooks

* fix: preserve macOS activation behavior

* fix: preserve hidden startup window state
2026-08-11 00:04:47 -05:00
Chris Tate 4c95b04539 Improve retained desktop frame performance (#313)
* Improve retained desktop frame performance

1. Fix retained animation pumping and Windows frame wake scheduling.

2. Make latency and frame profiling monotonic, observable, and regression-tested.

3. Add physical-display-aware macOS and Windows performance gates.

* Fix PR performance and package checks

- Align NativeSdkViewInfo declarations with runtime GPU telemetry.

- Calibrate hosted macOS animation budgets without weakening physical-device defaults.

* Harden Windows performance sampling and shutdown

- Correlate physical hover samples with their responding visual frames.

- Stop due-frame callbacks immediately when the Windows host exits.
2026-08-10 21:22:03 -05:00
Chris Tate c7e64b647c feat: add dynamic TypeScript menu bar items (#311)
* feat: add dynamic TypeScript menu bar items

- Drive status-item presentation and rich menus from committed TypeScript model state.
- Render and route dynamic status items through the native platform hosts.
- Add UTF-8 byte encoding, strict ASCII diagnostics, docs, examples, and end-to-end coverage.

* fix tray menu platform parity

* fix dynamic tray menu behavior

* fix: preserve dynamic tray platform state

* fix tray presentation and command routing
2026-08-10 19:33:42 -05:00
Chris Tate a404ca166d chore(release): prepare v0.8.4 (#310)
- Add v0.8.4 release notes and contributor credits.
- Synchronize CLI, core, platform, tool, and example version pins.
2026-08-10 13:26:30 -05:00
Chris Tate 7f6830a15b Polish component gallery interactions (#308)
* Polish component gallery interactions

- Fix keyboard navigation, scrolling, and focus-visible behavior across interactive canvas widgets.
- Rebuild gpu-components as an isolated TypeScript and Native markup gallery with complete interactive specimens.
- Add live model-driven Default and Geist theme switching with validation, documentation, and smoke coverage.

* fix component gallery focus and controls

* Fix TypeScript setup for component smoke CI
2026-08-10 12:32:31 -05:00
Chris Tate 7a29661384 Fix TS core PTY event key contract (#307)
- Align echoed PTY keys with the byte-text host and facade contract.
- Regenerate SDK declarations and update PTY conformance coverage.
- Compile the PTY event arm through the external-core E2E fixture.

Co-authored-by: NyxTools-M <243123737+NyxTools-M@users.noreply.github.com>
2026-08-10 10:24:13 -05:00
Chris Tate a727b1db68 Add customizable macOS DMG packaging (#304)
* Add customizable macOS DMG packaging

- Build polished drag-to-Applications archives with Retina-aware generated or custom backgrounds.

- Add manifest controls for Finder geometry, positioned items, files, directories, and links.

- Correct default optical alignment and document the packaging workflow.

* Fix DMG package validation gaps

* fix(packaging): make DMG generation reliable

* Validate TIFF payload ranges

* fix: tighten dmg validation and staging
2026-08-10 01:00:54 -05:00
Chris Tate 7e3a3157d0 feat(core): support streaming fetch responses (#300)
* feat(core): support streaming fetch responses

- Add a typed line-streaming Cmd.fetch overload and carry it through the command wire and runtime host.

- Keep stream lifecycle deterministic with loud cancellation and duplicate-key rejection.

- Cover the feature with contract, conformance, runtime, compiled-core, harness, example, and documentation updates.

* feat: stream AI chat through Vercel gateway

- Render chat-completion SSE deltas as they arrive.
- Pin the example to Vercel AI Gateway with official key config.
- Cover streaming, failure, and replay paths end to end.

* feat: add streaming fetch and chatbot example

* feat(chatbot): refine streaming chat experience

- Add a compact live model picker and immediate Stop action.

- Improve conversation layout, prompt focus, and caret retention.

- Expand chatbot documentation and end-to-end regression coverage.

* fix: harden streaming fetch limits

* fix: harden streaming fetch and textarea behavior

* fix(canvas): render lifted rich text
2026-08-09 21:02:29 -05:00
Chris Tate 283ab804c0 Add cross-platform audio capture and voice memo example (#303)
* Add cross-platform audio capture and voice memo example

- Add microphone and system-audio capture effects for macOS, Windows, and TypeScript core.
- Add a polished voice memo example with private app-data WAV saving and playback.
- Document platform support and packaging requirements with runtime and conformance coverage.

Co-authored-by: Marcus Schiesser <17126+marcusschiesser@users.noreply.github.com>

* Fix audio capture teardown and permissions

Co-authored-by: Marcus Schiesser <17126+marcusschiesser@users.noreply.github.com>

* Fix audio capture lifecycle and metadata

Co-authored-by: Marcus Schiesser <17126+marcusschiesser@users.noreply.github.com>

* Fix audio capture lifecycle edge cases

Co-authored-by: Marcus Schiesser <17126+marcusschiesser@users.noreply.github.com>

* fix: harden audio capture lifecycle

Co-authored-by: Marcus Schiesser <17126+marcusschiesser@users.noreply.github.com>

---------

Co-authored-by: Marcus Schiesser <17126+marcusschiesser@users.noreply.github.com>
2026-08-09 17:09:23 -05:00
Chris Tate 83a7aee721 fix(macos): smooth dialog backdrop blur (#299)
* fix(macos): smooth dialog backdrop blur

- Replace the flat box blur with an optimized three-pass Gaussian approximation.

- Preserve shadcn-compatible blur and scrim values while covering the host path with a build check.

* Fix macOS backdrop blur invalidation
2026-08-08 20:11:39 -05:00
Chris Tate 30c1410c10 chore(release): prepare v0.8.3 (#298)
- Add release notes for packaged assets, drag landing polish, and the expanded Kanban showcase.
- Synchronize CLI, core, platform package, and TypeScript example versions.
2026-08-08 17:26:42 -05:00
Chris Tate bfcc5ff8df fix(kanban): polish packaged board interactions (#297)
- Resolve boot image assets from packaged macOS bundle resources so agent avatars render after launch.
- Keep drag landing motion above swimlane clips while preserving clipped neighbor reflow.
- Double the seeded Jira-style tickets and remove issue glyphs from card metadata.
2026-08-08 16:41:23 -05:00
Chris Tate 3a580c9a8d chore: prepare v0.8.2 release (#296)
- Bump CLI, core, platform packages, and TypeScript examples to v0.8.2.
- Add complete release notes and contributors for the v0.8.2 range.
- Keep the Kanban core pin covered by release version sync and validation.
2026-08-08 14:44:47 -05:00
Chris Tate 4269233703 feat(examples): make kanban an agent ticket board (#295)
* feat(examples): make kanban an agent ticket board

- Add numbered agent tickets with OpenAI and Claude avatars from SVGL.
- Simplify the titlebar to an icon-only add action.
- Keep drag geometry and end-to-end coverage aligned.

* fix(examples): make kanban columns scrollable
2026-08-08 14:26:34 -05:00
Chris Tate b230b140b8 feat(core): add native drag and drop to TypeScript apps (#285)
* feat(core): add native drag and drop to TypeScript apps

- Carry native file drops and widget drag events through the TypeScript core contract.
- Rebuild Kanban in TypeScript with animated reordering, cross-column moves, and Escape cancellation.
- Add runtime, ABI, rendering, end-to-end, documentation, and example coverage.

Co-authored-by: John Lindquist <36073+johnlindquist@users.noreply.github.com>

* fix: correct canvas drag lifecycle

* fix: harden canvas drag dispatch

* fix drag lifecycle and preview rendering

* fix(runtime): preserve drag gesture arbitration

---------

Co-authored-by: John Lindquist <36073+johnlindquist@users.noreply.github.com>
2026-08-08 13:22:16 -05:00
Railly Hugo 04125b7d62 fix(canvas): preserve explicit zero padding (#288) 2026-08-08 14:41:13 -03:00
Chris Tate 21f6057041 Make TypeScript the clear authoring default (#284)
* Make TypeScript the clear authoring default

- Route agents to TypeScript cores and Native markup for new apps.
- Put TypeScript examples and code first across human-facing entry points.
- Keep Zig clearly documented as an explicit alternative and extension tier.

* Clarify TypeScript authoring guidance

* docs: clarify TypeScript native UI guidance
2026-08-04 22:15:13 -05:00
Chris Tate 4f0b57f2c2 feat(effects): add desktop notifications (#283)
* feat(effects): add desktop notifications

- Expose fire-and-forget notifications through the UiApp effects facade.
- Validate requests and suppress external alerts during fake execution and replay.
- Add focused runtime coverage and document the native Zig API.

* feat(ts-core): add desktop notifications

- Expose fire-and-forget notifications through Cmd.showNotification.
- Route the additive command wire opcode through the native platform effect.
- Cover compiled-core delivery and document the primary authoring path.
2026-08-04 20:14:54 -05:00
Chris Tate b21849c1bf chore: prepare v0.8.1 release (#282)
- Synchronize CLI, core, platform, and example package versions to 0.8.1.
- Add complete release notes for Markdown HTML, image handling, and payload-free HTTP writes.
- Record release-process improvements and contributor attribution.
2026-08-04 19:17:38 -05:00
Chris Tate ff6a1c2c32 fix(markdown): harden resolved image handling (#281) 2026-08-04 18:52:27 -05:00
Chris Tate b01851d03c feat(markdown): support safe HTML rendering (#280)
* feat(markdown): support safe HTML rendering

- Lower common GitHub-style inline and block HTML onto native widgets.
- Keep scripts, styles, embeds, and unsupported markup inert and literal.
- Add focused, hostile-input, documentation, and example coverage.

* fix(markdown): harden safe HTML block handling

* fix(canvas): handle multiline HTML markdown blocks

* fix(markdown): harden safe HTML rendering

* fix(markdown): harden HTML parsing

* fix(markdown): render deployment comment images

* fix(markdown): center table cell content
2026-08-04 18:40:42 -05:00
Railly Hugo 0f990c2007 fix(effects): send a zero-length body for payload-free POST, PUT and PATCH (#277)
`runFetch` routed every request without a payload to
`std.http.Client.sendBodiless`, which asserts the method carries no
body. A POST, PUT or PATCH with no payload therefore panicked on
`unreachable` and aborted the process in Debug builds; in ReleaseFast
the assert is compiled out and the request went out with no
Content-Length at all.

Send an explicit `content-length: 0` body for those methods instead.

The regression test drives a payload-free POST through the fixture
server and asserts the request head carries `content-length: 0`.
Without the fix it terminates with SIGABRT in `sendBodilessUnflushed`.
2026-08-04 13:02:41 -03:00
Chris Tate 4ceffdbbd5 chore: write changelog during releases (#278)
- Remove the changelog fragment workflow and merge script.
- Have the release agent write the complete entry from release history.
2026-08-04 10:10:24 -05:00
Chris Tate 030bf8df74 chore: prepare v0.8.0 release (#276)
- Synchronize SDK, core, platform, and example package versions
- Merge pending release notes into the v0.8.0 changelog
- Record release markers and contributor attribution
2026-08-03 12:42:10 -05:00
Chris Tate d976ab6351 native check ends with the compiler-truth pass (#275)
* feat: native check ends with the compiler-truth pass

- A TypeScript core's check now finishes by running the pinned external core compiler's analyzer over the entry with the shipped SDK declarations mapped, so the check verdict and the build verdict come from one compiler.
- Analyzer type errors fail the check with the compiler's diagnostics verbatim; an analyzer that cannot reach a verdict defers to the build instead of wedging check.
- The in-process 6.x provider remains the teachings engine, and its adapter header now states the split (the 7.x line ships no in-process API).

* fix: resolve the typecheck driver's own path portably

- fileURLToPath instead of URL.pathname, which percent-encodes spaces and mangles Windows drive paths — install locations with either broke the check pass.
2026-08-03 12:18:12 -05:00
Chris Tate d26428e11b TypeScript cores compile through the external compiler by default; the TS-to-Zig emitter is deleted (#271)
* feat: add a check-only frontend mode

- The @native-sdk/core CLI without -o checks the core (and writes the contract when asked) and emits nothing.
- native check runs the frontend in check-only mode; no scratch emission under .native/check.

* feat: TypeScript cores compile through the external core compiler by default

- The transpiled lane is gone: src/core.ts builds through the external core compiler with nothing stated, and core_compiler = "transpiler" is refused with a teaching naming the release that removed it.
- Mobile targets with a TypeScript core are taught before lane selection: TS cores are desktop-only until the external toolchain grows mobile targets; Zig/markup cores stay fully supported on mobile.

* feat: retarget the TypeScript-core suites to the compiled lane

- test-ts-core-e2e compiles every fixture core through the external core compiler in the build graph (no env gating; the compiler is a package dependency), with the markup battery in its own binary — one archive per process is the C-ABI contract.
- The paired/byte-compare machinery is gone (paired_core, gen_paired, extract.zig, test-contract-equivalence, test-compiled-core-parity); the conformance suite pins mirrors over frontend contracts and committed goldens, and the new test-external-core-abi suite holds the ABI laws over a real archive.
- The soundboard core-only dispatch budget is re-measured for the compiled lane (~5.3us on an M-class laptop, Debug; the C ABI crossing plus snapshot decode) and stays pinned at 1ms.

* feat: carry the mixed pair-return idiom on the compiled lane

- The contract sidecar gains additive init_returns_bare/update_returns_bare facts, and the generated facade narrows `Model | [Model, Cmd<Msg>]` returns (a tuple carries its command; a bare model the empty buffer).
- The scaffold starter bounds its counters with literal comparisons so the compiler's integer range proof takes them.

* feat: delete the TS-to-Zig emitter

- The transpiled lane's machinery is gone: emitter.ts (11.9k lines), the rt.zig kernel, the run1k gate, and the emitter/effects/run-fidelity suites; execution truth lives in the ts-core e2e batteries over real compiled archives.
- The frontend (transpile.ts -> frontend.ts) checks and emits the contract sidecar only; the CLI refuses -o with a teaching naming the release that removed the emitter.
- The conformance corpus and grammar matrices re-adjudicate: emitter-only gates (97 corpus cases, 7 matrix rows) are marked as accepted with their former emit-time rules kept readable.

* fix: ship the compiled lane in the npm CLI payload

- copy-framework mirrors packages/core/compile-surface + scripts and tools/corewire (the build compiles corewire from the dependency); the sync and files lists follow.
- scriptc rides as a regular dependency of @native-sdk/cli, pinned equal to packages/core by check-version-sync, and the build graph resolves its entry by node's ancestor walk from packages/core.

* ci: the compiled lane rides the package dependency

- Every ts-core-building job gets the compiler with the one npm ci in packages/core; the separate compiler install, the archive/sidecar env plumbing, and the opt-in example step are gone.
- The parity job becomes Core Compiler Fences: stage-core-contracts plus the determinism-fence negative control (the positive batteries ride zig build test in the Zig Core job).
- No compiler cache action on purpose: hosted runners are ephemeral, so runs stay hermetic by machine lifecycle.

* docs: the compiled lane is the documented truth

- The TypeScript docs, quick start, component pages, example READMEs, the scaffold templates, and the ts-core skill describe the check-and-compile pipeline; the eject-story and rt-kernel/frame-cap claims are gone, and the core dev loop is stated as restart-shaped with native dev --core for fast logic iteration.
- The evals grader checks cores with the frontend and grades ts harnesses against externally compiled archives through generated mirrors.
- The changelog fragment states the breaks deliberately: default lane switch, transpiled lane removed, mobile teaching, dev-loop latency, and the compiler dependency.

* fix: ownership.ts joins the frontend staleness set

- The checker, inference, and type layers import ownership.ts, so an edit there must re-run every cached check and contract step; the staleness array now carries it.

* chore: drop the unused TypeScript compat wrapper dependency (#274)

- Nothing imports the @typescript/typescript6 wrapper at run time — the frontend loads the exactly pinned @typescript/old alias directly — so the wrapper leaves both manifests and the lockfile.
- The version-sync check pins the alias on its own, the toolchain doctrine comments describe a stray consumer-tree wrapper (which resolution still ignores, as the twins' fixtures keep proving), and the prose pins follow the reworded doctrine.
2026-08-03 11:17:30 -05:00
Chris Tate 31c140e26f TypeScript cores gain an opt-in external compilation lane (#268)
* feat: emit the contract sidecar from the frontend

- packages/core emits core.contract.json directly from checked analysis (--contract), byte-identical to the extraction path, Wyhash identities included
- every corewire consumer (conformance shims, stage-core-contracts projections) now reads the frontend document
- test-contract-equivalence pins the two producers byte-identical per ts-core fixture

* feat: ship SDK declarations and the pinned external compiler

- generate and ship sdk/*.d.ts with the real tsc (freshness-pinned, ambient-clean), never hand-written
- add the external core compiler as the package's one exact-pinned runtime dependency and bump the compiled-core pin to 0.0.22
- promote the static compile surface into the shipped package (compile-surface/core.ts), one copy for fixtures and apps

* feat: opt-in external core compiler lane

- ts-core apps opt in via app.zon .core_compiler = "external" or -Dcore-compiler=external; the frontend still checks and emits the contract, corewire projects the compile entry/profile, the exact-pinned toolchain builds the archive, and the app links the generated mirror over it
- the staged module keeps the transpiler lane's exact shape, so the generated wiring runs unchanged over either lane
- env-gated example pin: soundboard-ts builds and tests on the lane in the compiled-core parity CI job

* fix: accept an argument-carrying external compiler command

- The compile driver took --compiler as one executable filename, so an interpreter-plus-script override failed with the missing-version teaching; a path that exists is taken whole, anything else splits on whitespace.

* fix: ship the compiled lane in the npm CLI payload

- copy-framework mirrors packages/core/compile-surface + scripts and tools/corewire (the build compiles corewire from the dependency); the sync and files lists follow.
- scriptc rides as a regular dependency of @native-sdk/cli, pinned equal to packages/core by check-version-sync, and the build graph resolves its entry by node's ancestor walk from packages/core.
2026-08-03 08:53:39 -05:00
Chris Tate e84ee28cbe chore(ovation): checkpoint agent step (#267) 2026-08-02 23:29:30 -05:00
Chris Tate 1789c68049 Add diff annotations to code component (#266)
* 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.

* Fix code diff metadata and selection layering
2026-08-02 18:44:13 -05:00
Chris Tate 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
2026-08-02 17:17:23 -05:00
Chris Tate 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>
2026-08-02 15:59:44 -05:00
Chris Tate 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
2026-08-02 12:22:58 -05:00
Chris Tate bdfce1ea7d ci: switch to vercel setup-zig (#255)
- Replace Zig setup across CI and release workflows.

- Keep generated scaffolds, tests, docs, and changelog aligned.
2026-08-01 15:35:51 -05:00
Chris Tate 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>
2026-08-01 15:05:56 -05:00
Chris Tate 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
2026-08-01 14:14:25 -05:00
Chris Tate 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.
2026-07-31 20:27:26 -05:00
Chris Tate 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
2026-07-31 20:09:24 -05:00
Chris Tate 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.
2026-07-31 19:10:33 -05:00
Chris Tate 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
2026-07-31 17:20:24 -05:00
Chris Tate 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
2026-07-31 08:08:47 -05:00
Chris Tate 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.
2026-07-30 14:30:44 -05:00
Chris Tate 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
2026-07-30 13:56:52 -05:00
Chris Tate 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.
2026-07-29 14:24:36 -05:00
Chris Tate 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.
2026-07-29 13:45:49 -05:00
Chris Tate 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
2026-07-29 13:06:47 -05:00
Chris Tate 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.
2026-07-28 19:12:07 -05:00
Chris Tate 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>
2026-07-28 17:59:47 -05:00
Chris Tate 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>
2026-07-27 21:50:47 -05:00
Chris Tate 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.
2026-07-26 09:56:26 -05:00
Chris Tate 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.
2026-07-26 01:33:26 -05:00
Chris Tate 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.
2026-07-26 00:59:21 -05:00
Chris Tate 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.
2026-07-25 21:51:26 -05:00
Chris Tate 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.
2026-07-25 14:55:29 -05:00
Chris Tate 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.
2026-07-25 14:13:09 -05:00
Chris Tate 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
2026-07-25 13:18:51 -05:00
Chris Tate 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
2026-07-25 12:09:40 -05:00
Chris Tate 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
2026-07-25 11:27:12 -05:00
Chris Tate 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
2026-07-25 09:06:55 -05:00
Chris Tate 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.
2026-07-25 01:33:35 -05:00
Chris Tate 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.
2026-07-24 23:24:53 -05:00
Chris Tate 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
2026-07-24 23:17:56 -05:00
Chris Tate 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.
2026-07-24 20:01:29 -05:00
Chris Tate 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
2026-07-24 18:09:34 -05:00
Chris Tate 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
2026-07-24 17:26:13 -05:00
Chris Tate 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
2026-07-24 17:26:04 -05:00
Chris Tate 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
2026-07-24 10:33:49 -05:00
Chris Tate 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
2026-07-24 07:57:38 -05:00
Chris Tate 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
2026-07-24 01:31:56 -05:00
Chris Tate 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
2026-07-24 00:15:07 -05:00
Chris Tate 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.
2026-07-24 00:09:50 -05:00
Chris Tate 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
2026-07-23 18:46:44 -05:00
Chris Tate 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
2026-07-23 14:52:48 -05:00
Chris Tate 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
2026-07-23 13:44:08 -05:00
Chris Tate 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.
2026-07-22 23:19:29 -05:00
Chris Tate 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.
2026-07-22 19:49:51 -05:00
Chris Tate 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.
2026-07-22 09:48:41 -05:00
Chris Tate 2eb9424c63 Native context menus on Windows and Linux; point-anchored fallback everywhere (#172)
* Anchor the context-menu fallback surface at the click point

- Thread the secondary click's pointer location through the fallback request into Ui.finalize, so the synthesized surface opens at the click like a native menu instead of the target row's bottom-left corner
- Give WidgetAnchor a point mode: a zero-size anchor rect at an explicit window-space point, reusing the existing flip-above and window-clamp edge rules
- Cover the point mode's placement, bottom-edge flip, and horizontal clamp at the geometry level, and the click-point mount end to end on a wide row

Co-authored-by: startewho <898009+startewho@users.noreply.github.com>

* Present native Windows context menus through TrackPopupMenu

- Mirror the tray popup discipline for the runtime's declared items: presentation defers to a fresh loop turn, TPM_RETURNCMD | TPM_NONOTIFY with SetForegroundWindow and the WM_NULL post, logical view-local request coordinates inverted through the presenting HWND's DPI scale and ClientToScreen
- Selection and dismissal emit the same journaled context_menu_action payload the macOS host produces (token echo, item id, 0 for dismissal), so session replay stays shape-identical across platforms
- Flip .context_menus to the tray's system-engine gate and unit-test the pure seam parts (item translation with separators and disabled flags, the action-event mapping); the TrackPopupMenu modal loop itself is only exercisable on a live Windows session

Co-authored-by: startewho <898009+startewho@users.noreply.github.com>

* Present native Linux context menus through GtkPopoverMenu

- Build a sectioned GMenu from the declared items (separators split sections, disabled items ride action enablement) wired to a per-invocation GSimpleActionGroup inserted on the presenting view widget, pointed at the click with a 1x1 rect in the widget's logical coordinates - the same space the inbound pointer path reports in
- Emit exactly one journaled context_menu_action per request (selection from the item action, dismissal from a one-turn-later teardown idle after the popover closes), the same token-echo payload the macOS and Windows hosts produce; refs stay balanced with weak pointers covering widgets that die while the menu is up
- Flip .context_menus to the system-engine gate, keep the new code outside the WebKitGTK stub fences, and update the docs, skill, and README claims that named native context menus macOS-only

Co-authored-by: startewho <898009+startewho@users.noreply.github.com>

* Gate context-menu resolution on per-request tokens

- Mint a fresh correlation token per presented (or automation-armed) request and check it before clearing the pending request, so a superseded menu's late dismissal can never resolve or clear its successor - even when both target the same widget.
- Give each GTK popover menu a per-invocation action-group namespace: the deferred teardown of a superseded menu now removes only its own group instead of the one the replacement just inserted on the same parent.
- Windows and macOS need no gate by construction (TrackPopupMenu blocks the loop thread and emits inline from a moved-out request; the macOS presentation block captures its token as a local while popUpMenuPositioningItem blocks the main queue) - noted at both fix sites.

* Add the native context-menus changelog fragment

- One feature fragment covering the Windows and Linux presenters, the .context_menus capability on both system-engine hosts, and the fallback surface's click-point anchoring.

* Cancel pending GTK context-menu teardown idles at host destroy

- Track every queued teardown idle on the host, linked through the menu states themselves: the popover's closed handler pushes the receipt the moment it exists, and native_sdk_context_menu_free unlinks on every exit path
- A superseded menu's deferred teardown captures its state and the host raw while host->context_menu tracks only the current menu, so destroy now removes each pending source and frees its captured state inline instead of letting the idle fire into a freed host
- The weak pointers keep covering widgets dying before the menu state; this covers the host dying, and cancelling deliberately drops the superseded request's owed dismissal since the runtime gating on that token is torn down with the host

* Re-point the notes context-menu token assertions at the recorded token

- Context-menu correlation tokens are minted per request and opaque, so the tests assert the null platform recorded a nonzero token instead of expecting the widget id

* Escape mnemonic ampersands in Windows context-menu and tray labels

- AppendMenuW treats & as a mnemonic marker, so an authored label like "R&D" rendered with the ampersand eaten and an accidental mnemonic armed; labels now cross the ABI with & doubled
- The escape lives in the shared translation helper covering both the app context-menu path and the tray path, whose labels are app-supplied too; a label the pool cannot hold passes through raw rather than truncated
- Pin the escaped output and the pool-exhaustion passthrough in the item-translation unit tests

* Escape mnemonic underscores in GTK context-menu labels

- GtkPopoverMenu treats _ in an item label as a mnemonic marker, so an authored label like "Save_As" rendered with the underscore eaten and an accidental mnemonic armed; labels now cross the ABI with _ doubled
- The escape lives in the context-menu translation helper (the GTK host strndup-copies every label before returning, so the caller stack pool is safe); a label the pool cannot hold passes through raw rather than truncated, and the menu bar keeps _ untouched as its intentional GTK mnemonic convention
- Pin the escaped output, the underscore-free pointer passthrough, and the pool-exhaustion passthrough in the item-translation unit tests

* Map GDK pointer buttons explicitly to the runtime's ordering

- GDK numbers secondary=3 and middle=2; the old subtract-one sent right
  clicks to the runtime as middle (never opening the menu) and middle
  clicks as secondary (opening it); press and release now share an
  explicit GDK-to-runtime switch, and the interactive-move stash keeps
  raw GDK numbering for gdk_toplevel_begin_move
- The Linux canvas smoke now owns its Xvfb display and drives a real
  xdotool right-click through a task row's declared context menu,
  asserting the selection's Msg dispatch ('1 done') in the snapshot
- ui-inbox task rows declare an honest one-item menu (Toggle done) so
  the smoke exercises an app-declared menu end to end

* Resolve context-menu selections from a present-time snapshot

- GTK popovers are asynchronous: a rebuild while the menu is open
  (timers or effects reordering conditional items) could redirect the
  visible selection through the live tree's handler table
- The runtime now emits canvas_widget_context_menu_shown after a native
  present; UiApp snapshots the shown items' dispatch Msgs keyed by the
  request token, and a token-matching selection resolves from that
  snapshot on every platform - never the rebuilt tree
- The automation verb and the fallback surface keep live-tree
  resolution: both validate against the tree they show

* Bump the automation protocol to v8 for per-request menu tokens

- Recorded context_menu_action tokens changed from widget ids to
  per-request generations; a v7 journal's selections would be silently
  swallowed by the token gate instead of refused
- The version handshake now turns that into the loud preamble mismatch,
  and the changelog fragment states the break deliberately

* Deep-copy context-menu snapshot slice payloads at present time

- The present-time selection snapshot copied MsgT by value, so a menu held open across two rebuilds dispatched slice payloads pointing into reset build-arena storage
- Snapshot Msgs now deep-copy every reachable slice into a token-scoped arena, released on resolve, supersession, and teardown
- Regression test presents a menu over an arena-derived payload, rebuilds twice with sentinel bytes, and asserts the dispatched Msg carries the bytes the user saw

* Drive the smoke's context-menu popover by pointer under Xvfb

- Xvfb has no window manager, so the GTK popover never receives keyboard focus and xdotool Down/Return died on the canvas beneath it; the menu's shown event fired but no selection ever dispatched
- Locate the popover's override-redirect X window by diffing the root's children across the right-click (topmost new viewable window) and click its center — the row declares exactly one arrowless item, so the center is the item
- Failure diagnostics now dump the X window list alongside the snapshot and app log; the workflow installs x11-utils for xwininfo

* Sharpen the snapshot copy: const slices only, exact types, loud OOM

- Mutable slice payloads pass by reference (update may write through app-owned storage; a copy would swallow the writes), while const slices copy with their declared alignment and sentinel and error unions recurse into successful payloads
- An out-of-memory copy now logs which item it disarmed instead of failing silently
- Direct unit test pins the copy semantics per shape; the integration test drives the error-union arm through the two-rebuild race

* Refuse un-copyable context-menu payload shapes with teaching errors

- A mutable slice's aliasing is unknowable to the deferred snapshot (arena bytes must be copied, model-owned storage must not be), so it is a compile-time teaching error instead of a silent guess either way
- Slice-bearing fixed arrays are refused too: an array's length says nothing about which elements are initialized, so walking a count-plus-buffer payload would interpret its undefined tail as slices; slice-free arrays and untagged unions still pass by value as plain bytes
- typeCanReachSlice gates the refusals so only genuinely un-copyable shapes are rejected; the unit test now covers the scalar count-plus-buffer shape with an undefined tail

* Scope the snapshot copy to const slices with a hop budget and a loud fallback

- The copier now walks only the blessed arena payload shape (const slices through structs, tagged unions, optionals, and error unions); mutable slices, fixed arrays, untagged unions, and non-slice pointers pass through under the deferred-Msg rule that their storage outlives the menu, so no payload shape is a compile error
- A slice-hop budget bounds the walk, turning cyclic or absurdly deep payload graphs into a handled error instead of unbounded recursion
- A failed copy (out of memory or over budget) keeps the visible item live: it dispatches the uncopied value with a warning naming the item, never a silent no-op

* Pin the presenting build's arena while its context menu is open

- Replace the snapshot payload copier outright: while a presented menu's token-keyed snapshot is armed, the arena generation that built the presented tree is exempt from the rebuild reset, so selection dispatches the ORIGINAL Msg value - same bytes and same pointer identity as the fallback surface and the automation verb, with no payload-shape restrictions, no copy allocation after present, and no recursive walk
- A new canvas_widget_context_menu_dismissed runtime notice releases the pin (and disarms the snapshot) when the menu closes without a selection; selection and supersession release it as before, and growth while a menu is open is bounded by its open span
- The regression test now also asserts pointer identity end to end and the pin's release on selection and dismissal

* Key the menu pin by window identity and double-buffer rebuilds under it

- The pin names its canvas by stable window identity, never slot index: removing a window swap-moves another slot into its place, and an index-keyed pin would protect the wrong arena; the pin-owning window's teardown now releases the snapshot and pin before its arenas deinit
- While pinned, every rebuild of that canvas routes through the partner arena on its normal reset cadence (the consecutive-build pattern the clearance retry already runs), so memory under an open menu holds at two trees regardless of rebuild count
- ANY superseding presentation or dispatch releases the previous app-menu request - a default edit/copy menu and the automation verb's direct dispatch now send the dismissed notice, not just app-over-app presentations
- Tests: pin follows its window across slot compaction (bytes and address), window removal releases, cross-kind and automation supersession release, and capacity stays flat across 14 rebuilds under an open menu

* Name the superseded menu's view in its dismissal and commit the successor first

- The pending request now carries a bounded copy of its view label, so the superseded-menu dismissal notice names the canvas it was presented on instead of an empty label (raw apps tracking per-canvas menu state need the correlation, and the view may already be gone)
- The replacement pending commits before the fallible dismissed-notice dispatch: if an app handler errors on the notice, the runtime's expected token still matches the menu the platform accepted, so the successor stays selectable
- Runtime-level test pins both: the notice carries the old token and the real view label, and the successor menu resolves after the supersession

* Keep the automation menu verb's synthetic selection ahead of a failing dismissal notice

- The widget-context-menu verb captures the superseded-menu dismissal notice's error and re-raises it only after the synthetic selection dispatches: unlike a presented menu, whose outcome the platform delivers later regardless, that dispatch is the armed request's only outcome, so no error path may leave a pending token with no presented menu and no delivered outcome
- The notice keeps its place in the event order (the old menu's dismissal before the successor's outcome), and its error still surfaces after the runtime's bookkeeping settles
- Test pins the supersession under an erroring dismissal handler: the selection still dispatches, the error still propagates, and no pending request survives

* Keep menu state sound when rebuilds fail or handlers close views

- A rebuild routed into the live tree's arena under an open menu's pin now drops the tree reference when the pass fails after its reset: handlers go quiet until the next successful rebuild instead of dangling into reset, partially rewritten storage, and the pinned snapshot still resolves the presented payload
- The superseded-menu dismissal notice runs arbitrary app code that can close views and compact their indices, so the shown event and the automation verb's synthetic selection now name their view from the committed request's own copy (showMenu returns it) instead of re-reading a cached index that a compaction can point at another view or another window's canvas
- Tests pin all three seams: the failed over-budget rebuild under a pin drops then restores the tree with the presented menu intact, and both supersession paths keep naming the presenting view after the dismissal handler closes a different one

* Restore a dropped tree on menu resolution and refuse a superseded automation verb

- A menu resolution that dispatches no Msg (dismissal, out-of-range or unmapped-item swallow) now restores a live tree the pinned-rebuild guard dropped: no Msg-driven rebuild is coming, and without a handler table every event silently no-ops until an unrelated resize or effect rebuilds
- The widget-context-menu verb now checks its freshly armed token survived the dismissal notice: a handler that synchronously presents a superseding menu replaces the pending request, so the verb refuses with ContextMenuSuperseded instead of reporting success while the token gate swallows its synthetic action
- Tests pin both: a dismissal after the failed over-budget rebuild rebuilds the tree once the model fits again, and the superseded verb errors by name while the handler's successor menu stays resolvable

* Announce nothing for a presentation superseded during its own notice

- showMenu now reports a three-way outcome: shown, refused, or superseded — the dismissal notice's app code can synchronously present a successor menu that replaces the freshly committed request, and a late shown event for it would overwrite the successor's snapshot with a token the action gate no longer accepts, stranding the stale pin
- A superseded presentation announces nothing and never falls back to the anchored surface, which would mount a second menu under the successor's native one
- Test pins the chain: menu A superseded by B whose notice presents C — three presentations, two announcements, the last carrying C's token, and C resolves normally

* Release the menu pin before its selection dispatches and refuse a closed-view verb

- A snapshot selection's Msg is stored by value and its pinned-arena payloads are consumed by update itself, so the pin now releases before the dispatch: the rebuild routes into the partner arena naturally, and a Msg whose update breaks a build budget fails the rebuild without resetting the live arena — input keeps working on the previous tree and the app's controls can recover the model
- The widget-context-menu verb revalidates its target view after the dismissal notice: a handler that closed it leaves a request that can never resolve, so the verb disarms it and refuses with ContextMenuViewClosed instead of reporting success while the action dispatch silently drops the selection
- Tests pin both: the poison selection leaves the tree live and a real click on the recovery control rebuilds in budget, and the closed-view verb errors by name with no orphaned token

* Disarm a presentation whose view died mid-notice and restore the tree before menu resolution

- showMenu now rechecks the presenting view after the dismissal notice: a handler that closed it leaves a request whose action can never deliver (or never arrive), so the request disarms with a view_closed outcome — never announced, never the anchored fallback on a dead view
- Arming a shown snapshot and resolving a snapshot-less selection both restore a live tree the pinned-rebuild guard dropped: a menu presented while the model was unbuildable still resolves once the model recovers, instead of falling through a null tree and dispatching nothing
- Tests pin both: the closed-view presentation stays silent, its token inert, a fresh present arms cleanly — and the snapshot-less selection dispatches the restored build's payload

---------

Co-authored-by: startewho <898009+startewho@users.noreply.github.com>
2026-07-22 00:52:30 -05:00
Chris Tate e67d3bd9c5 External-source channels: journaled events from app threads, sockets, and watchers (#165)
* Add the external-source channel effect family to the engine

- fx.openChannel/closeChannel with a generation-stamped thread-safe ChannelHandle.post that stages into a per-channel non-lossy FIFO, wakes the host, and reports back-pressure through per-channel drop counters
- channels share the keyed families' key space (occupied from open until the .closed terminal delivers) and deliver every event through the ordinary drain, journaled at the boundary
- journal format v8: the .channel effect-record kind with inline post bytes; replay feeds recorded events verbatim (no source thread), regenerates admission rejections, and damage-gates impossible records

* Cover the channel family: lifecycle, back-pressure, key space, replay

- direct-channel tests: open/post/deliver/close order, thread posting, duplicate-key and full-table rejects, drop accounting, post-after-close/teardown safety, shared key space with fetch and spawn
- record/replay acceptance: a session recorded with a live posting thread (one honest drop aboard) replays fingerprint-identical offline with no source thread; the duplicate open's rejection regenerates
- damage gates: over-bound post bytes and byte-carrying terminals refuse replay as damaged records

* Give transpiled cores the channel family: Cmd.channelOpen/channelClose

- wire opcodes 0x15/0x16 (additive within cmd_format_version 3) with rt builders, SDK types (ChannelState/ChannelEventArm/ChannelEventKind), and emitter lowering with the image-id literal gates
- ts_core_host routes every event through a five-field by-name arm (key/state/bytes/droppedPending/droppedTotal) on a non-retiring bridge entry; duplicate live keys reject at the post-cycle boundary echoing the key
- posting stays native-side API (Effects.channelHandle) - transpiled cores open, close, and receive; the bridge test drives a real handle post through the drain

* Add the channel-monitor example: a worker-thread source, zero polling

- an app-owned std.Thread samples its own process (uptime, peak RSS) and posts each reading through the channel handle; the UI updates only when events arrive
- Stop closes the channel and the detached worker winds down on the handle's false answer - safe past close and past teardown by the handle's construction
- tests swap the worker for a handle-capturing stub: posted samples land, no fx timer is ever armed, close kills the handle, a refused open reports rejected

* State the channel family and its journal v8 bump in the changelog

- one fragment covering the Zig surface, back-pressure contract, the conscious v8 format break, the TS tier, and the channel-monitor example

* Answer channel posts with a four-way PostResult instead of a bool

- ChannelHandle.post now returns PostResult { accepted, dropped_full, dropped_oversized, closed }: a producer can tell transient back-pressure (skip and continue) from closure (exit the loop) instead of guessing at a false; both drop answers keep the exact drop-counter semantics, and oversized gets its own member because its remedy differs (no retry of the same bytes can ever land)
- channel-monitor's worker now reads the answer honestly: dropped_full skips the sample and keeps sampling (the status line reports the delivered drop counters), closed winds the thread down - a transient 32-entry stall no longer stops monitoring forever while the UI says "monitoring"
- unmerged API, clean break: every post site, the SDK doc comment, the handle-lifetime docs, and the example's tests/README move to the enum

* Park replayed channel opens so a re-run source thread cannot diverge the stream

- under armReplay, openChannel registers the occupancy exactly as live (duplicate opens keep rejecting symmetrically, the shared key space holds) but allocates no staging and returns an INERT handle: every post answers .closed immediately, so app code following the documented open-and-spawn pattern sees its re-spawned worker exit on its first post instead of interleaving live posts with the journal-fed events
- closeChannel tolerates the parked occupancy's missing posting header; the fed .closed terminal retires the parked slot at its recorded position, the same causal instant live delivery frees the key
- new acceptance regression records the open-and-spawn pattern with a real posting thread and replays it offline fingerprint-identical (pre-fix the re-run worker's accepted posts diverged the checkpoints), plus direct coverage that inert-handle posts stage nothing and count no drops and that duplicate opens under replay still reject

* Teach the evals Cmd decoder the channel opcodes

- decode channel_open (0x15: key f64 LE + event_tag u8, exactly the bytes rt.zig's cmdChannelOpen builds - no max_pending rides the wire) and channel_close (0x16: key f64 LE) into new Op arms
- batch test pins both records' lengths so a trailing record still decodes; the unknown-op panic already names the offending byte and offset, so the next opcode gap stays a one-line diagnosis

* Never wake the host for a refused channel post

- A producer continuing through .dropped_full (the documented contract) enqueued one host wake per refusal, growing the main-loop queue without bound while the stage itself stayed bounded.
- The post site now documents the invariant: a wake is issued only when a post makes new work drainable - a full stage's entries already carry their accepted posts' wakes and the drop counters ride the next delivered event, an oversized post stages nothing, and a closed post stays a pure no-op.
- New regression test pins the pending-wake count flat across a 65-refusal storm and pins the accepted path's exactly-one-wake behavior before and after the stage refills.

* Document the channel family across the TS and skill surfaces

- The TypeScript-cores page gains Cmd.channelOpen/channelClose in the command table, a five-field event-arm example with the exact ChannelState union, and the channel stream in the Sub-vs-stream teaching.
- Both shipped skills now teach the family: native-ui gets the fx.openChannel section (verbs, event arm, back-pressure and replay contracts, test surface) and ts-core adds the channel ops to its streaming-ops set.
- A stale OpenChannelOptions comment still said overflow posts return false; it now states the .dropped_full PostResult, and the changelog names the refused-post-never-wakes contract.

* Never hold the channel staging mutex across the host wake

- Split the channel post's host wake out of ChannelShared.mutex into a ChannelWake half behind its own mutex, mirroring the media-surface producer's data/wake split: posts stage under the staging mutex, release, then wake, so drain, close, and teardown never contend with a slow or blocking platform wake hook.
- Arm the binding at openChannel and disarm it under the wake mutex on every close path (closeChannel, terminal retire, teardown) — the abandon fence: after a disarm returns, no post is inside the host call and none can start one.
- Document the lock-order invariant at ChannelShared.mutex (the staging mutex is never held across a host callback; the two locks never nest) and pin it with an injected wake hook that probes the poster's lock state.

* Coalesce channel post wakes behind one latched host wake

- Latch an atomic wake-pending flag in the channel's wake half on the first accepted post and ride it for the rest of the burst, following MediaSurfaceWake exactly: a fill/drain/refill producer now costs one host-queue entry per drain instead of a standing backlog of redundant wakes.
- Clear the latch at the drain pass boundary BEFORE snapshotting the post order (adoptMediaSurfaceFrames' clear-before-sample placement), so a post racing the drain either lands inside the pass's snapshot or observes the cleared latch and wakes afresh — nothing staged is ever left wakeless; the flag is per-channel, riding the generation-fenced binding the lock split introduced, and checked lock-free so a reentrant wake hook coalesces instead of deadlocking.
- Gate the contract in tests: a full-stage burst latches exactly one wake, a post after the pass boundary wakes afresh and delivers next pass, refused posts still never wake, and a wake hook posting back into its own channel completes instead of deadlocking.

* Surface a failed sampler start in the channel-monitor example

- Claim "monitoring" only after the source thread actually starts: the spawn reports failure through the start seam instead of silently returning with a live channel and no producer.
- On failure, close the just-opened channel (the .closed terminal frees the key for a retry) and render "sampler failed to start" in the status line.
- Exercise the failure branch through the injected-source seam the tests already use — std.Thread.spawn cannot be made to fail deterministically — including full recovery on a retry with a healthy source.

* Reserve channel-table capacity for an alloc-failed open until its rejection drains

- Live table admission now counts staged executor-truth channel rejections as occupied capacity, matching the slot replay parks for the same open until the journaled terminal feeds; regenerating refusals (occupied key, full table) deliberately never reserve, the same line the key window draws
- Route channel storage creation through a swappable allocator seam so tests can fail one open's start surgically
- Cover the reservation window, the table-limit record/replay boundary, regenerating no-reserve accounting, and teardown with a reservation pending

* Dispatch bridge-refused rejections in command-stream order across families

- Merge the TS core host's per-family rejection staging (spawn, image, channel) into one kind-tagged stage drained in wire order, so a mixed Cmd.batch's refusals reach update under the performed-in-order contract; a future family joins with one enum member and one switch arm
- Spawn rejections now share the spill discipline: a batch carrying more refused spawns than the effect table holds slots yields one rejection each instead of a panic
- Pin the order at both tiers: bridge tests drive mixed, reversed, and three-family refused batches, and the transpiled e2e fixture records a mixed-rejection session that replays with identical cross-family order

* Preserve mixed-provenance channel rejection order under replay

- A replay-parked open now reserves its pending-order slot at dispatch: the park consumes the pending_seq stamp a live executor-truth refusal would have staged at, and the fed park-retiring .rejected delivers through the pending stage at that stamp (ordered insert, slot retired at delivery) instead of trailing every younger regenerating refusal through the completion queue.
- Accepted opens stamp too - refusal-vs-accepted is only knowable at the first feed - but any non-.rejected feed vacates the stamp unused, so their fed streams keep riding the queue unchanged.
- Cover the mixed sessions end to end: alloc-fail-then-table-full replays in live order, the regenerating-first shape keeps its lead, and a rejection handler that opens a third channel sees the same table state on both sides.

* Publish the channel wake's services binding and sweep staged work at bind

- Posting threads now read the services binding through an atomically published mirror (release store at bind, acquire load under the wake mutex) instead of racing the loop thread's plain-field write; the release/acquire pair is the publication contract that makes the host state fully visible to the first cross-thread wake.
- bindServices sweeps: a post accepted before the binding could neither wake nor latch, so the bind issues one catch-up host wake when anything is staged - without it a one-shot producer that posted early stranded forever.
- Cover both shapes: the pre-bind post delivers off the bind sweep's wake with no further post, and an idle bind wakes nobody.

* Give channel occupancies a channel-owned u64 generation

- Channel handles now stamp from a dedicated monotonic u64 counter instead of the shared wrapping u32 effect counter: the permanent-closed guarantee is absolute, and a u32 wrap after 2^32 occupancies would let a long-lived stale handle match a reused slot and post into another producer's channel (the media-surface producer handle's width, mirrored).
- Fed channel entries carry the u64 generation on their own queue field; the slot families keep the shared u32 counter for their loop-internal gates.
- Pin the width, the counter's independence, and the wrap case itself: a seeded counter reproduces the exact u32 truncation collision and the stale handle still answers closed.

* Add ChannelHandle.live() and state the replay re-run honestly

- live() is the producer-launch check: false for parked replay handles, refused opens, closed or reused occupancies, and torn-down runtimes - advisory only, the post's own answer stays authoritative. The replayed open parks, but the opening update re-executes, so a producer launched unconditionally really starts (connects and blocking setup before its first post included) and is stopped only at that first post; gating the launch on live() keeps replay fully offline.
- The channel-monitor example consults live() before spawning the sampler - the Msg stream and model are identical either way because nothing model-visible branches on it - and its tests pin that a replay-armed start never invokes the source seam.
- Say it honestly everywhere it was claimed: the changelog fragment, the openChannel doc comment, the test-suite header, and both skill surfaces now state that replay never NEEDS the source rather than that no source thread runs, and one journal replays identically under both producer disciplines (acceptance-tested).

* Repair the bind/post wake handshake with a seq_cst store-buffer pairing

- bindServices' services publish, hasPending's mirror loads, the post's pending increment, and requestHostWake's services load all carry seq_cst: release/acquire never orders a store before the same thread's later load of another location, so both sides could read stale values and strand an accepted post with no wake
- Document the total-order argument at each of the four operations: whichever store lands later in the seq_cst order, that side's subsequent load sees the other's store, so the poster wakes or the binder sweeps
- Add a bounded concurrent regression (300 iterations, one post racing one bind) asserting a wake is always observed and the post drains, documented as probabilistic-but-real for this race class

* Deliver bridge refusals through the engine's one seq-ordered rejection stream

- A batch mixing an engine-refused record (cross-family occupied key, staged into the pending FIFOs for the next drain) with a bridge-refused one (host-side stage, dispatched at the cycle boundary) delivered the second rejection before the first, breaking Cmd.batch's performed-in-order contract across layers
- Add Effects.stageLoopMsg: a non-lossy caller-staged Msg stage sharing the pending seq stamp, never journaled and regenerating by contract; the TS bridge now stages its spawn/image/channel refusals there at refusal time and its finishCycle rejection stage retires, so one seq-ordered drain delivers every rejection in command order
- Pin the engine-then-bridge and bridge-then-engine compositions, the three-family mix, the journal provenance (bridge refusals journal nothing, engine refusals journal marked regenerable), and record/replay identity for mixed-authority rejection order

* Run the channel host wake with the wake mutex free

- An embedder wake_fn has no enqueue-only contract: holding wake.mutex through the call deadlocked against drainBoundary (and post's never-blocks contract) whenever a wake synchronously marshaled to the loop thread.
- Posts now mark an in-flight count under the mutex, release it for the host call, and re-acquire to clear; disarm clears the binding under the mutex and waits for in-flight to reach zero, so a returned disarm still means no producer is inside the host call.
- Gate the invariant with a wake hook that takes the drain's own pass boundary (deadlocked before, completes now) and asserts the wake mutex is free during the call.

* Refuse terminal feeds into a live channel occupancy

- feedChannelEvent is the replay/test seam and replay parks its opens with inert handles; feeding a terminal into a LIVE occupancy (open posting header, staged backlog, or armed close marker) raced the producer: the terminal's delivery destroyed the staging FIFO and stranded channel_pending_count, leaving hasPending() true forever.
- Feed answers error.ChannelLiveFeed for that shape instead; live occupancies end through closeChannel, and teardown already reconciles the count to zero when it discards staged entries.
- Tests pin the loud refusal (open and .closing shapes), the reconciled count after teardown-with-staged-entries, and the parked replay feeds staying green.

* State the bridge refusal timing break and align the TS replay claim

- The changelog names the rejection-timing change deliberately: bridge-produced refusals (duplicate-spawn keys, image validation, channel admission) moved from the command cycle's own boundary to the next host drain so every rejection arrives in one seq-ordered stream, and a frame may now render between the cycle and the rejection Msg.
- Cmd.channelOpen's doc drops the last 'no source thread at all' overclaim: replay never NEEDS the source, an unconditional producer is stopped at its first post, and a live()-gated one keeps replay fully offline.

* Reset the channel-monitor drop total on restart

- The start path reset samples and visible rows but not dropped_total, so a fresh run showed the previous run's drops until the first data event.
- The restart test now ends a run with a counted drop and pins a zeroed readout immediately after the fresh start.

* Split channel wake disarm into a non-blocking revoke and a teardown quiesce

- closeChannel and retireChannelSlot now revoke the wake binding without waiting out in-flight host calls: a supported wake hook that synchronously marshals to the loop deadlocked against the old spin-wait when the marshaled dispatch's handler closed the channel
- teardown keeps the blocking quiesce but bounds it (injectable deadline, abandoned-call counter): deinit runs as the loop stops servicing dispatches, so it cannot guarantee an in-flight marshal ever returns, and everything the abandoned call still touches lives in the process-lifetime header
- gate tests: the synchronous-marshal close completes with one .closed terminal, a stale in-flight call outliving close and reopen cannot unlatch the fresh occupancy, and teardown both waits out a slow hook and abandons a stuck one safely

* Release the wake coalescer latch when a takeMsg sweep observes an empty stage

- only drainBoundary cleared the latch, so a caller on the public bare-takeMsg drain stranded the next accepted post: it saw the stale latch and never woke the host
- the sweep clears the latch exactly when a channel's staged queue is observed empty, then re-checks the queue: a post racing the clear either re-latches a fresh wake or its entry is visible to the re-check and delivers — one redundant wake is acceptable, a stranded event is not
- gate tests: a bare-takeMsg drain to empty is followed by a fresh wake on the next accepted post, and an event-driven consumer that only drains on wake movement never strands a concurrent producer's accepted posts

* Condition the channel post never-blocks guarantee on an enqueue-only wake contract

- Document PlatformServices.wake_fn as bounded, non-blocking, and enqueue-only — the shape every first-party host already implements (macOS dispatch_async, GTK g_idle_add, Win32 PostMessageW) and the same contract the media-surface frame request documents — and condition ChannelHandle.post's never-blocks promise on it: the runtime holds no channel lock across the call, so a violating wake hangs only its own posting thread, never the runtime's lock graph.
- Reframe the loop-marshaling wake tests as violator-containment pins rather than supported-usage examples: the pinned behavior (no lock-cycle deadlock through drain, close, or reopen) stays, and the revoke/quiesce rationale now names the marshal shape as out of contract.
- Sweep the never-blocks wording in the TS SDK channel docs, the native-ui skill surface, and the changelog fragment's back-pressure bullet to carry the same conditioning.

* Leak the platform when teardown abandons a stuck channel wake call

- An abandoned in-flight wake_fn call still holds PlatformServices.context past Effects teardown, and the process-lived channel header only protects runtime bookkeeping after the call returns — so the quiesce-abandon path now reports the abandon to the platform through a new note_channel_wake_abandoned_fn services seam, synchronously, while the platform is still alive.
- Every first-party platform latches the report and gates its destruction on it: MacPlatform/LinuxPlatform/WindowsPlatform deinit (the app runner's and the generated runner's one destroy path), the null platform's reference-model deinit, and the mobile ui host's destroy all skip destruction and deliberately leak the host, process-lived, with one loud log — the abandoned-worker idiom, applied to the platform itself. A conforming enqueue-only wake never meets the teardown deadline, so the gate is violator containment only.
- Tests: the quiesce-abandon test now binds a real null platform behind the stuck hook and asserts the destroy path is suppressed (latch set, destruction skipped), and a new non-regression twin pins the healthy half — a conforming wake quiesces fast, nothing is abandoned, and the platform destroys normally.

* Make an abandoned channel wake call's whole dereference chain process-lived

- bindServices publishes an immutable process-allocator snapshot of the services table, so a poster suspended before its services.wake() dereference never reads Runtime-owned memory; a rebind swaps in a fresh snapshot and never writes the old one
- runners heap-allocate the desktop platform wrappers (createWithOptions/destroy) and gate the free on the abandon latch, extending the deinit gate to the wrapper storage the wake context actually points at
- new coverage: a stuck wake whose first context dereference happens only after teardown completes and the owning scopes die (poisoned), plus rebind-generation snapshot coverage

* Resolve a replay park's pending-order stamp exactly once

- A .rejected record targeting a park already vacated no longer reuses the stamp and appends a duplicate terminal to the one-entry-per-open pending ring; it refuses as journal damage (error.ReplayDamagedRecord) with a teaching that names the key and the one-terminal-per-open rule.
- Pins the refusal and the single delivered terminal in effects_channel_tests.

* Gate channel record provenance against recorder truth at replay

- A .data or .closed channel record stamped with .rejected provenance no longer slips into the regeneration skip and silently drops from the Msg stream; the damage gate refuses it, naming both fields.
- Channel records can only carry .exited or .rejected exit reasons (the two journal sites), so any other decoded value refuses as damage too; .rejected kind with .exited provenance stays the executor-truth feed path.
- Pins both mismatch directions, the out-of-range stamp, and the executor-truth positive path in effects_channel_tests.

* Materialize the services snapshot lazily and free it on clean teardown

- bindServices records the loop-thread services pointer only; the process-lived snapshot allocates at whichever comes last of the bind and the first live openChannel, so apps that never open a channel never allocate one, and the seq_cst publish-then-sweep Dekker pair holds at both sites.
- A clean teardown (every wake header quiesced, zero abandons this deinit) frees the snapshot through the channel-storage seam; it stays deliberately process-lived only past an abandon, and the ownership rule is documented at the new wake_snapshot field.
- Re-points the abandon and rebind coverage (an abandoned generation's snapshot stays intact for stale calls; a rebind's fresh snapshot never writes the old one) and adds laziness and clean-free pins in effects_channel_tests.

* Refuse any channel record fed past the open's terminal at replay

- A fed terminal now marks the park .terminated until its delivery retires the slot, so a damaged journal's post-terminal .data/.closed/.rejected records refuse as ReplayDamagedRecord instead of enqueueing events the retire would silently discard from an unverified replay's stream.
- A .rejected fed after the stream proved the open accepted live stays damage and leaves the park intact, so the real terminal still lands.
- Pins data-after-rejection, data-after-closed, and rejection-after-data in effects_channel_tests.

* Refuse an open that cannot publish the services snapshot

- openChannel pre-flights the snapshot before committing the occupancy and rejects as executor truth on allocation failure, so no live channel can exist whose accepted posts strand with producer wakes disarmed.
- The one remaining live-through-failure shape, a channel opened before bindServices whose bind-time publication failed, heals at every drain pass boundary: the retried publication's Dekker sweep un-strands posts accepted in the disarmed window.
- Pins the refusing open (with recovery on the next open) and the boundary heal in effects_channel_tests.

* Resolve a fed park only after its event actually enqueues

- A .closed refused by completion-queue back-pressure no longer marks the park .terminated first, so the replay pump's drain-and-feed-again retry of a valid journal lands instead of refusing as damage.
- The .data vacate moves behind the same commit point; the .rejected path stages through the pending ring, which never back-pressures, and keeps its transition.
- Pins the back-pressured terminal's clean retry in effects_channel_tests.

* Close open channels when the bind cannot publish the services snapshot

- A failed bind-site publication no longer leaves pre-bind channels accepting posts that may never deliver (the pass-boundary retry needed a loop event that idle apps never produce); the bind closes them, flushing backlogs and delivering .closed terminals through its own loop-side wake.
- Together with the open-site refusal this pins one invariant: no live channel ever runs with producer wakes disarmed while services are bound - so the now-unreachable drainBoundary heal is removed.
- Re-points the bind-failure test: pre-bind backlog flushes ahead of the terminal, the producer's next post answers .closed, and a later open republishes and wakes normally.
2026-07-21 20:41:47 -05:00
Chris Tate 349618a138 chore: prepare v0.5.4 release (#164)
- Synchronize CLI, core, platform, and example package versions to 0.5.4.

- Merge release notes and contributor credits from the pending changelog fragments.
2026-07-20 19:29:27 -05:00
Chris Tate f4b9d26258 Lower spread-arm ternaries into per-branch blocks instead of hoisting (#160)
* Lower spread-arm ternaries into per-branch blocks instead of hoisting

- The R7 narrowed-ternary paths emitted both arms into the surrounding ctx, so a spread literal's statements landed above the conditional: the capture was read before it bound (use of undeclared identifier) and both arms ran unconditionally; arms that lower statements now take the R17b temp lowering with the capture substitution active, while pure arms keep the tight if/orelse expression forms.
- Union payload captures of optional fields registered in memberSubst like null-narrowing captures, so zTypeOfExpr unwrapped ?f64 payload reads to f64 and routed reducers around the narrowing lowering; stillOptionalSubst records rename-only substitutions by exact replacement.
- The orelse fusion emitted its miss arm with no expected type, failing spread/plain literal arms with an internal NS9001; the arm now gets the unwrapped expected (or the target's inner type) and falls through to the narrowed lowering when it needs statements.

* Narrow optionals and union payloads after break/continue guards

- if (r === null) break; — and the continue, labeled, multi-statement, throw-exit, and if (x !== null) {...} else { exit } spellings — now narrow like the early-return guard: alwaysExits covers every statement tsc's flow analysis treats as never-falling-through, with breaks bound to the switch itself recognized as fall-through.
- Narrowing maps are flow-scoped at block boundaries now, so a guard's captures end with the loop body or branch they narrow instead of leaking into code where the Zig capture is out of scope and the narrowing wrong in TS.
- An unlabeled break that exits a switch from inside a clause body teaches at transpile time instead of emitting a Zig break that would jump past the enclosing loop; conformance pins every fixed shape in both optimize modes, and a run-fidelity case holds the loops byte-identical to node.

* Restore still-optional markers when scoped kind-narrowing exits

- withKindNarrow snapshotted memberSubst and narrowedUnion but not stillOptionalSubst, while narrowUnion mutates all three: a redundant kind guard inside a switch payload capture overwrote the optional marker for good, so the capture read as non-null and null tests routed around the narrowing lowerings (emitting ?f64 into float arithmetic).
- The kind-switch arm restore now drops its stillOptionalSubst additions alongside the memberSubst ones, keeping the marker map paired with the substitutions it annotates.
- Conformance pins the reviewer's shape (a redundant kind guard, expression and statement spellings, before a null test on the payload) in both optimize modes.

* Type inferred locals from narrowed ternaries by the non-optional arm

- zTypeOfExprRaw typed `q === null ? { ...fallback, price: 0 } : q` by the raw optional arm, so an UNANNOTATED local declared a `?Quote` temporary whose first non-optional use failed Zig compilation ("expected type 'Quote', found '?Quote'"); the annotated spelling masked it by supplying the expected type, which every fusion and lowering site then forwarded unwrapped.
- The conditional's computed type now applies the condition's own null test to the arm that reuses the tested value (both polarities), keeping the optional when the other arm is itself optional — its null still flows through.
- Conformance pins the inferred-local shape in both polarities and both optimize modes; runfidelity drives both branches of both polarities against node.

* Pin shadowed-name kind guards leaving the subject's payload optional

- The narrowing maps key on the tested expression's text, so a shadowing declaration of the same union-typed name collides the switch subject's key: a kind guard over the shadow overwrites the subject's optional-payload marker, and only the full-map restore in withKindNarrow puts it back on exit.
- The subset accepts variable shadowing, so the shapes pin directly: a callback parameter shadowing the subject with a ternary kind guard inside `.some` before a null test on the subject's payload (reverting the full-map restore emits the optional into integer arithmetic — "invalid operands to binary expression: 'optional' and 'int'"), plus the block-scoped statement spelling of the same shadow.
- Test-only: both cases compile in both optimize modes with the restore in place.

* Keep reassigned let bindings out of const orelse fusion

- The adjacent-declaration orelse fusion consumed reassigned `let` bindings and emitted `const`, so the later assignment failed Zig with "cannot assign to constant"; fusion now consults the reassignment tracking and skips them.
- The plain-path exit guard bound a capture that went stale at the assignment; reassigned targets now keep the plain null test and narrow through the live-variable `.?` spelling, which non-null writes keep alive.
- Pins: emitter shapes (no-fusion + const-fusion regression), conformance compile, and a runfidelity case asserting the spread reassignment lands.

* Repopulate switch-arm narrowing maps from the snapshot on exit

- The arm cleanup deleted keys absent from its snapshot but never restored OVERWRITTEN entries, so a redundant nested switch on the same subject left the inner capture name active and the continuation emitted it after its Zig block closed ("use of undeclared identifier").
- Both snapshotted maps (memberSubst, stillOptionalSubst) now clear-and-repopulate, matching every other restore in the family; audit found no remaining delete-only restore.
- Pins: emitter shape (continuation reads the outer capture) and a conformance compile case.

* Apply post-if narrowing on the else-if exit path from emitIf

- The else-if chain path returned from emitIf before the post-if narrowing ran, so an exiting null guard heading a chain left fall-through reads on the still-optional value ("optional type does not support field access").
- The narrowing tail is now a helper applied on both exit paths; conformance covers else-if, else-if-else, chained else-if-else-if, and the present-test polarity, plus an emitter shape pin.

* Keep branch-killed null narrowing dead past the merge

- Scope exits restore their narrowing-map snapshots for containment, which also resurrected a narrow an assignment inside the branch had killed; post-merge null checks then emitted `p.? == null` (Zig: comparison of 'f64' with null).
- Invalidation frames now ride the ctx: the assignment path records each permanently deleted memberSubst key into the innermost frame, and every snapshot/restore scope (blocks, switch arms, kind guards) re-applies its frame's kills after the restore and merges them outward through nested exits.
- Conformance pins the reassign-to-null, optional-call, else-branch, nested-block, read-then-kill, switch-arm, and kind-guard shapes plus a containment regression pin; runfidelity drives the null path whose output would differ under a resurrected narrow.

* Bracket chain-substitution scopes with the narrowing kill-frame protocol

- withSubsts and emitNullGuardChainUnwrap restored their full memberSubst snapshots after the branch had re-applied assignment kills, resurrecting a narrow killed under a compound null guard (post-merge re-check emitted `p.? == null`).
- Both scopes now push/pop kill frames like branch and switch-arm exits, so every full-map narrowing restore in the emitter re-deletes killed entries and merges them outward.
- Conformance pins the reviewer repro plus else-branch and kind-guard-nested variants; runfidelity drives the compound-guard kill path against node.

* Drop an always-exiting branch's narrowing kills at the merge

- A branch that killed a narrow and then always left the function (return, or a throw no enclosing try catches) merged that kill outward anyway, deleting the substitution a surviving read depends on: `if (p.v < 0) { p = null; return -1; } return p.v;` emitted field access straight onto the `?P` — tsc keeps p narrowed there because the killing path cannot reach it.
- emitBlockStatements now asks the existing alwaysExits predicate the stronger function-leaving question (returns count only where they emit as real returns, not the `break :label` lowering inside lifted callbacks; throws only where ctx.tryLabel is null; break/continue never — a break-guarded kill resumes after the loop, where it must hold) and pops its kill frame in drop mode when the list cannot fall through.
- The other kill-frame pops keep the unconditional merge: withKindNarrow, the chain-condition emitter, and withSubsts bracket branch or expression scopes whose statement bodies already made the drop/merge decision at their own block frame, and the switch-arm frame only ever receives kills from paths that resume at the arm's merge (a clause-terminating break binds the switch).

* Keep a lifted callback's trailing return inside its prefix's flow scope

- The straight-line callback lifting emitted the statement prefix under emitBlockStatements — whose exit restores the narrowing maps — and then emitted the trailing return's expression outside that scope, so a prefix guard's narrowing unwound before the expression it covers: `xs.map((p) => { if (p === null) throw bad; return p.v; })` read `p.v` against the raw `?P`.
- The flow-scope bracket now lives in withNarrowScope (snapshot, kill frame, restore, merge-or-drop), emitBlockStatements delegates to it, and the callback path brackets prefix plus trailing expression in ONE scope — the restore still runs before the callback's siblings in the emitted loop body, so its narrowing cannot leak.
- Conformance pins the throw-guard and kind-guard prefix spellings plus the labeled-block early-return lifting; runfidelity drives a throw-guarded map trailing read against node.

* Merge a try body's narrowing kills when its catch can fall through

- A try body ending in return still exits from any throwing call, so its kill frame drops only when no local catch can resume past the merge; a catch that itself always leaves the function re-enables the drop.
- try/finally adds no fallthrough of its own: the body inherits the enclosing handler's answer, stated at the emit site.
- Conformance pins the fall-through-catch, catch-always-returns, and finally shapes; runfidelity drives the thrown path through a swallowing catch to the post-try re-check.

* Decide narrowing-kill drops with an all-routes escape analysis

- Replace the final-statement leavesFn question and alwaysExits's fnExit flavor with allRoutesLeaveFunction: a list's kills drop only when every route out of it leaves the function — fallthrough, lowered returns, break/continue bound at or outside the list, and throws (or throwing calls) with a resumable handler all merge them outward instead.
- Compute a catch arm's resumability with the same analysis against the outer handler, so a throwing call inside an exiting catch that lands in an outer fall-through catch keeps the try body's kills alive at the outer merge.
- Pin the escaped break/continue, labeled break, do-while, and nested-try shapes in conformance plus a run-fidelity case for the break path, with inner-loop, switch-bound-break, and exiting-outer-catch drops as controls.

* Treat never-completing loops as terminal in the narrowing analysis

- alwaysExits now recognizes a constant-true loop no break binds (literal-true while/do-while, literal-true or omitted classic-for condition), so a kill sealed behind an infinite loop drops at the merge the way tsc's reachability keeps the surviving narrow.
- bindsBreak generalizes from switches to loops: a break bound to the loop makes its end reachable, so those kills still merge.
- Gate the callback whose every path loops forever or throws with a teaching; its labeled value block would carry a label no break uses, which Zig rejects.

* Trace a caught throw into the code after its try when classifying routes

- A fall-through catch resumes at the statements following the try, so the throw route's destination is the handler plus that continuation: allRoutesLeaveFunction now scans statement lists positionally and asks whether catch-then-rest leaves before calling the route resuming
- The catch's own routes still classify against the handler visible outside its try, and loop bodies, switch clauses, and inline callbacks keep a conservative resuming continuation
- Pins the drop, the fall-through-continuation merge, and the returning-catch composition in the kill-fallthrough corpus, plus a byte-fidelity run driving both the throw and no-throw routes

* Strip parentheses before matching narrowed targets in the ternary machinery

- normalize() folds top-level parens so keys and comparisons collide for (q) and q: the ternary value narrowing, the orelse arm match, and every memberSubst key/lookup site now treat parenthesized spellings as the tested target
- emptyTestOf and kindGuardOf skip parens on the condition, both operands, and the returned target, so guard recognition and downstream shape checks see the bare expression
- Pins the parenthesized spread arm, the fused statement-free arm, the doubly-parenthesized arm, and the parenthesized tested side in the ternary conformance corpus

* See through wrapped null literals in ternary and nullish decisions

- A parenthesized or assertion-wrapped null arm ((null), ((null)), null as T) typed the ternary non-optional, so the later guard's unwrap was skipped and field reads hit the raw optional
- New isNullLiteral helper strips value-preserving wrappers before every null-literal decision; applied to the ternary arm checks and the nullish-coalescing fold
- Conformance pins the wrapped spellings on both arms and the doubly-wrapped one

* Let narrowing flow through plain lexical blocks

- tsc's narrowing is flow-based and survives a fall-through block, but the emitter bracketed every block with a snapshot/restore, so a guard inside { } left the post-block read on the raw optional
- Plain blocks in a statement list now flatten into the enclosing list (name uniquing is function-wide, so Zig block scope was never load-bearing), which also keeps unwrap captures in scope for post-block reads; merge contexts (arms, loops, clauses, try/catch, labeled blocks) still bracket
- followingStatementsOf walks out through enclosing plain blocks so end-of-block guards still install their narrows; conformance and run-fidelity pin narrows, kills, nesting, arm containment, and name collisions across block boundaries

* Route caught-throw kills to the post-try continuation

- A branch whose only in-function routes are caught throws merged its narrowing kills into intra-try flow, stripping the surviving path's unwrap and emitting a field access on the raw optional
- Kills now travel the same edges control does: such a branch's kills wait in the enclosing try's pending set and apply at the post-try merge, alongside the body's normal-exit kills; nested tries land them at the inner continuation
- Branches with any normal fallthrough still merge intra-try, catch-block kills still post-try-merge, uncaught throws still drop; conformance and run-fidelity pin all four compositions

* Join sibling arms' narrowing kills at the construct exit

- An if/else arm's assignment kill applied to the shared maps before its
  siblings emitted, stripping them of narrowings tsc keeps (arms are
  alternatives from the entry state, not a sequence); merge-class kills
  now collect per construct and apply once after the last alternative,
  while always-leaving arms still drop and caught-throw-only arms still
  route to the try continuation.
- The join covers every multi-arm construct: if/else, else-if chains,
  the R7 dual/present capture forms, null-guarded chains, and all three
  switch lowerings (clauses are pure siblings under
  noFallthroughCasesInSwitch; stacked labels share one body).
- Exception-routed kills now apply at the catch's entry rather than
  after it: a narrow killed before an always-throwing arm's throw is
  dead inside the catch (tsc types the catch from the throw points), and
  the catch's fallthrough still carries the kill to the post-try state.

* Subtract joined branch kills from post-construct narrows

- applyPostIfNarrow and the tailored R7 dual/present installs applied
  the exiting arm's narrow unconditionally after the statement,
  resurrecting a narrowing a surviving arm had killed; every
  post-construct install (including the exit-dual guard chain's) now
  subtracts the statement's joined kill set — tsc's post-if state is
  the guard-implied narrow minus the arms' kills.
- Assignment kills now record whenever the assigned value can be null,
  not only when a substitution was live: the subtraction needs the
  semantic kill even before any narrow spelling was installed.
- branchReadsTarget now counts READS only: a write-only arm (a pure
  kill) declines the capture form instead of binding a capture the arm
  never uses — an unused-capture compile error in Zig.

* Key the narrowing maps by declaration identity

- memberSubst/stillOptionalSubst/narrowedUnion keys were normalized
  source text; emission flattens plain lexical blocks with no restore
  point, so a block-local shadow's entries stayed live for the outer
  name after the block — its null checks and reads bound the inner
  capture (invalid Zig at best, wrong values at worst), and an outer
  capture rewrote same-named callback parameters' reads.
- narrowKey qualifies the base identifier with a per-declaration id
  (member chains qualify the base: q#3.v); every key-derivation site —
  guard installs, narrowUnion, the switch scrutinee baseKey, assignment
  kills, the ?.-receiver, post-if narrows, builder/filter length keys —
  now goes through it, and unresolvable bases keep the plain-text key.
- Shadow suite covers the flattened block, an if-arm shadow under an
  active outer capture, a shadowing callback parameter (run-fidelity
  pins the behavior — the text-keyed emission compiled but filtered on
  the outer value), and two-deep nested shadows.

* Stage narrowing kills at the edges break, continue, and callback returns travel

- A scope exiting only along non-local edges no longer merges its kills at the fall-through: they stage per destination (break target's post-state, loop exit join, post-callback continuation, the try's pending catch set) via edge-kill stages the loop/switch/label/callback emitters bracket their emission with, generalizing the caught-throw channel into one routing.
- Continue edges need no separate back-edge machinery: tsc widens the loop-entry state for back-edge kills and the checker rejects unguarded reads, so the exit join is the one destination left to serve; pinned at tsc level.
- Clause-terminating switch breaks keep riding the sibling join to the same post-switch point the switch stage applies at, so a kill is neither applied twice nor dropped between the two mechanisms.

* Canonicalize erased wrappers in the narrowing key

- narrowKey now strips non-null assertions, as, and satisfies alongside parentheses, so every key consumer at once matches q! and (q as Quote) to the tested q — ternary arm identity, orelse fusion, and substitution lookups included.
- emptyTestOf and kindGuardOf strip the same wrappers on the condition and its operands, so a wrapped tested target (q! !== null) narrows through the capture form exactly like the bare spelling.
- The statement-level orelse fusion matches its guard by declaration identity instead of name text, and the ternary read gates use declaration identity too (a wrapped read is still a read of the declaration).

* Treat the global undefined as an empty in ternary and nullish arm decisions

- An undefined arm's internal type is void, so the ZType-based arm check read it as a non-empty value: the ternary typed non-optional, the later === undefined guard's unwrap was skipped, and the field read hit the raw optional (invalid Zig on a shape main compiles)
- New isEmptyLiteral helper (wrapper-stripped null keyword OR the global undefined identifier) replaces isNullLiteral in the ternary arm-emptiness checks and the nullish-coalescing fold, making the undefined path symmetric with the null path in both arm positions
- Conformance pins both arm positions plus the as-wrapped and doubly-parenthesized spellings; runfidelity drives both polarities and both branches against node

* Isolate probe and sibling-arm flow state in expression lowerings

- Throwaway probe ctxs shared the narrowing maps and kill-frame stack by reference, so a probe processing a callback assignment to an outer narrowed local deleted the narrow before the sibling arm emitted — the sibling read the raw optional, invalid Zig
- Every probe now brackets in withNarrowScope, and ternary arms follow the statement-level sibling law: each arm probes and emits from the entry state, real kills joining once after the last arm via applyJoinedNarrowKills; the isolation law is documented at childCtx
- Conformance pins the generic and capture-lowered flavors; runfidelity drives both arms and the empty-input corner against node

* Gate property-read detection on declaration-qualified keys, not source text

- The capture gates matched property chains by getText().includes, so a shadowed spelling inside a nested callback counted as a read of the outer target: the capture bound, the declaration-keyed substitution correctly declined to rewrite the shadow, and Zig rejected the unused capture
- New anyReadsKey walks the nodes' property accesses and compares canonicalized narrowKeys — the same identity substitutions are looked up by — and replaces the text gates in anyReadsTarget, branchReadsTarget, followingReadsTarget, and the switch payload-use check; diagnostics keep text
- Conformance pins shadow-only (no capture), outer-plus-shadow (used capture), guarded-branch, exit-guard, and payload-field shapes; runfidelity drives hit/miss and empty inputs against node

* Stage finally-clause kills until the try construct's exit

- A finally emits as a Zig defer whose text precedes the try body's but whose flow follows it: its assignment kills previously applied at emission time, stripping the body's reads of narrows tsc keeps there and emitting field accesses against raw optionals.
- The finally now emits in a bracketed scope whose kills stage until the construct's exit, and its entry state drops every key the try or catch may assign null to — tsc types a finally path-insensitively (an exception can enter it between any two statements), so such reads re-check the live variable, never a stale capture.
- Pins the emission-order case, the finally-reads-killed-narrow case, and three-region kills in conformance plus a byte-fidelity run against node.

* Route pending exception kills through the catch's own control flow

- Kills riding throw edges into a catch previously applied to the post-try state unconditionally, poisoning the normal fallthrough when the catch leaves by break, continue, or return — paths tsc types with the narrow intact, since only clean flow reaches the continuation.
- The pending set now enters as the catch list's input state: its reads still see the kills, and they ride the list's kill-frame routing out — post-try on fallthrough, the edge stage on break/continue, dropped when every catch route leaves the function.
- Pins catch-break in a loop, a labeled break out of nested loops, a re-killing fallthrough catch, and an always-returning catch in conformance plus a byte-fidelity run against node.

* Key element-access narrows by declaration and canonical index

- narrowKey qualifies element accesses recursively: literal indexes canonicalize by value, identifier indexes key their own declaration, and computed indexes decline narrowing entirely instead of falling back to raw source text, which let a shadowed declaration's narrow rewrite the outer name's reads across flattened blocks.
- Every consumer honors the keyless decline (no substitution, capture gates off, live optional reads); the optional-call receiver rewrite keeps working through an explicit node-scoped key.
- Conformance pins both chain directions, shadowed identifier indexes, and the computed-index decline; the run-fidelity corpus pins the outer read's value against node.

* Keep narrowing through provably non-null reassignments

- A capture substitution now transitions to the live-slot `.?` spelling when the assignment's value is provably non-null, matching tsc, which keeps the target narrowed; possibly-null assignments still kill.
- Guarded branches choose their form up front (captureServesBranch): a capture binds only when every reassignment is straight-line and a read precedes it; otherwise the branch takes the plain test with `.?` reads, which stay valid through nested reassigns, sibling joins, and loop back edges.
- Conformance pins all-reads-after, reads-around, conditional, else-arm, loop, and possibly-null shapes; the run-fidelity corpus pins the reassigned values against node.

* Emit a do-while's trailing test inside its body's narrowing scope

- tsc evaluates the do-while condition after the body and carries the body's flow state into it, but the while(true)+trailing-exit-test lowering closed the body's narrowing scope before emitting the lowered `if (!(cond)) break;` — a terminal guard's narrow unwound before the test it covers, and `do { if (p === null) return -1; n += p.v; } while (p.v > 0);` read `p.v` on the raw `?P` (transpile reported success; Zig rejected the field access). Body and test now share one scope via emitBlockStatements' trailing hook, restored only at the loop boundary.
- The early-exit guard's follower walk now sees that trailing test (trailingDoWhileTestOf), so a guard read only by the condition still binds its capture — and binds none for the first-pass-flag form (a body binding `continue` hoists the test ahead of the body, where the capture would be an unused Zig const) or an always-exiting body (no test emits). Continue-carried kills still widen the hoisted test onto the live optional (tsc rejects the bare read — pinned), and break-carried kills still land only post-loop.
- Conformance pins the guard/capture-only/back-edge-re-narrow/continue/break/nested spellings, emitter tests pin the tsc-level widenings (continue edge into the test; no body narrow across the back edge into the body top), and runfidelity drives the trailing-test read through zero, one, and multiple iterations against node.

* Judge terminality through stacked case labels and statement labels

- alwaysExits read each switch clause alone, so an empty stacked label
  (case "a": case "b": return x) judged non-exiting and the branch's
  narrowing kills merged into flow that cannot reach the merge — valid TS
  emitted raw optional arithmetic. Clauses now group to their
  statement-bearing tail, matching the switch emitters' label coalescing;
  a trailing run of empty labels keeps the switch nonterminal.
- A LabeledStatement always read as non-exiting, defeating the
  constant-true-loop and terminal-block judgments under it. A labeled
  statement is now terminal iff its wrapped statement is terminal and no
  break names the label (a break to the label resumes right after it).
- Conformance and run-fidelity cases pin both shapes and the
  merge-preserving variants: a stacked group that falls out of the
  switch, trailing empty labels, and break-to-label loops and blocks.

* Judge exhaustive defaultless value switches terminal through the scrutinee's type

- alwaysExits read every defaultless value switch as nonterminal, so a
  branch ending in a switch whose case labels cover the scrutinee's
  literal-union type merged kills from flow that cannot reach the merge -
  valid TS emitted raw optional arithmetic. The predicate now consults
  the checker's flow type at the scrutinee (string/number/boolean literal
  unions with literal case labels only; anything wider keeps the
  conservative merge) before applying the stacked-group exit rule.
- The lowering closes the same path the claim closes: a covered-by-type
  defaultless switch whose arms all exit emits `else => unreachable` for
  the never-reached fallthrough (string enums already emitted a
  Zig-exhaustive switch with no else), so an exhaustive switch can end a
  value-returning function with no reachable missing-return path.
- Conformance and run-fidelity cases pin full coverage (separate cases,
  a stacked group, numeric literal unions, function-final position) and
  the merge-preserving variants: one member uncovered, a plain-string
  scrutinee, and the boolean-scrutinee emission gate.

* Judge value-switch exhaustiveness against a sound scrutinee type, never the raw flow type

- tsc never widens a local's narrowing for assignments inside callbacks, so trusting its flow type at the scrutinee let a defaultless switch claim coverage real execution escapes — the lowering then armed the fallthrough with an `else => unreachable` that non-empty input REACHES (safety panic in Debug, undefined behavior in ReleaseFast) while node falls out and returns.
- Coverage now judges by the scrutinee's declared union (sound unconditionally: a local can hold any declared member at runtime), keeping the flow type only for an identifier local/param no nested function assigns, where the straight-line CFA is exact; property reads judge by the declared property type and unjudgeable shapes decline.
- Both consumers demote together through the one memoized judgment: when it declines, emitValueSwitch closes the fallthrough with a completing `else => {}` and alwaysExits answers nonterminal, so the kill merges and post-switch re-checks stay live.

* Count only tsc-reachable, same-scope assignments when widening a finally's entry narrows

- finallyEntryKills scanned the raw syntax of the try and catch, so assignments tsc's CFA never counts — code under a keyword-literal false condition, callback bodies — killed narrows tsc typed the finally with, stripping the substitution and emitting member access on a raw optional Zig rejects.
- Probed the checker provider and matched it exactly: `if (false)`, the else of `if (true)`, a `while (false)` body, and the short-circuited side of `false &&` / `true ||` keep the finally narrowed, while a `const NEVER = false` alias condition widens (so only bare keywords skip); a callback defined in the try that assigns the target never widens, called there or not.
- Genuinely conditional assignments (`if (flag) p = null`) still kill path-insensitively, and everything unpinned (do-while, non-literal conditions) stays on the conservative walk.

* Gate narrowing captures on reads, not uses, in ternary arms and guard chains

- anyReadsTarget counted a plain assignment's bare identifier target as a consuming use, so a write-only ternary arm bound a payload capture nothing reads — Zig rejects the unused capture; the arm now takes the plain comparison while reading arms keep the capture.
- The identifier walk now uses identifierRead, the same judgment branchReadsTarget and captureServesBranch already apply: compound assignments and ++/-- still read the slot, and property-chain targets are immutable model data, so their key walk is unchanged.

* Exclude tsc-unreachable branch arms from kill joins via one shared judgment

- Kills under a bare keyword-literal condition — `if (false)`, the else of `if (true)`, a `while (false)` body — merged at branch joins and stripped narrows tsc keeps, emitting member access on a raw optional; the arms still emit, only the flow bookkeeping skips them.
- Factored the judgment into tscExcludedArm, shared by the finally-entry scan and the join layer (if/else arms, else-if chains, the while body merge, and the loop's staged break edges).
- Pinned the boundaries: a do-while(false) body runs once (the test follows the body), so its kills still count, and a `const NEVER = false` alias condition never excludes.

* Judge scrutinee flow trust by whether a nested assigner can have run yet

- scrutineeFlowTrustable declined flow trust for ANY nested-function assignment in the enclosing function, including callbacks textually after the switch that cannot have executed before it — demoting flow-exhaustive switches to their declared unions and leaving guarded fall-through reads on raw optionals.
- Arrows and function expressions do not exist as values before their definition evaluates: they now count only when the definition precedes the switch, or a loop encloses both so the back edge re-enters the switch after the definition ran.
- Function declarations hoist and class-member bodies exist from their container's creation, so both still count regardless of position; the judgment sits on the OUTERMOST nested function.

* Stop break/continue-binding walks at function boundaries

- Labels are function-scoped in JS: a nested helper's `break outer` binds the helper's own `outer` label, never the enclosing function's loop — but bindsBreak walked into nested function bodies, reading an enclosing constant-true loop as fallible and merging branch kills tsc keeps, and labelReferenced kept emitting a Zig loop label nothing in the function consumes (an unused-label error).
- bindsBreak, bindsContinue, labelReferenced, and the early-break gate now stop at function boundaries — the same rule breaksToLabel, escapingEdgesOf, and the route analysis already applied, so every binding walk is uniform.

* Give tsc-excluded arms no routes and extend the exclusion to for(;false;)

- The route walks (allRoutesLeaveFunction, escapingEdgesOf) and the terminality binders (bindsBreak, breaksToLabel) now consult tscExcludedArm: a break/continue under an arm tsc's CFA cannot reach is not an edge, so it no longer stages kills at loop exits the surviving flow never carries. labelReferenced and bindsContinue deliberately keep counting — the arm's text still emits, so the label must stay referenced and the do-while flag form must host the emitted continue; the split is documented at the helper.
- tscExcludedArm gains the literal-false-condition ForStatement case (body and incrementor), mirroring while(false); wired through every consumer: the finally entry scan, the loop-dispatch join sites, and emitClassicFor's body scope. An omitted for condition stays the opposite judgment (infinite loop) — literal false never collides with for(;;).
- Conformance, emitter-pin, and runfidelity coverage for both directions: dead break/continue keep the post-loop narrow, real break/for kills still stage and re-guard.

* Close a claimed-terminal plain switch's chain with an unreachable else

- An inferred literal union (const k = x > 0 ? 1 : 2) passes the sound coverage judgment, so alwaysExits claims the switch terminal, but the emitter-level type is plain number and the lowering is emitPlainSwitch's if/else chain — left open, a callback value block or returning function falls off the chain's end into invalid Zig.
- emitPlainSwitch now reads the same memoized judgment through alwaysExits and closes the claimed-terminal defaultless chain with an unreachable else the claim has already proven no execution reaches; a non-exhaustive inferred union still declines and completes normally.
- The agreement invariant gains its third leg: no switch lowering may leave a completable fallthrough under a terminality claim — emitValueSwitch and emitPlainSwitch assert it, the kind switch closes by construction, and a test pins all three lowerings.
2026-07-20 18:46:56 -05:00
Chris Tate 4be334fd40 Dynamic images: load at runtime, journal the bytes, replay offline (#146)
* Add fx.loadImage: the runtime image-load effect with the audio source cascade

- New .image effect kind in effects.zig: local-path-then-url resolution with a content-addressed url cache (imageCachePath mirrors audioCachePath under images/), decode + registration through the registered-image seam at drain time, and one terminal Msg carrying loaded dimensions or the same error classes the runtime API raises
- Journal format v6: the .image effect-result record kind with outcome/dimensions and the blob-store content-address fields; replay feeds recorded terminals verbatim and re-registers journaled bytes best-effort
- Fake-executor seams (pendingImageLoad*, feedImageBytes, feedImageResult) plus effects_image_tests.zig covering the request shape, the full decode-register path, every failure class, cancel, and the real executor's local/network/cache-hit cascade against a loopback fixture

* Journal image results through a content-addressed session blob store

- session_blobs.zig: blobs/ beside the journal, one file per distinct payload named by SHA-256 prefix (the audio-cache hashing convention) — dedup by construction, verified on read; DirBlobStore for the app runner, MemoryBlobStore for tests
- The recorder moves an image record's encoded source bytes into the blob store at effect-result time and journals hash + length; replay resolves the blob, verifies address and length, and feeds the recorded bytes — byte-identical, offline, refusing loudly (ReplayMissingBlob) when the store is absent or damaged
- Session tests: record-replay equality with fingerprint checkpoints over two loads sharing one blob, a journaled decode failure, and a regenerating rejection; plus the no-blob-store recording refusal and the journal codec round-trip

* Markup <image>: the runtime-image leaf with a dynamic ImageId binding

- New element code 67 (image, widget_kind image, pictorial a11y class) reusing attr 38's binding grammar broadened to avatar+image; the binding is required on the leaf (an unbound image is dead markup, the icon-without-name policy)
- Both engines mirror the avatar guard: binding-only, integer-only, and negative model values fail the build with the teaching message instead of trapping in the u64 cast; teaching messages renamed to the shared image_binding_* vocabulary
- Pin test updated for the new element fingerprint (print-pins: elements 67, element_names 55); validator/interpreter/compiled/contract tests cover resolve, the 0 sentinel, every misuse, and hand-view parity; LSP docs, SKILL.md tables, and the docs vocab JSON carry the new entry

* Cmd.imageLoad: the TS tier's runtime image vocabulary end to end

- SDK: ImageState/ImageEventArm/ImageEventKind, ImageSource/ImageRoute, the image_load Cmd member and factory keyed by the app's numeric ImageId (any number expression — ids are model data)
- Transpiler: emitImageLoadCmd with the audio source-object grammar, the four-field arm shape check (state matched by member name, width/height/status numbers), and compile-time id/bounds gates; rt.zig gains opcode 0x10 additive within cmd_format_version 2 (the 0x0D-0x0F precedent) and the cmdImageLoad builder
- Host bridge: an id-keyed image table routing the ONE terminal by field name, the spawn discipline for duplicate live ids (bridge-side "rejected" at the post-cycle boundary), and image_cache_dir wiring deriving the content-addressed cache path from the URL like audio; covered by wire-layout, conformance, run-fidelity, and host e2e suites plus the SKILL.md vocabulary

* Document dynamic images: the load-show-replay story

- New docs page (/dynamic-images) covering Cmd.imageLoad TS-first with the Zig toggle, the source cascade and content-addressed cache, the honest result vocabulary and fixed limits, and the byte-identical offline replay guarantee via the session blob store
- Native UI's Images section now leads with the first-class load path and shows the <image> markup leaf; the TypeScript Cmd table gains the imageLoad row
- Changelog fragment states the journal v6 format break plainly (v5 journals refused at the preamble; re-record)

* Regenerate the docs component-preview wasm module

- The live-preview host embeds the runtime and markup engines, which now carry the image effect kind and the <image> element (schema code 67)

* Keep the dynamic-images doc sample inside the transpiled subset

- The core declares the ImageState alias itself (the named-alias rule the arm check enforces) and tracks the last state as a plain enum field

* Export the image effect surface through both roots

- src/root.zig and src/runtime/root.zig now carry EffectImageResult, EffectImageOutcome, imageCachePath, max_effect_image_path_bytes, max_effect_image_bytes, and effect_image_blob_hash_len beside the audio exports they mirror, so an external Zig app can write an Effects.imageMsg Msg arm and reason about the image bounds without module-internal paths.
- A compile-shaped export pin in effects_image_tests.zig (the media-surface pin's convention) holds both roots to the effects module's types, builds the Msg arm through the public constructor, and derives a cache path through the public root.

* Serve session replay decode through the host platform's image codec

- runSessionReplay built its null platform with the image decoder OFF, so production replay dropped every journaled load's pixels (and replayed screenshots lost images) while the branch's tests enabled the decoder by hand; the runner now installs the recording host's own context-free codec (CGImageSource / gdk-pixbuf / WIC) into the headless services via platform.installHeadlessImageCodec, decoding JOURNALED blob-store bytes only — the network stays absent.
- A codec-less null-platform build falls back to the null platform's strict test-PNG decoder, stated honestly in the installer's doc; the comptime platform dispatch keeps the desktop arms out of test-tier analysis so the unit suite links without host shims.
- The offline blob-store replay test now installs its decode seam through the SAME call the runner makes instead of flipping image_decode manually (the exact gap that let this slip), and a file-contains step pins the runner wiring plus all three per-platform installers.

* Reject the seventeenth in-flight image load instead of panicking

- A gallery screen (or one Cmd.batch) with 16 loads already in flight reached the bridge table's freeImageIndex panic, contradicting the documented exactly-one-result contract; a full table now routes pushImageReject like the sibling refusals — the engine's own slot-exhaustion vocabulary, and consistent with audio, whose single channel replaces in place and never crashes.
- The e2e suite fills all 16 entries with dynamic model-owned ids, proves the 17th dispatch answers state "rejected" through the event arm, and feeds one of the 16 live loads to its loaded terminal to show the refusal touched nothing else.

* Make the ImageId bound exclusive at 2^53 in both tiers

- The contract is a positive integer BELOW 2^53, but the emitter allowed literals <= 2 ** 53 and the host allowed dynamic ids <= 9007199254740992.0; 2^53 is the first f64 that aliases a neighbor (2^53 + 1), so both tiers now reject it — the emitter via Number.isSafeInteger with the NS1030 message naming the bound, the host via a strict < with the aliasing rationale in place.
- Boundary tests on both sides: conformance pins 2^53 - 1 building and 2^53 stopping at compile time (NS1030), and the e2e suite drives model-owned dynamic ids through the bridge — 2^53 - 1 parks a live load with the id intact, 2^53 answers state "rejected" without touching it.

* Echo the requested id through the image result arm

- The image event arm is now five fields matched by name (id/state/width/height/status): the engine's EffectImageResult always carried the id, but the arm dropped it, leaving two concurrent loads sharing one arm uncorrelatable in update.
- Bridge rejections echo the refused id too (0 for an id the wire cannot carry exactly — there is no honest integer to echo for one), staged as tag+id pairs at the same post-cycle boundary.
- Every tier that pins the arm shape learns the fifth field: SDK types, emitter conformance, the host's comptime shape check, the wire docs, the e2e fixtures (including a new concurrent-loads test distinguishing completions by the echoed id), and the docs page whose sample now adopts cover: msg.id.

* Stage image-load rejections without a user-reachable cap

- The reject staging buffer was fixed at the 16-entry table size with a panic past it, but the count is the app's to choose: a Cmd.batch of 17 loads against a full table crashed on the seventeenth instead of yielding 17 rejected results.
- Staging still exists because the walk cannot dispatch mid-command (the wire bytes are frame-arena resident and a nested cycle's frame reset would free them), so the stage now spills lazily: the table-sized inline buffer covers everyday cycles allocation-free, and the first overflow takes one engine-allocator block sized by the wire's own arithmetic bound (an image_load record is at least 30 bytes, so cmd.len / 30 caps the count without a second record parser to keep in sync).
- Delivery is unchanged — every reject still dispatches after the issuing cycle's frame reset, nows first, spawn rejects next, image rejects last, in record order — and the new e2e drives a 17-load batch against a full table to 17 boundary rejections with the 16 live loads untouched.

* Cmd.imageCancel: the TS tier's numeric-id image cancel

- The documented "cancelled" outcome was unreachable from TS: Cmd.cancel is string-keyed and never inspects the numeric image table, so a stale load pinned its id against same-id retries until it terminated. Cmd.imageCancel(id) routes to the fx.cancel(id) path the engine already covers — loud like spawn's cancel (the load's own event arm delivers state "cancelled" and the entry retires, freeing the id), a no-op for an id with no live load, mirroring how audioStop is audio's close rather than an overload of the silent string-keyed cancel.
- Wire record image_cancel [op 0x11][id f64 LE], additive within cmd_format_version 2; full tier coverage — SDK factory, emitter with imageLoad's NS1030 id-literal gates, conformance, wire-layout pin, host decode, and an e2e proving cancelled delivery plus a same-id retry parking a fresh load.
- The 0x11 opcode overlap with feat/menu-bar-lifecycle is known cross-branch and handled by the merge-order reconcile.

* Verify blob-store dedup hits instead of trusting the name

- Recording's dedup probe treated ANY existing hash-named file as the bytes, so a damaged blob let future recordings seal successfully while replay refused them. The probe now reads the existing file back chunk-wise against the incoming bytes (equality against the caller's bytes IS the hash check — their address is the file's name, and no blob-sized buffer is staged) and skips the write only on an exact match.
- A mismatch — damage, truncation, an unreadable file — is a repairable state while the true bytes are in hand: the write falls through to the same atomic partial+rename a fresh install uses, with a debug-scoped log line, never a hard failure. Cost: a dedup hit is one bounded read (<= the 1.25 MiB blob budget) instead of one existence probe.
- MemoryBlobStore had the same blind-dedup shape and repairs in place identically; tests pin corrupt-then-rewrite repair on both stores (dir store covering flipped bytes and truncation through the atomic path) alongside the existing one-blob-for-same-bytes dedup pins.

* Feed worker-origin image rejections under session replay

- A rejected image terminal journals from both sides of the executor seam, but replay skipped every one as regenerable loop-side validation - a worker-refused load (a host-less URL that passes the loop's scheme check, an executor that could not start) parked forever in the fake executor and diverged the replay.
- Loop-side validation refusals now mark themselves with the record's exit reason (the .host records' convention) and keep regenerating; unmarked rejections are executor truth and feed like every other worker terminal.
- Covered by a record/replay test that drives a host-less URL through the real executor's worker and asserts the rejected Msg delivers fingerprint-identical offline.

* Give each image cache install its own temp file

- Every install wrote <cache_path>.partial, so two concurrent loads of one URL truncated each other's temp and a rename could publish a half-written file into the cache name; the temp now carries the slot generation (operation-unique), keeping the rename atomic with last-whole-file-wins.
- A failed install deletes its own temp, so the cache directory never accumulates this process's debris; only a hard crash mid-install can leave one, in the OS-purgeable caches directory.
- Pinned by a temp-name uniqueness test on the extracted imageCachePartialPath seam plus a two-loads-one-url executor test asserting both terminals, an intact cache entry, and no .partial debris.

* Stage loop-side image terminals outside the lossy pending ring

- The shared pending ring evicts its oldest entry on overflow and folds the loss into a drop counter, but EffectImageResult carries none and loop-side validation rejections are unbounded per dispatch: 33 refused loadImage calls before a drain silently evicted one terminal, breaking the exactly-one-terminal-per-load contract and stranding the issuing model forever.
- Image terminals now stage in a dedicated non-lossy FIFO: the ring-sized inline buffer covers everyday bursts allocation-free, and a burst past it grows a heap ring geometrically (each staged entry answers exactly one loadImage call, so storage is bounded by the caller's own call count between drains; the spill frees when the stage drains empty). A shared enqueue stamp merges the stage with the ring at drain, so delivery order across both structures is unchanged.
- The new burst test drives 33 sourceless loads plus one rejected spawn through a single dispatch to 34 terminals in enqueue order, exactly one per load - restoring ring routing for image terminals fails it at 32.

* Make image cache install temps writer-unique, not generation-unique

- imageCachePartialPath named temps `<cache_path>.<generation>.partial`, but the slot generation is channel-local: two Effects channels in one process, or two app processes sharing the platform cache directory, can install toward one cache path at the same generation and recreate the truncate/rename race the temp exists to prevent.
- Each install now appends a random 64-bit token drawn from the operation's own executor io (the CSPRNG seam every worker already carries), unique across channels and processes; the generation stays in the name as debris provenance, and the formatter stays pure on every compile target - the wasm32-freestanding docs preview never runs an install (no executor io exists there), verified by a green docs-wasm-preview build.
- The temp-name test now pins the cross-channel case directly: colliding generations with distinct tokens produce distinct names, so reverting the token fails it; the failed-write/rename self-cleanup path is unchanged and the concurrent-load no-debris assertions still hold.

* Refuse fractional expectedBytes at both tiers instead of truncating

- The emitter accepted any finite nonnegative expectedBytes literal, and the host's representability check lacked the whole-number clause its id sibling has: a fractional 1.5 truncated through @intFromFloat to 1, so cache installs verified every download against a size the app never declared and re-fetched on every launch.
- The emitter literal gate is now the id gate's Number.isSafeInteger (>= 0), teaching "not a whole-number byte count below 2^53" in the NS1030 voice; the host adds the floor clause, so fractional and unrepresentable DYNAMIC counts map to 0 = "unknown size" - the honest degrade the comment already promised (verification skipped, never wrong).
- Conformance pins the literal pair (1.5 stops the build, 4096 builds) and the e2e drives model-owned 1.5 and 4096 through the wire to expected_bytes 0 and 4096 with both loads completing; reverting either tier's gate fails its test.

* Propagate a cancel that lands inside the image cache probe

- readImageCache swallowed every error into a cache miss, including error.Canceled; Zig Io cancel delivery is one-shot, so a cancel interrupting the probe was consumed and the load fell through into the network fetch with nothing left to interrupt it - explicit cancel, the timeout, and teardown all degraded to waiting out the fetch, and a timed-out load could deliver late as .loaded.
- The probe now returns error{Canceled}!bool: Canceled propagates and the cascade terminates .cancelled like its sibling probe arms, while every other failure stays an honest miss that refreshes from the network.
- Covered at both tiers: a unit test drives the probe with an Io whose open/read return Canceled (and pins the honest-miss half against std.Io.failing), and an e2e parks a real load inside the probe on a reader-less FIFO, cancels it, and asserts one .cancelled terminal with zero requests reaching the loopback fixture.

* Cmd.imageUnregister: the TS tier's registry release

- A TS gallery had no recourse past the 16-slot registry: Zig cores call fx.unregisterImage(id), but Cmd.imageCancel only ends in-flight loads, so every successfully loaded distinct id occupied its slot permanently and the 17th image answered registry_full forever. Cmd.imageUnregister(id) routes to fx.unregisterImage — the image-family verb naming (imageLoad/imageCancel precedent), synchronous registry surgery like registration itself: no result Msg (releasing a slot cannot fail the way I/O and decode can), a miss is the silent idle no-op imageCancel and audio_ctl keep (the engine's own bool answer treats a miss as an answer, not an error, and the wire has no channel to carry it). It frees only the CURRENT registration: a load in flight is untouched and its terminal still registers the pixels — pinned by e2e as the engine behaves today — so eviction under a live load is imageCancel first, then unregister.
- Wire record image_unregister [op 0x12][id f64 LE], additive within cmd_format_version 2; full tier coverage — SDK factory, emitter with the imageLoad/imageCancel NS1030 id-literal gate, conformance, wire-layout pin, host decode, and e2e proving the gallery eviction verbatim: sixteen loaded images, the next id refused registry_full, one unregister, and the freed slot accepting a 17th distinct image.
- The 0x10-0x12 image block's opcode overlap with feat/menu-bar-lifecycle's 0x10-0x11 window verbs is known cross-branch and handled by the merge-order reconcile; this commit extends this branch's contiguous image block by one.

* Survive two recorders writing the same session blob into one store

- DirBlobStore.write staged every blob through <hash>.partial — one shared file whenever two recorders write the same blob into one blobs/ directory (the designed shared layout), so the writers truncated each other mid-write and the loser's rename failed the whole write, refusing a correct recording. Each write now stages through a writer-unique temp, <hash>.<token>.partial with the token drawn from the store's io CSPRNG (the same entropy seam the effect executor's cache installs use) — unique across stores and processes, with the hash kept in the name as debris provenance.
- A rename that still fails gets the content-addressed grace: re-running the verifying probe against the addressed name decides the outcome — an exact byte match means a concurrent writer won the install with THIS content, so the write succeeded and returns cleanly; a verified mismatch or absence stays BlobIoFailed. The loser's temp is deleted on every failure path. MemoryBlobStore is untouched: its medium is its own entries array, never a shared directory, so the race has no analogue there (documented on the type).
- Tests pin both halves where each is deterministic: the two-writer window cannot be paused mid-call through write(), so the loser's exact rename-failure state is staged at the installPartial seam (reverting the grace fails it with BlobIoFailed), the writer-unique temp name is pinned at the blobPartialPath formatter (a name without the token collides), two DirBlobStore instances over one directory both succeed with one intact blob and zero temp debris, and the existing corruption-repair and dedup coverage still holds.

* Back-pressure replay image feeds instead of dropping bytes and order

- A fed image terminal that found the completion queue full took the pending-ring fallback: releaseFetchSlot freed the journaled bytes and the loop-side delivery jumped ahead of every queued result, so a recording whose drain pass carried more than 64 results replayed with the image pixels gone and the terminal reordered around the recorded lines.
- Under replay the feed now reports EffectQueueFull with the request still parked and its bytes intact, and the replay pump drains through the same .wake dispatch the platform delivers live before feeding once more - everything rides the queue, mirroring the real worker's blocking postImage retry; the byte-free fallback (with its derived .loaded-to-.rejected honesty rewrite) remains for non-replay test feeds, the only other traffic that can reach it.
- The saturation regression test records 64 line results plus a loaded image terminal as one unbroken journal run and asserts replay delivers them in recorded order, re-registers the recorded pixels, and matches every fingerprint checkpoint.

* Retire the image slot before its terminal reaches update

- imageWorkerMain posts the terminal entry first and stores .draining on the next line, so a drain racing ahead of that store could dispatch the result while the slot still read .running - a reload of the same id from inside the handling update (the gallery-refresh idiom) then rejected as a duplicate active key
- the drain's .image arm now stores .draining right after taking the buffer, so the slot is reclaimable by the time any update code reacts to the terminal; the worker-side store stays as the pre-drain transition (cancel targeting and early thread reclaim key off it)
- the new test reconstructs the preemption window (terminal queued, slot rewound to .running - the exact state a preempted worker leaves) and proves the in-handler reload parks and completes instead of rejecting

* Reject children on the image leaf instead of dropping them

- <image ...><text>Caption</text></image> validated through and both engines built the children, but widget layout gives an image no child slots - the caption silently vanished
- all three surfaces now refuse with icon's leaf policy exactly: the validator (native check), the interpreter's build failure, and the compiled engine's compile error, sharing one image_children_message ("image is a leaf - it takes no children")
- negative cases land in the validator table and the interpreter's image-misuse suite with the message asserted symbolically; the compiled engine's rejection is a comptime fail, covered by the documented compile-error strategy (the interpreter enumerates what it rejects)

* Mirror the image leaf's required binding and raw-child checks into both engines

- the validator refused <image /> without its image={binding} (dead markup), but unvalidated or hot-reloaded markup reaching the interpreter or the compiled engine still built the leaf with the default id and silently rendered nothing - both engines now refuse with the validator's image_missing_image_message (icon's missing-name precedent)
- the engines checked image children AFTER context-menu extraction while the validator checks the raw node, so <image on-press="..."><context-menu/></image> built in both engines yet failed validation - the engine checks now read the ORIGINAL node's children, so all three surfaces agree the leaf takes no children at all
- negative cases land in the interpreter's image-misuse suite and the validator table with messages asserted symbolically; the compiled engine's rejections are comptime fails, covered by the documented compile-error strategy (the interpreter enumerates what it rejects)

* Count undelivered draining image slots as occupied in loadImage

- A reload landing between an image worker's terminal post (slot already .draining) and the drain was accepted live but rejected under replay, where the first request is still a parked .running fake until its recorded terminal feeds - the second terminal then had no pending request to match.
- Delivery ends the occupied window: the drain takes the slot's buffer before the terminal Msg reaches update, so a non-null fetch_buffer under .draining is exactly "undelivered" and the reload-from-own-terminal handler idiom still parks as a fresh load.
- Tests: an unrelated dispatch reloading inside the window rejects (and parks again after delivery), plus a record-to-replay session proving both sides reject identically.

* Make the dynamic expectedBytes bound exclusive at 2^53

- The bridge accepted a dynamic expectedBytes of 2^53 while the emitter's literal gate (Number.isSafeInteger) rejects it: 2^53 + 1 aliases 2^53 on the f64 wire, so no one honest count exists - an installed 2^53 becomes a verification size every real download misses, silently re-fetching on launch.
- 2^53 now maps to "unknown size" (0) with the fractionals; 2^53 - 1, the last exactly-carried count, still installs verbatim. The image id checks were already exclusive.
- Boundary e2e: model-owned 2^53 - 1 parks with the count intact, 2^53 (and its 2^53 + 1 alias) parks with expected_bytes 0.

* Teach the eval harness cmdview decoder the image records

- image_load 0x12, image_cancel 0x13, image_unregister 0x14 decode in the harness-lib Cmd iterator, matching ts_core_host.zig's decoder as renumbered past main's window verbs
- Pinned-bytes tests cover the load record's full layout and the two one-field records advancing exactly their own length in a batch

* Regenerate the docs component-preview wasm module after the reconcile rebase

- The wasm build now carries main's window-verb wire records alongside the renumbered image records, so the checked-in artifact drifts and is refreshed per the branch convention

* Refuse replay of .loaded image records that claim a zero-length blob

- A journaled .loaded image record always carries source bytes (the drain journals .loaded only after those exact bytes decoded and registered, and empty bytes cannot decode), so a zero-length blob is journal damage; replay now refuses it with the new ReplayDamagedRecord class instead of skipping blob resolution and delivering a pixel-less loaded.
- State the record-time invariant at the drain's journal site and the replay gate, and add a session test that hand-zeros a recorded journal's image_blob_len (blob store present and intact) and asserts the exact refusal.

* Document image status 0 as the no-exchange signal for cache hits and local paths

- A cache hit delivers .loaded with status 0 on purpose: status is the HTTP status only for url loads that performed an exchange, and fabricating the origin's 200 for a non-network event would claim an exchange that never happened - 0 lets apps tell a cached load from a network one.
- Say so everywhere the field is described: EffectImageResult.status and the journal record in effects.zig, the cache-hit site itself, core.ts's ImageEventArm, rt.zig's wire prose, the ts-core SKILL, and the dynamic-images docs page.
- Extend the offline-cache-hit test: the network load reports the loopback fixture's real 200, the offline re-load of the same id/url reports .loaded with status 0.

* Bump the session-journal format to v7 for the image record layout

- main already ships v6 (the hidden window-frame flag), and this branch appends the .image effect-result kind plus blob-address fields to every effect record - a layout change, so the version gate moves to v7 and a main-written v6 journal refuses as version skew at the preamble instead of misreading as corruption
- the doc-comment history splits v6 (hidden state) from v7 (image records); the changelog fragment and the dynamic-images docs page state the v7 break the same way
- the version-skew test now pins a concrete v6-preamble journal to JournalUnsupportedVersion and its re-record teaching, before any record layout is consulted

* Hold same-pass effect completions to the next wake so journal event boundaries stay causal

- one drain pass now consumes only completions that existed when it began (Effects.DrainBoundary; UiApp.drainEffects and the ts-core host drain use the bounded takeMsgWithin): a load an update handler starts mid-pass that finishes before the pass ends delivers - and journals - under the NEXT wake's event record, so replay's file-order feed always finds the parked request instead of turning the chained fast load into a false ReplayEffectDivergence
- the journal header now states the causal premise its feed-then-dispatch ordering invariant rests on; this is the recorder-side guarantee, chosen over a replay-side retry because the documented contract promises file order IS a valid feed order for every effect family, not just images
- the fake executor gains the instant-load convention (fake_instant_image_bytes) that reproduces the race deterministically, and the chained same-wake session test records, replays fingerprint-identical, and pins the effect/wake interleaving in the journal

* Report registry allocation failure as alloc_failed, never decode_failed

- EffectImageOutcome gains alloc_failed and classifyImageRegisterError maps error.OutOfMemory (the registry slot's lazy pixel-buffer allocation) to it: resource exhaustion at registration is its own honest class, named for the failing stage like io_failed/decode_failed - valid bytes are never reported corrupt
- the fifteen-member contract ripples everywhere it lives: the SDK ImageState alias and ImageEventArm docs, the emitter's IMAGE_STATES build gate, rt.zig's wire docs, the conformance/effects/runfidelity and e2e fixtures, ts-core SKILL.md, the docs outcome table, and the changelog fragment
- a FailingAllocator at the registry seam delivers alloc_failed and the SAME bytes load once memory heals; the fed-classes coverage carries the new member and the TS e2e sees the fifteenth state by name through the host's name-matched mapping

* Make undelivered-terminal occupancy key-wide across effect families

- The effect families share one key space, but only loadImage counted the posted-but-undelivered .draining window as occupied, and only for image slots: a key whose fetch/spawn/file/clipboard terminal was queued but not yet drained was accepted live by any other family (and by its own), while under session replay the same request is still a parked .running fake until its journaled terminal feeds - the accepted duplicate then diverges the Msg stream.
- keyOccupiedUntilDelivery gates every keyed admission (spawn, fetch, file read/write, clipboard, loadImage, and hostRequest's cross-kind collision arm) on findUndeliveredTerminalSlot, whose per-family "terminal still pending" marker is derived from how each drain retires the slot: the fetch_buffer handoff (fetch/file/clipboard/host/image), the collect_buffer handoff (collect spawns), and a new exit_undelivered flag for .lines spawns, whose slots previously retired to idle before their exit drained; the drain clears the flag before the exit Msg reaches update, so the respawn-from-own-exit idiom still parks fresh.
- Tests: live duplicate admissions inside the window reject and delivery frees the key for fetch->fetch, spawn->spawn (plus the respawn idiom), fetch->loadImage, and image->spawn; two record-to-replay sessions prove the cross-family rejections regenerate identically on both sides.

* Hold an image id through its staged start-failure rejection until it drains

- loadImage's executor-start failures (the staged source buffer's allocation, ensureIo, Thread.spawn) stage NON-regenerating rejections - executor truth that journals and FEEDS under session replay, where the same request allocates its own buffer and stays parked until the recorded delivery position. Live, the id was free the moment the rejection was staged, so a reload inside that window was accepted live and rejected under replay.
- Admission (keyOccupiedUntilDelivery, plus hostRequest's collision arm) now also consults the staged pending-image FIFO for non-regenerating entries: those hold their id until the drain delivers them, which covers both start-failure paths and the fake cancel's staged terminal with one rule - occupancy mirrors exactly the records replay feeds. Regenerating validation refusals deliberately do NOT occupy: replay re-runs the same loop-side checks at the same dispatch, so both sides refuse identically with the key never held.
- Tests: a surgical one-shot allocator fails exactly the staged source buffer, then a same-id reload inside the window rejects and delivery frees the id (live), and a record-to-replay session proves the start-failure terminal feeds while the in-window reload's refusal regenerates - model and fingerprint identical on both sides.

* Feed replayed image terminals verbatim past a stale cancel mark

- A fed image terminal is executor truth: check entry.image_fed before the cancelled rewrite so replay's differently-timed fake-slot mark cannot rewrite the journaled outcome
- Repro: a cancel that loses to a slotless staged start-failure rejection live (no-op, .rejected journals) used to resurface as .cancelled at replay, diverging model and fingerprint
- Session test records that window and asserts the fed terminal stays .rejected with identical model and fingerprint

* Publish spawn exit occupancy before the exit is consumable

- Set the lines-mode undelivered-exit marker before postExit: the queue mutex orders the mark before any consume, so the drain-side clear can never be overtaken by a late worker-side set that would strand the slot in .draining and hold the key forever
- Keep the .draining release store after the post: reclaimSlots joins draining workers, and a worker parked in postExit's full-queue retry must stay unjoinable until the loop can drain
- Cover the drain-side clear directly and prove slot handback with more delivered exits than the pool has slots

* Accept exact-fit blob reads in DirBlobStore

- A read that filled the buffer conflated "blob is exactly buffer-sized" with "blob is larger", rejecting valid exact-fit reads as BlobOverBudget before the hash check - callers size the buffer from the journal record's byte length, so the exact fit is the designed case
- Disambiguate with a one-byte probe past the buffer's end: a byte there is genuinely over budget, EOF falls through to the hash check, and a probe failure maps to BlobIoFailed like the main read
- Pin under/exact/over parity between DirBlobStore and MemoryBlobStore, and that an exact-fit read of damaged bytes still reports BlobCorrupt

* Refuse journaled image dimensions the recorder can never produce

- Replay's damage gate now checks decoded dims: .loaded must carry nonzero width/height whose RGBA8 bytes fit max_registered_canvas_image_pixel_bytes (overflow-checked product), every other outcome must carry 0x0
- A hand-edited maxInt width previously fed verbatim into the app's Msg and, on the TS core host, into an @intCast to i64-classed arm fields - a safety panic
- status needs no twin gate (u16 at the journal codec, completion entry, and EffectImageResult, so every downstream cast holds by type); four new session tests hand-damage journals and assert error.ReplayDamagedRecord

* Hold EventKind state unions to their exact members in stock tsc

- ImageEventKind and AudioEventKind now check state BOTH directions: the & constraint held the arm's states to the SDK union, and a tuple-wrapped [ImageState] extends [M["state"]] refuses a narrower union as never instead of letting it type-check
- The docs promise exactly the fifteen ImageState / six AudioState members; a narrower arm silently dropped states the host emits until the transpiler's own shape check caught it
- Four conformance tests pin the rule in tsc itself: narrower unions fail, the exact-member arms keep passing and transpiling

* Journal fire-and-forget image start failures so replay retires the parked fake

- Stage non-regenerating image terminals even with no on_result: only the Msg is handler-gated, so the staged entry still occupies the id (stagedImageOccupiesKey) and journals at drain, and replay's parked fake request retires when the record feeds instead of holding the key and a slot forever.
- Journal before the handler gate in the pending-image, clipboard, and host drain arms: executor-truth terminals record regardless of the route; regenerating validation refusals stay handler-gated because replay re-runs the same refusal at the same dispatch.
- Pin both sides deterministically: a handlerless start failure holds the id until its terminal drains, and a recorded session feeds the handlerless terminal on replay with an identical Msg stream and fingerprint.

* Retire spawn, fetch, and file slots before their terminal handlers run

- Store .draining consumer-side in the exit, response, and file drain arms, mirroring the image arm: workers store it only after posting the terminal, so a drain riding another wake could dispatch the Msg while the slot still read .running and a same-key retry from the handler (the poll/reload/restart idiom) would reject as a duplicate - nondeterministically, and unlike replay, whose fed fakes retire before feeding.
- Keep the exit arm's retire generation-gated beside the undelivered-marker clear, with the happens-before argument documented at the site; the worker-side store stays after the post (cancel targeting, joinability, full-queue retry) and the re-store is idempotent. Clipboard and host need no consumer store: their terminals stage on the loop thread with the store sequenced before the enqueue, and host replaces rather than rejects.
- Pin the idiom per family: fetch and file handler retries park fresh (unit and record/replay session coverage), and the spawn respawn-from-own-exit test runs a second full cycle.
2026-07-19 23:32:52 -05:00
Chris Tate 6f526a9c1e Menu-bar app lifecycle: close-to-hide windows and app show/quit verbs (#155)
* Track alive-but-hidden window state through frame events and the journal

- WindowState/WindowInfo gain a hidden flag: the window is alive and open but off the glass (the close_policy .hide shape), distinct from minimized and from closed
- window_frame_changed events carry it into the runtime window table, and the session journal round-trips it (format v6)

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Add the close_policy window declaration: .quit stays the default, .hide is the menu-bar shape

- Per-window close_policy threads app.zon (top-level and shell windows) through WindowOptions to the hosts, .quit by default so every existing app is unchanged
- macOS: windowShouldClose orders a .hide window out instead of closing (both hosts), the Dock reopen re-shows policy-hidden windows, runtime closes bypass the policy; Windows: WM_CLOSE hides via SW_HIDE and the tray stays the re-show affordance
- Linux GTK has no status item to bring a hidden window back, so .hide refuses loudly: a comptime teaching for manifest windows and error.UnsupportedWindowClosePolicy at runtime create, gated by the new window_hide_on_close platform feature

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Add the showWindow and quitApp effects verbs on the window-action seam

- fx.showWindow(label) un-hides and activates a window (deminiaturize + order front on macOS, SW_RESTORE + foreground on Windows, present on GTK) and fx.quitApp() terminates through the same shutdown event path a last-window close takes
- Both follow the existing verbs' shape exactly: mirror counts in windowActionState, fire-and-forget with no journaling of their own, honest no-ops for unknown labels, mirror-only under the fake executor
- Null platform models both with pinned seams (show counts, quit requests); a recorded hide/reopen/quit session replays its window states through the journal

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Mirror the window verbs into the TypeScript Cmd vocabulary

- Cmd.showWindow(label) and Cmd.quitApp() ride new additive wire records (window_show 0x10, quit_app 0x11; cmd_format_version 3) and decode onto the same fx.showWindow/fx.quitApp verbs the Zig tier calls
- The emitter lowers both with the string-literal label discipline effect keys use (window labels are declarations; a dynamic label is taught)
- Conformance pins the emitted shapes and the dynamic-label teaching; the effects gate asserts the exact wire bytes; the host-bridge suite pins the mirror counts

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Add the menu-bar example: the tray-player lifecycle in one small app

- Zero-config canvas app whose main window declares close_policy "hide"; the status item's Open/Quit rows map to fx.showWindow/fx.quitApp through the ordinary on_command path, with a model-driven title showing transport state while hidden
- Tests drive the real tray-selection-to-verb loop and pin the declaration, the mirror counts, and the exactly-once stop hook
- A purpose-built example keeps the pattern reference minimal instead of repurposing soundboard's close behavior

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Document the menu-bar lifecycle and make the tray quit example real

- The tray page's "app.quit" example now handles the command (runtime.quitApp) and a lifecycle recipe walks the whole pattern: close hides, tray Open/Quit drive showWindow/quitApp, the Dock reopen re-shows
- close_policy documented on the windows and app.zon pages, the new verbs on the runtime and TypeScript pages, and the platform matrix states the per-platform truth including Linux's loud refusal
- Changelog fragment for the feature (defaults unchanged, no breaks)

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Thread close_policy through the generated runner template

- The scaffolded runner's manifestWindow now mirrors the SDK runner: an app.zon window close_policy rides WindowOptions (.quit stays the default), instead of being silently dropped so a "hide" declaration kept quit-on-close
- The "hide" declaration is refused at comptime on linux with the same teaching as the SDK runner - nothing could bring the hidden window back
- The template test pins the field and the linux refusal, so dropping either fails the writeDefaultApp test again

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Expose alive-but-hidden in the JS bridge's window JSON

- writeWindowJsonToWriter emits "hidden" beside open/focused: a close_policy = "hide" window read open:true/focused:false over the bridge, indistinguishable from a visible unfocused one
- NativeSdkWindowInfo carries the field, the window bridge test drives a policy-hidden window through window.list, and the windows doc names the bridge JSON among the surfaces where hidden shows up
- The webview JSON twin stays untouched: policy-hidden is window state, webviews have none

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Stop listing hidden as persisted WindowState in the windows doc

- The type table said hidden persists while the close-policy prose and the implementation say the opposite; the row now marks it deliberately session-transient (never persisted, every launch starts shown) so nobody re-adds it to the store

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Thread shell-startup window state through the generated runner template

- The scaffold runner ignored every `.shell.windows` startup declaration that is host state fixed at create time: mirror the SDK runner's manifestShellStartupTitlebar/Resizable/ShowMode/MinSize/ClosePolicy threading (including the Linux comptime refusal of close_policy "hide") into the windows.len == 0 branch, so a scene-first app.zon no longer silently keeps quit-on-close, standard chrome, immediate show, and no min-size floor.
- Thread titlebar and min_width/min_height through the top-level manifestWindow too — the same silent-drop class close_policy shipped with once.
- Pin the shell-startup threading and the new window fields in the template test, so dropping any of them fails the scaffold contract loudly.

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Route focus of a policy-hidden window through the show verb

- focusWindow on a window hidden by close_policy .hide left state inconsistent: the hosts' focus paths order a window forward without touching their policy-hidden bookkeeping (macOS reported hidden=true on a visible window; Windows never shows an SW_HIDE'd window at all), so the runtime now drives showWindow first — the seam that clears the hosts' policy-hidden sets, emits consistent state, and rolls back on platform failure — before focusing.
- One rule at the runtime seam covers every focus ingress: the app verb and the JS bridge's native-sdk.window.focus both resolve hidden-then-focus the same way.
- Tests pin the routing through the null platform's show count on both paths: the runtime seam (hidden clears, focus lands, no second show for a visible window) and the bridge twin (the focus response never reports a focused window that is still hidden).

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Queue the quit verb's stop so app_shutdown emits after the requesting dispatch

- fx.quitApp() lands mid dispatch (the tray/menu command's update returned it), and the macOS and GTK hosts emitted SHUTDOWN synchronously from the stop call: the shutdown dispatch nested inside the command's, the session recorder's nested commit + finish() sealed the journal, the outer commit no-oped, and the journal lost the very command (and model mutation) that quit the app — replay diverged. Both hosts (AppKit, macOS CEF, GTK) now queue the identical emitShutdown + stop onto their loop's next turn (dispatch_async onto the main queue / g_idle_add, the same seams the wake paths ride); Windows already posted. The pre-run-loop inline emit stays on macOS so a failed START handler's synchronous shutdown request keeps working, and stop's posted wake event still unwinds a loop whose quit was the last thing that ever happened.
- The null platform's quit seam models the queued shape: quitApp records the request and takeQueuedQuit() hands the test the deferred app_shutdown to dispatch as its own loop turn, exactly once — the userCloseWindow pattern.
- A recorded-session test drives the whole verb chain (menu command -> update -> fx.quitApp -> queued host echo) with a recorder attached and pins the contract: the journal is not sealed by the quit dispatch alone, contains BOTH the command and app_shutdown in order, and replays fingerprint-identical; a build check step pins the queued stop in all four host sources so reverting to the synchronous emit fails the suite.

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Occlude policy-hidden windows in the Windows pacing and spectrum gates

- Both occlusion answers in the WebView2 host were IsIconic-keyed only, so a window hidden by close_policy .hide (the menu-bar shape — hidden for days behind its tray icon) kept its full-rate canvas frame loop and its 25 Hz spectrum emissions running for glass nobody can see: pure background CPU burn. gpuSurfaceOccludedPacingActive now answers occluded for a policy-hidden window's surfaces (the ~1 Hz heartbeat, same first-present and input exemptions), and audioAnyWindowReachesGlass stops counting policy-hidden windows as visible, so an app with every window minimized or hidden goes honestly quiet.
- Re-show restores full cadence without dropping a beat: SW_SHOW on a same-size window dispatches no WM_SIZE (the minimize path's restore re-arm), so the show verb itself supersedes a parked heartbeat emission at the frame-grid delay after clearing policy_hidden.
- This is C++ host logic the Zig suite cannot execute: the contract is pinned by the test-gpu-occluded-frame-heartbeat file-contains step (both policy_hidden gates plus the re-show re-arm), verified by cross-target -fsyntax-only compiles of both WebView2 configurations, and written into the windows and capabilities docs; the pacing comments now name both occlusion facts honestly.

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Refuse a .hide main window at Linux platform init instead of silently quitting on close

- LinuxPlatform.initWithOptions creates the MAIN window before any runtime exists, so the runtime's create-time .hide gate (window_storage's error.UnsupportedWindowClosePolicy) and the generated runner's comptime refusal never covered a direct-SDK or custom-runner caller: a .hide declaration was silently dropped and the user got quit-on-close. The init now refuses with the same error and the same teaching text the comptime check prints — one message, all seams — through an extern-free gate that is unit-tested on every host, with a file-contains step pinning both its wiring into initWithOptions and the cross-seam message lockstep.
- Three-platform pre-created-window audit: macOS threads the declared policy into the host right after create (applyWindowClosePolicy -> windowShouldClose) and Windows does the same (applyWindowClosePolicy -> the WM_CLOSE hide hook), so neither drops it; Linux was the only silent seam and now refuses.

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Park a pre-run quit verb and drain it after the boot dispatch returns

- A quit requested before [NSApp run] exists (App.start's update, a boot command during the synchronous first canvas frame) rode the failed-START inline emit on the macOS hosts, nesting app_shutdown inside the very dispatch that requested it — the session recorder sealed the journal before the boot turn committed and replay refused the recording.
- The seams are now split: native_sdk_appkit_request_stop (the quit verb, AppKit and CEF alike) parks pre-run quits in pendingPreRunStop and runWithCallback drains them at top level after the START dispatch and again after the remaining pre-run emits, while native_sdk_appkit_stop keeps the byte-for-byte inline pre-run emit for the host-side failed-START request.
- The recorded-quit session tier gains the quit-from-boot case (the journal carries the start event and app_shutdown in order, and replays fingerprint-identical through the modeled park-then-drain seam), and test-quit-pre-run-pending pins the host wiring so an inline pre-run revert fails the build.

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Track, coalesce, and destroy-remove the GTK quit-stop idle source

- Every native_sdk_gtk_stop queued a bare untracked g_idle_add: a second stop requested while the first executed (the shutdown handler's error path can make one) left an idle source holding a freed host after the loop quit.
- The pending source id now lives on the host: stop coalesces while a turn is queued and skips entirely once did_shutdown is set, the idle clears the id as its first act, and native_sdk_gtk_destroy removes a still-pending turn before the host is freed.
- test-gtk-stop-idle-tracked pins the tracking, coalescing, and removal textually — this suite cannot run the GTK loop on macOS; the linux-dev-smoke CI job exercises the real path at runtime.

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Clear hidden with open when the app closes a policy-hidden window

- closeWindow flipped open and focused optimistically but left hidden alone, so an app-driven close of a policy-hidden window parked {open=false, hidden=true} in the runtime table — visible through the JS bridge, which exposes hidden; a closed window is not hidden, it is gone.
- hidden now clears beside open/focused and restores on platform failure through the same rollback; the null platform gains a fail_next_close_window injection seam (error.CloseFailed, consumed on use) so rollback paths are drivable.
- The new ui_app_window test walks the whole arc: .hide close hides, a refused platform close rolls every flag back (still open, still hidden), and the successful close reads open=false and hidden=false — dropping the hidden clear fails it.

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Clear policy-hidden membership on the CEF app-close exit that skips windowWillClose

- The CEF host's app-driven close of a browser-bearing window exits through orderOut + an open=false emit without ever reaching windowWillClose, so the window stayed in policyHiddenWindows: the frame encoder derives hidden from set membership, so app-closing a policy-hidden window emitted {open=false, hidden=true} (overwriting the runtime's optimistic hidden clear), and a later Dock reopen — which re-shows every set member — could resurrect the closed window.
- The branch now leaves the set before its emit; after this every close exit removes and only hideWindowWithId (the user close of a live window) adds, so set membership implies a live, policy-hidden window and the reopen handler cannot resurrect. The AppKit host needs no change: all of its close paths run windowWillClose, whose cleanup already precedes the emit.
- A file-contains gate pins both hosts' cleanup-before-emit ordering, including the CEF orderOut branch's own cleanup.

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Mirror the close's hidden clear in the null platform and gate its reopen on liveness

- NullPlatform.closeWindow cleared open/focused but left hidden standing, and userReopenApp re-showed on the hidden flag alone — so the modeled host resurrected an app-closed policy-hidden window (hidden=false, focused=true, a frame event), the same hole the real macOS hosts' set hygiene closes.
- The close mirror now clears hidden with open (matching the runtime table's own close flip), and userReopenApp skips windows that are not open — a closed window keeps its slot in the mirror, so hidden alone is not liveness.
- The new test drives the whole arc through the runtime: close a policy-hidden window, reopen emits NO event and the runtime table is untouched, then force a stale hidden flag onto the closed slot to prove the liveness gate holds on its own — reverting either fix fails it, and the existing reopen re-shows test still passes for genuinely hidden-open windows.

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Move runtime focus with a successful showWindow: the show verb activates

- The service contract is show AND activate — every host's show path makes the window key — but the runtime's showWindow flipped only hidden, so listWindows and the JS bridge reported the shown window unfocused while it stood frontmost on the glass.
- After the platform accepts the show, focus now moves through the same setFocusedIndex seam focusWindow uses (the dethroned window's key-loss consequence fires); a refused show still rolls hidden back and moves no focus, drivable through the null platform's new fail_next_show_window injection (error.ShowFailed, consumed on use).
- Tests pin both halves: show a hidden window and the runtime table reads it focused with every other window unfocused (the bridge's window.list JSON carries the same truth), the failure injection leaves hidden and focus untouched, and dropping the focus update fails both tests.

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Teach the evals Cmd decoder the v3 window_show and quit_app records

- cmdview.zig self-identified as cmd_format_version 2 and panicked on op bytes past 0x0F, so any graded core returning Cmd.showWindow or Cmd.quitApp (alone or in a batch) crashed the ts-track behavioral harness mid-eval, reading as the graded app's failure.
- It now decodes window_show 0x10 ([op][label_len][label]) and quit_app 0x11 (bare op), mirroring the runtime decoder in src/runtime/ts_core_host.zig; a repo sweep found no other Cmd opcode table outside packages/core and src/runtime, both already v3.
- The decoder gains its first tests (round-trip of both records against the encoders' pinned bytes, plus batch adjacency), wired into `zig build test` as test-evals-cmdview since eval-time is too late to learn the decoder lags the format. The 0x10/0x11 overlap with feat/dynamic-images remains merge-order-reconciled.

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Execute the CEF show and minimize verbs synchronously on the main thread

- native_sdk_appkit_show_window queued its work with dispatch_async and returned 1 immediately, while the close verb runs synchronously (direct on main, dispatch_sync hop otherwise): the runtime's post-success focus flip ran while the window was still in policyHiddenWindows, so frame emits in the gap carried {focused:true, hidden:true}, and a show-then-close in one dispatch re-ordered — the queued show landed AFTER the synchronous close and put a retained (ordered-out) closed window back on the glass. showWindowWithId: now owns closeWindowWithId:'s exact on-main/off-main split, with the window lookup inside the block so a window closed before an off-main hop lands is a no-op.
- The minimize sibling had the same queued shape and the same close race (a queued miniaturize captured past a synchronous close genies an app-closed window into the Dock); it moves into a miniaturizeWindowWithId: with the same discipline. Minimize mirrors no runtime window state, so it emits no frame event — the ordering race was its only exposure.
- The AppKit host's show and minimize were already synchronous direct calls; a file-contains gate pins the CEF block shapes, the shims' call-then-return-1 ordering, and the AppKit symmetry, so reverting any verb to a queued dispatch fails the suite.

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Gate the runtime window verbs on slot liveness before the platform call

- Runtime.showWindow resolved the id and called the platform: a closed window keeps its table slot (ids and labels release lazily, at the next create), so a retained dead slot was accepted — the null platform reported {open:false, focused:true} and the CEF host, which retains browser-bearing windows past their close, would visibly re-order one onto the glass. Dead slots now answer error.WindowNotFound (the runtime's one answer for not-open slots) before any platform call, gating the resurrection race independent of host timing.
- focusWindow had the same hole (close clears hidden with open, so a dead slot skipped the hidden-routing and reached the platform's focus verb directly) and minimizeWindow too (the CEF host would genie the retained closed window into the Dock); both get the identical gate.
- The new test closes a window and drives all three verbs: each answers WindowNotFound, the null recorder counts stay 0, and the runtime table and platform mirror stay closed/unfocused/un-hidden — dropping the showWindow gate fails it (verified).

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Expose the full PlatformFeature cohort to the JS bridge and typings

- Backfill camelCase aliases in platformFeatureFromString for the seven members past gpuSurfaces: gpuSurfaceScrollDrivers, contextMenus, viewSurfaceAdoption, audioPlayback, audioStreaming, audioSpectrum, windowHideOnClose
- Extend NativeSdkPlatformFeature with both spellings of each and sync the builtin-commands feature enumeration
- Pin both window_hide_on_close spellings to the null platform seam (true by default, false when flipped) plus the audioSpectrum alias in the bridge test

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Teach check-runtime-types the PlatformFeature union and alias table

- Derive every expected union member (snake plus camel form) from the PlatformFeature enum in src/platform/types.zig and fail naming any missing or extra name in NativeSdkPlatformFeature
- Parse platformFeatureFromString's manual alias entries and fail on missing, extra, or mistargeted camelCase aliases
- Accept a newline after = when locating exported type unions, matching the multiline union layout

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Correct the quit_app_fn contract to the queued shutdown emit

- The doc comment mandated a synchronous app_shutdown emit — the exact shape that nests the shutdown inside the requesting dispatch and seals the recorder's journal before the triggering command commits; every shipped host defers, so the written contract now does too.
- States the queued shape (emit on the NEXT loop turn, after the requesting dispatch returns), the pre-run park-and-drain case, the exactly-once app.stop guarantee, and names the null platform's quit_pending/takeQueuedQuit seam as the reference model.

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Bring every narrative command-set surface to v3 with the window verbs

- Four surfaces still said v2 after the window verbs landed: the rt.zig format heading, sdk/core.ts's command-set header, ts_core_host's command-walk doc, and the ts-core skill's command-set line — each now says v3 (the opcode tables and version-history prose beneath them were already v3-complete and unchanged).
- sdk/core.ts's header enumeration and the ts-core skill gain the window-verb pair in the set's voice: Cmd.showWindow(label) (un-hide + activate the declared label, the tray Open consequence) and Cmd.quitApp() (the graceful terminate through the last-window-close shutdown path).
- The typescript docs page already carried both verbs; version-cohort test names and the v1/v2-additivity history in rt.zig name past versions correctly and stay as they are.

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Serialize the JS window-close response from the post-close table

- closeWindowFromJson answered from a pre-close snapshot with open and focused hand-cleared, so a policy-hidden window's close told JS {open:false, hidden:true}; the response now reads the table slot after closeWindow, which owns the full set of flags a close clears
- The slot index stays valid across the close: closed windows keep their table slot, and slots only compact inside window create when a dead id or label is re-used
- The bridge test now closes a policy-hidden palette window and pins "hidden":false in the response

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Gate secondary startup windows on the window_hide_on_close feature

- loadStartupWindows creates index > 0 windows through the platform services directly, bypassing the runtime create path's close-policy support check: a .hide declaration on a secondary startup window was silently accepted on hosts that cannot re-show a closed window, so the user's close really closed it
- The same loud error.UnsupportedWindowClosePolicy gate now rides that direct create; the host-created main window keeps its own init-time refusal
- New test drives the GTK-shaped null platform (window_hide_on_close=false) through a .quit main plus .hide secondary startup pair and pins the refusal at load; the supported host loads both windows

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Require the tray capability for .hide on Windows and downgrade tray-less hides

- close_policy "hide" on windows now requires the "tray" capability at every validation seam: the SDK runner and generated-template comptime checks refuse the declaration (SW_HIDE removes the taskbar entry and windows has no dock-reopen path, so the status item is the only re-show affordance; macos stays exempt via the Dock), and the runtime create gates inherit the refusal because the Windows host answers window_hide_on_close only when the manifest declares a tray (new AppInfo.declares_tray, threaded like has_web_content), with a Linux-style init refusal for the host-created main window
- Runtime backstop for a tray that validation accepted but creation lost: the WM_CLOSE hide hook consults the host's live tray_active state and downgrades a tray-less .hide close to a real close with a loud stderr line - a visible close beats a running, invisible, unreachable app
- Tests: pure-fn coverage for the conditional supports answer and the init refusal, generated-template text pins for the windows teaching and the declares_tray threading, and a textual pin holding the WM_CLOSE tray consult ahead of the SW_HIDE with the downgrade line present (the C++ branch itself is beyond the suite; both WebView2 configurations cross-compile clean to x86_64-windows-gnu)

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Count UTF-8 bytes in the emitter's literal length gates

- Cmd.showWindow's label gate (and every sibling string-literal gate: host/request names, cancel/delay/request/spawn/audioPlay/audio-verb/Sub.timer keys, and the asciiBytes literal bound) counted UTF-16 code units while teaching bytes, so a 200-character CJK label passed the gate and arrived as 600 UTF-8 bytes against the wire's 255-byte length prefix; every user-text gate now measures Buffer.byteLength (Cmd.fetch header names/values stay on .length - they are ASCII-enforced right above, where the counts agree)
- rt.cmdWindowShow's std.debug.assert compiles out of ReleaseFast, where the length-byte @intCast would then truncate and corrupt the wire record silently - it is now an explicit panic in every build mode, the loud backstop behind the emitter's build-time teaching
- Conformance pins both sides of the byte bound (100 CJK chars = 300 bytes teaches; an 84-char/250-byte multibyte label emits and compiles) and the effects harness pins the wire record for a CJK label carrying its 9-BYTE length prefix

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Document the Windows tray prerequisite for close_policy hide

- The windows, app.zon, and tray pages now state the tray-capability requirement and the runtime downgrade the round-10 guard introduced.

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

---------

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>
2026-07-19 10:37:55 -05:00
Chris Tate c09a12669e Document registered fonts and prove the CJK path on Windows CI (#144)
* Document registering fonts for scripts beyond bundled coverage

- New Fonts page: the Options.fonts / registerCanvasFont path with a CJK anchor example, every registration error and its teaching, ownership until Runtime.deinit, how typography tokens resolve a registered face, and the honest platform + TypeScript-tier status
- Cross-link the tofu guard's teaching in Native UI and the theming typography group to the new page

* Teach the register-a-font path where the tofu guard fires

- font_coverage_message, the ui builder's Debug diagnostic, the markup CLI usage text, and the native-ui skill now name registering a covering font (UiApp Options.fonts) alongside vector icons and plain words
- Platform Support gains a text-and-registered-fonts matrix row: first-class on the three desktops per the code and suite, stated unverified on mobile where no test registers a font and host measurement has no registered-font seam

* Prove registered CJK faces render Chinese text on Windows in CI

- Commit a 2.2 KB OFL-licensed fixture (Noto Sans SC instanced at wght=400, subsetted to 你好世界 plus notdef, license beside the file) and a receipt test: a scaffold-shaped UiApp registers it through Options.fonts, every ideograph resolves to its own glyph with nonzero rasterized ink, and the reference screenshot differs from the bundled-face tofu twin
- Add zig build test-canvas-fonts (the font-registry suite filtered from the desktop tests) and run it natively on the Windows CI lane; the same tests keep running in zig build test via the canvas-frame shard
- Note the Windows-native receipt in the platform matrix footnote

* Scope the fonts page's claims to what each tier delivers

- The example now teaches the mono_font_id slot instead of claiming whole-app coverage, and the lockstep guarantee names the desktop paths and defers mobile to the platform notes.

* Pin the Chinese receipt against the registered face's own tofu

- The end-to-end receipt now renders a third screenshot: the same registered face showing four ideographs the fixture deliberately does not map (each pinned with glyphIndex() == 0 first), and asserts the real-CJK shot differs from that self-calibrating control.
- A renderer that wrongly resolved every ideograph to the registered face's notdef glyph and inked its outline would differ from the bundled-face shot and pass nonblank, yet match this control byte-for-byte; sabotage-verified by forcing the uncovered string into both slots and watching the new assertion fail.
- The scaffold model gains a show_uncovered_cjk toggle so the control renders through the identical dispatch-rebuild-screenshot pipeline as the receipt itself.

* Name the one render-time refusal and add the release fragment

- The fonts page's registration-time claim now carries its one honest exception: point-matched composite placement is the per-glyph refusal maxp cannot gate, surfacing at first raster as the deterministic block fallback — never a crash, never a silent skip — and no measured production face uses it.
- New changelog.d fragment covers the branch's user-visible changes: the tofu-guard teachings now name font registration first, the fonts docs page, and the Windows-native CJK receipt lane.
- src/runtime/testdata/fonts/OFL.txt drops one trailing space (line 21) so git diff --check runs clean; the license wording stays verbatim.

* Evict a re-registered font id's cached NSFonts in the AppKit host

- The per-(id, size) NSFont cache is per-process while font-id permanence is per-runtime, so an embedder destroying a runtime and registering a different face under the same id measured and drew the first face from stale cache entries.
- native_sdk_appkit_register_font now purges the id's "id/"-prefixed cache entries under the descriptor table's @synchronized guard before installing the new descriptor, and the comment claiming cached NSFonts never go stale states the real lifetime instead.
- No SDK test tier links appkit_host.m (only managed app builds compile it), so the eviction wiring is pinned by a file-contains check step like the other AppKit host contracts; the CEF host's register_font is a stateless accept with no cache, so nothing to evict there.

* Rebuild every installed surface when a font registers late

- noteCanvasFontsChanged forced a repaint but installed UiApps never rebuilt (the frame handler rebuilds only on install, scale change, or size change), so the repaint re-inked widget frames and baked text layouts measured before the face joined — breaking the fonts page's every-open-surface-re-measures promise.
- The registered-font count is the runtime's fonts generation (registration is permanent, no unregister): UiApp adopts it at install and both frame handlers compare it per presented frame, rebuilding ALL installed surfaces — main canvas and declared windows — through rebuildAllViews when a face joined late; rebuildEmitsTokens also treats a text-measure provider change as an emit reason so static-token apps push the font-aware provider into the stored tokens.
- Test: an app with no declared fonts installs a main canvas plus a declared window, registers the CJK fixture through the runtime seam, and one arriving frame must move BOTH surfaces' text frame widths (mixed CJK+Latin text — the Latin tail measures at Geist's sub-em advances before and the face's 1.0 em notdef after); sabotage-verified: with the rebuild call removed, exactly this test fails.

* Split the matrix's text row so mobile text is not denied

- The single "Text & registered fonts" row rendered "Not available today" for iOS/Android, false for bundled text: mobile hosts present the same reference-renderer pixels the desktop software hosts ink.
- Now two rows: "Text" is full on all five platforms, and "Registered fonts" keeps full desktop with the honest none-unverified mobile note; footnote 3 already narrates both halves, so fn={3} stays on every cell and nothing renumbers.

* Invalidate the AppKit measured-width cache when a font id re-registers

- The width NSCache in native_sdk_appkit_measure_text survived re-registration: the round-3 eviction covered only the NSFont size cache, so a new runtime reusing an id kept measuring the previous face's widths while drawing the new one.
- NSCache cannot enumerate keys, so prefix eviction is impossible there; instead registration bumps a per-id generation (stored beside the descriptor table, under the same @synchronized) and the width-cache key includes it, so the old generation's entries become unreachable and age out under the cache's own count limit — keeping NSCache's bounding and memory-pressure purging, which a dictionary conversion would forfeit for a 16384-entry text-keyed cache.
- The test-appkit-registered-font-cache-eviction pin step now covers the width-cache half (generation table, bump inside register_font, generation-carrying key), same textual tier as round 3 since no SDK test tier compiles the ObjC host.

* Scope the fonts page's late-registration and no-cascade promises to the truth

- Late registration: automatic re-measure and rebuild is UiApp wiring, so the sentence now says UiApp surfaces rebuild automatically while callers driving the runtime directly re-emit their display lists after registering.
- No-cascade: true for the engine paths (reference renderer, glyph atlas, SDK-rasterized packet text), but macOS host-drawn text follows platform shaping including system-font fallback, so uncovered codepoints there may render from a substituted family instead of notdef — the paragraph now states both halves.

* Return the host-side font registration at Runtime.deinit

- New optional platform service unregisterGpuSurfaceFont(id): Runtime.deinit calls it per registered id before freeing the bytes, so the macOS host's per-process font state (CoreText descriptor, size cache, width-cache generation) no longer outlives the runtime that registered it; the CEF host accepts statelessly and platforms without the register seam keep answering UnsupportedService.
- The null platform records the teardown call, the embed cycle/idempotence tests assert deinit makes it exactly once, and the AppKit eviction pin step now holds the ObjC removal and the deinit call site.
- Fonts page: deinit returns the host-side registration too, and host font state is one face per id per process (concurrent runtimes sharing an id see the last registration - a deliberate current constraint).

* Adopt the late-font rebuild count only after the rebuild succeeds

- rebuildForRegisteredFonts assigned fonts_built_count before rebuildAllViews, so under production's degrade policy a failed rebuild (widget budget, allocator pressure, a secondary window's emit) marked stale layouts as font-current and never retried.
- New test drives a late registration into a budget-failing rebuild, asserts the count stays unadopted, and proves the next healthy frame retries, re-measures with the registered face, and only then adopts.

* Scope the matrix footnote's text-parity claim to the reference path

- Footnote 3 opened with "identical code on every platform" while its own next sentence described macOS resolving packet text through CoreText; the opening now names the SDK's TrueType pipeline as the shared reference path (goldens, screenshots, software presents), matching the distinction the fonts page draws.
- Every other fact in the footnote is unchanged.

* Key AppKit width-cache invalidation on a process-global font token

- Replace the per-id registration generations with one monotonic process-global token counter: registration stamps the id with a fresh never-repeated token and the measured-width NSCache key carries it, so no registration can ever reach a previous life's cached widths.
- Because tokens never repeat, unregister now deletes the id's token record outright instead of retaining a bumped entry per retired id — descriptor, size-cache entries, and token record all drop, leaving zero host state per retired id, which is what the fonts page's runtime-cycle sentence promises.
- Align the file-contains pins with the token names and additionally pin the token-record removal inside native_sdk_appkit_unregister_font so the zero-retained-state property cannot silently regress.

* Return host font registrations through the owner captured at registration

- Each CanvasFontEntry now captures the platform's unregister fn and context when the registration is pushed to the host, and Runtime.deinit returns the registration through that captured pair — never live options.platform, which is publicly mutable and may name a different host (or a null seam) by teardown time, the owned_allocator identity-freeze doctrine applied to the host seam.
- Pin the capture and the deinit call site in the file-contains step, and add an embed test that registers through platform A, swaps options.platform to platform B, and asserts the deinit unregister lands on A (count 1) and never on B (count 0); reverting to the live-options read fails both the test and the pin.
- One services read in registerCanvasFont now serves both the host sync and the captured return path, so the host that hears the registration is exactly the owner the entry names.

* Guard host font unregistration with a per-registration ownership token

- registerGpuSurfaceFont now returns the host's ownership token for the registration (0 from stateless hosts); the runtime stores it in the font entry beside the captured owner and Runtime.deinit presents it at unregister, so the AppKit host removes an id's descriptor and caches only while the id's current registration still carries that token — an older runtime's deinit can no longer tear down a newer runtime's live face under a shared id, and a stale token is a no-op accept.
- The null platform gains an opt-in host-font mirror (id-keyed, last-wins, monotonic tokens — the AppKit table shape without CoreText) plus a token field on its unregister recorder; a new embed test registers one id through two runtimes and proves the newer registration survives the older runtime's deinit.
- The AppKit pin step now pins the two-argument unregister signature, the token-match guard line, the reported out-token, and the three-argument deinit call, so reverting the host to id-keyed removal fails the pins.

* Snapshot font token and face in one AppKit critical section

- native_sdk_appkit_measure_text read the registration token and resolved the font under separate lock acquisitions, so a registration landing between them paired token 0 with the new registered face and cached registered widths under the reusable token-0 key — served as stale registered widths after teardown, when token 0 means built-in resolution.
- NativeSdkRegisteredFontSnapshot now reads the id's token and resolves its registered face inside one @synchronized section (shaping stays outside the lock: a stale-but-consistent pair keys a retired token no lookup reaches); built-in resolution is split into NativeSdkBuiltInFontForFontId so token 0 only ever pairs with built-in widths.
- measure_text_advances audited: it memoizes nothing host-side (the engine caches the batch under its own measure generation), so it has no (token, face) pairing to tear; the pin step now pins the snapshot signature and its measure_text call site.

* Clear the AppKit measured-width cache when a font registration is torn down

- unregister_font purged the descriptor, token, and size cache but the measured-width NSCache was a function-local static inside measure_text, unreachable from teardown: retired-token entries could never be served again (tokens never repeat) yet stayed resident — up to 16,384 keys carrying full text strings — until memory pressure, contradicting the zero-retained-state contract.
- The cache is hoisted to a shared accessor (the same shape as the NSFont size cache) and the token-matched unregister path clears it wholesale — NSCache cannot enumerate keys, so per-id eviction is impossible; the trade is stated at the site (teardown-frequency event, live ids re-warm in one measure each), and a stale-token no-op never clears.
- The cache-eviction pin step holds the accessor, the clear, and the measure_text call site; sabotage-verified (pin fails with the clear removed). No behavioral tier can observe the ObjC cache, so textual pins are the honest coverage.

* Cover the PR's user-visible fixes and API break in a changelog fragment

- Seven review rounds accumulated user-visible surface beyond the original docs/diagnostics/CI fragment: the late-registration rebuild, the macOS host font teardown, and the registerGpuSurfaceFont token break with the new unregisterGpuSurfaceFont service.
- A new fix-tagged fragment covers them, per the one-tag-per-fragment convention; the Breaking bullet states the break deliberately with a matter-of-fact migration note for custom-platform embedders.
- Internal-only work (pins, test machinery) stays out, matching the fragments' user-facing voice.

* Recheck the registration token before caching a measured width

- measure_text shapes outside the descriptor guard, so an unregister landing mid-shape cleared the width cache and then had it repopulated by the in-flight write: a retired-token entry no lookup can serve but resident until memory pressure
- Registered-token writes now re-enter the guard and cache only while the id still carries the snapshotted token; token 0 (built-in resolution, never unregistered) keeps the unconditional write
- Pin the recheck shape in test-appkit-registered-font-cache-eviction; advances batch and packet drawing re-audited as memoizing nothing host-side that the window could touch
2026-07-19 00:13:24 -05:00
Chris Tate 4a947ef2d5 Stop embedding the 16 MiB registered-image pixel pool in every Runtime (#143)
* Allocate registered-image slot buffers on demand instead of embedding 16 MiB

- canvas_image_pixels becomes per-slot heap slices: one slot-budget allocation from the init-frozen owned_allocator at a slot's first registration, reused across re-registrations and unregister/register churn, freed only by Runtime.deinit — a runtime that never registers an image allocates nothing.
- registerCanvasImage (and the fx bindings) now surface error.OutOfMemory when a slot buffer cannot be allocated; the allocation runs before any registry mutation, so a refusal leaves the registry unchanged and the registration retryable.
- unregisterCanvasImage compacts by swapping slot buffer pointers instead of memcpying pixels; the vacated slot keeps its buffer for reuse, matching the media-surface pool's deinit-only ownership story.

* Pin the lazy image slot-buffer lifecycle with an allocation-discipline test battery

- Zero-allocations-until-first-registration, frozen-allocator ownership (a mutated options.allocator sees neither the allocs nor the frees), pointer-swap compaction with pixel verification across cycles, and the OOM refusal/retry path through a FailingAllocator.
- Changelog fragment states the on-demand footprint and the new error.OutOfMemory surface on register.
2026-07-18 14:52:15 -05:00
Chris Tate 4f22071c82 Keep the WebView stub compile silent and support GLib 2.72 (#152)
* Remove the informational pragma from both WebView stub paths

- zig renders every clang diagnostic of a failing C compile as error:, so the note masqueraded as the build-killer whenever a real error joined it
- the stub branches keep their explanatory comments; the misconfigured web-build #error stays
- build graphs (build/app.zig and the ejected template) document why the expected state is silent: runtime WebViewNotFound is the teaching channel

Co-authored-by: nextpointer <110530249+nextpointer@users.noreply.github.com>

* Compile the GTK host against GLib 2.72

- G_APPLICATION_DEFAULT_FLAGS is GLib 2.74+; the host's GTK floor is 4.10, whose own GLib floor is 2.72
- distros backporting GTK 4.10 onto a 2.72 base (Ubuntu 22.04 derivatives) now compile canvas apps out of the box

Co-authored-by: nextpointer <110530249+nextpointer@users.noreply.github.com>

* Pin diagnostic-free stub compiles in both canvas-smoke CI lanes

- zig cc passthrough is the one channel where C warnings reach stderr, so each lane compiles its stub host and asserts zero diagnostics
- fails on the old pragma, passes after its removal; the webkit-less build + ELF audit receipts already live in these lanes
- changelog fragment for the user-visible fix

Co-authored-by: nextpointer <110530249+nextpointer@users.noreply.github.com>

* Run the stub diagnostic receipts against a cold zig cache

- on a cache hit zig cc replays nothing, stderr included, so a restored cache would hide the exact diagnostics the steps pin against
- throwaway ZIG_GLOBAL/LOCAL_CACHE_DIR per step keeps every run cold

Co-authored-by: nextpointer <110530249+nextpointer@users.noreply.github.com>

* Print the captured compiler output when a stub receipt compile fails

- Actions runs steps under bash -e, so a nonzero command substitution killed the step at the assignment and swallowed the forensics the step captured; the || arm keeps errexit out of the capture.

Co-authored-by: nextpointer <110530249+nextpointer@users.noreply.github.com>

* Pin the GLib 2.72 fallback with an error-set receipt on stock 22.04

- No stock image pairs old glib with GTK 4.10, so the receipt asserts the error set: GTK-age failures only, never a glib symbol - a future 2.74+ symbol without a fallback trips it.

Co-authored-by: nextpointer <110530249+nextpointer@users.noreply.github.com>

* Whitelist GTK-age diagnostic shapes in the GLib 2.72 receipt

- The prefix blacklist missed non-undeclared shapes (unknown glib type names); the whitelist rejects everything that is not a GTK-age root or its cascades, with the cascade rationale in the script.

Co-authored-by: nextpointer <110530249+nextpointer@users.noreply.github.com>

* Demand positive evidence from the GLib 2.72 receipt

- Unlocated error shapes (driver/invocation failures) reject instead of sailing past the located-diagnostic parser, and fewer than five GTK-age roots means the compile proved nothing.
- Renamed to the allowlist vocabulary.

Co-authored-by: nextpointer <110530249+nextpointer@users.noreply.github.com>

* Narrow the conversion cascade to its signature and document the lattice

- Int-to-pointer lines only pass when converting from 'int' (the undeclared-function-returns-int shape); the docstring now records why cascade allowances are sound: full-GTK lanes compile the same file, so only the old-glib delta reaches this filter, and glib regressions always reject at their root.

Co-authored-by: nextpointer <110530249+nextpointer@users.noreply.github.com>

---------

Co-authored-by: nextpointer <110530249+nextpointer@users.noreply.github.com>
2026-07-18 13:23:03 -05:00
Chris Tate e59091060f Fix the Linux Debug startup crash at the GTK create-view seam (#153)
* Fix the Linux Debug startup crash at the GTK create-view seam

- Force the LLVM backend for the app executable on x86_64 (and in the ejected template's build): zig 0.16.0's self-hosted backend shifts native_sdk_gtk_create_view's stack-passed arguments, so Debug `native dev` runs crashed reading a garbage role pointer.
- Cap the GTK host's view string lengths at the platform limits with a teaching refusal, so a corrupted C-ABI boundary fails loudly instead of faulting in strndup.

Co-authored-by: codehz <13158903+codehz@users.noreply.github.com>

* Add a Linux Debug-scaffold runtime smoke to CI

- linux-dev-smoke scaffolds the default ts-core template with the CLI, builds it -Doptimize=Debug with automation, and drives it under Xvfb to ready + first presented pixels.
- Pins the Debug half of the Linux runtime story: release-shaped lanes always use the LLVM backend and can never see a Debug-only x86_64 codegen fault.

Co-authored-by: codehz <13158903+codehz@users.noreply.github.com>

* Apply the x86_64 LLVM-backend workaround to the checked-in example builds

- The web-frontend examples own expanded build.zigs that created their exe without use_llvm, leaving Debug x86_64 runs exposed to the same self-hosted-backend C-ABI miscompile the app graph now guards against.

Co-authored-by: codehz <13158903+codehz@users.noreply.github.com>

* Restore the windows-canvas-smoke job key the dev-smoke insertion ate

- Duplicate name/runs-on/steps keys collapsed both jobs into one: the Wine smoke ran mislabeled and the new Debug scaffold smoke never executed.

Co-authored-by: codehz <13158903+codehz@users.noreply.github.com>

---------

Co-authored-by: codehz <13158903+codehz@users.noreply.github.com>
2026-07-18 12:14:13 -05:00
Chris Tate b566850b46 Scale the comptime canonicalize quota by stamped source size, not a tree walk (#154)
* Scale the comptime canonicalize quota by stamped source size, not a tree walk

- canonicalizeComptime sized its @setEvalBranchQuota by recursively measuring the document inside the quota argument, which evaluates under the caller's default 1000-branch budget - so CompiledMarkupView failed on .native documents past ~10KB while the runtime interpreter handled them fine.
- MarkupDocument now carries source_bytes, stamped by both parsers, both import resolvers (the runtime resolver sums every visited file; the comptime resolver uses the embedded set's total, a superset of the merged tree), and the NSUI decoder; the quota reads it in O(1).
- The per-byte constant (400) keeps its headroom: source length includes every byte the old measured tree size counted (node text, names, attr names/values) plus all markup syntax, so the new scale is always >= the old one. resolveImportsComptime's own size-summing loop now gets a set-length-scaled quota first, hardening the same invariant.

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

* Guard the compiled engine against the comptime document-size cliff

- New compile-cost fixture: a ~20KB comptime-generated .native document (36 realistically nested sections) built and rendered through CompiledMarkupView, plus a tiny root importing a 16KB+ template file through CompiledMarkupImports so the resolver's source_bytes accounting covers imported trees.
- Both tests reproduce the old "evaluation exceeded 1000 backwards branches" failure when the fix is reverted, and neither raises @setEvalBranchQuota itself - the engine must get by on its own derived quotas.

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>

---------

Co-authored-by: perminder-klair <3984412+perminder-klair@users.noreply.github.com>
2026-07-18 11:50:10 -05:00
Chris Tate 57bf56bc58 Prepare v0.5.3 release (#145)
- Bump Native SDK package versions and platform pins to 0.5.3

- Merge pending changelog fragments into the marked 0.5.3 release notes

- Fold contributor credits for the v0.5.2..HEAD release range
2026-07-17 16:51:10 -05:00
Chris Tate 4a19caa338 The media surface: a dynamic texture channel composited like any widget (#142)
* Add the media-surface element and its dynamic texture channel

- media-surface (element 66, widget kind 61) composites producer-pushed RGBA8 textures like any widget; surface (attr 81) binds the model-owned u64 surface id in the runtime-image-id grammar, wired through both engines, the validator, the model contract, LSP docs, and docs previews
- Runtime.acquireMediaSurfaceProducer hands out an any-thread handle over a process-lived mailbox (latest-wins staging, generation/owner-tag fencing, no runtime pointers), adopted on the compositor's presented-frame clock with fingerprint damage gates at both the push and adoption boundaries
- texture contents stay presentation chrome: adopted textures ride the image pipeline as presentation_only resources the deterministic reference renderer skips (goldens, screenshots, replay marks render the id-derived placeholder), and bit 63 of the ImageId space is reserved for the texture namespace

* Pin the media-surface channel with the synthetic producer battery

- pins latest-wins burst adoption on the frame clock, damage short-circuits at both the push and adoption boundaries, viewport clipping plus the surface's own radius mask, and reference-placeholder byte-identity with a producer pushing real frames
- pins fingerprint exclusion (a11y line is producer-independent) and record/replay: a session recorded with a live producer replays fingerprint-identical with NO producer attached
- pins the thread contract: cross-thread pushes, and a producer outliving its runtime landing in inert process-lived slots with no cross-talk into a successor runtime's claim

* Prove the media surface through markup, contract, wire, and TS tiers

- validator teachings pinned: surface is one binding, required, media-surface-scoped; the contract checker requires an integer-producing model binding with the same message
- NSUI round-trips the element and attribute under codes 66/81
- the TS markup fixture declares a media-surface bound to a transpiled-core integer field, adopted into the retained tree through the full e2e (record/replay suites included)

* Document the media surface and its producer recipe

- components/media-surface reference page (markup-first, placeholder-policy honest) plus the Media Producers recipe under Mobile & Embedding — the external-renderer/mpv answer's home, explicit about the Zig-tier producer API
- native-ui guide and the native-ui skill gain the element row; changelog fragment states the feature, the determinism policy, and the reserved bit-63 id namespace

* State the software-fallback placeholder honestly in the producer recipe

- textures composite through the GPU packet pipeline; hosts on the software pixel fallback render the reference path's placeholder in this release

* Let media-surface uploads through at the frame budget the producer enforces

- The upload side-channel capped every image at the registered-image slot bound (1 MiB), so a 1920x1080 RGBA8 frame the producer accepted (8 MiB budget) was staged, adopted, and then refused at presentation with InvalidGpuSurfaceImage.
- Bounds are now keyed by the id namespace: ordinary registered images keep the avatar-scale registry bound (anything larger is an engine bug the registry already refused), while the reserved media-surface namespace is bounded by the producer's own frame budget, pinned in lockstep with canvas_limits.
- New coverage: a real 1080p frame end to end through push, adoption, packet present, and host upload; the exact-budget/one-over boundary refusing at the producer and never downstream; the per-namespace bounds at the PlatformServices seam every host (AppKit included) sits behind.

* Remove the host-side texture when a media-surface entry is reclaimed

- The reclaim path overwrote the retained entry without removeGpuSurfaceImage, so hosts that retain copied side-channel textures (AppKit's NSImage store) grew unboundedly as surface ids rotated, and a widget still drawing the reclaimed id could resolve the stale host image instead of its placeholder.
- Reclaim now mirrors unregisterCanvasImage's best-effort teardown, issued after the slot lock drops so guarded sections stay bounded memcpys; hosts without the seam and hosts that never uploaded the id are both safe no-ops.
- The reclaim-on-exhaustion path also self-deadlocked the moment the entry table first filled: the active-slot scan relocked the non-reentrant spin mutex the adoption loop already held. The scan now reads the held slot lock-free; the other paths (explicit release retains the paused frame deliberately, and a runtime never outlives its platform) need no removal.
- New coverage: reclaim removes the host image and the widget serves the placeholder in both engines' resolution paths; a surface-id rotation loop keeps the host store bounded by the channel budget; reclaim is safe on hosts without the upload seam.

* Export MediaSurfaceProducer from the public roots

- The producer handle type was public only inside media_surface.zig, so the docs' typed mpv callback (producer: media.MediaSurfaceProducer) could not be written as shown; it now rides the runtime root and the SDK root beside Runtime.acquireMediaSurfaceProducer, with the channel budgets exported like the image registry's.
- A compile-shaped test drives the docs example's exact signature against the public exports, and a docs-contract step pins the signature in the page and its test mirror in lockstep.

* Wake the idle compositor when a media producer stages new bytes

- pushFrame requests ONE coalesced frame through the platform's thread-safe request_frame_fn (the automation watcher's wake path) when it stages new bytes, so an idle demand-driven host adopts a 24/30fps producer instead of stalling and a late-starting producer is adopted promptly; damage-skipped and stale-generation pushes wake nothing, and a per-slot pending flag makes a burst of pushes cost at most one platform call
- The binding lives in the slot's process-lived wake half behind its OWN spin mutex with the platform call made UNDER it (the effects executor's abandon-fence doctrine): disarmMediaSurfaceWakes — run-loop exit defer, TestHarness.destroy, embed host destroy — takes the same mutex, so after disarm returns no producer thread is inside the host call and none can start one, preserving the orphan-producer UAF-safety story
- frame_requested dispatch now adopts staged frames (adoption's invalidation then arms the prompt gpu-surface frame via the existing noteCanvasImagesChanged path); tests pin the idle wake-adopt round trip, burst coalescing across threads, no-wake on damage-skip/released/invalid pushes, the disarm fence, and teardown with a wake pending

* Close the nested-slot-lock class behind the cross-runtime reclaim deadlock

- The retained-entry reclaim scan locked OTHER mailbox slots while the adoption loop held the drained one: two runtimes with full texture tables adopting concurrently could each hold a different slot and spin on the other's forever (ABBA) — the same class as the shipped self-deadlock fix, one runtime wider
- mediaSurfaceHasActiveSlot now snapshots ownership LOCK-FREE: the claim/release sites store the owner-tag/surface-id/active triple with atomic stores (active last, .release) and the scan reads them with atomic loads (active first, .acquire), which is what the memory model honestly supports — per-field tear-free, triple possibly stale, acceptable because it feeds only the reclaim heuristic and self-corrects on the next adoption (documented at the fn)
- SpinMutex.lock asserts in Debug/ReleaseSafe that a thread never holds two media-surface mutexes (data or wake) — no nesting means no lock order to violate, so any reintroduction fails loudly in every debug test run instead of hanging a stress run; a bounded two-runtime full-table concurrent-adoption stress test pins the racing shape itself

* Clip cover-fit media surfaces to their frame like the image widget

- emitMediaSurfaceWidget emitted the fit-expanded texture draw with no rectangular clip: cover expands past the widget frame on one axis, the draw-level radius mask is nothing at radius zero, and packet hosts that only mask radii painted the overflow over siblings — the exact overflow emitImageWidget already crops with a push_clip, now mirrored (part id 3, rect = frame) around the media draw
- Reference renders are unchanged by construction (the planner crops draws to their requested dst), so goldens stay byte-identical; the rounded case keeps its draw-level mask and gains the same bounds crop
- Tests pin the emitted command sequence (placeholder fill, rect clip, cover draw, pop before the sibling paints; no clip for non-cover; radius preserved under the clip) and the reference render's byte containment of the fit-expanded draw

* Allocate adopted media-surface texture buffers on demand instead of embedding 32 MiB

- Runtime.media_surface_pixels (4 x 8 MiB inline) becomes one lazy frame-budget allocation per texture entry from the new Options.allocator at first adoption, freed by the new Runtime.deinit (wired through the app runner's five run paths, TestHarness with the leak-checked test allocator, the embed host's destroy, and the docs wasm preview host); zero media use = zero bytes, OOM at adoption drops that frame loudly and retries — measured on the docs wasm preview host as 169.5 MB -> 137.5 MB per component tile (the -32 MiB pool exactly; 12-tile cap: -384 MB), the registered-font-pool regression's twin
- Repairs the branch's broken wasm32-freestanding build so the measurement (and the docs preview) compile again: the adoption path's std.debug.print gets the session recorder's freestanding guard (analyzing it dragged std.Io.Threaded in), and the reclaim scan's ownership atomics route through single-threaded-aware accessors (wasm32 lacks 64-bit atomics; with no second thread the plain access is exact)
- A FailingAllocator test pins that construction, startup, a live claim, and staged pushes allocate nothing, that a refused allocation degrades without a torn entry, and that the first adoption allocates exactly one frame-budget block reused across frames

* Freeze the runtime's owning allocator at init so media buffer frees never chase a mutated options.allocator

- Runtime.options is public and mutable, so the lazy media-texture buffer allocating through a live read of options.allocator at adoption and freeing through another at deinit could split the pair across two allocators (silent UB); initAt now captures Options.allocator into a private Runtime.owned_allocator and the adoption alloc, deinit frees, and all ownership docs go through that frozen identity
- The zero-allocations-until-adoption test now injects its counting allocator through the real capture site (re-initAt) and flips the frozen allocator's fail_index for the OOM-refusal phase; a new test adopts a frame through a tracked allocator, swaps options.allocator to a poisoned one, and pins deinit's free to the frozen identity with zero activity on the swap-in
- Audited the branch's other allocator wiring (app runner run paths, TestHarness, embed host, docs wasm preview host): all pass the allocator at init and only deinit reads it back, so the adoption-vs-deinit pair was the only differing-time read

* Reset the push-boundary fingerprint when adoption drops a staged frame

- Both adoption failure paths (registry full, lazy texture-buffer OOM) cleared slot.staged but kept last_push_fingerprint, so a producer re-pushing byte-identical pixels (a paused video's frame, album art) hit the push dedup gate — no stage, no wake, the frame was lost until the pixels changed; both paths now reset the fingerprint to 0, the same "no previous push" state claim and release use (frameFingerprint maps real hashes away from 0), so an identical retry stages and wakes again
- Push-side dedup is untouched: identical pushes between presents still short-circuit at the hash, and the OOM path's comment now states the honest retry contract (the producer's next push retries, not "the next adoption")
- Tests drive both drops with a synthetic producer — the OOM path through the frozen owned_allocator's FailingAllocator seam, the registry-full path by manufacturing the raced-release shape the branch guards — then re-push the same bytes and pin that the retry stages, wakes one coalesced frame, and adopts

* Range-check markup's signed-to-u64 id casts instead of trapping

- The surface and avatar image bindings cast the expression's i64 straight to u64 in both engines, so a signed model field (surface: i64 = -1) trapped safe builds at the @intCast; both seams now refuse negatives with the existing teaching messages (failVoid in the interpreter, the runtimeFail latch in the compiled engine), mirroring the tooltip-delay range guard
- Message-payload coercion gets the same guard against its payload's own integer bounds (minInt/maxInt, so signed payloads keep their negative range), and key conversion maps negative integer ids bijectively via @bitCast — keys are identity, so a -1 sentinel is a distinct key, never a trap and never a spurious failure
- Negative-binding tests cover surface and avatar through both engines, plus a positive control pinning the u64 id's ride into the widget's image_id

* Regenerate the docs wasm preview over the rebased base

- The committed artifact now bakes in main's CJK font, paste, and pinch work alongside the media surface.

* Refuse reserved media-surface ids before the image decode

- registerCanvasImageBytes now rejects ids with the reserved media-surface bit before invoking the platform codec, so callers get error.InvalidImageId instead of a codec error and never pay a decode for an unusable id
- Test registers PNG bytes under a bit-63 id and asserts the null platform's decode counter stays at zero

* State the honest surface-id range everywhere docs describe it

- A surface id is any nonzero value below the reserved media-surface texture bit (bit 63, refused at acquire): the producer recipe, the component page, the markup attribute docs plus regenerated vocab JSON, the native-ui skill, and the widget/schema comments now all say so instead of implying any nonzero u64
- Fix the producer recipe's binding example to {player_surface} — markup binding resolution is exact-name, and the model field is player_surface
2026-07-17 15:19:03 -05:00
Chris Tate 2c028094bb Registered CJK fonts render dense glyphs (#139)
* Size glyph outline budgets from real CJK faces and gate maxp at parse

- Raise the outline budgets to 1024 points / 128 contours, sized from measured maxp maxima of production TrueType builds (Noto Sans JP/SC/TC/KR, Noto Serif JP, brush-style Yuji Mai; worst measured 738 points / 87 contours), so dense kanji ink as outlines instead of block fallbacks
- Face.parse now refuses any face whose maxp declares maxima beyond the budgets (loud, registration-time), leaving the per-glyph checks as backstops for under-declaring fonts; declaredGlyphMaxima exposes a refused face's numbers for teaching callers
- Derive the reference renderer's glyph path capacity from the parser budgets and prove both gate sides with in-test synthetic TrueType fixtures (dense rings, budget-edge composites, over- and under-declaring maxp)

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>
Co-authored-by: IFTC-XLKJ <151902522+IFTC-XLKJ@users.noreply.github.com>

* Refuse over-budget faces at registration and admit full CJK font files

- registerCanvasFont maps the parser's glyph-budget refusal to error.FontExceedsGlyphBudgets, and UiApp's fonts teaching names the face's declared maxima against the budgets, restoring "a registered id always resolves at render time"
- Raise the per-font registration bound to 24 MiB (matched by the gpu-surface side-channel bound): measured Google Fonts TrueType builds run 9.6 MB (Noto Sans JP) to 17.8 MB (Noto Sans SC); memory stays fixed address space with pages touched only as fonts register
- Cover the gate at the registry and UiApp seams with a maxp-patched fixture, plus a /tmp-guarded end-to-end test registering the real Noto Sans JP and outlining dense kanji (skips where the download is absent)

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>
Co-authored-by: IFTC-XLKJ <151902522+IFTC-XLKJ@users.noreply.github.com>

* Allocate registered font bytes on demand instead of embedding a 192 MiB pool

- Runtime.canvas_font_bytes (8 x 24 MiB inline after the CJK bound raise) becomes an exact-size heap copy from the new Options.allocator at registration; zero fonts = zero bytes, freed only by the new Runtime.deinit (registration is permanent, so ownership is one alloc and one free)
- Measured on the docs wasm preview host (one Runtime per component tile, linear memory never overcommits): per-instance bytes 313.5 MB -> 121.5 MB (pre-branch base: 137.5 MB), linear memory with one active tile 528.9 MB -> 144.9 MB (base: 272.9 MB)
- TestHarness routes font bytes through the leak-checked test allocator and deinits on destroy; a FailingAllocator test pins that a fresh runtime performs zero font-byte allocations until a registration happens, and that the first allocation is exactly the file's size

* Gate flattened-composite maxima at parse so composite-heavy faces refuse at registration

- Face.parse and GlyphMaxima/declaredGlyphMaxima/withinBudgets now read maxp.maxCompositePoints/maxCompositeContours (offsets 10/12): a face whose composites flatten past the path capacity was passing the simple-only gate and silently blocking at render, the exact failure the gate exists to prevent
- Composite budgets equal the simple budgets (1024 points / 128 contours): a composite flattens through the same path sink and per-glyph capacity, so densest-renderable-glyph is one answer; measured ground truth (Geist 89/5, Geist Mono 104/10, Yuji Mai 198/5, Noto CJK 0/0) sits 5-12x below, and the reference glyph path capacity derives from max(simple, composite) maxima
- Registration teachings (parseFailureReason and the UiApp fonts warn) name the composite numbers; tests cover over-budget composite declarations refused at parse and at the registry, an at-budget composite flattening denser than any simple glyph rendering within the derived capacity, and the bundled faces' pinned composite maxima

* Deinit the embedded runtime when the UI host is destroyed

- UiAppHost.destroy freed the host without returning the runtime's heap-owned font copies, leaking every registered font per host recreation cycle; MobileHostApp.destroy already deinits — the UI-host sibling now matches.

* Give EmbeddedApp a public deinit so direct embedders can end the lifecycle

- EmbeddedApp.deinit (idempotent) returns the embedded runtime's heap-owned registered-font bytes; the documented lifecycle now ends at deinit, not stop, and the docs show the defer embedded.deinit() idiom
- MobileHostApp.destroy and UiAppHost.destroy route teardown through embedded.deinit — one lifecycle owner — so native_sdk_app_destroy reaches the same deinit through its host
- Leak-checked create-register-destroy cycle tests through the Options.allocator seam cover the direct embedder, idempotence, and the C ABI destroy path

* Derive the glyph raster budgets from the outline budgets the gate admits

- vector.zig grows a GlyphRasterizer whose edge/crossing capacities are derived from the font gate's outline budgets (quads <= 1024+128, edges <= quads*16+128 = 18,560, crossings <= edges), lockstep-pinned against font_ttf's constants, so a truthfully-declared budget-maximal glyph never trips VectorPathTooComplex
- Tolerance-exact flattening has no finite bound (nothing bounds text size), and even the generic 48-segment cap costs 1.06 MiB of edges, so glyph fills clamp at 16 segments per curve: binds only above ~128-px ems, error stays under em/512 (0.2%), goldens byte-identical
- The reference renderer inks glyphs through a per-thread heap GlyphRasterizer (lazy_tls, ~508 KiB, only on threads that ink glyphs); zigzag adversarial tests prove a gate-admitted 1024-point contour rasterizes at the max raster size on both text paths instead of degrading to the block fallback

* Freeze the ownership allocator at init so font bytes free through the identity that made them

- Runtime captures Options.allocator into owned_allocator at initAt: options is public and mutable, so an embedder swapping options.allocator between registration and deinit split the alloc/free identity (silent wrong-allocator free); registration allocs, refusal-path errdefer frees, and the deinit frees now all resolve through the frozen field
- The field is general (every runtime-lifetime heap ownership, not font-specific) so future on-demand owners share the same frozen identity
- New test registers through a counting allocator, mutates options.allocator to a poisoned one, and deinits: counts balance on the original and the mutated allocator sees zero activity; existing tests that retargeted options.allocator post-init now re-freeze owned_allocator before anything is owned

---------

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>
Co-authored-by: IFTC-XLKJ <151902522+IFTC-XLKJ@users.noreply.github.com>
2026-07-17 13:42:09 -05:00
Chris Tate 2027ff03b8 First-class trackpad pinch input for GPU-surface apps (#141)
* Add trackpad pinch input vocabulary, macOS host emission, and journal v5

- GpuSurfaceInputKind gains phase-explicit pinch_begin/pinch_change/pinch_end and GpuSurfaceInputEvent a scale field carrying the per-event magnification delta (NSEvent.magnification semantics: cumulative scale is the product of 1 + delta); Windows/GTK sources are staged follow-ups noted at the vocabulary.
- The AppKit host implements magnifyWithEvent: with begin/changed mapping directly, cancelled folding into pinch_end (deltas are applied as they arrive, so there is no transient to roll back), and change deltas emitted uncoalesced because summing coalesced deltas would drift from the product.
- Bump the session journal to v5: gpu-surface input records gain the scale field (a layout change a v4 reader would misparse) plus the pinch kind codes 12-14, with the round-trip test pinning both.

Co-authored-by: jhodges10 <18431344+jhodges10@users.noreply.github.com>

* Deliver pinch to app cores through the on_pinch/pinchMsg channel

- Zig cores gain Options.on_pinch (the on_key shape): pinch is view-global, so the raw journaled gpu_surface_input pinch kinds surface as a phase-explicit PinchEvent with the change delta and view-local centroid, and the Msg dispatch replays from the same journaled event.
- TS cores gain the pinchMsg(pinch) export: the wiring adapter maps the named begin/change/end phase alias by member name and widens the numbers, with the record shape validated at transpile time (NS1033) and mirrored in @native-sdk/core/events as PinchPhase/PinchEvent.

Co-authored-by: jhodges10 <18431344+jhodges10@users.noreply.github.com>

* Add the widget-pinch automation verb driving real pinch platform events

- widget-pinch <view-label> <scale> [x y] synthesizes pinch_begin, one pinch_change carrying scale - 1 (the product of 1 + delta lands exactly on the commanded cumulative scale), and pinch_end at the given or view-center centroid.
- Plain input synthesis per the widget-key discipline: every event journals as itself and replays through the same dispatch - pinch is not a widget verb, so no accessibility-action record.
- Bump the automation protocol to v7 for the new verb string.

Co-authored-by: jhodges10 <18431344+jhodges10@users.noreply.github.com>

* Class the pinch record's fields as boundary floats in the transpiler

- Magnification deltas are ~0.01..0.3 per event and the centroid is sub-point, so pinchMsg's parameter record fields are host values, never provable integers: without boundary marking, a core's pinch.scale === 0 comparison int-claimed the slot and every zoom product rounded to whole numbers.
- frameMsg/keyMsg records keep their historical by-usage classing; the emitter test pins the f64 emission and the NS1033 shape refusal.

Co-authored-by: jhodges10 <18431344+jhodges10@users.noreply.github.com>

* Test pinch delivery end to end: both core tiers, the verb, and replay

- ui_app_tests drives begin/change/change/end plus the widget-pinch verb into a Zig core, pinning product-of-deltas cumulative scale (two +25% steps land on 1.5625, never a sum's 1.45), centroid delivery, default view-center aim, non-pinch isolation, and the non-positive-scale refusal.
- The ts-core markup fixture exports pinchMsg (phase-gated on change) and the e2e drives raw events plus the automation verb through the transpiled core.
- The record/replay reference session pinches once raw and once through the verb; replay re-derives the identical binary-exact zoom and fingerprints from the journaled scale fields alone.

Co-authored-by: jhodges10 <18431344+jhodges10@users.noreply.github.com>

* Document the pinch channel, the widget-pinch verb, and the journal bump

- native-ui gains a Trackpad pinch section (both tiers' channel shapes, product-of-deltas semantics, centroid, staged platforms); the automation/cli pages and the automation skill list widget-pinch.
- Changelog fragment covers the channel, the verb (protocol v7), and journal v5.

Co-authored-by: jhodges10 <18431344+jhodges10@users.noreply.github.com>

* Pin the macOS pinch ABI conversion

- The kind codes 12-14 map to the pinch phases and the ABI event's scale field carries the magnification delta with the converted centroid on x/y — the headless half of the magnifyWithEvent: path.

Co-authored-by: jhodges10 <18431344+jhodges10@users.noreply.github.com>

* Forward the terminal pinch magnification instead of discarding it

- AppKit documents every magnifyWithEvent: as carrying the delta since the previous event, the Ended/Cancelled one included; the terminal branch now emits a nonzero delta as one last PINCH_CHANGE before PINCH_END so the cumulative product of (1 + delta) matches what the OS delivered
- Cancelled deliberately shares the path: pinch applies deltas incrementally with no rollback, so the honest stream reports every measured delta, then ends
- Pin the ordering in build.zig's host-source checks and fold a terminal-delta Ended plus a zero-delta Ended into the on_pinch product test

* Name the pinch x/y for what it is: the pointer anchor

- AppKit reports gesture events at locationInWindow — the pointer location, never a midpoint between the fingers (raw touch positions are trackpad-normalized and have no view-space meaning) — so the API was right and the word "centroid" was wrong
- Redocument x/y as the pointer anchor (zoom-at-cursor anchoring) across the platform vocabulary, the app-channel docs in both tiers, the automation verb's usage, the docs pages, the changelog fragment, and test names; the word has zero survivors

* Carry the source identity on the pinch channel

- PinchEvent gains window_id/label in Zig and windowId/label in TypeScript (the on_frame GpuFrame identity shape): x/y are view-local, so a coordinate without its view is not a position, and multi-window apps could not tell pinches apart
- handlePinch forwards the identity the journaled platform event already carries; the TS adapter, NS1033 shape validation, and the emitted-record pins extend to the six-field contract, with the wholesale boundary classing covering the new numeric field
- Pin distinguishable pinches in a two-window/two-view Zig fixture and mirror windowId plus a label match through the transpiled markup core end to end

* Normalize AppKit's additive magnification into multiplicative pinch deltas

- NSEvent.magnification is additive (Apple: gesture total = 1 + sum), so forwarding raw chunks made the product of (1 + delta) depend on event chunking; the macOS host now tracks the gesture's additive sum in f64 and emits per-event ratios, so the product equals Apple's total no matter how the driver sliced the gesture — the app rule is unchanged (zoom *= 1 + scale, memoryless).
- A sum crossing -1 (a pinch cannot invert through zero scale) clamps at a floor of 2^-10, keeping every emitted factor positive; the arithmetic lives in tiny pure functions pinned by a file-contains step and mirrored runnably in a Zig test (chunking invariance, terminal-delta participation, round-trip to unity, clamp).
- Product pins across ui_app/markup-e2e/session tests update to the normalized truths (1.5/1.75 instead of 1.5625/1.953125), and the widget-pinch docs now say plainly that <scale> is the gesture's final multiplicative zoom — the verb synthesizes events downstream of AppKit, untouched by the normalization.

* Forward raw AppKit magnification as the multiplicative pinch delta

- Revert the running-sum additive normalization of NSEvent.magnification: WebKit (ViewGestureControllerMac.mm: m_magnification += m_magnification * scaleWithResistance) and Chromium (pinch_update.scale = magnification + 1.0) both compound raw magnification multiplicatively, and Apple's own Event Handling Guide example multiplies — only the API reference's loose prose says "add". Raw magnification IS the multiplicative per-event delta; the doctrine comment at magnifyWithEvent: carries the receipts, and the test-appkit-pinch-magnification-doctrine step pins the comment, the raw forwarding, and the floor so a sum-based transformation cannot come back quietly.
- The one guard that stays is per-event: a single magnification at or below -1 (a factor <= 0 on the wire — a zoom inverted through zero scale, physically impossible) clamps to -1 + 2^-10, keeping every emitted factor positive; the terminal-delta forwarding and synthesized-begin structure are unchanged.
- Product pins return to the raw-forwarding truths (ui_app 1.5625/1.953125 and "Zoom 1.95", markup-e2e exact 1.5625/3.125, session 1.40625), the Zig normalization mirror test is deleted with the machinery it mirrored, and every contract/doc surface now states the engine convention instead of host-side normalization.

* Refuse widget-pinch scales whose f32 delta rounds to a zero factor

- The parser's finite-and-positive guard is not enough on the f32 wire: any scale at or below 2^-25 (e.g. 1e-20) rounds `scale - 1` to exactly -1, dispatching a pinch_change with factor 1 + delta = 0 — a zoom through zero scale no gesture can perform and no downstream product can recover from.
- The dispatch now validates the computed wire delta and refuses with error.PinchScaleBelowWireMinimum before anything dispatches (no partial gesture reaches the journal); the minimum accepted scale — the smallest f32 above 2^-25, ~2.9802326e-8 — is named at the guard.
- Tests pin both sides: `widget-pinch <view> 1e-20` is refused with the named error and moves no model state, and the smallest accepted scale round-trips a positive factor (delta -1 + 2^-24, model product exactly 0x1.8p-25).

* Class export-list wiring entries at the host boundary like the modifier

- Integer inference keyed its host-ABI marking (number params of the entry's exported functions, and pinchMsg's wholesale record classing) on the inline export modifier only, so the first-class list spelling — `function pinchMsg(...) {...}` plus `export { pinchMsg };` — passed validation and wired the channel but skipped the marking: `pinch.scale === 0` int-claimed the slot to i64, every real magnification delta rounded to 0, and the adapter's zero gate silently dropped the events. The seam now reuses exportListBindings (the same export-list truth the checker, emitter, and model-helper surfaces already resolve by): an un-renamed entry-module list binding marks exactly like the modifier, and renamed bindings stay out on purpose — renamed wiring names never reach inference (NS1014/NS1047 fence them first) and a renamed ordinary function keeps its historical classing.
- Family audit: pinchMsg was the only wiring entry observably broken (it force-classes its record's fields; frameMsg/keyMsg/commandMsg carry no number param slots and keep by-usage field classing, appearanceMsg/chromeMsg/envMsgs are consts the emitter already reads list-aware, and model helpers were already list-aware in types.ts) — but the same hole covered every export-list function's number params, where a `=== 0` comparison int-claimed an i64 signature that would truncate host f64 arguments; the one seam fixes both.
- Tests pin the list spelling end to end: emitter mirrors of the pinch boundary-float classing and the frameMsg channel, an f64 host-boundary signature pin, an NS1014 pin that a renamed list entry still cannot bind a wiring name, a run-fidelity case whose 0.25 probe an i64 signature cannot even accept, and the markup e2e fixture's pinchMsg now exports by list so the native battery proves 0.25 deltas survive to the 1.5625 zoom product.

---------

Co-authored-by: jhodges10 <18431344+jhodges10@users.noreply.github.com>
2026-07-17 12:19:53 -05:00
Chris Tate 1dfb0db4f2 Single-line inputs strip pasted line breaks and never paint a second line (#140)
* Strip line breaks from single-line text-field inserts at the edit seam

- Sanitize derived edits for input/text_field/search_field/combobox at the keyboard choke point, BEFORE the stamp, so the retained editor, the app's on_input mirror, and replay all hear identical stripped bytes (HTML value-sanitization rule; covers shortcut and context-menu paste, typed/automation text_input, and IME composition).
- Suppress inserts that strip to nothing so pasting bare newlines inserts nothing and a host-stuffed Enter payload never deletes a live selection; the app-side fallback derivation applies the same rule so both derivations agree.
- Sanitize direct runtime editCanvasWidgetText writes through the same shared rule.

Co-authored-by: IFTC-XLKJ <151902522+IFTC-XLKJ@users.noreply.github.com>

* Present line-broken single-line values as one clipped line

- Lay out, measure, and paint a single-line field's value with \n/\r presented as spaces (byte-for-byte, so caret/selection/hit-test offsets address the raw value) — one line on the GPU engine, the reference renderer, and packet hosts alike; copy, semantics, and automation still read the raw model value.
- Force the content-rect clip whenever the raw value holds a line break, the independent guard that keeps even a presentation-scratch fallback inside the field's rounded border.
- Persist presented bytes into a render-walk pool (the chart-label scratch precedent) so emitted commands survive until the runtime copies the display list.

Co-authored-by: IFTC-XLKJ <151902522+IFTC-XLKJ@users.noreply.github.com>

* Pin single-line newline sanitization and containment across the batteries

- Paste, automation set_text, IME composition, and the Tree fallback derivation all pin stripped inserts (textarea keeps its breaks); bare-newline pastes insert nothing and Enter stays not-an-insert even with a host-stuffed newline payload.
- Model-set values holding a newline pin one presented line under a forced clip in both render walks and through the runtime display list, with semantics still reporting the raw value.
- The session reference recording now copies a textarea's two lines and pastes them into the search field, pinning that a recorded multi-line paste replays to the identical sanitized value and fingerprint.

Co-authored-by: IFTC-XLKJ <151902522+IFTC-XLKJ@users.noreply.github.com>

* Sanitize clipboard pastes before clamping them to capacity

- clampCanvasWidgetPasteText (shared by the cmd+V shortcut and the context-menu Paste) now strips single-line targets' line breaks BEFORE measuring against remaining capacity, so a near-limit paste never spends free bytes on breaks the seam strips anyway ("a\nbc" into 3 free bytes lands "abc", not "ab"), and a paste that sanitizes to nothing is not reported truncated.
- Boundary tests pin both paste paths at exactly three free bytes: retained editor and the app's stamped edit hear the identical whole sanitized suffix with no false truncation flag.

* Move emit-built text bytes into the display-list builder

- The builder contract (pinned at Builder.allocPathElements) lets a display list accumulate across several emit calls or be held while another builder emits; presented single-line values and formatted chart labels lived in thread-local pools RESET at each emit entry, so a second emit overwrote text an earlier list still sliced.
- Builder now owns the bytes beside its path-element store: allocChartLabelBytes keeps the chart label budget's loud ChartLabelBytesFull, and allocTextBytes holds presented values under a per-view-text-budget-sized store (lockstep-tested against max_canvas_text_bytes_per_view) with the raw-value fallback intact; both thread-local pools and the per-emit resets are gone.
- Regression tests emit twice — into another builder and accumulated into one — and pin both lists' draw_text bytes intact for presented fields and tick labels; both fail against the pool-reset design.

---------

Co-authored-by: IFTC-XLKJ <151902522+IFTC-XLKJ@users.noreply.github.com>
2026-07-17 10:01:21 -05:00
Chris Tate 1e6b615674 Prepare v0.5.2 release (#138)
- Synchronize the CLI, core, platform packages, and examples at version 0.5.2.

- Merge pending fragments into the marked v0.5.2 release notes with contributor credits.
2026-07-16 22:39:42 -05:00
Chris Tate a1fa2d0285 Anchored tooltips gain hover intent: show delay, warm window, tooltip-delay attribute (#130)
* Teach the registry the anchored tooltip and its tooltip-delay attribute

- ui_schema: tooltip (39) becomes anchorable; fresh attr code 80 tooltip-delay (.whole, field tooltip_delay) with the pins test re-pinned for the addition
- Options/Widget carry tooltip_delay (ms; -1 follows the new ControlMetricTokens tooltip_show_delay_ms/tooltip_warm_window_ms defaults of 700/300)
- Validator scopes tooltip-delay to tooltip beside anchor with teaching messages, mirrored in the LSP/docs attribute tables and covered in ui_markup_tests

Co-authored-by: Marcus Schiesser <17126+marcusschiesser@users.noreply.github.com>

* Land the anchored-tooltip hover-intent state machine in the runtime

- Anchored tooltips become runtime-owned chrome: adoption stamps them hidden, hover on their trigger arms the show delay, leaving disarms, a dwell past the deadline shows on the presented frame's recorded timestamp, and hiding opens the shared warm window that shows the next trigger's tooltip instantly
- Every transition steps on journaled input/frame timestamps (canvasRenderAnimationStartNsForView at pointer dispatch, GpuSurfaceFrameEvent.timestamp_ns at frame advance) and an armed delay rides the render-animation frame pump, so recorded sweeps replay byte-identically
- Escape dismissal clears the intent machine with the surface, and five behavior tests cover sweep-shows-nothing, dwell-shows, leave-disarms, warm transfer/expiry, and tooltip-delay=0

* Hold the engines in parity on the anchored-tooltip declaration

- Interpreter test: anchor + tooltip-delay stamp the widget declaration, the token-default and static leaves keep -1/null so existing documents lower byte-identically
- Compiled-vs-interpreter parity: identical trees and identical anchor/delay stamps across both engines for declared, defaulted, and static tooltips

* Pin the tooltip hover dwell in record-and-replay

- The markup e2e fixture gains an anchored tooltip (tooltip-delay="200") on the Add trigger through the stack pattern
- A recorded dwell arms, shows on the frame at the deadline, and hides on leave; two recordings are byte-identical and the replay verifies every per-frame fingerprint checkpoint on the journaled clock

* Document anchored-tooltip hover intent across docs, vocab, and the UI skill

- Tooltip reference page teaches the anchored stack pattern, the 700ms/300ms hover-intent windows, tooltip-delay=0, and gains the scoped attribute table (tooltip joins the anchor family in the generated vocab)
- native-ui skill enumerates anchor on tooltip and the tooltip-delay attribute; changelog fragment tells the feature story

* Reveal tooltips on keyboard focus, dismiss on press, validate owners

- Keyboard focus-visible now shows a trigger's anchored tooltip instantly (blur hides, never warms), pointer-down and Space/Enter cancel the armed reveal, dismiss the shown tooltip, and close the warm window, and the rebuild prune validates the owning trigger — a removed, rekeyed, disabled, or re-parented owner resets armed/shown/warm state and re-stamps hidden — all per shadcn's Base UI-backed defaults.
- Retune the timing tokens to Base UI parity: tooltip_show_delay_ms 700 -> 600 and tooltip_warm_window_ms 300 -> 400, with the components page, markup vocab, native-ui skill, and changelog fragment updated to match (and to describe the focus and press behaviors).
- Six new canvas_widget_floating_tests cover focus reveal/blur/Escape, press-on-armed, press-on-shown without instant re-show, keyboard activation, and the three owner-invalidation rebuilds; each fails when its fix is reverted.

* Hold anchored tooltips open while the pointer hovers their content

- A shown tooltip's own frame now holds it open, and the anchor gap crosses through a bounded safe-polygon transit corridor (WCAG 1.4.13 hoverable content; Base UI's hoverable default) — the tooltip stays out of hit-testing, so interaction routing and the a11y tree keep treating it as presentation chrome.
- The corridor's grace re-arms on every in-corridor move and resolves on the recorded frame clock (400ms of stillness), so slow deliberate transits never race a timer while parked pointers and replays stay deterministic.
- Motions away from the tooltip keep hiding on the move itself, pinned by the existing sweep/warm-window tests plus two new sabotage-verified travel tests.

* Route every scroll path's hover change through the tooltip machine

- Wheel, kinetic steps, native scroll drivers, and keyboard scrolling now reconcile hover through one wrapper that steps the tooltip intent transition a pointer move would: scrolled-away triggers disarm/hide (usual warm window), newly-arrived triggers arm per normal.
- The step is point-blind on purpose — the content moved, not the pointer, and Base UI closes tooltips on scroll — but the wheel's live position re-seeds the transit-corridor apex for whatever it armed.
- Sabotage-verified tests pin the wheel transition chain (disarm, re-arm, frame-clock show, warm transfer, hide over dead space) and the point-blind path via End-key scrolling a shown trigger out of the tree.

* Make every pointer-down dismiss tooltips, drag and context downs included

- Secondary-button downs consumed by the context-menu gesture and primary downs consumed by a window-drag region now run the same press reset (armed cancels, shown dismisses, warm closes) before their early exits — the documented pointer-down-dismisses contract (Base UI's close-on-press default; macOS help tags vanish on any click) now holds for all buttons.
- The context menu still presents and the OS drag still starts; sabotage-verified tests pin both paths against a shown tooltip.

* Reset tooltip state when a canvas view loses focus

- Both focus seams — per-view focus moves (setFocusedView, input- and command-driven) and window-level focus loss (clearFocusedView) — now drop the blurred view's whole tooltip conversation: armed delay, shown tooltip (keyboard- and pointer-owned), warm window, transit grace, and re-stamp hidden.
- Extends the focus-shown blur-hides contract (shadcn's Base UI-backed default) to the view: a tooltip painted in a view the keyboard left is a stale affordance whose semantics node kept claiming visible.
- Sabotage-verified test pins the keyboard-shown and pointer-shown registers across a sibling-view focus switch, including the semantics tree carrying no stale node.

* Hide focus-owned tooltips when programmatic focus moves the keyboard

- Autofocus, accessibility focus, and automation focus all funnel through focusAutomationCanvasWidget, which now steps updateCanvasTooltipIntentForProgrammaticFocusMove: a focus-owned tooltip hides (no warm window) and the new target's never reveals, the same focus-visible guard rationale as the click-focus exclusion.
- Pointer-owned tooltips stay untouched by focus moves; re-focusing the tooltip's own trigger is not a move and leaves it alone.

* Re-hit-test point-blind scrolls from the stationary pointer's last position

- Kinetic steps, native drivers, and keyboard scrolling borrow the view's last journaled pointer position (canvas_last_pointer_position, cleared on pointer_cancel — the view-exit event): the pointer did not move, so hover and tooltip ownership follow the post-scroll tree honestly — triggers scrolled off the pointer release, ones scrolled under it arm.
- With no trustworthy position (keyboard-only session, or the pointer left the view) the pointer's tooltip intent closes — armed, shown, warm window — instead of guessing; Base UI closes on scroll, we do strictly better only where the re-hit-test is sound.
- The wheel path keeps its live position; whichever position is used re-seeds the transit-corridor apex.

* Normalize runtime tooltip visibility onto the scratch tree before the layout diff

- setCanvasWidgetLayout now prunes tooltip intent against the reconciled tree and stamps runtime-owned anchored-tooltip visibility onto the scratch BEFORE diffing, so an unchanged rebuild carrying a hidden anchored tooltip diffs clean instead of reporting the runtime's own hidden stamp as a spurious visibility invalidation every rebuild.
- A shown tooltip's scratch node is stamped visible (prune-aware), so rebuilds never pass it through a hidden state — no hide-then-show frame pair — while a rebuild that breaks the shown binding still diffs the hide honestly; adoption re-runs prune+stamp as the structural backstop.

* Range-check whole-number attrs against their field type in both engines

- tooltip-delay="2147483648" (or an equally large model binding) trapped in the unchecked @intCast; both engines now teach the grid-lines out-of-range error instead
- the field's own integer type is the honest upper bound - no semantic ms cap is invented, matching resize-duration whose only bound is likewise its u32
- boundary values (0, i32 max) pin as accepted in both engines, literal and binding paths alike

* Collapse degenerate corridor triangles to their boundary segments

- an apex exactly on a tooltip or trigger edge's line zeroed all three cross products for every collinear point, so the sign test read the whole infinite line as in-corridor and motion along it re-armed the transit grace forever
- a zero-area (or epsilon-area) fan triangle now contains only the segments between its actual vertices; the epsilon is half a canvas point over the longest edge, scale-honest for slivers of any length and commented at the constant

* Short-circuit pointer cancel ahead of the tooltip hover-transition gate

- a pointer-shown tooltip held open by its own hovered content reads hovered_id == 0, so cancel-to-0 was no transition and the tooltip stranded visible after the pointer left the view
- .cancel now closes the whole pointer-owned conversation (armed, content-held shown, warm window, corridor state) through the same close the point-blind scroll staleness arm uses; no warmth survives a pointer we cannot place
- the focus-shown tooltip survives a pointer cancel - the keyboard holds it - unlike view blur, where the keyboard itself leaves (commented at the seam)

* Re-hit-test the stationary pointer against every adopted layout

- the adoption prune validates only tooltip/owner identity and hover survives by ID, so a rebuild that MOVED the same-ID trigger away from the stationary pointer left armed intent able to fire and a shown tooltip visible until the next pointer event
- setCanvasWidgetLayout now re-hit-tests canvas_last_pointer_position after the pose restores settle, reusing the point-blind scroll reconcile: armed disarms, shown hides with the usual warmth, a trigger arriving under the pointer arms, and the content hold is re-checked against the tooltip's NEW frame (no transit corridor - the content moved, not the pointer)
- no trustworthy position closes pointer intent exactly like a blind scroll; focus-shown tooltips survive, and a rekeyed trigger under the pointer earns a FRESH dwell instead of inheriting the old widget's show

* Reconcile tooltip intent through one cause-fed choke point

- reconcileCanvasTooltipIntent owns position bookkeeping, hover re-hit-testing, containment, transitions, and the deadline-derived frame-pump kick; every entry point (pointer, consumed streams, scroll, adoption, focus, blur, cancel) is now a thin caller naming its cause
- Closes four input-path holes: transit graces armed by 0-to-0 hovers now pump the idle frame clock, scrolls re-check the content hold even when the hover id is unchanged, a released hold reprocesses the trigger already under the pointer, and consumed secondary/window-drag streams keep the stored pointer position truthful
- Five sabotage-verified regression tests pin the closed holes

* Make the rebuild's tooltip prune transactional against adoption failure

- The pre-diff visibility stamp now reads a pure prune VERDICT (canvasTooltipShownIdSurvivingLayout) instead of mutating the live registers; the mutation lands in copyWidgetLayoutTree's own prune, after the fallible diff and retained-pool validation/copy succeed
- A failed adoption previously left the OLD tree stamped visible with cleared registers: an unhideable tooltip no transition could reach
- Regression test forces the failure through the anchored-surface budget and asserts the shown tooltip stays register-owned and hideable

* Drop tooltips with the whole window on app deactivation and key-loss

- App deactivation and window key-loss now feed the tooltip choke point with the existing view_blur cause for every affected canvas view: setFocusedIndex becomes the one window-key seam (window_focused, frame-change echoes, focusWindow, and native adoption all land there), so a focused→unfocused transition drops that window's tooltip conversations without touching per-window focus memory.
- The stored pointer position deliberately survives the lifecycle blur: pointer truth belongs to the pointer channel, whose own cancel (macOS mouseExited on key-window-scoped tracking) clears it on hosts that stop hover delivery.
- Three tests pin the contract: deactivation hides focus-shown and pointer-shown tooltips (warm dies, armed disarms, no stale a11y visibility), key-loss blurs only the losing window's views, and reactivation/re-key reveals nothing because both reveal paths are transition-edge-triggered.

* Arm and reveal tooltips whose binding changed beneath a stable owner

- A rebuild that mounts, replaces, or rekeys a tooltip under a hovered trigger whose own ID survived produced no hover delta and no stale register, so the new tooltip could never arm until leave/re-enter; setCanvasWidgetLayout now snapshots the outgoing tree's owned-tooltip bindings next to the prune verdict and the layout_adoption arm compares them against the adopted tree, arming a fresh dwell for the hovered owner (never an insta-show; warmth only when genuinely live) and revealing immediately for the standing keyboard focus-visible owner.
- New canvas_widget_focus_visible_keyboard register records whether the ring came from the keyboard contract, so pointer/programmatic/automation rings and the focus-return seams keep the click-focus guard at adoption time too.
- Six floating tests pin the matrix: mount-mid-hover dwell, rekey-under-shown re-earn, focus-visible immediate reveal (pointer-blind path included), caret-ring provenance gate, unmount disarm, and the unchanged-binding rebuild staying inert.

* Gate tooltip reveals and arms on app-active and window-key state

- A rebuild from the deactivation callback (or any adoption/hover in a non-key window) could reveal or arm a tooltip the blur had just cleared; every reveal/arm path now checks the new app_active register plus the owning window's key state, while the focus provenance and stored pointer stay preserved for the next honest transition.
- The frame promote disarms rather than defers under suppression, so re-key and re-activation can never spontaneously reveal.
- Sabotage-verified tests: deactivation-callback rebuild, non-key mount beneath a hovered trigger, and the reworked two-window key-loss fixture.

* Teach the Native UI guide the anchored tooltip's hover intent

- The guide predated anchored tooltips: it scoped the anchored-floating family to dropdown-menu alone and described tooltips as static leaves, contradicting the component page and skill.
- The element table row and the layout-attributes paragraph now carry the runtime-owned hover-intent summary: 600ms dwell, 400ms warm window, immediate focus-visible reveal, and tooltip-delay (0 = instant, a teaching error without anchor).

* Scope anchored-surface lookups so a tooltip never shadows the menu

- A stack can now anchor several surfaces at once (dropdown-menu + tooltip), but the menu lookups still grabbed "the anchored child" and kind-checked the winner: a focus-visible tooltip mounted after the menu made ArrowUp/Down unable to walk into the open menu and Tab departure unable to close it.
- Every consumer of the anchored-child walk now names its population: Escape and automation dismiss keep .any (topmost-first, one surface per gesture), outside-click dismissal and Tab focus scoping take .interactive (tooltips are hover chrome the intent machine owns), and the open-select keymap plus Tab departure take .menu — the scope filters during the scan, never after selection.
- Coexistence tests pin the keymap against a select + open dropdown-menu + focus-shown tooltip stack: arrows enter the menu beneath the tooltip, Tab departure dismisses the menu and leaves the tooltip alone, and Escape peels one surface per press, topmost (tooltip) first.

* Say precisely which tooltip hide opens the warm window

- The runtime warms only when a pointer-hovered tooltip hides on leave — focus departure, Escape, press, blur, and prune are each a deliberate no-warm decision in canvas_widget_events.zig — but the docs claimed "any tooltip hides", contradicting the tooltip page's own next paragraph.
- Corrected the tooltip page, the Native UI guide table, the changelog fragment, the native-ui skill card, and the markup doc strings; regenerated component-vocab.json from them (webp previews untouched).

* Observe window key-loss on the focused flag's own edge

- Windows and GTK announce key changes loss-first: the state echo wrote focused=false directly, so the later gain's dethroning loop saw the old window already unfocused and the tooltip key-loss reset never fired — the tooltip stayed painted and a11y-visible in the inactive window.
- setWindowFocused is now the one writer of a tracked window's focused flag (setFocusedIndex, applyNativeInfo, and updateWindowState all route through it); the reset fires on the flag's true→false edge regardless of event ordering, including a loss with no subsequent gain.
- closeWindow's transactional flip stays outside the seam deliberately: its views are removed with the window on success, and the rollback on platform failure must not have fired a reset.

* Spend the standing focus reveal on explicit tooltip dismissal

- Keyboard activation and Escape preserved keyboard provenance, so the adoption binding-reconcile treated the ring as standing intent: an activation whose own model rebuild replaced or rekeyed the tooltip resurrected it one frame after dismissal, contradicting "stays down while focus rests on the trigger".
- Both dismissal seams now consume canvas_widget_focus_visible_keyboard when the ring rests on the dismissed tooltip's owner — safe because its only readers are the two tooltip reveal gates, while the focus ring renders from canvas_widget_focus_visible_id and stays painted; Tab away and back re-grants the contract at the one provenance write, and pointer hover re-earns its dwell untouched.

* Say which hide warms in the token and state doc comments

- The two source doc comments still claimed ANY tooltip hide opens the warm window; only a pointer-hovered tooltip hiding on pointer leave warms, matching the implementation and user docs.

---------

Co-authored-by: Marcus Schiesser <17126+marcusschiesser@users.noreply.github.com>
2026-07-16 22:12:50 -05:00
Chris Tate eefa3690c9 Polish the live-smoke ledger: grid slots, dark focus ring, system monitor states (#128)
* Keep declared grid column slots when children run short

- gridColumnCount no longer clamps the declared column count to the child count, so a filtered grid's children keep their column-slot width instead of stretching across the freed row
- pin the rule in the widget layout tests and in both soundboard e2e batteries (Zig example + TS port), which search the album grid down to one match and hold the tile at its natural width

* Desaturate the accent focus ring in dark appearance

- canvas.accentOverrides now takes the resolved scheme and derives the dark focus ring at half the accent's HSL saturation (canvas.accentFocusRing), matching the built-in packs' per-scheme ring moves
- the Zig soundboard theme states the same derivation so both authoring tiers land the identical ring; pins in the token tests and both soundboard suites

* Polish the system monitor footer and empty state in both tiers

- label the footer sample time UTC: local rendering would need a journaled tz channel to keep replay byte-identical, so the honest label wins for this sweep
- clear the transient 'terminate request delivered' notice on the next applied sample (failure notes still stick); pinned in both e2e batteries
- hint the top-128-by-CPU search scope in the no-match empty state, derived from the sampler cap

* Note the per-scheme accent ring in the soundboard-ts README

- the theme_accent bullet now names the dark scheme's desaturated focus-ring derivation

* Floor the dark accent ring at 3:1 and harden grid row math

- accentFocusRing's dark step now lifts the desaturated ring's HSL lightness until it holds 3:1 (WCAG non-text) against the default dark background whenever the accent itself cleared that bar, and never below the accent's own contrast when it did not — halving saturation alone dropped #008000 from 3.9:1 to ~2.6:1; a hue-sweep test pins the floor, and the changelog fragment now states the accentOverrides ColorScheme parameter as a deliberate pre-1.0 break with the .light migration line.
- gridRowCount ceil-divides as 1 + (count - 1) / columns so an unclamped huge declared column count no longer overflows the additive form in safe builds; both the layout and intrinsic-size paths already share the helper, and a unit test covers maxInt columns and zero children.

* Floor the accent ring on the lightest adjacent dark tone and finish the grid row sweep

- accentFocusRing's dark floor now measures against the lightest tone controls commonly sit on (house surface_subtle #262626) instead of the background, so rings drawn on cards and muted chrome clear 3:1 too — green's ring rises from ~2.72:1 to 3.49:1 on the dark surface, and the never-invent escape hatch caps at the accent's own contrast against that same reference
- Route intrinsicGridChildrenSize's row count through gridRowCount: the additive ceil-div still panicked when a parent intrinsically measured a nonempty grid with maxInt columns (the earlier fix only covered the placement path)
- Extend the hue sweep to assert 3:1 per adjacent tone across both packs and add the nested-grid intrinsic regression (verified to panic before the fix)

* Gate the transient-note clear on a sample launched after the kill

- A ps sample already in flight when the kill confirmed collected its rows before the signal, so its ps_done must not retire the delivered notice; both cores now bump a sample generation at launch and clear only when an applied sample's generation exceeds the kill_done stamp (pure Msg-driven state, replay-deterministic)
- Pin the race in both batteries: the stale in-flight sample applies with the notice surviving into the rendered footer (kill exit and stale ps exit drained in one batch), and the first sample launched after the kill retires it
- Rework the live kill-note pins to drive real launches through the cadence so delivered-clears and failure-persists keep their semantics under the generation gate
2026-07-16 11:01:50 -05:00
Chris Tate 4d83ce2f0d Derive every editor mutation through one seam so Escape reaches your core (#129)
* Derive every keyboard editor mutation through one stamped-edit seam

- updateCanvasWidgetTextFromKeyboard now stamps the edit it derives and applies onto the routed event, so the app dispatch hears exactly what the retained editor did — Escape's search-field clear, its composition cancel, and the single-line ArrowUp/Down caret jumps were runtime-only before and never reached the model.
- The app-side msgForKeyboard consumes the stamped edit first and keeps its own derivation only as the fallback for events that never crossed the runtime.
- The context-menu edit path routes through the same seam and dispatches the stamped event.

* Route automation composition verbs through the real ime input path

- widget-action set_composition/commit_composition/cancel_composition now dispatch the ime gpu input events a live IME session produces: journaled for replay, applied by the editor, and mirrored to the core's on_input — direct editor writes kept the model out of the loop.
- set_selection synthesizes the stamped keyboard event the clipboard edits use, so the core's selection mirror follows; it stays un-journaled (no selection input kind exists on the wire).
- The accessibility action dispatch and the mobile widget-action ABI funnel through the same verbs, so all three surfaces converge.

* Pin the edit-derivation seam across mirror, replay, and both tiers

- ui_app tests drive a search field through Escape, ArrowUp, ime cancel, and the automation composition/selection verbs, asserting the model mirror and the retained editor agree.
- The reference record/replay session types into a search field and Escape-clears it, so replay must re-derive the same clear from the raw journaled events.
- Soundboard e2e batteries (Zig live app and transpiled TS core) pin Escape clearing the query and unfiltering the library.

* Add changelog fragment for the Escape edit-derivation fix

- User-facing story: Escape in a search field now reaches your core, plus the automation composition verbs riding the real input path.

* Journal accessibility actions outer-wins so replay dispatches once

- Suppress event records staged while a journaled widget_accessibility_action dispatch is on the recorder's staging stack: the verb's synthesized key/text/drop children are deterministic derivations the replayed action re-runs, so recording both dispatched every child twice on replay.
- Effect results still write through mid-action in feed-then-dispatch order, and event_count/checkpoint ordinals stay coherent with the records a replay reader actually sees.
- Pin the class end to end: repeated AX press and set_text plus the composition verbs record and replay with exactly-once input counts and one journal record per action.

* Journal direct-surface accessibility verbs as outer-wins action records

- Stage a synthetic widget_accessibility_action inside dispatchCanvasWidgetAccessibilityAction when no platform tag-23 event is on the recorder's staging stack: the direct verb surfaces (embed widgetAction, automation widget_action — now delegated through the same dispatch) journaled only untargeted focus-routed children while the verb's focus write stayed unjournaled, so a first-in-session composition or set_text replayed against the wrong editor or none at all; nested inside a staged platform AX event the synthetic stage is a suppressed placeholder, keeping exactly one record.
- Add the composition kinds to the platform WidgetAccessibilityActionKind enum (values 11-13, additive) with both widget_bridge mappings, so the journal and replay's tag-23 arm carry every verb.
- Pin the class: composition first-in-session, composition while another field holds focus, and set_text first-in-session all record action-only journals and replay onto their target editor; a recorder mechanics test pins the nested placeholder.

* Suppress the open arrow's caret edit and bump the journal to v4

- A CLOSED combobox's ArrowUp/Down are its open keys: the app dispatch resolves the press before any stamped edit, so the caret derivation now yields no edit there and the retained editor agrees with the model mirror; an open picker's arrows already walk into the mounted menu, and an expanded trigger without one keeps the caret jump both sides hear.
- Bump the session journal format to v4: this branch serializes the composition action codes 11-13 into accessibility-action records, which a v3 reader would have called corrupt instead of refusing as version skew; the skew message now names the version this build reads, and the skew test pins refusal of older journals too.
- Rewrite the journal's stale coverage note: every automation verb journals now (direct-surface verbs as the outer-wins widget_accessibility_action record), so the v1 do-not-journal caveat is gone.
2026-07-16 10:00:35 -05:00
Chris Tate f7aa92af6d Prepare v0.5.1 release (#127)
- Bump CLI, core, platform, and example versions to 0.5.1
- Merge pending changelog fragments into the marked release entry
- Refresh exact package pins for reproducible publishing
2026-07-13 14:17:59 -05:00
Chris Tate 4b9d40b871 Carry the TypeScript toolchain as a CLI dependency (#123)
* Carry the transpiler's TypeScript toolchain as exactly-pinned CLI dependencies

- @native-sdk/cli gains @typescript/typescript6 6.0.2 and @typescript/old npm:typescript@6.0.3 as regular dependencies, so npm installs the whole toolchain (the wrapper AND the real compiler it re-exports) in the same transaction as the CLI
- packages/core pins the same two exact versions as devDependencies (replacing the ^6.0.2 range) and its lockfile follows
- check-version-sync asserts both pins string-equal across the two manifests and shape-exact: X.Y.Z for the wrapper, npm:typescript@X.Y.Z for the alias

* Gate TS verbs and direct zig build on toolchain resolution, teach per layout

- transpilerResolves walks node's ancestor node_modules from packages/core and requires the wrapper's manifest + entrypoint AND the aliased real compiler resolving from the wrapper's own directory (nested, hoisted, and global layouts alike); partial extractions read as unresolved
- the gate runs before any zig spawn (check, dev --core, build-graph verbs) and never runs npm: checkouts (the packages/core/test signal) are taught the one npm ci --include=dev against a resolved absolute path, npm layouts are taught the reinstall (BrokenToolchainInstall)
- build/app.zig carries the twin predicate for direct zig build and fails configure with a clean teaching instead of a panic; test-ts-toolchain-twins pins the twins' alias probes and teachings in lockstep

* Add the toolchain-as-dependency changelog fragment

- first TS-verb use needs no network, no install step, and never runs npm
- repo checkouts are taught the one npm ci command; direct zig build teaches instead of panicking

* Exit quietly on the broken-install teaching

- BrokenToolchainInstall already prints its reinstall guidance; list it in failVerb's expected-error switch so the CLI exits without a Zig error-return trace.

* Scope the pre-spawn TS toolchain gate to CLI-generated graphs

- Ejected apps pin their own SDK in build.zig.zon, so gating the CLI's
  resolved SDK false-failed healthy apps whose direct zig build works;
  they now flow to the spawn, where build/app.zig's tsCoreStage teaching
  names the app's actual dependency SDK.
- Name the one generated-vs-ejected decision (isEjectedAt) so the
  preflight and the argv assembly share a single predicate, and keep
  check/dev --core gated: they always transpile against the CLI's SDK.
- Cover both paths in a verbs test: an ejected-shaped TS layout skips
  the gate against a toolchain-less SDK; the generated layout still
  teaches, and passes once the toolchain resolves.

* Raise the TypeScript-tier node floor to 22.15 with a fail-fast teaching

- ts_run.mjs now teaches "upgrade to Node.js 22.15+" and exits before importing a node_modules-resident target when module.registerHooks is missing, instead of dying inside node with the raw ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING; repo-checkout targets keep running natively on any Node 22.
- Every "Node.js 22+" surface (nodeMissing teaching, build/app.zig panic, scaffold README, quick-start docs, changelog fragment) now names 22.15.
- packages/core/test/ts_run.test.ts pins both tiers by spawning the runner with a --import preload that deletes module.registerHooks, plus the real hook-stripping path against the package's own toolchain install.

* Strip every .ts through the runner hook and pin-check the resolved compiler

- build/ts_run.mjs now strips ALL .ts modules (node's default stripping is 22.18+, so the 22.15-22.17 checkout fall-through died raw); hooks-absent teaches for any .ts target, and the hook requires @typescript/old directly with a sane direct-run teaching when the dev install is missing
- typed_ast.ts imports @typescript/old instead of the wrapper, so a consumer tree's conflicting hoisted copy can never shadow the SDK's exact pin (the wrapper stays the declared dependency)
- both resolution twins (ts_core.zig transpilerResolution, build/app.zig tsToolchainResolution) read the resolved @typescript/old version and hold it against the npm:typescript@X.Y.Z pin parsed from packages/core/package.json, teaching resolved-vs-pinned on mismatch; fixtures gain version/pin manifests and mismatch tests cover both layouts

* Validate the compiler from the packages/core origin runtime resolves from

- Both resolution twins now walk @typescript/old from packages/core — the origin typed_ast.ts and ts_run.mjs actually load from — instead of holding the wrapper's origin against the pin, which false-rejected npm's own conflict shape (consumer's hoisted compiler + our exact pin nested under the CLI); the unused @typescript/typescript6 wrapper is no longer probed at all, though it stays a declared dependency.
- Make the node requirement branch-aware everywhere it is taught (22.15+ admits 23.0-23.4 numerically, but registerHooks only landed in 22.15 and 23.5): one phrasing across ts_run.mjs, both gate teachings, the templates README, the quick-start docs, and the changelog fragment.
- Correct the changelog fragment's claim that repo checkouts run on any Node 22: every .ts module rides the same registerHooks stripping, so the floor is uniform across layouts.
2026-07-13 14:00:54 -05:00
Chris Tate 6915fc440a Join effect worker threads before teardown frees the channel (#124)
* Join effect worker threads before teardown frees the channel

- Store spawn/fetch/file worker handles on their slots instead of detaching, join them in reclaim and unconditionally in deinit: the old ~5s give-up abandoned workers still holding slot/queue/io pointers into memory the owner frees right after, which is how a torn-down harness segfaulted the next test inside a stale slot's child_mutex.
- Spawn each child into its own POSIX process group and kill the group on cancel/teardown, so shell-wrapped commands' grandchildren cannot hold the stdout pipe open and stall the worker (and now the join) past the cancel.
- Regression tests: teardown mid-stream joins and returns promptly with no running slots, and an 8-round teardown storm (immediate and cancel-racing) over recycled harness memory.

* Make the ts-core e2e battery tolerant of congested CI runners

- Scale the host battery's real-child wait budget 10x (20s -> 200s): the waits prove correctness and poll, so a healthy run still returns in milliseconds while a loaded runner gets the slack it needs to schedule and reap /bin/sh children.
- A blown wait budget now tears the effects channel down (kill children, join workers) before surfacing TestTimedOut, so one slow test can never cascade a straggling child into the next test's harness.
- The soundboard dispatch-latency test asserts the best of three attempts with unchanged budgets: scheduler contention only adds time, so real regressions still fail while parallel-suite noise no longer does.

* Bound file-worker teardown: interrupt, then abandon-and-leak, never hang

- Effects.deinit joined every worker unconditionally, but a file worker blocked in I/O that nothing converges (a write to a FIFO with no reader, a stalled network filesystem) made teardown hang forever behind it; file workers now get an injectable budget (file_join_deadline_ms, 15s default) with a best-effort cancel of the blocked task at the halfway mark, and past it teardown detaches the thread, warns once naming the stuck op and path, and deliberately leaks everything the worker can still reach (its context, its data buffer, the executor io) so the owner can free the channel safely. Spawn and fetch joins stay unconditional.
- The blocking phase moved out of the channel: each file worker supervises its op as a cancelable Io task (mirroring fetchWorkerMain, which also supplies the platform interruption: SIG.IO signaling on POSIX, NtCancelSynchronousIoFile on Windows, via the threaded io) against a heap FileWorkerContext holding the path copy, buffer, and a commit/abandon handshake - the worker only touches the slot and queue after committing under the context mutex, so an abandoned worker that wakes later walks only leaked memory.
- Regression tests: a write against a reader-less FIFO is abandoned within a tiny injected deadline (loud counter, healthy process after, the woken worker exercised under the leak invariant), the default interruption path joins the same posture with no leak, and the happy-path teardown pins the abandon counter at zero.

* Move the abandonable leak into process-lifetime storage and reject uncancelable fetches

- Allocate FileWorkerContext, its private data buffer, and the shared IoThreaded executor from a process-lifetime allocator (page_allocator) so an abandoned worker never walks memory that dies with the owner's arena or GPA; the happy path frees all three through the same seam (joinWorker, deinit), and a committed read publishes its bytes into the slot's channel-owned delivery buffer.
- Refuse a fetch whose exchange cannot start as a cancelable task instead of running it inline: an inline exchange observes neither cancel nor the timeout and would hang deinit's unconditional fetch join, so the honest terminal is one journaled .rejected (replay reproduces it like any transport failure), with an injectable fetch_concurrent_start seam and a warn-once counter.
- Pin both with tests: an arena-backed channel abandons a FIFO-stuck worker, dies, and the woken worker walks only process-lived memory (leak-checked happy path alongside); the fetch rejection seam delivers exactly one .rejected and teardown returns promptly.

* Bound spawn-worker teardown: interrupt, then abandon-and-leak, never hang

- Group-kill cannot guarantee spawn convergence: a descendant that leaves the child's process group (setsid, a shell's set -m background job) keeps the inherited stdout write end open, so the worker's read never sees EOF and deinit's unconditional join hung forever; spawn workers now get the file workers' full discipline — an injectable budget (spawn_join_deadline_ms, same 15s default) with a best-effort cancel of the blocked task at the halfway mark, then a detach-warn-and-leak abandon with its own abandoned_spawn_workers counter, so spawn, fetch, and file teardown all share one terminal guarantee: bounded return, and every byte a live thread can still touch stays valid forever (Windows, where the direct-handle terminate never reached descendants, is bounded by the same net; job objects remain the future strengthening).
- The blocking phase's world moved out of the channel into a process-lived SpawnWorkerContext (argv/stdin copies, the published-child kill handshake, private framing/collect buffers, the stderr tail ring, drop accounting), supervised as a cancelable Io task exactly like file ops; unlike a file op a spawn DELIVERS while it blocks, so streaming lines enqueue under the context's abandon fence (produceSpawnLine re-checks the abandon with every channel touch) and the committed epilogue publishes collect payloads into the slot's channel-owned delivery buffer, preserving cancel semantics, the stale-event window, and record/replay byte-identity.
- Regression tests pin the escaped-descendant shape (/bin/bash -c 'set -m; sleep 300 & echo $!' — portable setsid): the default interruption path joins it with no leak, the disabled-interruption path abandons it within a tiny injected deadline (loud counter, healthy process after, the woken worker proven to reap its zombie child through only leaked memory), the arena-lifetime test wakes an abandoned worker after the owner's allocator died, and the existing mid-stream teardown pins abandon-count zero.
2026-07-13 10:25:13 -05:00
Chris Tate 8e37536acb Windows fixes: caption clearance, GUI subsystem, elision budget, dark titlebars (#122)
* Hand the full snap quantum back to the text wrap/elision budget

- Edge snapping rounds each frame edge independently, so an exact-fit text frame at a fractional position can lose up to a FULL device pixel of width — the old 0.5/scale hand-back plus the 0.125 elision slack under-covered it, and the TS scaffold's centered counter digit painted as "…" on Windows at scale 1
- textWrapMaxWidth now returns width + 1/scale with the epsilon policy documented at the seam; regression tests sweep fractional origins for every digit at scales 1/1.25/1.5/2 plus the exact centered-row scaffold shape

* Reserve the Windows caption cluster in drag-header layout

- A window-drag header that never consumed the chrome channel's trailing inset laid right-aligned content under the DWM min/max/close cluster; the caption punch-out then visibly truncated it (system-monitor-ts's header status)
- The runtime now runs a one-retry clearance pass: when the built layout leaves drag-header CONTENT under the platform-reported control cluster, it stamps the cluster into DesignTokens.window_controls and rebuilds once, and widget layout trims the drag row's content box on the cluster's side - apps that already pad (soundboard) never trigger and keep byte-identical layout
- Regression: engine tests for both cluster sides and the content-only trigger, plus runtime tests faking Windows-shaped chrome through the null platform for the naive and the contract header shapes

* Give Windows release exes the GUI subsystem and pin it at packaging

- Every app exe was console-subsystem (zig's default), so dev AND packaged apps flashed a terminal behind the window; the posture is now: Debug keeps the console (dev logs live there), release-shaped builds get /SUBSYSTEM:WINDOWS - redirected logging still works because only console AUTO-allocation is subsystem-gated
- native package reads the wrapped exe's PE subsystem (peSubsystem), warns with the rebuild teaching when it wraps a console binary, and reports it in PackageStats.windows_console_subsystem; tests pin the reader and both packaging outcomes over synthetic PE headers
- Verified on the Windows box: ReleaseFast ts-smoke.exe reads subsystem=2, launches headed with no console, automation and file-redirected logs intact

* Dark-mode titlebars on Windows and a kinder scaffold empty state

- Standard-chrome windows set DWMWA_USE_IMMERSIVE_DARK_MODE from the OS app scheme at creation (pre-show, no light-caption flash) and on appearance broadcasts; hidden-titlebar windows keep their pixel-sampled caption fidelity
- The TS scaffold's status bar branches on {stampedMs < 0} to say "press Stamp for a timestamp" instead of "stamped: -1ms"; fresh scaffold verified through native check and the ts-core e2e scaffold pins

* Emit the GUI-subsystem posture into the scaffold's standalone build.zig

- The web-frontend scaffold (Next/Vite/React/Svelte/Vue, native init --full) generates its own build.zig and never set exe.subsystem, so those apps still shipped console-subsystem Windows release exes - and the packaging warn had no remedy, since rebuilding can't fix a scaffold that never sets it
- The generated build.zig now carries the same release-only assignment as the SDK build graph (Debug keeps the console for dev logs), with the condensed rationale
- The Vite template test pins the emitted condition and assignment lines

* Resolve a drag header's anchored children against the cleared rect

- The collision scan counts anchored descendants of the drag header, but the remedy only trimmed the flow content box - an anchored floater triggered the one retry and then stayed under the caption cluster (retry paid, nothing moved)
- layoutWidgetDepth now passes the anchor base through windowControlsClearedContent for window_drag widgets only, covering both the trailing (Windows) and leading (macOS) clusters; non-drag widgets' anchored children are untouched by construction
- Engine test pins the anchored-only collision converging in one pass (scan fires, remedy moves it, re-scan stays quiet), the macOS mirror, and byte-identical non-drag anchoring under stamped tokens

* Run the window-control clearance retry in secondary windows too

- rebuildWindowSlot laid out exactly once, so a model-declared hidden-inset window's drag header still rendered content under the OS caption cluster; it now runs the same collision scan + one-retry pass as the main rebuild, stamping the cluster into a local copy of the slot's tokens.
- windowControlsReservation takes the canvas label so both rebuild paths share it, and the slot's build+layout pass is factored into buildWindowSlotPass (same arena reuse as the main path's buildLayoutPass retry).
- ui_app_window_tests: a colliding secondary drag header re-lays clear of the cluster while the main canvas stays unstamped, and a padded secondary header keeps its own layout.

* Read only the PE headers for the packaging subsystem check

- peIsConsoleSubsystem slurped the whole exe through readPath (capped at 128 MiB) and swallowed every error as false, so a console exe over the cap packaged without the promised warning and packaging allocated the entire binary to read 2 bytes.
- peSubsystemAtPath reads the 0x40-byte DOS header, bounds the rest to e_lfanew + 94 bytes through the existing pure peSubsystem parser, and rejects e_lfanew past a 1 MiB ceiling as not-a-real-PE; only genuinely-unreadable files still degrade to no-claim.
- Tests: a sparse console exe past the old cap still warns, and a bogus 8 MiB e_lfanew answers nothing under the failing allocator (no full-file or offset-sized allocation).

* Persist the subsystem verdict in the report and stats summary

- package-manifest.zon now carries .subsystem = "gui"/"console" whenever the posture check ran (a Windows package with a binary), threaded through writeReport like asset_count; other targets make no claim
- the stats summary prints "subsystem: gui" or "subsystem: console (a terminal window opens behind the app - rebuild with `native build`)" alongside the web-layer and signing verdicts
- PackageStats.windows_console_subsystem is now a ternary (?bool) so an unprobed package cannot pass for a GUI verdict; the synthetic-PE tests pin both report fields and the no-binary null

* Stop claiming a gui subsystem the PE parse never established

- Replace the bool verdict with WindowsSubsystem { gui, console, unknown }: gui and console only when the optional header said so, unknown for non-PE/truncated bytes, benign read errors, and unmodeled subsystem values; null still means the check never ran
- Diagnostic prints "subsystem: unknown (unrecognized executable format)" and the report writes .subsystem = "unknown" instead of affirming gui; console warning behavior and OutOfMemory propagation unchanged
- Pin unknown for a non-PE file, a truncated PE, and a native-subsystem exe; retarget the sparse-oversized and offset-ceiling probes onto the verdict helper

* Package a release-shaped exe from the web-frontend scaffold

- The emitted build.zig defaulted -Doptimize to Debug for everything, so the documented `zig build package` wrapped a Debug, console-subsystem exe; the package step now builds its own exe that defaults to ReleaseFast (mirroring `native build`) while an explicit -Doptimize or --release still pins both roles.
- Registers -Doptimize by hand instead of standardOptimizeOption so the graph can tell unset from explicit, the same optimizeMode split build/app.zig uses; the --optimize arg, the artifact name, and the Windows GUI-subsystem posture all key on the package exe's actual mode.
- Extends the Vite template pins to the package-exe shape and forbids the stale dev-exe wiring.

* Judge drag-header text by its painted bounds in the caption scan

- The collision scan intersected each candidate's full frame with the caption cluster, so a grow/stretch centered title spanning the header row false-positived while its glyphs sat clear — and the paid retry visibly shifted the title.
- Single-line text leaves now intersect their aligned painted bounds: measured width through the same tokens seam the layout ran with, capped at the frame, placed per text_alignment; controls keep the frame test, and span paragraphs or explicit newlines fall back to it conservatively.
- Threads tokens from both windowControlsReservation call sites, pins the centered-title no-retry case at engine and runtime level, and keeps the trailing true-positive, the macOS leading mirror, and post-remedy re-scan convergence green.
2026-07-13 09:00:17 -05:00
Chris Tate 07b259f4f3 Give @native-sdk/core the provenance repository metadata npm requires (#121)
- npm publish --provenance rejected @native-sdk/core@0.5.0: the manifest carried no repository.url to validate against the workflow's repository. Add the repository (with the monorepo directory) and homepage, matching the CLI package.

- check-version-sync now pins packages/core repository.url and homepage to the main package, exactly as it already did for the eight platform packages, and the package-manifest suite pins the fields as publish contract.
2026-07-12 22:54:05 -05:00
Chris Tate e2627ee07f Prepare v0.5.0 release (#120)
- Bump the CLI, platform packages, @native-sdk/core, and runtime version to 0.5.0, and drop the private flag from @native-sdk/core — the release workflow publishes it from this version on.

- Fold the pending changelog fragments into the marked v0.5.0 entry: TypeScript authoring (#119) plus the signing (#118) and static-TLS (#117) fixes.

- Credit co-authors from the release range in the v0.5.0 contributor list.
2026-07-12 22:45:05 -05:00
Chris Tate 584dbbbaa9 TypeScript authoring: write app cores in TypeScript (#119)
* TypeScript authoring: write app cores in TypeScript

- App cores can be authored in TypeScript and compiled ahead of time to arena-backed native code: the complete language minus the ecosystem and purity violations, checked by tsc plus a teaching checker (NS1001-NS1060), emitting readable Zig with 83ns dispatch, no JS engine, and no GC
- The full platform surface reaches TS cores: the Cmd and Sub effects vocabulary bridged to the real engine, markup views binding the committed model, record and replay byte-identical to node semantics, stock-IDE support, multi-file cores with @native-sdk/core library modules, and native init scaffolding TypeScript by default with Zig first-class by choice
- Two showcase ports prove the bar with zero hand-written Zig: soundboard-ts at pixel parity with its Zig original and system-monitor-ts sampling the real OS, each with end-to-end batteries including replayed sessions with zero host calls
- Docs lead TypeScript-first with a segmented language toggle and a markup-first components reference; the eval suite gains dual-track realistic cases measuring both authoring tiers' health and efficiency

* Ship packages/core in the npm package and run its .ts modules from any layout

- copy-framework.js stages the @native-sdk/core closure (src/, sdk/, rt/, package.json + package-lock.json; test/ and scripts/ stay out), the sync check pins each staged entry plus the dep.path coverage, and package.json "files" covers the mirrored paths
- build/ts_run.mjs runs the transpiler tier's .ts modules on every layout: node refuses builtin type stripping under node_modules, so the runner strips those modules with the transpiler's own installed TypeScript and passes repo checkouts through untouched; build/app.zig, native check, and native dev --core all invoke through it
- the missing-dependency teaching now names the real dependency root (works verbatim on the npm-installed layout, where npm ci runs in the shipped packages/core against its shipped lockfile)

* TS scaffolds ship a CI workflow

- the --full ts-core template now writes the Zig full template's workflow (logic tests + Linux automation smoke, no WebKitGTK) with the node tier added to both jobs: setup-node and one npm ci in the fetched SDK's packages/core, the same install native build's teaching names
- slim scaffolds keep shipping no workflow (zero-config parity with the slim Zig template), now pinned by the ts slim template test

* Wire @native-sdk/core into the release automation

- sync-version.js stamps packages/core (manifest + lockfile own-package fields) and the committed TS examples' pins with the CLI release version, check-version-sync.js refuses a half-bumped tree, and the npm version script stages the stamped files; packages/core rides 0.4.4 from here on and scaffold pins follow the bundled manifest automatically
- the release publish step gains the packages/core publish gated on its "private" flag: private (until the 0.5.0 cut, by design) skips with a loud flip-requirement comment; dropping the flag is the publish switch, no workflow edit needed
- the TS scaffold README notes npm install is optional (the CLI materializes and refreshes the editor package itself), closing the pre-publish gap window honestly

* Provide node to the CI jobs that build TypeScript cores

- The Native Examples job panicked on the missing transpiler dependency, and the Zig Core and tooling jobs were silently skipping every node-gated ts-core suite; all three now set up node and npm ci packages/core
2026-07-12 21:37:07 -05:00
Chris Tate 2d623ac4f6 Move large per-thread canvas scratch out of static TLS (#117)
* Move large per-thread canvas scratch out of static TLS

- Add canvas.lazy_tls.LazyTls: per-thread scratch behind one TLS pointer, heap-allocated and default-initialized on a thread's first use
- Convert the planner/diff/cache scratch giants (advance cache, span wrap cache, frame planner arrays, image decode buffer, probe tables) to lazy per-thread state; only threads that actually plan frames pay for them
- Windows cloned the full static TLS template per thread (~6.5 MiB x every window-host/COM/accessibility/worker thread); the template now carries pointers instead

Co-authored-by: SunkenInTime <76637177+SunkenInTime@users.noreply.github.com>

* Add changelog fragment for the static-TLS working-set fix

- Working-set drop, .tls shrink, and smaller executables, told from the user's side

---------

Co-authored-by: SunkenInTime <76637177+SunkenInTime@users.noreply.github.com>
2026-07-12 19:38:07 -05:00
Chris Tate 87d859fd4f Fail packaging loudly when codesign fails and sign spaced output paths (#118)
- codesign/ditto/notarytool/stapler now exec argv arrays instead of sh -c strings, so bundle paths, identities, and entitlements with spaces reach each tool as one argument
- a signing mode that claims to sign (adhoc or identity) either delivers a signature that passes codesign --verify --deep --strict or fails the package with codesign's own stderr; the report proves the outcome with a "signing: <mode> (signed, verified)" line
- tests pin spaced-path adhoc packaging end to end on darwin hosts, the loud failure paths, and every pipeline argv shape

Co-authored-by: sepehr-safari <25853688+sepehr-safari@users.noreply.github.com>
2026-07-12 19:32:21 -05:00
Chris Tate ce3e42dfd5 Prepare v0.4.4 release (#111)
- Bump the CLI, platform packages, and runtime version to 0.4.4.

- Fold the v0.4.4 release notes for #105, #106, #107, and #110 into the marked changelog entry.

- Credit co-authors in the v0.4.4 release notes and repair the v0.4.3 contributor list.
2026-07-11 13:20:26 -05:00
Chris Tate 49aa5e7481 Native-only Linux host: compile out WebKitGTK when no web intent is declared (#110)
* Compile the GTK host without WebKitGTK for native-only apps

- NATIVE_SDK_ALLOW_WEBKITGTK_STUB mirrors the Windows WebView2 seam: the define wins over header visibility, compiles out every WebKit-touching path (opaque never-non-NULL web-view pointers keep the GTK-only bookkeeping shape), and stubs the exported webview entry points
- Both Linux build graphs (build/app.zig and the generated template) compile gtk_host.c with the stub and drop the webkitgtk-6.0 link when the web layer is excluded; the native scaffold's generated CI stops installing libwebkitgtk-6.0-dev

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>

* Audit the web layer in Linux ELF executables

- tools/audit_web_layer.zig auto-detects PE vs ELF and hand-rolls an ELF64 reader over the section headers: DT_NEEDED entries naming libwebkitgtk/libjavascriptcoregtk and webkit_/jsc_ dynamic symbols are the evidence, refusal (never a pass) for anything it cannot parse
- native package grows the ELF twin of the PE guard: a WebKitGTK-linking binary packaged under a native-only decision is refused with the same teaching message, pinned by synthetic-ELF tests covering both evidence channels

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>

* Prove the Linux seam in CI from both directions

- linux-webkitgtk gains test-linux-web-layer-audit (seam under webkit-PRESENT conditions: native-only ELF scans clean, web ELF keeps its references) and linux-canvas-smoke drops libwebkitgtk-6.0-dev so the build itself is the native-only link test, with the ELF audit run on the real binary
- vendor pins keep the stub define wired through gtk_host.c and both build graphs; the macOS gpu-dashboard smoke asserts a native-only session spawns zero new WebKit helper processes

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>

* Pin native-only behavior: automation sessions, record/replay, docs

- An automation-driven session on a native-only canvas app proves normal command dispatch plus the WebViewLayerNotBuilt teaching error on a webview attempt, with zero webviews reaching the platform host
- The session record/replay reference journal round-trips identically under web_layer=false; the capabilities and app.zon pages note the user-visible Linux consequence (no WebKitGTK to build or run)

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>

* Forward the web engine to native package from the SDK build graph

- A Chromium exe packaged through zig build package shipped no CEF runtime because the CLI defaults to the system engine; forward --web-engine, --cef-dir, and --cef-auto-install exactly as the generated build graph already does

---------

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>
2026-07-11 12:53:47 -05:00
Chris Tate 9b4f62d040 Infer the web layer and enforce native-only builds end to end (#107)
* Infer the web layer and enforce native-only builds end to end

- The build graph parses app.zon and strips the Windows webview layer, loader staging, and dev PATH wiring when nothing declares web use; a webview_layer manifest field and -Dweb-layer flag override inference in both directions
- Conflicting declarations are rejected with one teaching message at validate, configure, runner compile, and package time, and a native-only build that reaches webview creation fails fast with WebViewLayerNotBuilt instead of a blank window
- A PE cross-audit build step pins that native-only Windows exes never reference the loader while webview apps must, and native check prints the web-layer verdict

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>

* Unify web-layer inference behind one shared contract

- app_manifest.web_layer owns the declaration scan, engine folding, and include/exclude decision, usable at comptime by the runner and at runtime by the build graph, validator, CLI, and generated scaffolds, with boundary ownership documented where each adapter lives
- Packaging decides from the resolved engine so --web-engine overrides cannot skew the layer, the runner guard covers shell views and manifest chromium, and the full scaffold emits the same inference, conflict panic, and conditional Windows wiring as the SDK graph
- A contract matrix test runs every manifest shape through every boundary form so the definitions can never diverge again

* Document the webview_layer override and Chromium web intent

- The capabilities page counts a Chromium-resolved engine as web intent and points at the override; the app.zon reference gains the webview_layer field, its inference and include/exclude semantics, and the exclude-conflict rule with the shipped error's remedy

* Carry the web-layer resolution into packaging

- Both build graphs forward their computed web-layer decision to native package via a new --web-layer flag, so the exe and the package can never disagree; a confirming flag keeps the manifest's reason while an overriding one names itself
- Packaging PE-scans Windows binaries and refuses to package a loader-referencing exe under a loaderless decision, closing the mismatch for hand-built binaries too
- Fixes an adjacent buildgraph bug where a sentinel-terminated path allocation was returned as a plain slice

* Honor the webview stub define before header visibility

- NATIVE_SDK_ALLOW_WEBVIEW2_STUB now excludes the embedded layer even when WebView2 headers are globally visible, so a native-only build can never reintroduce the loader reference; the vendor pins lock the guard order
- The stub message says the layer is excluded by configuration instead of claiming the header is missing

---------

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>
2026-07-11 11:12:12 -05:00
Chris Tate edbb2045b2 Create the GTK main webview lazily (#106)
- Window create builds only GTK chrome; the main WebKitWebView materializes on first web use with the zero scheme and bridge registration riding along, so canvas apps never start WebKit processes on Linux
- The canvas smoke now asserts zero WebKit processes and drops the sandbox workaround it existed for; child-webview bridge responses no longer require a main webview to exist

Co-authored-by: WhiteHades <44260523+WhiteHades@users.noreply.github.com>
2026-07-10 17:31:31 -05:00
Chris Tate d1852d5dc2 Teach the Zig 0.16 idioms agents and humans trip on (#105)
- New zig skill and a docs reference page pair each pre-0.16 habit with its verbatim compile error and the repo idiom that replaces it, all compile-verified against the pinned toolchain
- native build points at the guidance when a failed step's errors name missing std members
2026-07-10 17:23:59 -05:00
Chris Tate c5bb87afc2 Prepare v0.4.3 release (#96)
- Bump @native-sdk/cli and platform packages to 0.4.3.

- Mark the v0.4.3 changelog entry for #89, #90, and #92.

- Remove changelog fragments merged into the release notes.
2026-07-10 14:24:08 -05:00
Chris Tate 36d295ac5c Route cross-volume SDK dependencies through a junction on Windows (#92)
* Route cross-volume SDK dependencies through a junction on Windows

- A project and the npm-global SDK on different drives have no relative path, so the generated build graph now creates a .native/sdk directory junction and references the framework through it; same-volume layouts keep plain relative paths
- Junctions are created via the NT reparse API (no admin rights), refreshed idempotently on retarget or dangling, and never replace real directories; ejected and full-scaffold builds get a teaching error instead since the CLI cannot keep a junction fresh in user-owned build files

Co-authored-by: fleeting-zone <44354736+fleeting-zone@users.noreply.github.com>

* Tidy fallback ownership and route cross-volume errors in mobile packaging

- nativeDependencyPath dupes the dot fallback before freeing the empty relative path, so the errdefer owns it exactly once on every path
- package --target ios and android exit quietly on CrossVolumeFramework instead of dumping an error trace after the teaching text; a failed junction means the generated project cannot build, so no libraryless package is produced

---------

Co-authored-by: fleeting-zone <44354736+fleeting-zone@users.noreply.github.com>
2026-07-10 13:51:10 -05:00
Chris Tate 908e3deb58 Scroll and clip overflowing single-line text inputs (#90)
- Single-line fields clip their text, selection, and caret to the content rect when the value overflows, matching the textarea treatment
- A retained horizontal offset keeps the caret in view: edits, programmatic set-text, pointer-placed carets, and field resizes all re-run ensure-visible, and the offset rides the existing retained value channel so replay and schema pins are untouched

Co-authored-by: kvnwdev <47703820+kvnwdev@users.noreply.github.com>
2026-07-10 12:04:17 -05:00
Chris Tate e71338f872 Blend geometry edge coverage in linear light (#89)
- Anti-aliased fringes of opaque geometry (rounded rects, path fills and strokes) now composite in linear light through a LUT, removing the dark rim sRGB-space blending grows on curved edges; interiors stay byte-identical via an opaque fast path
- Glyph coverage and translucent sources keep sRGB blending so text weight and overlay brightness are unchanged, and tests pin the split in both directions
2026-07-10 08:26:57 -05:00
Chris Tate 20bc1eb6f3 Prepare v0.4.2 release (#88)
- Bump @native-sdk/cli and platform package pins to 0.4.2

- Mark the 0.4.2 changelog entry with release notes from the full post-0.4.1 range

- Merge and remove the pending WebView2 changelog fragment
2026-07-09 23:46:47 -05:00
Chris Tate 33da7101fa Stroke the checkbox mark through the vector core (#87)
* Stroke the checkbox mark through the vector core

- The check was two diagonal draw_line commands, and the line rasterizer's binary capsule test has no anti-aliasing, so the mark stair-stepped at every scale while the stroke icons around it rendered clean
- One stroked polyline with round caps and a round join now rasterizes the same shape with real coverage; a test pins the anti-aliased property so the mark can never regress to hard edges

* Own path elements in the builder and carry the stroke cap to the GPU host

- Path elements now live in builder-owned storage instead of threadlocal frame scratch, so commands from separately emitted trees can never alias each other's geometry; charts and the spinner adopt the same lifetime
- The stroke cap rides the packet command, both wire encodings (v4), and the fingerprint, and the AppKit host applies it plus the reference renderer's round join when stroking paths
2026-07-09 23:14:12 -05:00
Chris Tate 26df3687f4 Make the Windows embedded WebView layer real (#86)
* Make the Windows embedded WebView layer real

- Vendor the WebView2 SDK header and loader under third_party/webview2 (BSD-3-Clause, license preserved) so repo state alone compiles the embedded layer; a missing header is now a hard error instead of a silent stub
- Fix the conformance blockers behind the guard: a local WRL callback factory for mingw, the uncaptured bridge-handler variable, an EventToken shim, and STA COM initialization on the host thread
- Stage the arch-matched loader beside built, run, packaged, and scaffolded apps, mirror it into the npm payload, and pin the wiring with vendor and loader-layout tests

* Carry the SDK root through package shortcuts and generated builds

- packageShortcut and package-ios now pass the environ map into createPackage like the package verb, so NATIVE_SDK_PATH resolves the framework root for loader staging from standalone binaries
- Generated frontend builds export NATIVE_SDK_PATH on the package command and stage the loader dir on the dev command's PATH, mirroring the SDK-dependency graph
2026-07-09 21:48:41 -05:00
Chris Tate 512298b474 Anti-alias rounded primitives and adopt Per-Monitor V2 DPI on Windows (#81)
* Anti-alias rounded primitives and adopt Per-Monitor V2 DPI on Windows

- Rounded-rect fills and strokes render through one continuous signed-distance coverage field, so curved edges ramp smoothly with no silhouette drift; a supersampled ground-truth test pins shape fidelity and radius-0 rects stay bit-identical
- Hairline borders snap to whole device pixel columns at emit time and geometry snapping is on by default in the house and Geist packs, keeping 1px borders crisp while arcs stay smooth; pure-SDF geometry remains available by disabling pixel_snap.geometry
- Windows apps declare Per-Monitor V2 DPI awareness in the embedded manifest and the Win32 host sizes windows, child views, and surfaces in physical pixels with WM_DPICHANGED re-rasterization, so canvases render at device scale instead of being bitmap-stretched

* Re-pin example reference signatures for the snap default

- gpu-dashboard and gpu-components pin their reference-surface signatures inside the example suites, which only test-examples-native runs; the geometry-snap default changed those pixels

* Re-apply explicit webview frames on DPI change and stamp static tokens with surface scale

- WM_DPICHANGED now re-applies explicit child webview frames rooted at the message window, matching the native-view pass
- effectiveTokens stamps pixel_snap.scale onto a copy of static tokens and scale changes rebuild static-token apps, so hairline snapping stays on the device grid across monitor density changes

* Lay out the components scene with the tokens it renders with

- The catalog laid out under default tokens (geometry snapping off) and rendered under pack tokens (snapping on), so the ceil rule for label-exact widths no-oped and per-edge frame rounding elided the third theme tab
- Layout builders now take the token set, the live app lays out with its surface tokens, and a regression test asserts the theme strip never elides under snapping in either pack

* Snap hairline borders to the lighter whole-pixel width

- Within the existing snap-eligibility window, fractional hairline widths now floor to the lighter device-pixel count instead of rounding, so a 1px border at 1.5x covers one crisp device column instead of two
- Sub-half-pixel strokes still never snap and 1x, 1.25x, and 2x rendering is pinned byte-identical by the updated tests

* Adopt resize-carried density and give each window its own snap scale

- handleResize adopts the event's scale factor before rebuilding, so a DPI-only monitor move re-stamps tokens and re-emits even when the logical size is unchanged
- Window slots own a per-window pixel_snap_scale stamped into their token emission, so secondary windows on different-density monitors snap on their own device grid

* Round native view frames once from accumulated logical coordinates

- Native child view origins accumulate logical x/y through the parent chain and every physical edge rounds exactly once, so nested controls no longer drift a pixel at fractional scales and abutting frames share pixel columns
- The app manifest declares an ordered DPI awareness chain (PerMonitorV2, PerMonitor, legacy dpiAware) so pre-1607 systems degrade gracefully instead of losing awareness entirely

* Round hidden-titlebar content sizes like the standard path

- hiddenOuterSizeForContent rounds scaled content extents through a shared helper instead of truncating, so hidden-titlebar windows and min-size floors match the requested logical size at fractional scales
- check-framework-sync now explains that the package mirror is generated and points at copy-framework.js instead of implying the mirror should be committed

* Chain window DPI resolution through monitor and system fallbacks

- dpiForWindow now mirrors the manifest's awareness chain: GetDpiForWindow, then GetDpiForMonitor via shcore, then the system DPI, so pre-1607 systems that the manifest makes DPI-aware no longer render tiny 1x content
- gpuSurfaceScale delegates to the shared helper and the build pin asserts the chain alongside the manifest elements
2026-07-09 21:32:50 -05:00
Chris Tate c17c64e4c9 Align repository URLs with the renamed GitHub repo (#80)
* Align repository URLs with the renamed GitHub repo

- Point repository.url at vercel-labs/native in all eight platform packages so npm provenance validation passes (the v0.4.1 publish failed on the first package)
- Guard in check-version-sync.js: platform repository.url and homepage must match the main package
- Sweep remaining vercel-labs/zero-native links and zero-native.dev domains across docs, templates, and fixtures to the new canonical names

* Sync repository and homepage fields in sync-version.js

- version:sync now stamps repository and homepage from the main package into each platform package, making the check script's remediation hint accurate
2026-07-09 10:29:58 -05:00
Chris Tate 750824580b Change repository URL in package.json (#78)
Updated the repository URL in package.json to point to the new GitHub repository.
2026-07-09 08:46:57 -05:00
Chris Tate bd8fb61e8c Prepare v0.4.1 release (#77)
- Bump the Native SDK CLI and all platform package versions to 0.4.1.

- Mark the v0.4.1 changelog entry for the npm assets packaging fix.

- Leave the previous v0.4.0 changelog entry unmarked for release publishing.
2026-07-09 07:46:02 -05:00
Chris Tate 79afe58156 Ship assets/ in the npm package (#75)
- Stage the repo-root assets/ dir in copy-framework.js and list it in package.json files so generated apps can resolve assets/native-sdk.manifest (and the macOS icon/entitlements) from the installed package
- Guard against regressions: check-framework-sync.js now fails when any dep.path reference in build/app.zig is missing from the staged mirror or the files array

Fixes #72

Co-authored-by: lzitser23 <10744132+lzitser23@users.noreply.github.com>
2026-07-09 07:36:26 -05:00
Chris Tate a9ba74a5d4 Add a mobile menu to the doc site header (#69)
- Add a hamburger button on mobile that opens the site nav (Home, Docs, Components) in a sheet
- Hide the search box and theme toggle below the md breakpoint so the mobile header stays minimal
2026-07-08 20:02:48 -05:00
Chris Tate b47111069c Native SDK: the complete toolkit for building native desktop apps (#67)
zero-native becomes the Native SDK. Apps are authored as native markup plus Zig on a deterministic runtime and rendered by the toolkit's own engine into real OS windows — no browser, no WebView, no interpreter in the binary.

- Desktop is complete on macOS, Windows, and Linux: native rendering with per-platform titlebar fidelity, audio playback with streaming, a verified track cache, and real spectrum analysis, native context menus, packaging with sealed code signing, and a deterministic automation and record-replay story.
- Experimental iOS and Android host tiers ship behind the same app manifest: simulator and emulator dev loops, archive-ready packaging, real platform tab bars and push navigation, with embedding over the C ABI underneath.
- The docs site, component catalog, theme packs, showcase apps, and CHANGELOG carry the full account.
2026-07-08 18:51:43 -05:00
Chris Tate c915991175 Merge pull request #66 from vercel-labs/fix/docs-dependency-security-bumps
Bump docs next and postcss past security advisories
2026-07-02 17:18:16 -05:00
Chris Tate e75e6337a5 Bump docs next and postcss past security advisories
- next 16.2.5 -> 16.2.9 (middleware/proxy bypass advisory, needs >= 16.2.6)
- pnpm override pins transitive postcss >= 8.5.10 (stringify XSS advisory)
2026-07-02 14:16:04 -05:00
Chris Tate a9c6008ced Merge pull request #65 from vercel-labs/ctate/native-list-rows
Add native list item view kind
2026-06-27 20:03:41 -05:00
Chris Tate 3c03182503 Add native list item view kind
- Add list_item to manifest, runtime, platform enums, and desktop host rendering.

- Route list_item commands like native row selections across macOS, Linux, and Windows.

- Document the new view kind and expose it through TypeScript view APIs.
2026-06-27 19:42:30 -05:00
Chris Tate c24b33859f Merge pull request #64 from vercel-labs/ctate/native-first
Advance Zero Native’s native-first app model
2026-06-27 08:56:57 -05:00
Chris Tate 95827f9c6b Fix package mirror check in CI 2026-06-27 02:27:49 -05:00
Chris Tate f92e4d976b Harden CEF external URL wildcards 2026-06-27 02:10:00 -05:00
Chris Tate 54b8c59a46 Harden external URL wildcard policies 2026-06-27 02:02:38 -05:00
Chris Tate 3270b8a9f4 Add package framework sync check 2026-06-27 01:44:04 -05:00
Chris Tate 660b87cd91 Validate native view parent links 2026-06-27 01:18:05 -05:00
Chris Tate 69d8f29ccc Fix dialog buffer overflow handling 2026-06-27 00:57:49 -05:00
Chris Tate f5d49207cd Fix Linux core test credential probe 2026-06-27 00:39:14 -05:00
Chris Tate f8aee3a96a Fix CI content checks and GTK prototype 2026-06-27 00:34:29 -05:00
Chris Tate a32d206884 Fix parented WebView view frames 2026-06-27 00:18:06 -05:00
Chris Tate 137193f113 Fix bridge validation regressions 2026-06-26 22:04:44 -05:00
Chris Tate 5b272a28e9 Fix Android mobile stop lifecycle 2026-06-26 21:54:21 -05:00
Chris Tate ebdc79e6ac Restore main webview state on shell rollback 2026-06-26 21:33:21 -05:00
Chris Tate 2591284640 Fix generated manifest window options 2026-06-26 21:16:19 -05:00
Chris Tate 7af0168c9d Fix native shell rollback and GTK asset routing 2026-06-26 20:38:21 -05:00
Chris Tate 7ef829affa Fix menu shortcut validation 2026-06-26 09:20:34 -05:00
Chris Tate 7edeef2b5f Fix GTK asset origin fallback 2026-06-26 09:05:37 -05:00
Chris Tate 2b1f64b367 Fix native capability edge cases 2026-06-25 15:04:42 -05:00
Chris Tate 9a8eb99c0e Fix macOS CEF packaged asset child webviews 2026-06-25 14:52:59 -05:00
Chris Tate 3c9e4a2e3d Fix packaged asset child webviews 2026-06-25 14:40:05 -05:00
Chris Tate 126ba7cccd Fix package web engine and archive handling 2026-06-25 14:06:13 -05:00
Chris Tate 6eba88a706 Fix reload sources and desktop Exec quoting 2026-06-25 13:48:15 -05:00
Chris Tate 30d4c12d82 Fix Windows packaged asset loading 2026-06-25 13:37:54 -05:00
Chris Tate aaa17b3913 Fix native shell smoke resize assertion 2026-06-25 13:20:31 -05:00
Chris Tate 10cc3c24af Fix file drop paths and MIME validation 2026-06-25 13:03:44 -05:00
Chris Tate ab5a418b85 Fix native packaging review issues 2026-06-25 12:46:42 -05:00
Chris Tate c086cdcaed Fix WebView label collision validation 2026-06-25 12:29:29 -05:00
Chris Tate 34cda8754b Fix native view bridge clearing 2026-06-25 12:19:22 -05:00
Chris Tate d068763c86 Fix native source ownership and Kotlin escaping 2026-06-25 12:06:17 -05:00
Chris Tate b354fd4517 Fix native shell layout validation 2026-06-25 11:55:04 -05:00
Chris Tate e390c806d0 Fix window bridge error codes 2026-06-25 11:41:45 -05:00
Chris Tate 02b1aaebb2 Fix startup window identity handling 2026-06-25 11:32:28 -05:00
Chris Tate 9af28f75d5 Fix Windows main WebView bridge and accessibility 2026-06-25 11:20:19 -05:00
Chris Tate 230fe3ff00 Fix chromium backend gating 2026-06-25 10:53:58 -05:00
Chris Tate 7eb9699c73 Fix native view cleanup 2026-06-25 10:28:01 -05:00
Chris Tate 53e8cf38f0 Fix mobile package asset loading 2026-06-25 10:05:02 -05:00
Chris Tate ff11c11704 Map mobile shell metadata to host config
- Generate iOS and Android native shell config files from mobile ShellView metadata.

- Add shell declarations to mobile examples and document the package-time mobile mapping.

- Keep Android generated manifests launchable when app identity changes the package id.
2026-06-25 04:38:15 -05:00
Chris Tate 6e9a2432e1 Export the full mobile C ABI
- Add missing activation, command, and command-name symbols to the static library export list.

- Verify the built archive exposes the documented mobile ABI symbols.
2026-06-25 04:29:13 -05:00
Chris Tate 1e1f75b5f7 Bundle mobile package assets
- Copy package assets into generated iOS and Android mobile artifacts.

- Wire generated mobile hosts to set the embedded runtime asset root.

- Extend mobile package tests to cover asset output and host wiring.
2026-06-25 04:28:03 -05:00
Chris Tate c833a3cd94 Load mobile embedded asset roots
- Make zero_native_app_set_asset_root select an asset WebView source for embedded mobile hosts.

- Keep the inline mobile source as the fallback and cover reset behavior in tests.
2026-06-25 04:25:29 -05:00
Chris Tate 3dd3c9c533 Emit Android skeleton app theme
- Generate the AppTheme resource referenced by Android mobile manifests.

- Extend the mobile skeleton layout test to cover Android resources.
2026-06-25 04:23:27 -05:00
Chris Tate 79b7e62f40 Use app metadata for mobile packages
- Render iOS package identity from manifest metadata.

- Render Android package identity with a sanitized application id.

- Cover generated mobile identity files in tooling tests.
2026-06-25 04:21:44 -05:00
Chris Tate 095b279a02 Create mobile skeleton library drop-ins
- Create iOS and Android native library directories in generated skeletons.

- Add a tooling test for the mobile skeleton output layout.
2026-06-25 04:19:46 -05:00
Chris Tate a49f416300 Generate mobile native command shells
- Upgrade iOS and Android package skeletons from bare WebView hosts to native command shells.

- Add Android Kotlin, CMake, and JNI pieces for the generated native host.

- Guard the mobile package templates with tooling tests.
2026-06-25 04:18:40 -05:00
Chris Tate f1afdde69f Publish AppKit accessibility roles
- Map AppKit native view kinds to platform accessibility roles.

- Add a root test guard for the AppKit role mapping.
2026-06-25 04:14:46 -05:00
Chris Tate e392ccae52 Repair focus after native view removal
- Move focus off native views when they become hidden, disabled, or closed.

- Keep close responses and bridge view state from reporting closed surfaces as focused.
2026-06-25 04:11:50 -05:00
Chris Tate b5fb32c4a9 Accept string selectors for view helpers
- Allow JS views.focus("label") and views.close("label") across desktop host bridges.

- Update TypeScript, docs, and build checks for injected helper parity.
2026-06-25 04:07:38 -05:00
Chris Tate 09f6695024 Use native startup bounds for shell layout
- Preserve native or restored startup window frames when loading shell scenes.

- Cover startup shell layout when manifest, surface, and native bounds differ.
2026-06-25 04:03:55 -05:00
Chris Tate 941f16a0a3 Use native bounds for created shell layouts
- Lay out created shell windows from returned native frame

- Cover restored-size shell layout behavior
2026-06-25 03:59:15 -05:00
Chris Tate fd9c8c66c6 Preserve toolbar command source for nested controls
- Walk native view ancestors when classifying command source

- Cover nested toolbar controls and non-toolbar controls
2026-06-25 03:56:47 -05:00
Chris Tate 5887a59b20 Reject duplicate shell window labels
- Validate shell window labels against compatibility windows

- Cover app.zon tooling and primitive manifest validation

- Document the cross-list shell label contract
2026-06-25 03:52:28 -05:00
Chris Tate d7c67cdec4 Support label-first view updates
- Add window.zero.views.update(label, patch) across injected host bridges

- Type and document the label-first helper form

- Guard the injected helper contract in build checks
2026-06-25 03:48:49 -05:00
Chris Tate 48b0235643 Add stable generic view ids
- Add ViewId to generic ViewInfo and bridge JSON

- Preserve runtime view identity across list and update paths

- Document and guard the public view identity contract
2026-06-25 03:46:56 -05:00
Chris Tate b70aacce45 Include save dialog in bridge policy docs
- Add the save dialog command to explicit builtin bridge policy examples

- Guard the security and builtin-command docs snippets
2026-06-25 03:42:39 -05:00
Chris Tate 5b80f8e3d5 Refresh starter native-shell wording
- Update frontend starter READMEs away from WebView-only shell language

- Clarify the deprecated webview build flag help

- Guard frontend example positioning in the example test group
2026-06-25 03:41:24 -05:00
Chris Tate a40fd72765 Correct command docs view update
- Update the command handler example to mutate visible view text

- Add a docs guard for command bridge and view update contracts
2026-06-25 03:39:21 -05:00
Chris Tate 2b563a51aa Align capabilities event bridge name
- Use the runtime DOM event prefix in the capabilities example

- Guard the bridge event name in native example checks
2026-06-25 03:37:47 -05:00
Chris Tate db9154fad1 Tighten generic WebView view types
- Require url and frame for WebView-backed view creation

- Guard the package type surface for generic view contracts
2026-06-25 03:35:19 -05:00
Chris Tate f96059b5bd Type command catalog bridge API
- Add package types for window.zero.commands.list

- Guard command catalog types in the root package check
2026-06-25 03:33:39 -05:00
Chris Tate 1474c24e80 Declare mobile host commands
- Add stable mobile command metadata to iOS and Android manifests

- Verify mobile host command IDs in example checks
2026-06-25 03:31:44 -05:00
Chris Tate 478f766f59 Cover platform support selectors
- Test name-based platform support bridge payloads

- Lock package types and docs to the selector API
2026-06-25 03:29:55 -05:00
Chris Tate 0726a86708 Cover package platform feature types
- Add package type checks for native platform feature names

- Document the full platform support query feature set
2026-06-25 03:27:57 -05:00
Chris Tate 5b0efd676f Cover platform support aliases
- Exercise JavaScript camelCase support aliases for native controls

- Cover file-drop and activation feature discovery through the bridge
2026-06-25 03:26:04 -05:00
Chris Tate 387da281fa Cover automation command file consumption
- Exercise native command parsing through Server.takeCommand

- Assert command files are acknowledged with done
2026-06-25 03:24:45 -05:00
Chris Tate 774b8aa694 Pin mobile-shell command metadata
- Adds shared mobile command IDs for back and refresh actions.

- Extends mobile example checks to verify platform, capability, and command metadata.
2026-06-25 03:21:23 -05:00
Chris Tate c1713f6e04 Cover capability package metadata
- Adds file association and URL scheme metadata to the capabilities example.

- Tests the example manifest metadata in the headless capabilities test path.
2026-06-25 03:19:01 -05:00
Chris Tate ab8d80acb7 Add aggregate example test step
- Adds a root test-examples step covering frontend, native-first, and mobile example checks.

- Documents the aggregate command alongside the narrower example test groups.
2026-06-25 03:16:24 -05:00
Chris Tate bcbbba905d Document automation focus commands
- Adds focus automation commands to the Automation and CLI docs.

- Updates testing docs for native-shell focus traversal and automation protocol test coverage.
2026-06-25 03:14:05 -05:00
Chris Tate 94296a02eb Drive native-shell focus automation
- Adds automation commands for named view focus and focus traversal.

- Covers the automation protocol in the root test graph.

- Extends the native-shell smoke to verify focus movement through snapshots.
2026-06-25 03:11:39 -05:00
Chris Tate 1fdca1f9d6 Surface native docs from README
- Adds native surfaces, commands, and platform support to the public documentation list.
2026-06-25 03:04:15 -05:00
Chris Tate 1738ecb3a8 List native-first examples in README
- Adds command, shell, panels, capabilities, and mobile-shell examples to the public example list.
2026-06-25 03:03:04 -05:00
Chris Tate 92f7eb05e5 Align docs metadata with native positioning
- Updates social metadata to describe native apps, WebView surfaces, controls, and OS capabilities.
2026-06-25 03:02:15 -05:00
Chris Tate d013e5ac8f Cover mobile-shell metadata in root tests
- Adds the shared mobile-shell metadata files to test-examples-mobile.

- Documents the mobile example layout check alongside other headless test entry points.
2026-06-25 03:00:51 -05:00
Chris Tate 91173a2998 Expose native example test group
- Adds a root test-examples-native step for command, shell, panels, and capabilities examples.

- Documents the native example coverage path from testing docs and the capabilities example.
2026-06-25 02:59:01 -05:00
Chris Tate cb825cd45f Lock down Chromium capability reporting
- Adds platform tests for Chromium native-view and command capability gaps.

- Preserves documented system-engine support expectations across macOS, Linux, and Windows hosts.
2026-06-25 02:55:15 -05:00
Chris Tate a39705e273 Exercise native-shell child WebViews
- Adds preview commands that create and close a child WebView from the native-shell app.

- Extends native-shell smoke coverage and docs for child WebView lifecycle checks.
2026-06-25 02:52:19 -05:00
Chris Tate 4c51ce54e4 Expose command-source automation CLI
- Adds automate subcommands for menu, native view, and shortcut command events.

- Routes native-shell smoke through the public CLI actions and documents them.
2026-06-25 02:48:30 -05:00
Chris Tate 31c608e955 Test native-shell resize automation
- Adds an automation resize command that drives the runtime surface resize path.

- Extends native-shell smoke coverage and docs for resize relayout checks.
2026-06-25 02:46:24 -05:00
Chris Tate 7c29914053 Exercise native-shell command sources
- Add automation command injection for menu, native view, and shortcut events
- Extend native-shell smoke coverage across bridge, menu, toolbar, and shortcut paths
- Document command-source automation actions
2026-06-25 02:40:31 -05:00
Chris Tate 372749543d Add native-shell smoke coverage
- Add an opt-in macOS native-shell automation smoke step
- Verify native view snapshots and bridge command refresh behavior
- Document the smoke target in testing and example docs
2026-06-25 02:35:45 -05:00
Chris Tate 0443d0ee25 Preserve Android mobile orientation
- Keep Android embedded hosts alive across orientation and screen-size changes
- Request a frame after configuration changes in the sample and template
- Document orientation handling in mobile host docs
2026-06-25 02:31:28 -05:00
Chris Tate c656746c1b Document mobile host support matrix
- Add iOS and Android embedded host capability rows
- Clarify current mobile ShellView mapping boundaries
- Keep mobile support status in public docs
2026-06-25 02:29:00 -05:00
Chris Tate 5cd81d41be Handle mobile keyboard resizing
- Add iOS keyboard-frame layout handling for the WebView surface
- Opt Android mobile hosts into soft-keyboard resize
- Document native-host keyboard avoidance
2026-06-25 02:27:46 -05:00
Chris Tate 73979e5b46 Route mobile back commands
- Add native Back command controls to iOS and Android shells
- Dispatch Android system Back through embedded command routing
- Document mobile navigation command behavior
2026-06-25 02:24:48 -05:00
Chris Tate 7501ea2c34 Forward mobile activation lifecycle
- Add embedded activate/deactivate C ABI hooks
- Wire iOS and Android lifecycle callbacks into the runtime
- Document mobile lifecycle forwarding
2026-06-25 02:21:19 -05:00
Chris Tate febf954fa6 Route mobile chrome commands
- Add embedded C ABI support for native command dispatch
- Wire iOS and Android header actions through mobile.refresh
- Document mobile command lifecycle support
2026-06-25 02:15:35 -05:00
Chris Tate 57e9259036 Add native view focus traversal
- Add runtime and bridge focusNext/focusPrevious traversal for visible enabled views
- Let system hosts focus WebView-backed generic view labels
- Document traversal commands and TypeScript helpers
2026-06-25 02:09:18 -05:00
Chris Tate a51707276d Reserve GPU surface support
- Add an explicit gpu_surfaces support query that returns false on current hosts
- Reject reserved gpu_surface view creation in the null runtime path
- Document the support selector in docs and TypeScript types
2026-06-25 02:01:53 -05:00
Chris Tate f5cf1b4245 Document mobile host boundaries
- Describe the current iOS and Android embedded host lifecycle.

- Clarify that mobile native shells are host-owned today, not ShellView materialized.

- Link mobile support boundaries from platform docs and example READMEs.
2026-06-25 01:57:13 -05:00
Chris Tate 1f32a9f77d Expose accessibility labels in automation
- Include native view accessibility labels in automation snapshots.

- Prefer explicit accessibility labels in accessibility tree names.

- Update automation and bridge docs for the label metadata.
2026-06-25 01:55:13 -05:00
Chris Tate 5fdd0e63d4 Add native view accessibility labels
- Add explicit accessibility label metadata to manifests, runtime views, and WebView bridge helpers.

- Apply native labels through AppKit and GTK while preserving role/text fallback behavior.

- Update docs, examples, and public JS typings for the new field.
2026-06-25 01:52:36 -05:00
Chris Tate 4d4f30a394 Expose command catalog to WebViews
- Add a built-in bridge command for command catalog listing

- Wire window.zero.commands.list across host injections

- Document and exercise WebView command catalog access
2026-06-25 01:37:24 -05:00
Chris Tate cd425220de Load manifest command catalogs
- Wire manifest command metadata into runtime options

- Teach generated runners to hydrate command catalogs

- Document command catalog access for native code
2026-06-25 01:31:10 -05:00
Chris Tate c69e9f21e3 Add manifest command metadata
- Parse and validate app.zon command catalogs

- Document command metadata alongside menus and shortcuts

- Declare commands in native shell examples
2026-06-25 01:26:05 -05:00
Chris Tate 334f9388a3 Align mobile example manifests
- Declare native view and native module capabilities for mobile hosts

- Refresh example index wording for tray and capability coverage
2026-06-25 01:21:59 -05:00
Chris Tate 7c038fa00e Refresh package positioning
- Align npm metadata with native-first framework positioning
2026-06-25 01:21:03 -05:00
Chris Tate d400aa6289 Broaden capabilities example
- Exercise OS URL, reveal, and recent-document services

- Cover app activation events in the example test

- Align the example manifest with the native capability pack
2026-06-25 01:20:02 -05:00
Chris Tate 3fc3adeaa5 Add manifest native menus
- Parse and validate app.zon menu declarations

- Load manifest menus in generated runners and examples

- Document menu metadata alongside shortcuts
2026-06-25 01:16:12 -05:00
Chris Tate 1f7db7a687 Route command app tray actions
- Add a tray item that dispatches the same app.sync command path

- Cover toolbar, menu, tray, shortcut, and bridge sources in the example test
2026-06-25 01:08:17 -05:00
Chris Tate f3d7844bb9 Broaden manifest capability metadata
- Accept native-first capabilities for views, menus, shortcuts, tray, and OS events

- Align example manifests and capability docs with the expanded vocabulary
2026-06-25 01:05:42 -05:00
Chris Tate 11f7605cfc Type platform support aliases
- Add camelCase platform feature aliases to the published TypeScript union

- Document the snake_case and camelCase support-query forms
2026-06-25 01:01:44 -05:00
Chris Tate 648457d767 Expose platform support in Chromium helper
- Add window.zero.platform.supports to the macOS Chromium bridge helper

- Keep the injected helper surface aligned with the system WebView hosts
2026-06-25 00:59:17 -05:00
Chris Tate dafd599a12 Exercise capability support queries
- Add guarded platform.supports coverage to the capabilities example

- Document support discovery beside native capability bridge access
2026-06-25 00:57:52 -05:00
Chris Tate 371cc948b3 Add dialog manifest capability
- Accept dialog as a first-class app.zon capability

- Align capability docs and the capabilities example metadata
2026-06-25 00:54:34 -05:00
Chris Tate 6d723a32c8 Add runtime dialog methods
- Add validated Runtime wrappers for native file and message dialogs

- Implement NullPlatform dialog behavior for headless tests

- Document Runtime dialog APIs in capability docs
2026-06-25 00:51:12 -05:00
Chris Tate d05e1f3a7d Document dialog bridge permission
- Add native dialogs to the capabilities matrix

- Show dialog permission grants in bridge policy docs
2026-06-25 00:47:25 -05:00
Chris Tate 84b88c0b94 Tighten README starter manifest
- Match the sample app.zon permissions to generated defaults
2026-06-25 00:46:00 -05:00
Chris Tate 465c60f2e8 Ignore generated package skill copies
- Keep package prepack skill outputs out of the working tree
2026-06-25 00:45:07 -05:00
Chris Tate 21e018e1f3 Align example manifest permissions
- Use command and view grants in native bridge example manifests

- Add dialog permission metadata to the capabilities example
2026-06-25 00:43:26 -05:00
Chris Tate d79a222d6f Clarify automation snapshot docs
- Document native and WebView metadata in automation smoke checks
2026-06-25 00:42:26 -05:00
Chris Tate 74a8bbd979 Clarify JS helper permissions
- Align app model docs with command view and window bridge grants
2026-06-25 00:41:08 -05:00
Chris Tate a54279cce4 Add narrow bridge permissions
- Add command view and dialog permission grants

- Gate JS command and view helpers with narrower permissions while preserving window compatibility

- Update docs and examples to use explicit bridge grants
2026-06-25 00:39:42 -05:00
Chris Tate 54c5117083 Add native surfaces docs
- Define the public native surfaces model

- Link surface guidance into docs navigation and getting-started paths

- Document compatibility with WebView and view APIs
2026-06-25 00:32:58 -05:00
Chris Tate 5252439220 Add mobile shell example
- Show native mobile headers with WebView workspace content

- Update iOS and Android hosts to match the mobile shell shape

- Document the mobile-shell example entry
2026-06-25 00:30:30 -05:00
Chris Tate f4f24c1207 Add capabilities example
- Demonstrate guarded notification clipboard credential and dialog bridge calls

- Cover capability bridge policies and file-drop events in the example test

- Document capabilities in the examples guide
2026-06-25 00:28:01 -05:00
Chris Tate 86951f4d14 Add native panels example
- Demonstrate split native panels around WebView content

- Cover stacked native controls and view-list bridge inspection

- Document native-panels in the examples guide
2026-06-25 00:23:33 -05:00
Chris Tate 4a37163653 Add command routing example
- Demonstrate shared command handling across native and web entry points

- Cover toolbar menu shortcut and bridge sources in the example test

- Document the new example in the examples guide
2026-06-25 00:19:19 -05:00
Chris Tate c90dbdaa55 Fix Android doctor target
- Use the Android Linux target triple

- Match mobile example build guidance
2026-06-25 00:14:12 -05:00
Chris Tate 8034dd6b53 Refresh native-first positioning
- Reframe README around native surfaces

- Add native shell and capability concepts

- Align npm README with public docs
2026-06-25 00:13:08 -05:00
Chris Tate b25e998ff6 Align view helper docs
- List split and stack as supported view helper kinds

- Keep bridge docs aligned with platform support
2026-06-25 00:11:54 -05:00
Chris Tate 7a4d40447e Expose WebView focus in JS types
- Add focused to WebView info typing

- Keep TypeScript surface aligned with bridge JSON
2026-06-25 00:11:10 -05:00
Chris Tate c3bdb68d4f Pass view commands through JS helpers
- Include command in view create and update payloads

- Keep injected helpers aligned across hosts

- Cover bridge-created command views
2026-06-25 00:09:44 -05:00
Chris Tate 541bc5ec2f Add split pane shell layout
- Add row/column split child layout

- Preserve absolute WebView frames under parents

- Document split panes with runtime coverage
2026-06-25 00:06:30 -05:00
Chris Tate 61d9c68947 Add stack axis layout
- Add row/column shell stack axis

- Apply axis-aware parent layout

- Cover docs, native-shell, and tests
2026-06-25 00:02:01 -05:00
Chris Tate f561e62f12 Report focused native views
- Track focused view state per window

- Expose focus in view JSON and automation snapshots

- Update docs, types, and tests
2026-06-24 23:56:07 -05:00
Chris Tate b592fc69b6 Add shell view size constraints
- Add min/max shell view fields

- Clamp shell layout frames during resize

- Cover parser, docs, example, and tests
2026-06-24 23:51:27 -05:00
Chris Tate 673a12ff0a Add icon button native controls
- Add icon_button view kind

- Implement desktop host mappings

- Cover docs, types, example, and tests
2026-06-24 23:46:37 -05:00
Chris Tate 4499e04729 Add segmented native controls
- Add segmented_control view kind

- Implement desktop native host mappings

- Cover docs, types, example, and layout tests
2026-06-24 23:40:23 -05:00
Chris Tate 497660f578 Add platform support discovery
- Add typed platform feature checks

- Expose support checks to the bridge

- Document runtime support queries
2026-06-24 23:31:48 -05:00
Chris Tate d6f03ea23d Add declarative scene startup
- Add App.scene_fn for shell scenes

- Materialize scene windows through the runtime

- Update native-shell docs and example
2026-06-24 23:22:49 -05:00
Chris Tate 916c4900b0 Route tray items through commands
- Add command names to tray menu items

- Preserve tray item ids on command events

- Validate and test tray command routing
2026-06-24 23:17:02 -05:00
Chris Tate 4fe0d8af8a Add runtime tray APIs
- Expose tray creation and menu updates through Runtime

- Validate tray options before platform dispatch

- Record tray state in NullPlatform tests
2026-06-24 23:12:53 -05:00
Chris Tate de771c608c Support split and stack native containers
- Enable split and stack view kinds on system hosts

- Treat split and stack as native container islands

- Update native surface support docs
2026-06-24 23:09:34 -05:00
Chris Tate d2ac9d000a Add Linux credential store support
- Wire Linux system WebView credential service hooks

- Store credentials through optional Secret Service/libsecret

- Update Linux credential support docs and templates
2026-06-24 23:06:59 -05:00
Chris Tate 9146f7073b Add Linux recent document support
- Wire Linux system WebView recent document service hooks

- Store app recent items through GLib bookmarks

- Update support docs for Linux recent documents
2026-06-24 23:00:58 -05:00
Chris Tate edaa68db29 Add Linux file drop events
- Add GTK file drop target plumbing

- Emit Linux files_dropped runtime events

- Update file drop support documentation
2026-06-24 22:57:24 -05:00
Chris Tate 9f4c80f417 Add Linux rich clipboard support
- Wire GTK MIME-aware clipboard service hooks

- Support rich clipboard data on Linux system WebView

- Update clipboard support documentation
2026-06-24 22:54:00 -05:00
Chris Tate e30c3e0a65 Add Windows rich clipboard support
- Wire Windows MIME-aware clipboard service hooks

- Support UTF-8 text, HTML, and RTF clipboard formats

- Update clipboard support documentation
2026-06-24 22:50:25 -05:00
Chris Tate db28c80de6 Emit package registration metadata
- Add macOS Info.plist document and protocol declarations

- Add Linux desktop/MIME metadata and Windows registration script

- Document package registration support
2026-06-24 22:45:22 -05:00
Chris Tate 6a6f51df40 Add Linux native menu support
- Implements data-driven GTK menus for the Linux system WebView host.

- Dispatches Linux menu selections through the runtime command event path.

- Updates menu and platform support docs for Linux coverage.
2026-06-24 22:38:53 -05:00
Chris Tate 4d00f8f6bc Add Windows system tray support
- Implements Windows tray icons and popup menus through the notification area.

- Routes tray menu selections through the existing tray action command path.

- Updates tray and platform support docs for Windows coverage.
2026-06-24 22:34:01 -05:00
Chris Tate 203b40f916 Add Windows native menu support
- Implements data-driven Win32 menus for the Windows system WebView host.

- Dispatches menu selections through the existing runtime command event path.

- Updates menu and platform support docs for Windows coverage.
2026-06-24 22:30:52 -05:00
Chris Tate ee882de65a Add Windows notification service
- Implements system notifications for the Windows host through the notification area.

- Wires Windows notifications into PlatformServices and built-in OS commands.

- Updates capability and platform support docs for Windows notification coverage.
2026-06-24 22:26:54 -05:00
Chris Tate 5a5d12f7b4 Add Windows dialog services
- Implements native open, save, and message dialogs for the Windows system host.

- Wires Windows dialogs into PlatformServices and built-in bridge paths.

- Updates platform support docs for Windows dialog coverage.
2026-06-24 22:23:31 -05:00
Chris Tate 37b62c3a5d Reposition docs around native surfaces
- Updates homepage metadata and quick start around native surfaces, controls, and capabilities.

- Adds a platform support matrix for desktop hosts and web engines.

- Links platform support from docs navigation and page titles.
2026-06-24 22:18:50 -05:00
Chris Tate 4248159b51 Add Windows file drop events
- Accepts dropped files in the Windows host and emits newline-separated drop paths.

- Maps Windows drops into the existing runtime and JavaScript drop event path.

- Updates capability docs for Windows file-drop support.
2026-06-24 22:15:33 -05:00
Chris Tate 623d1a81c5 Add Linux activation lifecycle events
- Emits app activation and deactivation from GTK window active-state changes.

- Maps Linux lifecycle events into the runtime stream and capability docs.
2026-06-24 22:14:01 -05:00
Chris Tate 6628d89ff8 Add Windows activation lifecycle events
- Emits app activation and deactivation from WM_ACTIVATEAPP in the Windows host.

- Maps those host events into the runtime lifecycle stream and docs.
2026-06-24 22:11:09 -05:00
Chris Tate cd271dab5c Add Windows credential services
- Implements set, get, and delete credential support through Windows Credential Manager.

- Uses dynamic Advapi32 lookup to avoid new app link requirements.

- Updates capability docs for Windows credential support.
2026-06-24 22:09:41 -05:00
Chris Tate 1b944a6e6a Add Linux notification service
- Implements showNotification for the Linux system WebView host with GNotification.

- Keeps Linux Chromium explicitly unsupported and updates capability docs.
2026-06-24 22:06:53 -05:00
Chris Tate bd068ad156 Add Windows recent document services
- Implements addRecentDocument and clearRecentDocuments for the Windows system host.

- Updates OS capability docs to reflect Windows recent-document support.
2026-06-24 22:04:34 -05:00
Chris Tate 5128bfdd4c Add desktop open and reveal services
- Adds Linux and Windows system-host implementations for openUrl and revealPath.

- Keeps Linux Chromium ABI complete with unsupported native view and OS service stubs.

- Updates capability docs for desktop OS command support.
2026-06-24 22:01:26 -05:00
Chris Tate 7ec674ea26 Add Windows native control views
- Adds Win32 native view creation/update/focus/close support for the Windows system WebView host.

- Routes button-style native controls through the shared native command event.

- Links common controls for Windows examples and updates native control support docs.
2026-06-24 21:56:19 -05:00
Chris Tate ffdce1e3ec Add Linux native control views
- Adds GTK native view creation/update/focus/close paths for the Linux system WebView host.

- Routes GTK native control actions through the shared native command event.

- Updates docs to list Linux system-WebView native control support.
2026-06-24 21:49:22 -05:00
Chris Tate 0ff5d15f3e Route generic WebView views on desktop hosts
- Registers create_view hooks for Linux and Windows platform services.

- Routes ViewKind.webview through the existing child WebView implementations while keeping native-only kinds explicitly unsupported.
2026-06-24 21:41:00 -05:00
Chris Tate 39ab3e7c76 Parse progress indicator shell views
- Aligns app.zon shell parsing with the documented progress_indicator native view kind.

- Covers metadata and parsed shell conversion for progress indicator views.
2026-06-24 21:37:28 -05:00
Chris Tate 87caccaa76 Add native progress indicator view
- Adds progress_indicator to generic view kinds, shell manifests, TypeScript declarations, and bridge parsing aliases.

- Implements macOS system-WebView support with an indeterminate AppKit progress indicator.

- Documents the supported kind and covers shell layout/default sizing in runtime tests and the native-shell example.
2026-06-24 21:35:43 -05:00
Chris Tate 26c00d73cd Expose native view text in automation
- Includes generic view role/text metadata in automation snapshots.

- Uses visible view text as the accessibility snapshot name when available.

- Updates automation docs and runtime coverage for native view snapshot semantics.
2026-06-24 21:32:30 -05:00
Chris Tate fed7c7789c Add explicit native view text
- Adds a text field to generic native view create/update/list flows while preserving role fallback semantics.

- Wires text through AppKit native controls and all injected window.zero view helpers.

- Updates types, docs, examples, and runtime coverage for visible native labels/placeholders.
2026-06-24 21:30:48 -05:00
Chris Tate 01e1684fe8 Add native file drop events
- Add platform and runtime file-drop events with trusted window.zero drop:files emission.

- Implement macOS system WebView file URL drag/drop delivery through AppKit.

- Update TypeScript declarations, docs, native-shell handling, and runtime tests.
2026-06-24 21:19:40 -05:00
Chris Tate f1edbb098b Add typed clipboard capability
- Add MIME-aware clipboard runtime and guarded bridge commands alongside text clipboard compatibility.

- Implement macOS pasteboard support for text, HTML, and RTF while preserving text fallbacks on other hosts.

- Update TypeScript declarations, product docs, and unit coverage for clipboard policy and data handling.
2026-06-24 21:14:08 -05:00
Chris Tate 1aa2267b69 Advance native-first app model
- Add generic native view/shell APIs with command routing, automation snapshots, and a native-shell example.

- Add native OS capability coverage for notifications, external links, recent documents, and credentials with guarded bridge APIs.

- Update manifests, TypeScript declarations, product docs, and platform hosts for the new native-first surface.
2026-06-24 21:06:24 -05:00
Chris Tate 7c8df23ab6 Merge pull request #63 from vercel-labs/ctate/v0.3.0
Prepare v0.3.0 release
2026-06-24 18:18:34 -05:00
Chris Tate 5400f35ade Prepare v0.3.0 release
- Bump zero-native package and CLI versions to 0.3.0

- Mark the 0.3.0 changelog entry for release automation
2026-06-24 18:14:21 -05:00
Chris Tate 031c548e89 Merge pull request #62 from vercel-labs/ctate/keyboard-shortcuts
Add keyboard shortcut support
2026-06-24 15:36:50 -05:00
Chris Tate 4a2ea98262 Address shortcut review findings 2026-06-24 15:22:37 -05:00
Chris Tate 43ae9149ec Fix shortcut matching edge cases 2026-06-24 15:05:54 -05:00
Chris Tate a6063f6444 Fix shortcut event delivery 2026-06-24 14:51:14 -05:00
Chris Tate 030811fd02 Address shortcut review issues 2026-06-24 14:24:56 -05:00
Chris Tate 373346eea7 Fix shortcut backend event handling 2026-06-24 14:16:40 -05:00
Chris Tate 22a9435218 Fix shortcut event regressions 2026-06-24 13:08:52 -05:00
Chris Tate f502dd0b26 Fix shortcut backend behavior 2026-06-24 12:45:24 -05:00
Chris Tate fb98891782 Validate portable shortcut keys 2026-06-24 12:31:17 -05:00
Chris Tate 670e8a488d Add keyboard shortcut support
- Add shortcut API, app.zon parsing, and runtime dispatch to Zig and JS listeners.

- Register app.zon shortcuts automatically from generated and example runners, with Zig overrides for dynamic lists.

- Document shortcut configuration and update package JavaScript typings.
2026-06-24 12:19:29 -05:00
Chris Tate b48c171774 Merge pull request #40 from vercel-labs/ctate/v0.2.0
Prepare 0.2.0 release
2026-05-13 11:45:56 -05:00
Chris Tate 1b0330c905 Expand 0.2.0 changelog 2026-05-13 11:38:37 -05:00
Chris Tate 5b86c81088 Prepare 0.2.0 release 2026-05-13 11:30:40 -05:00
Chris Tate 058a4ade3b Merge pull request #39 from vercel-labs/ctate/fix-missing-x
Fix macOS package executable permissions
2026-05-13 11:25:36 -05:00
Chris Tate b57efc963f Fix macOS package executable permissions
Ensure macOS app bundles always mark their CFBundleExecutable as runnable, even when the copied build artifact lacks executable mode, and cover the behavior with regression tests.

Co-authored-by: Richard Oliver Bray <1377253+RichardBray@users.noreply.github.com>
2026-05-13 11:11:53 -05:00
Chris Tate 78b56821c9 Merge pull request #38 from vercel-labs/ctate/issue-36-ai-skill
Add zero-native app builder skill
2026-05-13 11:00:17 -05:00
1703 changed files with 885944 additions and 11543 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""The GLib 2.72 receipt's error-set allowlist.
Compiling gtk_host.c on stock ubuntu 22.04 (GLib 2.72, GTK 4.6) cannot
succeed: the toolkit's GTK floor is 4.10, so GTK-age failures are the
expected steady state. What this receipt pins is that NOTHING ELSE
fails - a glib/gio symbol needing 2.74+ without a version-checked
fallback shows up here as a diagnostic outside the allowlist below.
The allowlist is by diagnostic SHAPE, not symbol prefix:
- undeclared gtk_/GTK_ functions are the GTK-age roots;
- undeclared plain (non-glib-namespaced) identifiers are their
cascades (locals whose declaring line failed);
- int-conversion lines are cascades of undeclared functions returning
int, and incidentally name glib types (GListModel), so a prefix
denylist would false-positive on them.
Everything else fails the step: unknown type name 'G...', undeclared
g_/G_ symbols, missing members, any located shape not seen before,
and any error line WITHOUT a file:line:col location (driver failures
like "error: Unknown Clang option" never classify as diagnostics, so
they must reject rather than sail through an empty error set).
The receipt also demands positive evidence it ran: at least
MIN_GTK_ROOTS allowlisted GTK-age root diagnostics. A compile that
produced no classifiable error set (wrong file, broken include path,
invocation failure) proves nothing and must fail loudly - the clean
run produces ~21 roots, so the floor sits far below real variance
while catching "nothing actually compiled".
Why the cascade allowances are sound despite looking broad: this
receipt is one lane in a lattice, not the sole guard on gtk_host.c.
Every full-GTK lane (linux-webkitgtk, the canvas smokes, macOS)
compiles the same file cleanly, so a typo'd local or an independent
conversion bug is a red build elsewhere before it ever reaches this
filter - the only errors unique to this lane are the old-glib delta.
And within that delta, regressions always announce themselves through
a REJECTED root before their cascades matter: a missing glib function
is "call to undeclared function 'g_...'" (only gtk_/GTK_ roots are
allowed), a missing glib type/macro is an unknown-type-name or
undeclared-G_-identifier line - all rejected. The cascades allowed
below can only follow roots this filter already failed the step for,
or GTK-age roots it exists to permit.
"""
import re
import sys
MIN_GTK_ROOTS = 5
located = re.compile(r"^[^:\n]+:\d+:\d+: error: (.*)")
rejected = []
gtk_roots = 0
for line in sys.stdin:
if "error:" not in line:
continue
m = located.match(line)
if not m:
rejected.append(line.rstrip() + " [unlocated error shape - driver or invocation failure]")
continue
msg = m.group(1)
if re.match(r"call to undeclared function '(gtk_|GTK_)", msg):
gtk_roots += 1
continue
if re.match(r"use of undeclared identifier '(?!g_|G_|G[A-Z])", msg):
continue
if "incompatible integer to pointer conversion" in msg and "from 'int'" in msg:
# Only the cascade signature: an undeclared function defaults to
# returning int, so its assignment lines convert FROM 'int'.
# Conversions from any other type are not that cascade - reject.
continue
rejected.append(line.rstrip())
if rejected:
print("non-GTK-age diagnostics against GLib 2.72 - the pre-2.74 fallback story regressed:")
print("\n".join(rejected))
sys.exit(1)
if gtk_roots < MIN_GTK_ROOTS:
print(
f"only {gtk_roots} GTK-age root diagnostics (need >= {MIN_GTK_ROOTS}) - "
"the compile did not exercise the old-GTK error set, so this receipt proved nothing"
)
sys.exit(1)
print(f"fallback receipt ok: every error is GTK-age by shape ({gtk_roots} roots)")
+278
View File
@@ -0,0 +1,278 @@
#!/usr/bin/env bash
# Linux canvas smoke under Xvfb — run on a machine with NO WebKitGTK dev
# package, which makes the build itself the native-only link test.
#
# Exercises the Linux gpu_surface software path without a display server:
# builds examples/ui-inbox (a native-only app, so its GTK host compiles
# with the WebKitGTK stub seam and never links webkitgtk-6.0) with
# -Dplatform=linux -Dweb-engine=system -Dautomation=true, runs it under
# Xvfb, and asserts against the automation snapshot:
#
# 1. the built ELF carries no WebKitGTK reference (no libwebkitgtk
# DT_NEEDED entry, no webkit_/jsc_ dynamic symbol), audited on the
# real binary by tools/audit_web_layer.zig
# 2. snapshot ready=true (app booted, automation server live)
# 3. gpu_backend=software (the software present path is active)
# 4. gpu_nonblank=true (real pixels were presented)
# 5. widget-click "Add task" -> '4 open' (automation input mutates state)
# 6. a real X11 right-click opens a task row's declared context menu and
# clicking its item dispatches the Msg ('1 done')
# 7. automate screenshot renders a non-empty PNG
# 8. ZERO WebKit helper processes for the whole run (a native-only app
# has no web layer to boot WebKit with)
#
# Deliberately NOT `set -e` (same as windows-canvas-smoke.sh): grep exits 1
# on zero matches, and under `set -e` an assignment like `x=$(grep ...)` or
# a swallowed `$(cli 2>&1)` capture dies with NO output — this job failed
# three times with nothing in the log but the exit code. Every assertion
# goes through fail(), which dumps the snapshot and the app log.
set -u
# No WebKit sandbox workaround: a native-only app's host is compiled
# without the web layer entirely (and even in web builds the main
# WebView is lazy), so no WebKit helper processes start and the runner's
# user-namespace restrictions never come into play. The zero-WebKit
# assertion below keeps it that way.
# GTK_A11Y=none: under Xvfb there is no session bus providing org.a11y.Bus,
# and GTK4's a11y init blocks ~25 s on the GDBus name lookup before warning
# and continuing — the app's first runtime event landed after the readiness
# window had already expired (reproduced in a local container: without this
# the wait times out at startup; with it the full smoke passes).
# Accessibility is not what this smoke tests.
export GTK_A11Y="${GTK_A11Y:-none}"
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
app_dir="$repo_root/examples/ui-inbox"
snap="$app_dir/.zig-cache/native-sdk-automation/snapshot.txt"
cli="$repo_root/zig-out/bin/native"
app_log="${TMPDIR:-/tmp}/linux-canvas-smoke-app.log"
# Readiness budget. Even with GTK_A11Y=none, shared ubuntu-24.04 runners
# show a consistent ~27 s stall between EGL init and the app's first
# runtime event (measured in runs 28690855597 pass / 28691951139 fail —
# the SAME stall in both; the old hard 30 s `automate wait` window flipped
# green/red on one or two seconds of runner noise). Local containers show
# no stall at all. Widen the budget here instead of weakening the CLI
# default; every correctness assertion stays strict.
ready_timeout_ms=90000
app_pid=""
xvfb_pid=""
cleanup() {
[ -n "$app_pid" ] && kill "$app_pid" >/dev/null 2>&1
# Reap the app and our Xvfb directly so local runs exit clean (CI would
# otherwise rely on the runner's orphan sweep).
pkill -f "$app_dir/zig-out/bin/ui-inbox" >/dev/null 2>&1
[ -n "$xvfb_pid" ] && kill "$xvfb_pid" >/dev/null 2>&1
}
trap cleanup EXIT
diagnostics() {
echo "---- diagnostics ----"
echo "-- snapshot ($snap):"
if [ -f "$snap" ]; then tr '|' '\n' < "$snap" | sed 's/^/ /'; else echo " (missing)"; fi
echo "-- app log head ($app_log):"
head -20 "$app_log" 2>/dev/null | sed 's/^/ /'
echo "-- app log tail ($app_log):"
tail -40 "$app_log" 2>/dev/null | sed 's/^/ /'
# The X window list names what is actually on the glass — the context
# menu popover is an override-redirect X window that never appears in
# the automation snapshot, so this is the only record of whether it
# (or anything else) was mapped when a step failed.
echo "-- X windows (xwininfo -root -children):"
if [ -n "${DISPLAY:-}" ] && command -v xwininfo >/dev/null 2>&1; then
xwininfo -root -children 2>/dev/null | sed 's/^/ /'
else
echo " (no DISPLAY or xwininfo not installed)"
fi
echo "---------------------"
}
# The hex ids of every child of the root window, popovers included
# (override-redirect windows never pass through a window manager — Xvfb
# has none anyway — but they are always children of the root).
x_window_ids() {
xwininfo -root -children 2>/dev/null | grep -oE '0x[0-9a-f]+'
}
fail() {
echo "FAIL: $1"
diagnostics
exit 1
}
# Canvas apps must never spawn WebKit: the window's main WebView is
# created lazily and nothing in this app materializes it, so any
# WebKitWebProcess/WebKitNetworkProcess during the run means an eager
# creation regressed (and with it launch latency, resident helper
# processes, and the sandbox trouble this smoke used to work around).
assert_no_webkit() {
local helpers
helpers=$(pgrep -af 'WebKit(Web|Network)Process' 2>/dev/null)
if [ -n "$helpers" ]; then
echo "-- WebKit helper processes found ($1):"
echo "$helpers" | sed 's/^/ /'
fail "canvas app spawned WebKit processes ($1)"
fi
}
# ---- build ----------------------------------------------------------------
(cd "$repo_root" && zig build) || fail "root zig build (CLI) failed"
(cd "$app_dir" && zig build -Dplatform=linux -Dweb-engine=system -Dautomation=true) \
|| fail "ui-inbox Linux build failed (a native-only app must build without the WebKitGTK dev package)"
# ---- 1: the native-only ELF carries no WebKitGTK reference -----------------
(cd "$repo_root" && zig run tools/audit_web_layer.zig -- "$app_dir/zig-out/bin/ui-inbox" absent) \
|| fail "native-only ELF audit failed (the binary references WebKitGTK)"
echo "== native-only ELF audit ok"
# ---- launch ---------------------------------------------------------------
# The script owns its Xvfb (instead of wrapping the app in xvfb-run) so
# the xdotool step below shares the app's display. -displayfd picks a
# free display number, the modern equivalent of xvfb-run -a's probing.
display_file="$(mktemp)"
Xvfb -displayfd 4 -screen 0 1280x800x24 4>"$display_file" &
xvfb_pid=$!
for _ in $(seq 1 100); do
[ -s "$display_file" ] && break
sleep 0.1
done
[ -s "$display_file" ] || fail "Xvfb never reported a display number"
export DISPLAY=":$(cat "$display_file")"
echo "== Xvfb on $DISPLAY"
cd "$app_dir" || fail "missing $app_dir"
rm -rf .zig-cache/native-sdk-automation
"$app_dir/zig-out/bin/ui-inbox" > "$app_log" 2>&1 &
app_pid=$!
# ---- 2: automation snapshot becomes ready ---------------------------------
# `automate assert` self-reports on timeout (missing patterns + snapshot
# tail) and prints the measured latency on success, so green logs carry
# the readiness margin.
"$cli" automate assert --timeout-ms "$ready_timeout_ms" 'ready=true' \
|| fail "snapshot never became ready"
# ---- 3 + 4: software backend presented non-blank pixels --------------------
"$cli" automate assert --timeout-ms 30000 'gpu_nonblank=true' \
|| fail "gpu_nonblank never became true"
grep -q 'gpu_backend=software' "$snap" || fail "gpu_backend is not software"
echo "== canvas: $(grep -o 'gpu_backend=[a-z]*' "$snap" | head -1)" \
"$(grep -o 'gpu_nonblank=[a-z]*' "$snap" | head -1)"
assert_no_webkit "after first presented frame"
echo "== zero WebKit processes after first presented frame"
# ---- 5: automation widget-click mutates the model --------------------------
echo "== open before click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"
add_id=$(grep -o 'widget @w1/inbox-canvas#[0-9]* role=button name="Add task"' "$snap" \
| grep -o '#[0-9]*' | tr -d '#')
[ -n "$add_id" ] || fail "Add task button not found in snapshot"
"$cli" automate widget-click inbox-canvas "$add_id" || fail "CLI widget-click failed"
"$cli" automate assert --timeout-ms 30000 '4 open' \
|| fail "widget-click did not reach '4 open'"
echo "== open after click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"
# ---- 6: a real right-click opens and drives a task-row context menu --------
# What this proves: an X-level SECONDARY-button press (GDK button 3)
# travels the whole GTK path — click gesture -> button mapping -> runtime
# secondary check -> declared-menu lookup -> native popover — and
# clicking the popover's "Toggle done" item dispatches the row's Msg,
# observable as the model change '1 done' in the snapshot (the popover
# itself is an OS surface and never appears in the snapshot, so the
# dispatched selection is the provable signal).
# The menu is driven by POINTER, never keyboard: under Xvfb there is no
# window manager, so the popover's X window never receives keyboard
# focus and `xdotool key Down/Return` dies on the canvas beneath it —
# but pointer events resolve by position (and the popover holds the
# pointer grab), so a click on the popover surface reaches the item. The
# popover is an override-redirect X window of its own: it is found by
# diffing the root's children across the right-click, and because the
# row declares exactly one item and the popover draws no arrow, the
# center of that new window IS "Toggle done".
# Regression coverage: the swapped GDK button mapping this smoke was
# blind to made every right-click arrive as MIDDLE and never open the
# menu — under that defect the popover window never appears and the
# step fails at the popover lookup, before any selection.
# Limit: '1 done' proves the toggle Msg dispatched; it cannot attribute
# the dispatch to the popover VISUALLY (no snapshot record of the OS
# menu), but the click lands on the popover's own X window, so no
# canvas-level path can consume it — the canvas never sees the press.
echo "== done before right-click: $(grep -oE '[0-9]+ done' "$snap" | head -1)"
row_line=$(grep -o 'widget @w1/inbox-canvas#[0-9]*[^|]*context_menu=\["Toggle done"\][^|]*' "$snap" | head -1)
[ -n "$row_line" ] || fail "no task row with the declared context menu in snapshot"
bounds=$(echo "$row_line" | grep -o 'bounds=([^)]*)')
bx=$(echo "$bounds" | sed -n 's/bounds=(\([0-9.-]*\),.*/\1/p')
by=$(echo "$bounds" | sed -n 's/bounds=([0-9.-]*,\([0-9.-]*\) .*/\1/p')
bw=$(echo "$bounds" | sed -n 's/.* \([0-9.]*\)x[0-9.]*).*/\1/p')
bh=$(echo "$bounds" | sed -n 's/.* [0-9.]*x\([0-9.]*\)).*/\1/p')
[ -n "$bx" ] && [ -n "$by" ] && [ -n "$bw" ] && [ -n "$bh" ] || fail "could not parse row bounds: $row_line"
win=""
for w in $(xdotool search --name "Inbox" 2>/dev/null); do win="$w"; done
[ -n "$win" ] || fail "app X window not found"
eval "$(xdotool getwindowgeometry --shell "$win")"
# Xvfb has no compositor, so GTK draws no CSD shadow and the X window is
# exactly the client area; correct by the measured height difference the
# same way windows-canvas-smoke.sh does, in case a runner image ever
# composites.
client_h=$(grep -o 'window @w1 "[^"]*" bounds=([^)]*)' "$snap" | head -1 \
| sed -n 's/.*x\([0-9]*\)[^x]*$/\1/p')
[ -n "$client_h" ] || client_h=$HEIGHT
y_off=$((HEIGHT - client_h))
[ "$y_off" -ge 0 ] 2>/dev/null || y_off=0
cx=$(awk "BEGIN{printf \"%d\", $X + $bx + $bw / 2}")
cy=$(awk "BEGIN{printf \"%d\", $Y + $y_off + $by + $bh / 2}")
xdotool windowactivate "$win" >/dev/null 2>&1 || xdotool windowfocus "$win" >/dev/null 2>&1
command -v xwininfo >/dev/null 2>&1 || fail "xwininfo not installed (x11-utils) — required to locate the popover's X window"
# xdotool reports decimal window ids, xwininfo hexadecimal; compare in hex.
win_hex=$(printf '0x%x' "$win")
pre_windows=" $(x_window_ids | tr '\n' ' ') "
echo "== right-clicking task row $bounds at ($cx,$cy)"
xdotool mousemove "$cx" "$cy" click 3
# Wait for the popover's X window: a viewable, non-trivial root child
# that did not exist before the right-click.
popover=""
popover_geom=""
for _ in $(seq 1 50); do
# Root children list in stacking order (bottom to top): scan top-down
# so the just-mapped popover wins over any other new surface.
for w in $(x_window_ids | tac); do
case "$pre_windows" in *" $w "*) continue ;; esac
[ "$w" = "$win_hex" ] && continue
geom=$(xwininfo -id "$w" 2>/dev/null)
echo "$geom" | grep -q 'Map State: IsViewable' || continue
pw=$(echo "$geom" | sed -n 's/^ *Width: *\([0-9]*\).*/\1/p')
ph=$(echo "$geom" | sed -n 's/^ *Height: *\([0-9]*\).*/\1/p')
[ -n "$pw" ] && [ -n "$ph" ] && [ "$pw" -gt 10 ] && [ "$ph" -gt 10 ] || continue
popover="$w"
popover_geom="$geom"
break
done
[ -n "$popover" ] && break
sleep 0.2
done
[ -n "$popover" ] || fail "context-menu popover X window never appeared after the right-click"
px=$(echo "$popover_geom" | sed -n 's/^ *Absolute upper-left X: *\(-*[0-9]*\).*/\1/p')
py=$(echo "$popover_geom" | sed -n 's/^ *Absolute upper-left Y: *\(-*[0-9]*\).*/\1/p')
pw=$(echo "$popover_geom" | sed -n 's/^ *Width: *\([0-9]*\).*/\1/p')
ph=$(echo "$popover_geom" | sed -n 's/^ *Height: *\([0-9]*\).*/\1/p')
[ -n "$px" ] && [ -n "$py" ] || fail "could not parse popover geometry for $popover"
mx=$((px + pw / 2))
my=$((py + ph / 2))
echo "== popover $popover at ${pw}x${ph}+${px}+${py}; clicking its only item at ($mx,$my)"
xdotool mousemove "$mx" "$my" click 1
"$cli" automate assert --timeout-ms 30000 '1 done' \
|| fail "right-click menu selection did not dispatch the toggle Msg ('1 done')"
echo "== done after menu selection: $(grep -oE '[0-9]+ done' "$snap" | head -1)"
# ---- 7: screenshot renders a non-empty PNG ---------------------------------
"$cli" automate screenshot inbox-canvas || fail "CLI screenshot failed"
test -s .zig-cache/native-sdk-automation/screenshot-inbox-canvas.png \
|| fail "screenshot PNG missing or empty"
# ---- 8: still zero WebKit processes at the end of the run -------------------
assert_no_webkit "at end of run"
echo "== zero WebKit processes at end of run"
echo "PASS: linux canvas smoke"
exit 0
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
# Linux DEBUG-scaffold smoke under Xvfb — the `native dev` shape, pinned.
#
# The linux-canvas-smoke job builds at the graph's release default, and
# Release builds always use the LLVM backend — so a Debug-only x86_64
# codegen fault can pass every release-shaped CI lane and still crash the
# very first thing a new user runs (`native init` + `native dev` builds
# Debug). This smoke is the receipt for that gap: it scaffolds the ts-core
# template with the CLI (the `native init` default), builds it exactly as
# the dev loop does (-Doptimize=Debug), runs it headless, and requires the
# startup scene to actually come up — app_start, scene load, the
# gpu_surface shell view created through the GTK host's C seam, and real
# presented pixels. The original failure this pins: zig 0.16.0's
# self-hosted x86_64 backend (the Debug default without use_llvm) shifts
# the stack-passed arguments of `native_sdk_gtk_create_view`, so the host
# read a garbage `role` pointer and segfaulted before the first frame.
#
# Deliberately NOT `set -e` (same reasoning as linux-canvas-smoke.sh):
# every assertion goes through fail(), which dumps the snapshot and the
# app log instead of dying silently on a swallowed capture.
set -u
# GTK_A11Y=none: under Xvfb there is no session bus providing org.a11y.Bus,
# and GTK4's a11y init blocks ~25 s before continuing (see
# linux-canvas-smoke.sh, which measured it). Accessibility is not what
# this smoke tests.
export GTK_A11Y="${GTK_A11Y:-none}"
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cli="$repo_root/zig-out/bin/native"
work_dir="${TMPDIR:-/tmp}/native-linux-dev-smoke"
app_name="dev-smoke"
app_dir="$work_dir/$app_name"
snap="$app_dir/.zig-cache/native-sdk-automation/snapshot.txt"
app_log="${TMPDIR:-/tmp}/linux-dev-smoke-app.log"
# Same widened cold-start readiness budget as linux-canvas-smoke.sh:
# shared runners stall tens of seconds before the first runtime event.
ready_timeout_ms=90000
app_pid=""
cleanup() {
[ -n "$app_pid" ] && kill "$app_pid" >/dev/null 2>&1
pkill -f "$app_dir/zig-out/bin/$app_name" >/dev/null 2>&1
}
trap cleanup EXIT
diagnostics() {
echo "---- diagnostics ----"
echo "-- snapshot ($snap):"
if [ -f "$snap" ]; then tr '|' '\n' < "$snap" | sed 's/^/ /'; else echo " (missing)"; fi
echo "-- app log head ($app_log):"
head -20 "$app_log" 2>/dev/null | sed 's/^/ /'
echo "-- app log tail ($app_log):"
tail -40 "$app_log" 2>/dev/null | sed 's/^/ /'
echo "---------------------"
}
fail() {
echo "FAIL: $1"
diagnostics
exit 1
}
# ---- build the CLI ---------------------------------------------------------
(cd "$repo_root" && zig build) || fail "root zig build (CLI) failed"
# ---- scaffold the default template (ts-core, native frontend) --------------
rm -rf "$work_dir"
mkdir -p "$work_dir" || fail "cannot create $work_dir"
(cd "$work_dir" && "$cli" init "$app_name" --framework "$repo_root") \
|| fail "native init failed"
# ---- build the app the way the dev loop does (Debug) ------------------------
# `native build` forwards -D flags to the generated graph verbatim;
# -Doptimize=Debug pins the exact mode `native dev` uses, and
# -Dautomation=true arms the snapshot publisher this smoke asserts on.
(cd "$app_dir" && "$cli" build -Doptimize=Debug -Dautomation=true) \
|| fail "Debug scaffold build failed"
# ---- launch headless --------------------------------------------------------
cd "$app_dir" || fail "missing $app_dir"
rm -rf .zig-cache/native-sdk-automation
xvfb-run -a "$app_dir/zig-out/bin/$app_name" > "$app_log" 2>&1 &
app_pid=$!
# ---- startup scene reached the host: ready, then presented pixels ----------
# ready=true alone proves the crash site passed: the snapshot publishes
# only after app_start dispatched and the scene's shell views (the
# gpu_surface view with its role/accessibility strings) were created
# through native_sdk_gtk_create_view.
"$cli" automate assert --timeout-ms "$ready_timeout_ms" 'ready=true' \
|| fail "snapshot never became ready (startup scene did not come up)"
"$cli" automate assert --timeout-ms 30000 'gpu_nonblank=true' \
|| fail "gpu_nonblank never became true"
# ---- the template's UI actually built over the scene ------------------------
"$cli" automate assert --timeout-ms 30000 'role=button name="Reset"' 'total: 0' \
|| fail "template widgets missing from the snapshot"
echo "PASS: linux dev smoke (Debug scaffold booted its startup scene)"
exit 0
+192
View File
@@ -0,0 +1,192 @@
#!/usr/bin/env bash
# Windows canvas smoke under Wine.
#
# Exercises the Windows gpu_surface Direct2D path (src/platform/windows/
# webview2_host.cpp: child HWND, WM_TIMER frame events, retained binary
# packets) under Wine: cross-compiles examples/ui-inbox for
# x86_64-windows-gnu, runs the .exe under Xvfb + Wine, and asserts against
# the automation snapshot:
#
# 1. snapshot ready=true (app booted, automation server live)
# 2. gpu_backend=direct2d (the retained packet path is active)
# 3. gpu_nonblank=true (real pixels were presented)
# 4. widget-click "Add task" -> '4 open' (automation input mutates state)
# 5. real X11 click + typing lands in the draft textbox (XTEST -> Wine ->
# WM_LBUTTONDOWN/WM_CHAR -> runtime)
#
# Step 5 (xdotool) is deliberately included: it is the only coverage of the
# Win32 pointer/keyboard input mapping in webview2_host.cpp. It is also the
# flakiest step (window lookup, focus without a window manager), so every
# failure path dumps the X window list, the snapshot, and the app log.
#
# Deliberately NOT `set -e`: grep exits 1 on zero matches inside the poll
# loops, and we want explicit, diagnosable failures instead of silent early
# exits. Every assertion goes through fail(), which dumps diagnostics.
set -u
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
app_dir="$repo_root/examples/ui-inbox"
snap="$app_dir/.zig-cache/native-sdk-automation/snapshot.txt"
cli="$repo_root/zig-out/bin/native"
app_log="${TMPDIR:-/tmp}/windows-canvas-smoke-app.log"
# Wine needs an X display; when none is present (CI), re-exec the whole
# script under a private Xvfb server so the app and xdotool share it. The
# explicit screen size beats xvfb-run's 640x480x8 default: the app window is
# 720x520 and Wine wants a 24-bit visual.
if [ -z "${DISPLAY:-}" ]; then
exec xvfb-run -a --server-args="-screen 0 1280x800x24" "$0" "$@"
fi
export WINEPREFIX="${WINEPREFIX:-$repo_root/.zig-cache/wineprefix}"
export WINEDEBUG="${WINEDEBUG:--all}"
app_pid=""
cleanup() {
[ -n "$app_pid" ] && kill "$app_pid" >/dev/null 2>&1
wineserver -k >/dev/null 2>&1
}
trap cleanup EXIT
diagnostics() {
echo "---- diagnostics ----"
echo "-- X windows:"
xdotool search --name "." 2>/dev/null | while read -r w; do
echo " $w: $(xdotool getwindowname "$w" 2>/dev/null)"
done
echo "-- snapshot ($snap):"
if [ -f "$snap" ]; then tr '|' '\n' < "$snap" | sed 's/^/ /'; else echo " (missing)"; fi
echo "-- app log tail ($app_log):"
tail -40 "$app_log" 2>/dev/null | sed 's/^/ /'
echo "---------------------"
}
fail() {
echo "FAIL: $1"
diagnostics
exit 1
}
# poll <seconds> <pattern>: wait until $snap contains <pattern>.
poll() {
local deadline=$((SECONDS + $1))
while [ "$SECONDS" -lt "$deadline" ]; do
[ -f "$snap" ] && grep -q "$2" "$snap" && return 0
sleep 0.5
done
return 1
}
# Print the first snapshot match, retrying reads that land between the
# runtime's truncate and rewrite on a presented frame.
snapshot_match() {
local match=""
for _ in $(seq 1 20); do
match=$(grep -o "$1" "$snap" | head -1)
if [ -n "$match" ]; then
printf '%s\n' "$match"
return 0
fi
sleep 0.1
done
return 1
}
# ---- build ----------------------------------------------------------------
(cd "$repo_root" && zig build) || fail "root zig build (CLI) failed"
(cd "$app_dir" && zig build -Dtarget=x86_64-windows-gnu -Dplatform=windows -Dweb-engine=system -Dautomation=true) \
|| fail "ui-inbox Windows cross-compile failed"
# ---- wineprefix -----------------------------------------------------------
# First run initializes the prefix (measured ~10-30s on CI-class machines);
# subsequent runs are instant, so no cache step is needed.
start=$SECONDS
wineboot --init >/dev/null 2>&1
wineserver --wait >/dev/null 2>&1
echo "== wineprefix ready in $((SECONDS - start))s ($WINEPREFIX)"
# ---- launch ---------------------------------------------------------------
cd "$app_dir" || fail "missing $app_dir"
rm -rf .zig-cache/native-sdk-automation
mkdir -p .zig-cache/native-sdk-automation
wine zig-out/bin/ui-inbox.exe > "$app_log" 2>&1 &
app_pid=$!
# ---- 1: automation snapshot becomes ready ---------------------------------
poll 180 'ready=true' || fail "snapshot never became ready"
echo "== ready: $(head -1 "$snap" | cut -d'|' -f1)"
# ---- 2 + 3: Direct2D backend presented non-blank pixels --------------------
poll 60 'gpu_nonblank=true' || fail "gpu_nonblank never became true"
poll 10 'gpu_backend=direct2d' || fail "gpu_backend is not direct2d"
echo "== canvas: $(grep -o 'gpu_backend=[a-z]*' "$snap" | head -1)" \
"$(grep -o 'gpu_nonblank=[a-z]*' "$snap" | head -1)" \
"$(grep -o 'gpu_sample=0x[0-9a-f]*' "$snap" | head -1)" \
"$(grep -o 'gpu_present_mode=[a-z]*' "$snap" | head -1)"
# ---- 4: automation widget-click mutates the model --------------------------
echo "== open before click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"
add_id=$(snapshot_match 'widget @w1/inbox-canvas#[0-9]* role=button name="Add task"' \
| grep -o '#[0-9]*' | tr -d '#')
[ -n "$add_id" ] || fail "Add task button not found in snapshot"
"$cli" automate widget-click inbox-canvas "$add_id" || fail "CLI widget-click failed"
poll 30 '4 open' || fail "widget-click did not reach '4 open'"
echo "== open after click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"
# ---- 5: real X11 input through the Win32 path ------------------------------
# Root-coordinate math. Hidden-titlebar windows keep the full overlapped
# frame and reclaim the caption band through WM_NCCALCSIZE, so the Win32
# client area starts at the very top of the window. Under a WM-less Wine
# the X11 driver still places (and reports) the client X window at the
# DEFAULT frame offset - one caption band lower - so the X origin sits a
# band BELOW where Win32 client coordinates actually map, and the reported
# X height is short by exactly that band (measured: X window 718x489 at
# y=30 for a 718x519 client whose clicks land at y=0). The snapshot knows
# the true client height, so the height shortfall IS the y correction; a
# standard-frame window reports matching heights and corrects by zero.
win=""
for w in $(xdotool search --name "." 2>/dev/null); do
case "$(xdotool getwindowname "$w" 2>/dev/null)" in
*[Ii]nbox*) win="$w" ;;
esac
done
[ -n "$win" ] || fail "app X window not found"
eval "$(xdotool getwindowgeometry --shell "$win")"
# The runtime rewrites snapshot.txt on every presented frame. Retry the
# extraction instead of treating a read that lands between truncate and
# write as a standard-frame window: that fallback erases the 30px Wine
# caption correction and turns the real-input receipt into a flaky miss.
client_line=$(snapshot_match 'window @w1 "[^"]*" bounds=([^)]*)')
[ -n "$client_line" ] || fail "could not read client bounds from snapshot"
client_h=$(printf '%s\n' "$client_line" | sed -n 's/.*x\([0-9]*\)[^x]*$/\1/p')
[ -n "$client_h" ] || fail "could not read client height from snapshot"
y_off=$((client_h - HEIGHT))
[ "$y_off" -ge 0 ] 2>/dev/null || y_off=0
echo "== x window $win: pos=($X,$Y) size=${WIDTH}x${HEIGHT} client_h=$client_h y_off=$y_off"
xdotool windowactivate "$win" >/dev/null 2>&1 || xdotool windowfocus "$win" >/dev/null 2>&1
draft_line=$(snapshot_match 'widget @w1/inbox-canvas#[0-9]* role=textbox[^|]*')
[ -n "$draft_line" ] || fail "draft textbox not found in snapshot"
bounds=$(echo "$draft_line" | grep -o 'bounds=([^)]*)')
bx=$(echo "$bounds" | sed -n 's/bounds=(\([0-9.]*\),.*/\1/p')
by=$(echo "$bounds" | sed -n 's/bounds=([0-9.]*,\([0-9.]*\) .*/\1/p')
bw=$(echo "$bounds" | sed -n 's/.* \([0-9.]*\)x[0-9.]*).*/\1/p')
bh=$(echo "$bounds" | sed -n 's/.* [0-9.]*x\([0-9.]*\)).*/\1/p')
[ -n "$bx" ] && [ -n "$by" ] && [ -n "$bw" ] && [ -n "$bh" ] || fail "could not parse draft bounds: $draft_line"
cx=$(awk "BEGIN{printf \"%d\", $X + $bx + $bw / 2}")
cy=$(awk "BEGIN{printf \"%d\", $Y - $y_off + $by + $bh / 2}")
echo "== clicking draft field $bounds at ($cx,$cy)"
xdotool mousemove "$cx" "$cy" click 1
# The click must move widget focus into the textbox before any keys are
# sent: spaces in the typed string would otherwise activate whatever
# widget held focus (a button press adds a task and the real failure -
# input landing in the wrong widget - would read as missing text).
poll 10 'role=textbox[^|]*focused=true' || fail "draft textbox did not take focus from the click"
sleep 1
xdotool type --delay 120 "hi from wine"
poll 30 'hi from wine' || fail "typed text never appeared in the snapshot"
echo "== draft after typing: $(grep -o 'widget @w1/inbox-canvas#[0-9]* role=textbox[^|]*' "$snap" | head -1 | cut -c1-160)"
echo "== input latency: $(grep -o 'gpu_input_latency_ns=[0-9]*' "$snap" | head -1)"
echo "PASS: windows canvas smoke"
exit 0
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env bash
# Windows effects smoke under Wine.
#
# Exercises the effect system's live Windows path (src/runtime/effects.zig
# worker threads -> PostMessageW wake in src/platform/windows/
# webview2_host.cpp -> loop-thread drain) without Windows hardware:
# cross-compiles examples/effects-probe for x86_64-windows-gnu, runs the
# .exe under Xvfb + Wine, and asserts against the automation snapshot and
# the app's trace log:
#
# 1. snapshot ready=true (app booted, automation server live)
# 2. gpu_backend=direct2d + nonblank (the canvas presented real pixels)
# 3. widget-click "Start stream" (fx.spawn launches cmd.exe under
# Wine; streamed lines land in the
# model and grow the snapshot)
# 4. app log shows event=effects_wake (the worker's PostMessageW wake was
# marshalled through the message
# loop -- the wake path itself, not
# just the frame-tick drain)
# 5. widget-click "Cancel" (fx.cancel terminates the child;
# status shows "cancelled")
# 6. the line count freezes (no lines arrive after cancel,
# sampled across ~5 more would-be
# line intervals)
#
# Known caveat: the timer present mode also drains effect completions on
# every frame tick (ui_app.zig handleFrame), so line delivery alone cannot
# isolate the wake; that is why step 4 checks the trace log for the wake
# events directly instead of inferring the wake from model updates.
#
# Deliberately NOT `set -e`: grep exits 1 on zero matches inside the poll
# loops, and we want explicit, diagnosable failures instead of silent early
# exits. Every assertion goes through fail(), which dumps diagnostics.
set -u
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
app_dir="$repo_root/examples/effects-probe"
snap="$app_dir/.zig-cache/native-sdk-automation/snapshot.txt"
cli="$repo_root/zig-out/bin/native"
app_log="${TMPDIR:-/tmp}/windows-effects-smoke-app.log"
# Wine needs an X display; when none is present (CI), re-exec the whole
# script under a private Xvfb server. The explicit screen size beats
# xvfb-run's 640x480x8 default: the app window is 560x480 and Wine wants a
# 24-bit visual.
if [ -z "${DISPLAY:-}" ]; then
exec xvfb-run -a --server-args="-screen 0 1280x800x24" "$0" "$@"
fi
export WINEPREFIX="${WINEPREFIX:-$repo_root/.zig-cache/wineprefix}"
export WINEDEBUG="${WINEDEBUG:--all}"
app_pid=""
cleanup() {
[ -n "$app_pid" ] && kill "$app_pid" >/dev/null 2>&1
wineserver -k >/dev/null 2>&1
}
trap cleanup EXIT
diagnostics() {
echo "---- diagnostics ----"
echo "-- snapshot ($snap):"
if [ -f "$snap" ]; then tr '|' '\n' < "$snap" | sed 's/^/ /'; else echo " (missing)"; fi
echo "-- app log tail ($app_log):"
tail -40 "$app_log" 2>/dev/null | sed 's/^/ /'
echo "---------------------"
}
fail() {
echo "FAIL: $1"
diagnostics
exit 1
}
# poll <seconds> <pattern>: wait until $snap contains <pattern>.
poll() {
local deadline=$((SECONDS + $1))
while [ "$SECONDS" -lt "$deadline" ]; do
[ -f "$snap" ] && grep -q "$2" "$snap" && return 0
sleep 0.5
done
return 1
}
# The status bar renders "{N} lines total · {M} dropped"; extract N.
total_lines() {
grep -o '[0-9]* lines total' "$snap" 2>/dev/null | head -1 | grep -o '^[0-9]*'
}
# widget_id <name>: find a widget id in the snapshot by accessible name.
widget_id() {
grep -o "widget @w1/probe-canvas#[0-9]* role=button name=\"$1\"" "$snap" \
| grep -o '#[0-9]*' | tr -d '#'
}
# ---- build ----------------------------------------------------------------
(cd "$repo_root" && zig build) || fail "root zig build (CLI) failed"
# effects-probe is a zero-config app (app.zon + src, no build.zig): the CLI
# synthesizes its build graph. -Doptimize=Debug keeps the smoke binary at
# the debug shape (`native build` alone would inject ReleaseFast).
"$cli" build "$app_dir" -Dtarget=x86_64-windows-gnu -Dplatform=windows -Dweb-engine=system -Dautomation=true -Doptimize=Debug \
|| fail "effects-probe Windows cross-compile failed"
# ---- wineprefix -----------------------------------------------------------
start=$SECONDS
wineboot --init >/dev/null 2>&1
wineserver --wait >/dev/null 2>&1
echo "== wineprefix ready in $((SECONDS - start))s ($WINEPREFIX)"
# ---- launch ---------------------------------------------------------------
cd "$app_dir" || fail "missing $app_dir"
rm -rf .zig-cache/native-sdk-automation
mkdir -p .zig-cache/native-sdk-automation
wine zig-out/bin/effects-probe.exe > "$app_log" 2>&1 &
app_pid=$!
# ---- 1: automation snapshot becomes ready ---------------------------------
poll 180 'ready=true' || fail "snapshot never became ready"
echo "== ready: $(head -1 "$snap" | cut -d'|' -f1)"
# ---- 2: Direct2D backend presented non-blank pixels ------------------------
poll 60 'gpu_nonblank=true' || fail "gpu_nonblank never became true"
grep -q 'gpu_backend=direct2d' "$snap" || fail "gpu_backend is not direct2d"
echo "== canvas: $(grep -o 'gpu_backend=[a-z]*' "$snap" | head -1)" \
"$(grep -o 'gpu_nonblank=[a-z]*' "$snap" | head -1)"
grep -q 'idle' "$snap" || fail "probe did not start idle"
# ---- 3: Start spawns the stream and lines arrive ---------------------------
start_id=$(widget_id "Start stream")
[ -n "$start_id" ] || fail "Start stream button not found in snapshot"
"$cli" automate widget-click probe-canvas "$start_id" || fail "CLI widget-click Start failed"
poll 30 'streaming:' || fail "status never showed streaming (spawn failed under Wine?)"
# The Windows stream paces ~1 line/s (cmd for /L + ping); wait for at
# least 2 visible lines so cancel provably interrupts an active stream.
poll 60 'stream line 2' || fail "stream lines never reached the model"
echo "== streaming: $(grep -o 'streaming: [0-9]* lines' "$snap" | head -1), status-bar: $(total_lines) lines total"
# ---- 4: the PostMessage wake path fired ------------------------------------
# handleFrame also drains completions on every frame tick, so lines in the
# model alone cannot isolate the wake. The runner's default -Dtrace=events
# sink prints every runtime event; effects_wake records prove the worker's
# PostMessageW -> kWakeMessage -> kWake -> .effects_wake marshalling ran.
grep -q 'event="effects_wake"' "$app_log" || fail "no effects_wake events in the app log (PostMessage wake never fired)"
echo "== effects_wake events so far: $(grep -c 'event="effects_wake"' "$app_log")"
# ---- 5: Cancel terminates the child ----------------------------------------
cancel_id=$(widget_id "Cancel")
[ -n "$cancel_id" ] || fail "Cancel button not found in snapshot"
"$cli" automate widget-click probe-canvas "$cancel_id" || fail "CLI widget-click Cancel failed"
poll 30 'cancelled: code' || fail "status never showed cancelled"
frozen=$(total_lines)
[ -n "$frozen" ] || fail "could not read line count after cancel"
echo "== cancelled at $frozen lines: $(grep -o 'cancelled: code [0-9-]* after [0-9]* lines' "$snap" | head -1)"
# ---- 6: the line count is frozen -------------------------------------------
# ~5 more lines would have arrived at the ~1s cadence if the child were
# still alive or queued lines were still draining.
sleep 6
after=$(total_lines)
[ "$after" = "$frozen" ] || fail "line count moved after cancel ($frozen -> $after)"
grep -q 'streaming:' "$snap" && fail "status went back to streaming after cancel"
echo "== count frozen at $after lines across 6s"
echo "PASS: windows effects smoke"
exit 0
+7 -7
View File
@@ -49,7 +49,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
- uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
@@ -96,13 +96,13 @@ jobs:
esac
find "$CEF_ROOT/build/libcef_dll_wrapper" -name "$wrapper" -print -quit | xargs -I{} cp "{}" "$CEF_ROOT/libcef_dll_wrapper/$wrapper"
- name: Prepare zero-native runtime archive
- name: Prepare native-sdk runtime archive
if: ${{ inputs.source == 'official' }}
shell: bash
run: |
zig build
cli="zig-out/bin/zero-native"
if [[ "${{ runner.os }}" == "Windows" ]]; then cli="zig-out/bin/zero-native.exe"; fi
cli="zig-out/bin/native"
if [[ "${{ runner.os }}" == "Windows" ]]; then cli="zig-out/bin/native.exe"; fi
"$cli" cef prepare-release --dir "$CEF_ROOT" --output zig-out/cef --version "${{ inputs.cef_version }}"
- name: Build CEF from source and prepare runtime
@@ -116,7 +116,7 @@ jobs:
--platform "${{ matrix.platform }}"
--version "${{ inputs.cef_version }}"
--output zig-out/cef
--zero-native-bin zig-out/bin/zero-native
--native-sdk-bin zig-out/bin/native
)
if [ -n "${{ inputs.cef_branch }}" ]; then
args+=(--cef-branch "${{ inputs.cef_branch }}")
@@ -129,5 +129,5 @@ jobs:
tag_name: cef-${{ inputs.cef_version }}
name: CEF ${{ inputs.cef_version }}
files: |
zig-out/cef/zero-native-cef-${{ inputs.cef_version }}-${{ matrix.platform }}.tar.gz
zig-out/cef/zero-native-cef-${{ inputs.cef_version }}-${{ matrix.platform }}.tar.gz.sha256
zig-out/cef/native-sdk-cef-${{ inputs.cef_version }}-${{ matrix.platform }}.tar.gz
zig-out/cef/native-sdk-cef-${{ inputs.cef_version }}-${{ matrix.platform }}.tar.gz.sha256
+420 -13
View File
@@ -15,53 +15,180 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
- uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
- uses: actions/setup-node@v4
with:
node-version: 22
# TypeScript cores compile through the external core compiler at
# build/test time; the compiler and the frontend's toolchain both
# arrive with this one install (without it the ts-core suites skip
# silently, so CI must provide it). No SCRIPTC_NO_CACHE and no
# cache action on purpose: hosted runners are ephemeral, so any
# per-run compiler cache dies with the VM and runs stay hermetic
# across commits by machine lifecycle.
- run: npm ci --prefix packages/core
- name: Service surface tooling and claims
run: node --test packages/core/test/surface_tools.test.ts
- run: zig build test
- run: zig build validate
core-compiler-fences:
name: Core Compiler Fences
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
- uses: actions/setup-node@v4
with:
node-version: 22
# The exact-pinned external core compiler and the frontend's
# toolchain, one install (packages/core/package.json is the one
# place the pin lives).
- run: npm ci --prefix packages/core
# Per-fixture contract artifacts the fixture driver consumes: the effective sidecar plus its generated entry module and compiler profile, under zig-out/core-contracts.
- run: zig build stage-core-contracts
# Determinism-fence negative control: the pristine markup fixture compiles and its co-emitted sidecar attests deterministic: true, then one injected ambient read (Date.now() in update) must be refused by the profile's fences — proving the fences fire, not merely that clean cores pass under them. The positive batteries (every fixture's e2e suite over its real archive) ride `zig build test` in the Zig Core job; this job holds the refusal half.
- name: Determinism fences fire (negative control)
run: tests/compiled-core/fence_check.sh .zig-cache/fence-check
macos-webview:
name: macOS WebView
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
- uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
- uses: actions/setup-node@v4
with:
node-version: 22
# gpu-components is a TypeScript-core app, so its smoke build needs
# the frontend compiler and exact-pinned TypeScript toolchain.
- run: npm ci --prefix packages/core
# The mobile aggregate runs on Linux for Android. Exercise the other
# store-capable cross-target here against the real iPhone simulator SDK.
- run: zig build test-example-mobile-canvas-lib-ios-store
- run: zig build test-webview-system-link
- run: zig build test-webview-smoke
# The zero-config TypeScript runner must load app.zon menus before
# automation can select their registered command ids.
- run: zig build test-menu-bar-smoke
# Signed-package seal pin: an ad-hoc signed package must pass
# codesign --verify --strict (macOS runners are the only tier with
# codesign; the step skips loudly anywhere else).
- run: zig build test-package-signing
# Shared macos-14 runners are far noisier than a dev box (the second
# CI run measured a 576 ms automation-ready against the 500 ms local
# ceiling), so widen the smoke budgets here instead of weakening the
# local defaults. NATIVE_SDK_SMOKE_BUDGET_MS raises the first-frame latency
# budget and the automation-ready ceiling together; every correctness
# assertion in the smokes stays strict.
- run: zig build test-gpu-dashboard-smoke
env:
NATIVE_SDK_SMOKE_BUDGET_MS: "1500"
- run: zig build test-gpu-components-smoke
env:
NATIVE_SDK_SMOKE_BUDGET_MS: "1500"
macos-gpu-perf:
name: macOS GPU Perf
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
# Percentile perf check: 5 cold launches asserting p90
# first-frame latency, 5 steady-state widget clicks asserting p90 input
# latency, then a reset-scoped retained-animation cadence sample.
# Its own job so a shared-runner slowdown is visible in isolation and
# never blocks the correctness smokes.
# Shared macos-14 runners are far noisier than a dev box (first CI run
# measured a 581 ms cold-start outlier against the 300 ms default), so
# widen the budgets here instead of weakening the local defaults: this
# job exists to catch step-function regressions, not runner noise.
- run: zig build test-gpu-dashboard-perf
env:
NATIVE_SDK_PERF_BUDGET_MS: "1500"
NATIVE_SDK_PERF_INPUT_BUDGET_MS: "500"
# The virtual display is commonly 25-35 Hz even though AppKit
# reports the window visible. Keep physical/dev defaults at the
# strict 60 Hz-class 45/20/34; this hosted job remains a
# step-function regression sentinel rather than a display gate.
NATIVE_SDK_PERF_ANIMATION_MIN_FRAMES: "30"
NATIVE_SDK_PERF_ANIMATION_P90_MS: "50"
NATIVE_SDK_PERF_ANIMATION_MAX_MS: "100"
linux-webkitgtk:
name: Linux WebKitGTK
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
- uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
- name: Install WebKitGTK dependencies
run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev
- run: zig build test-webview-system-link -Dplatform=linux
# Declare-to-use, proven on real Linux executables WITH the
# WebKitGTK dev package installed — the seam, not the environment:
# the native-only ui-inbox binary must carry no libwebkitgtk
# DT_NEEDED entry and no webkit_/jsc_ dynamic symbol even though
# the headers were right there, while the webview example must
# keep them. (The environment half — building native-only on a
# runner with no WebKitGTK dev package at all — is the
# linux-canvas-smoke job.)
- run: zig build test-linux-web-layer-audit
windows-webview:
name: Windows WebView
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
- uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
# Builds the WebView example with the system engine, compiling the
# full embedded-WebView host against the vendored WebView2 SDK
# header (third_party/webview2). The host refuses to fall back to
# the stubbed layer, so a regression — a missing header, a broken
# include path, or a conformance error in the embedded layer — is a
# compile failure here, not a silent WebViewNotFound at runtime.
- run: zig build test-webview-system-link -Dplatform=windows
# Effects.spawn is a pipe-backed background transport. Run its
# Windows-only PowerShell probe natively so removing CREATE_NO_WINDOW
# cannot leave the platform-neutral and Wine lanes green.
- run: zig build test-windows-effects-no-console
# The registered-font receipt, natively on Windows: runs the
# font-registry suite — registration validation, the glyph-budget
# gate, present/reference pixel parity, and the Chinese-receipt
# test that registers the committed subsetted Noto Sans SC through
# the app-fonts seam and proves the rendered string is real glyphs,
# not tofu — on real Windows, not under emulation. The font
# pipeline is platform-neutral Zig, so this lane pins that neutral
# truth on a Windows host (the Linux run lives in the Zig Core
# lane's `zig build test`).
- run: zig build test-canvas-fonts
cef-platform-tooling:
name: CEF Platform Tooling
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
- uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
- uses: actions/setup-node@v4
with:
node-version: 22
# The TypeScript core suites compile through the external core
# compiler at build/test time; without the install they skip
# silently, so CI must provide it.
- run: npm ci --prefix packages/core
- run: zig build test-tooling
npm-package:
@@ -72,15 +199,246 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm --prefix packages/zero-native run version:check
- run: npm --prefix packages/zero-native run scripts:check
- run: npm --prefix packages/native-sdk run version:check
- run: npm --prefix packages/native-sdk run scripts:check
native-example-shards:
name: Native Examples (${{ matrix.name }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- name: 1/4
step: test-examples-native-shard-1
- name: 2/4
step: test-examples-native-shard-2
- name: 3/4
step: test-examples-native-shard-3
- name: 4/4
step: test-examples-native-shard-4
steps:
- uses: actions/checkout@v4
- uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
- uses: actions/setup-node@v4
with:
node-version: 22
# The TypeScript examples compile through the external core
# compiler at build time; the compiler and the frontend toolchain
# arrive with this install.
- run: npm ci --prefix packages/core
# Every example test uses the null backend, so this lane needs no
# GTK/WebKitGTK packages. The root build owns the round-robin shard
# membership, keeping CI and the complete local group in one registry.
- run: zig build ${{ matrix.step }}
windows-web-layer-audit:
name: Windows Web Layer Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
# Declare-to-use, proven on real Windows executables: the
# canvas-only ui-inbox cross-compiles without the embedded WebView
# layer (no WebView2Loader.dll reference, no loader installed) and
# the webview example keeps it.
- run: zig build test-windows-web-layer-audit
# Preserve the established `CI / Native Examples` required-check name
# while making it an aggregate receipt for every shard and the audit.
native-examples:
name: Native Examples
if: ${{ always() }}
needs:
- native-example-shards
- windows-web-layer-audit
runs-on: ubuntu-latest
steps:
- name: Confirm every native example lane passed
env:
EXAMPLE_SHARDS_RESULT: ${{ needs.native-example-shards.result }}
WINDOWS_AUDIT_RESULT: ${{ needs.windows-web-layer-audit.result }}
run: |
test "$EXAMPLE_SHARDS_RESULT" = success
test "$WINDOWS_AUDIT_RESULT" = success
linux-canvas-smoke:
name: Linux Canvas Smoke
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
# Deliberately NO libwebkitgtk-6.0-dev: ui-inbox declares no web
# use, so its host compiles with the WebKitGTK stub seam and this
# job doubles as the native-only Linux LINK test — the build can
# only succeed if nothing in a native-only app needs the WebKitGTK
# headers or pkg-config entry. (The seam under webkit-PRESENT
# conditions is the linux-webkitgtk job's ELF cross-audit.)
# x11-utils provides xwininfo, which the smoke script uses to find
# the context-menu popover's override-redirect X window (and to
# dump the X window list in failure diagnostics).
- name: Install GTK, Xvfb, xdotool, and x11-utils
run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev xvfb xdotool x11-utils
# The stub is the expected, configured state of every native-only
# Linux app, so compiling the GTK host with the stub define must be
# diagnostic-FREE, not merely successful: zig renders every clang
# diagnostic of a failing translation unit as `error:` (serialized
# clang diagnostics carry no severity into its error bundle), so
# even an informational #pragma message in this path masquerades as
# the build-killing error the moment any unrelated real error
# appears in the file. `zig cc` runs clang in passthrough mode,
# where warnings actually reach stderr — `zig build` only surfaces
# C diagnostics on failure, which is exactly the escalation this
# step pins against. The throwaway cache dir keeps the compile
# cold: on a cache hit zig replays nothing, stderr included, so a
# warm compiler cache would hide the diagnostics
# this step exists to catch.
- name: WebKitGTK stub compile is diagnostic-free
run: |
export ZIG_GLOBAL_CACHE_DIR=$(mktemp -d) ZIG_LOCAL_CACHE_DIR=$(mktemp -d)
status=0
out=$(zig cc -c src/platform/linux/gtk_host.c -DNATIVE_SDK_ALLOW_WEBKITGTK_STUB $(pkg-config --cflags gtk4) -o /tmp/gtk_host_stub.o 2>&1) || status=$?
if [ "$status" -ne 0 ] || [ -n "$out" ]; then
echo "the WebKitGTK stub compile must succeed with zero diagnostics (exit $status):"
echo "$out"
exit 1
fi
# Drives the gpu_surface software path under Xvfb: snapshot ready,
# gpu_backend=software, gpu_nonblank=true, automation widget-click,
# a real xdotool right-click driving a task row's native context
# menu end to end, a rendered screenshot, an ELF audit that the
# built binary carries no WebKitGTK reference, and ZERO WebKit
# helper processes (canvas apps never boot WebKit). A11y env, the widened cold-start
# readiness budget (shared runners stall ~27 s before the first
# runtime event), and failure forensics (dump snapshot, app log,
# and X window list) all live in the script.
- name: Build and drive ui-inbox headless
run: .github/scripts/linux-canvas-smoke.sh
# Durable receipt for the GLib 2.72 fallback (the pre-2.74
# G_APPLICATION_DEFAULT_FLAGS shim in gtk_host.c): no stock image
# pairs an old glib with GTK >= 4.10 (only backport distros do),
# and ubuntu 22.04 ships GTK 4.6 — so a clean compile is
# impossible here by design. This pins the error SET instead:
# every diagnostic must match a GTK-age shape (the allowlist
# script holds the shapes, why cascades are allowed, and the
# positive-evidence floor) — a 2.74+ glib symbol, type, or
# member used without a version-checked fallback surfaces as a
# rejected diagnostic and fails this step, and so does a compile
# that produced no classifiable error set at all. If this compile ever succeeds outright, the premise
# changed (newer GTK in the image) and the receipt must be
# re-verified rather than trusted.
- name: GTK host GLib 2.72 fallback holds (error-set receipt)
run: |
docker run --rm -v "$PWD:/src" -v "$(dirname "$(which zig)"):/zig" ubuntu:22.04 bash -ec '
export DEBIAN_FRONTEND=noninteractive
apt-get update -q >/dev/null && apt-get install -y -q libgtk-4-dev pkg-config >/dev/null
export ZIG_GLOBAL_CACHE_DIR=$(mktemp -d) ZIG_LOCAL_CACHE_DIR=$(mktemp -d)
status=0
out=$(/zig/zig cc -c /src/src/platform/linux/gtk_host.c -DNATIVE_SDK_ALLOW_WEBKITGTK_STUB $(pkg-config --cflags gtk4) -ferror-limit=0 -o /tmp/gtk_host_2272.o 2>&1) || status=$?
if [ "$status" -eq 0 ]; then
echo "unexpected clean compile on ubuntu 22.04 - this receipt assumes GTK-age errors; re-verify what it proves now"
exit 1
fi
echo "$out" | python3 /src/.github/scripts/glib272_error_allowlist.py
'
linux-dev-smoke:
name: Linux Dev Smoke (Debug scaffold)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
- uses: actions/setup-node@v4
with:
node-version: 22
# The scaffold default is the TypeScript core; its frontend and
# compiler run at build time from this checkout's packages/core.
- run: npm ci --prefix packages/core
# No WebKitGTK dev package, same as linux-canvas-smoke: the scaffold
# declares no web use, so its host compiles with the stub seam.
- name: Install GTK and Xvfb
run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev xvfb
# The Debug half of the Linux runtime story: linux-canvas-smoke runs
# at the graph's release default, and Release always uses the LLVM
# backend — a Debug-only x86_64 codegen fault (self-hosted backend,
# the mode every fresh `native init` + `native dev` builds) stays
# invisible to it. This job scaffolds the default template with the
# CLI, builds it -Doptimize=Debug with automation, runs it under
# Xvfb, and requires the startup scene to come up (ready=true,
# gpu_nonblank=true, template widgets in the snapshot).
- name: Scaffold and drive the Debug template headless
run: .github/scripts/linux-dev-smoke.sh
windows-canvas-smoke:
name: Windows Canvas Smoke (Wine)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
- name: Install Wine, Xvfb, and xdotool
run: sudo apt-get update && sudo apt-get install -y wine xvfb xdotool
# The Windows twin of the linux-canvas-smoke stub receipt: the
# WebView2 stub is the expected, configured state of every
# native-only Windows app, so cross-compiling the host with the
# stub define must be diagnostic-free (see that job's step comment
# for why even an informational #pragma message is dangerous, and
# why the compile must run against a cold cache).
- name: WebView2 stub cross-compile is diagnostic-free
run: |
export ZIG_GLOBAL_CACHE_DIR=$(mktemp -d) ZIG_LOCAL_CACHE_DIR=$(mktemp -d)
status=0
out=$(zig c++ -target x86_64-windows-gnu -std=c++17 -DNATIVE_SDK_ALLOW_WEBVIEW2_STUB -c src/platform/windows/webview2_host.cpp -o /tmp/webview2_host_stub.o 2>&1) || status=$?
if [ "$status" -ne 0 ] || [ -n "$out" ]; then
echo "the WebView2 stub cross-compile must succeed with zero diagnostics (exit $status):"
echo "$out"
exit 1
fi
# Cross-compiles ui-inbox for x86_64-windows-gnu and drives the
# retained Direct2D packet path (child HWND + WM_TIMER) under Wine:
# snapshot ready, gpu_backend=direct2d, gpu_nonblank=true,
# automation widget-click, and real XTEST pointer/keyboard input.
# Wineprefix init happens inline in the script (measured 21s from
# scratch in an ubuntu-24.04 container, so no cache step).
- name: Build and drive ui-inbox.exe under Wine
run: .github/scripts/windows-canvas-smoke.sh
windows-effects-smoke:
name: Windows Effects Smoke (Wine)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
- name: Install Wine and Xvfb
run: sudo apt-get update && sudo apt-get install -y wine xvfb
# Cross-compiles examples/effects-probe for x86_64-windows-gnu and
# proves the effect system's live Windows path under Wine: fx.spawn
# launches cmd.exe, streamed lines land in the model, the worker's
# PostMessageW wake shows up as effects_wake events in the trace
# log (frame ticks also drain, so the log is the wake's evidence),
# and fx.cancel terminates the child with the line count frozen.
- name: Build and drive effects-probe.exe under Wine
run: .github/scripts/windows-effects-smoke.sh
frontend-examples:
name: Frontend Examples
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
- uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
- run: zig build test-examples-frontends
@@ -90,7 +448,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
- uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
- run: zig build test-examples-mobile
@@ -100,20 +458,69 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
- uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
- uses: actions/setup-node@v4
with:
node-version: 22
# The default scaffold is a TypeScript core: its build runs the
# @native-sdk/core frontend and the external core compiler from
# this checkout's own install.
- run: npm ci --prefix packages/core
- run: zig build
- name: Scaffold and test the zero-config native app
run: |
set -euo pipefail
app=".zig-cache/scaffold-native-slim"
rm -rf "$app"
./zig-out/bin/native init "$app"
# Slim scaffold: no build files — the CLI's generated graph drives it.
test ! -f "$app/build.zig"
test ! -f "$app/build.zig.zon"
# The editor surface landed: package.json + tsconfig.json + the
# materialized @native-sdk/core copy stock tsc resolves.
test -f "$app/package.json"
test -f "$app/tsconfig.json"
test -f "$app/node_modules/@native-sdk/core/sdk/core.ts"
# ...and none of it is build truth: every verb works without it.
rm -rf "$app/node_modules"
./zig-out/bin/native test "$app" -Dplatform=null
./zig-out/bin/native check "$app"
# check self-healed the editor copy back into place.
test -f "$app/node_modules/@native-sdk/core/sdk/core.ts"
./zig-out/bin/native eject "$app"
(cd "$app" && zig build test -Dplatform=null)
- name: Scaffold and test frontend templates
run: |
set -euo pipefail
for frontend in next vite react svelte vue; do
for frontend in native next vite react svelte vue; do
app=".zig-cache/scaffold-${frontend}"
rm -rf "$app"
./zig-out/bin/zero-native init "$app" --frontend "$frontend"
(cd "$app" && zig build test -Dplatform=null && ../../zig-out/bin/zero-native validate app.zon)
./zig-out/bin/native init "$app" --frontend "$frontend" --full
(cd "$app" && zig build test -Dplatform=null && ../../zig-out/bin/native validate app.zon)
# Every scaffold ships a CI workflow; parse it as real YAML.
test -s "$app/.github/workflows/ci.yml"
python3 -c 'import sys, yaml; yaml.safe_load(open(sys.argv[1]))' "$app/.github/workflows/ci.yml"
done
evals-typecheck:
name: Evals Typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: pnpm/action-setup@v4
with:
version: 10.23.0
package_json_file: evals/package.json
- run: pnpm install --frozen-lockfile
working-directory: evals
- run: pnpm typecheck
working-directory: evals
docs:
name: Docs
runs-on: ubuntu-latest
+105 -48
View File
@@ -32,10 +32,10 @@ jobs:
- name: Compare package.json version to npm and check GitHub release
id: check
run: |
LOCAL_VERSION=$(node -p "require('./packages/zero-native/package.json').version")
LOCAL_VERSION=$(node -p "require('./packages/native-sdk/package.json').version")
echo "Local version: $LOCAL_VERSION"
NPM_VERSION=$(npm view zero-native version 2>/dev/null || echo "0.0.0")
NPM_VERSION=$(npm view @native-sdk/cli version 2>/dev/null || echo "0.0.0")
echo "npm version: $NPM_VERSION"
if [ "$LOCAL_VERSION" != "$NPM_VERSION" ]; then
@@ -51,14 +51,14 @@ jobs:
missing=0
for asset in \
CHECKSUMS.txt \
zero-native-darwin-arm64 \
zero-native-darwin-x64 \
zero-native-linux-arm64 \
zero-native-linux-x64 \
zero-native-linux-musl-arm64 \
zero-native-linux-musl-x64 \
zero-native-win32-arm64.exe \
zero-native-win32-x64.exe
native-sdk-darwin-arm64 \
native-sdk-darwin-x64 \
native-sdk-linux-arm64 \
native-sdk-linux-x64 \
native-sdk-linux-musl-arm64 \
native-sdk-linux-musl-x64 \
native-sdk-win32-arm64.exe \
native-sdk-win32-x64.exe
do
if ! printf '%s\n' "$EXISTING_ASSETS" | grep -Fx "$asset" >/dev/null; then
echo "Missing release asset: $asset"
@@ -92,7 +92,7 @@ jobs:
uses: actions/checkout@v4
- name: Setup Zig
uses: mlugg/setup-zig@v2
uses: vercel-labs/setup-zig@v1
with:
version: 0.16.0
@@ -108,35 +108,11 @@ jobs:
fi
echo "Extracted release notes for $VERSION ($LINES lines)"
- name: Build native release asset
- name: Build native release assets (all platforms)
run: |
mkdir -p /tmp/zero-native-release
build_asset() {
target="$1"
name="$2"
rm -rf zig-out
zig build -Dtarget="$target" -Doptimize=ReleaseSmall
src="zig-out/bin/zero-native"
case "$name" in
*.exe) src="${src}.exe" ;;
esac
cp "$src" "/tmp/zero-native-release/$name"
chmod 755 "/tmp/zero-native-release/$name"
}
build_asset aarch64-macos zero-native-darwin-arm64
build_asset x86_64-macos zero-native-darwin-x64
build_asset aarch64-linux-gnu zero-native-linux-arm64
build_asset x86_64-linux-gnu zero-native-linux-x64
build_asset aarch64-linux-musl zero-native-linux-musl-arm64
build_asset x86_64-linux-musl zero-native-linux-musl-x64
build_asset aarch64-windows zero-native-win32-arm64.exe
build_asset x86_64-windows zero-native-win32-x64.exe
(cd /tmp/zero-native-release && shasum -a 256 zero-native-* > CHECKSUMS.txt)
# Cross-compiles the CLI for all eight platforms and writes the
# flat release assets + CHECKSUMS.txt into zig-out/release/.
bash packages/native-sdk/scripts/build-binaries.sh
- name: Create GitHub Release
run: |
@@ -153,8 +129,8 @@ jobs:
fi
gh release upload "$TAG" \
/tmp/zero-native-release/zero-native-* \
/tmp/zero-native-release/CHECKSUMS.txt \
zig-out/release/native-sdk-* \
zig-out/release/CHECKSUMS.txt \
--clobber
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -167,7 +143,7 @@ jobs:
&& needs.check-release.outputs.should_release == 'true'
&& (needs.github-release.result == 'success' || needs.github-release.result == 'skipped')
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 15
environment: Release
permissions:
contents: read
@@ -182,17 +158,98 @@ jobs:
node-version: "24"
registry-url: "https://registry.npmjs.org"
# Publishing uses npm trusted publishing (OIDC): the job's id-token
# permission lets npm mint short-lived credentials, so no npm token
# secret exists anywhere in this repo. All nine packages —
# @native-sdk/cli plus the eight @native-sdk/cli-* platform packages
# under packages/native-sdk/npm/ — must each be configured on
# npmjs.com with a GitHub Actions trusted publisher pointing at
# repository vercel-labs/native, workflow release.yml,
# environment Release. If a package is missing that configuration,
# npm publish fails loudly with an OIDC authentication error before
# anything is uploaded for that package. Trusted publishing requires
# npm >= 11.5.1, satisfied by the npm bundled with Node 24.
# @native-sdk/core (packages/core) joins that publish set the moment
# its "private" flag drops (see the publish step below) — its
# npmjs.com trusted-publisher configuration must exist BEFORE that
# flip lands.
- name: Check version sync
run: npm --prefix packages/zero-native run version:check
run: npm --prefix packages/native-sdk run version:check
- name: Check package scripts
run: npm --prefix packages/zero-native run scripts:check
run: npm --prefix packages/native-sdk run scripts:check
- name: Download release binaries
run: |
VERSION="${{ needs.check-release.outputs.version }}"
mkdir -p /tmp/native-sdk-release
gh release download "v$VERSION" \
--pattern 'native-sdk-*' \
--pattern 'CHECKSUMS.txt' \
--dir /tmp/native-sdk-release
(cd /tmp/native-sdk-release && sha256sum -c CHECKSUMS.txt)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Stage binaries into the platform packages
run: |
# Release-asset name -> npm platform package (same table as
# packages/native-sdk/scripts/build-binaries.sh).
stage() {
asset="$1"; key="$2"; ext="$3"
mkdir -p "packages/native-sdk/npm/$key/bin"
cp "/tmp/native-sdk-release/$asset" "packages/native-sdk/npm/$key/bin/native$ext"
chmod 755 "packages/native-sdk/npm/$key/bin/native$ext"
}
stage native-sdk-darwin-arm64 darwin-arm64 ""
stage native-sdk-darwin-x64 darwin-x64 ""
stage native-sdk-linux-arm64 linux-arm64-gnu ""
stage native-sdk-linux-x64 linux-x64-gnu ""
stage native-sdk-linux-musl-arm64 linux-arm64-musl ""
stage native-sdk-linux-musl-x64 linux-x64-musl ""
stage native-sdk-win32-arm64.exe win32-arm64 ".exe"
stage native-sdk-win32-x64.exe win32-x64 ".exe"
- name: Publish to npm
run: |
if [ "${{ github.event.repository.visibility }}" = "public" ]; then
npm publish --provenance --access public
VERSION="${{ needs.check-release.outputs.version }}"
# Platform packages first, so the main package's
# optionalDependencies pins are resolvable the moment it lands.
# Re-runs skip anything already on the registry at this version.
publish_dir() {
dir="$1"
name=$(node -p "require('./$dir/package.json').name")
if npm view "$name@$VERSION" version >/dev/null 2>&1; then
echo "$name@$VERSION already published, skipping"
return 0
fi
(cd "$dir" && npm publish --provenance --access public)
}
for dir in packages/native-sdk/npm/*/; do
publish_dir "${dir%/}"
done
# @native-sdk/core (packages/core) rides the same version and
# release gate as the CLI (sync-version.js stamps it; the
# version check above refuses a half-bumped tree).
#
# ============================ LOUD FLIP REQUIREMENT ==========
# packages/core/package.json keeps "private": true until the
# 0.5.0 cut BY DESIGN, and that field IS the publish switch:
# while private, the guard below skips the package (npm would
# refuse to publish it anyway); the 0.5.0 change that drops
# "private" makes this same step start publishing it with no
# workflow edit. Before dropping the flag, configure the
# @native-sdk/core trusted publisher on npmjs.com (see the note
# above) or the first public release fails here.
# =============================================================
if [ "$(node -p "require('./packages/core/package.json').private === true")" = "true" ]; then
echo "@native-sdk/core is private (pre-0.5.0): skipping publish by design"
else
npm publish --access public
publish_dir packages/core
fi
working-directory: packages/zero-native
publish_dir packages/native-sdk
+36 -3
View File
@@ -1,10 +1,32 @@
.DS_Store
.zig-cache/
zig-out/
# Project-local package store for examples with third-party Zig
# dependencies (zig fetches into it beside the build).
zig-pkg/
# CLI-generated build graph for zero-config apps (the examples here) — the
# same entry `native init` writes into a new app's .gitignore.
.native/
# Native binaries in zero-native npm package (built by CI or locally)
packages/zero-native/bin/zero-native-*
packages/zero-native/src/
# Native binaries in the @native-sdk/cli npm packages (built by CI or locally)
packages/native-sdk/bin/native-sdk-*
packages/native-sdk/npm/*/bin/
# SDK payload mirrored into the npm package at pack time (copy-framework.js)
packages/native-sdk/src/
packages/native-sdk/assets/
packages/native-sdk/skills/
packages/native-sdk/skill-data/
packages/native-sdk/build/
packages/native-sdk/build.zig
packages/native-sdk/build.zig.zon
packages/native-sdk/app.zon
packages/native-sdk/third_party/
packages/native-sdk/packages/
packages/native-sdk/tools/
packages/native-sdk/LICENSE
# npm pack output
packages/native-sdk/*.tgz
packages/native-sdk/npm/*/*.tgz
# Downloaded/prepared CEF runtimes are large local artifacts.
third_party/cef/macos/
@@ -16,3 +38,14 @@ third_party/cef/linux/
# TypeScript build info (generated)
docs/tsconfig.tsbuildinfo
.claude/
# Dev-tool dependency trees (installed per package, never committed)
packages/core/node_modules/
.pnpm-store/
# The CLI-materialized editor copy of @native-sdk/core inside the TS example
# apps (node_modules is editor surface, never source — the same entry the
# TS scaffold writes into a new app's .gitignore)
examples/soundboard-ts/node_modules/
examples/system-monitor-ts/node_modules/
+37 -11
View File
@@ -1,16 +1,42 @@
# Agent Rules
# Agent Guide
## Releasing
Guidance for agents (and humans) working on this repository.
Releases are manual, single-PR affairs. The maintainer controls the changelog voice and format.
## App authoring default
To prepare a release:
Native SDK itself is implemented in Zig, but Native SDK **apps are authored in TypeScript + Native markup by default**. Do not infer the app-authoring language from this repository's implementation language or from older Zig-core examples.
1. Create a branch (e.g. `prepare-v1.2.0`)
2. Bump the version in `packages/zero-native/package.json`
3. Run `npm --prefix packages/zero-native run version:sync` to update all version references
4. Write the changelog entry in `CHANGELOG.md`, wrapped in `<!-- release:start -->` and `<!-- release:end -->` markers
5. Remove the `<!-- release:start -->` and `<!-- release:end -->` markers from the previous release entry; only the latest release should have markers
6. Open a PR and merge to `main`
- For a new app, use `native init <path>` and expect `src/core.ts`, `src/app.native`, and `app.zon`. Ordinary compiled TypeScript work that needs filesystem, process, JSON, regex, classes, or other static-tier APIs belongs under optional `src/services/`, reached from the core with `Cmd.request`; do not import a service from the core. Do not add Zig app code unless the user explicitly chooses `--template zig-core` or the feature requires a toolkit extension.
- Before changing an existing app, inspect its tree. A `src/core.ts` app stays TypeScript; a `src/main.zig` app stays Zig unless the task is specifically a migration.
- For default app work, read `skill-data/native-ui/SKILL.md` and `skill-data/ts-core/SKILL.md`; also read `skill-data/ts-services/SKILL.md` when the tree has `src/services/` or the task needs ordinary TypeScript beyond the core subset. `skill-data/core/SKILL.md` covers shared/runtime concerns; `skill-data/zig/SKILL.md` is for Zig-core apps and SDK implementation work.
- The `-ts` suffix on a few examples only distinguishes ports from older Zig originals. New TypeScript apps need no suffix because TypeScript is the default.
CI compares the version in `packages/zero-native/package.json` to what's on npm. If it differs, it publishes the CLI package and creates the GitHub release automatically. If npm already has the version but the GitHub release is missing, CI creates the GitHub release from the marked changelog entry.
## Build, test, and gate
```bash
zig build test # root engine + runtime suites
zig build validate # sample app.zon manifest check
zig build test-example-<name> # one example's suite (e.g. test-example-notes)
scripts/gate.sh fast [ref] # affected-only local gate for your diff (default base: main)
scripts/gate.sh full # everything CI-shaped that runs locally
```
Run `scripts/gate.sh fast` before finishing any change; it maps your diff to the suites that cover it. The docs site checks with `pnpm --dir docs check` (the gate runs it only when `docs/` changed).
This repository builds with Zig 0.16.0. If a build fails with "no member named" errors on std APIs (`std.fs.cwd`, `ArrayList.init`, `std.io`), you are writing pre-0.16 idioms — `skill-data/zig/SKILL.md` maps each such compile error to the current idiom as this codebase writes it.
Pinned goldens (pixel signatures, schema fingerprints, command counts) are updated deliberately: review the rendered output or the counted commands first, and keep the pin's comment a self-contained description of what the value represents.
## Changelog
Do not edit `CHANGELOG.md` as part of regular feature or fix work. The release agent reviews the git history since the previous release and writes the complete changelog entry during release preparation; see [RELEASING.md](./RELEASING.md).
## Where things live
- `src/` — the engine and runtime; `src/primitives/canvas/` holds the widget, markup, and vector core.
- `examples/` — the showcase apps, most zero-config (`app.zon` + `src/`).
- `docs/` — the documentation site; `docs/AGENTS.md` has its MDX conventions.
- `skills/` and `skill-data/` — the agent skills the CLI ships (`native skills list`).
- `tools/` and `scripts/` — dev tooling and the local gate.
Releases are maintainer-run; see [RELEASING.md](./RELEASING.md).
+809 -3
View File
@@ -1,19 +1,825 @@
# Changelog
All notable changes to zero-native will be documented in this file.
All notable changes to the Native SDK (formerly zero-native) will be documented in this file.
## 0.1.9
## 0.9.2
<!-- release:start -->
### New Features
- **Flash-free accessory startup**: Apps can opt into accessory activation from `app.zon` to launch without a Dock icon or foreground flash, with tray-affordance validation, runtime composition, packaging support, and an updated menu-bar example (#358).
- **Logical canvas radio groups**: Nested radios now form accessible single-selection groups with roving focus and consistent keyboard, pointer, handler, and naming semantics (#361).
- **Budget-aware photo decoding**: Dynamic encoded images are downsampled across desktop and mobile codecs to fit a configurable registered-pixel budget, with independent source bounds, deterministic replay, and platform-level regression coverage (#366).
### Bug Fixes
- **Correct anchored surfaces**: Floating and modal surfaces now dismiss without requiring focus, relayout after scroll restoration, resolve against the correct root, and behave consistently across window contexts (#363).
- **Reliable autofocus and caret reveal**: Keyboard focus, autofocus, and automation now transactionally reveal offscreen targets while preserving collapsed end-caret selections in text editors (#364).
- **Explicit link decoration**: Linked text spans now honor their underline flag while Markdown-generated links retain conventional underlines (#368).
- **Stable macOS window geometry**: Fresh windows now distinguish restored, explicit, and default placement, while AppKit and CEF frame events consistently report content geometry without titlebar drift (#369, #370).
### Improvements
- **Consistent canvas controls and surfaces**: Checkbox and radio labels can contain markup consistently, while actionable states, disabled colors, variant accents, selection geometry, compact layouts, and zero-width strokes now render uniformly across the schema, runtime, accessibility tree, and documentation (#367).
### Contributors
- @ctate
- @sepehr-safari
<!-- release:end -->
## 0.9.1
### New Features
- **Multi-item macOS menu bars**: Apps can now manage independent, keyed status items with model-driven updates, events, automation, journaling, and regression coverage (#343).
- **Complete TypeScript file effects**: Secure, permission-gated effects now support bounded streaming reads, atomic writes, stat, append, and deletion while preserving deterministic record and replay behavior (#339, #350).
- **Actionable desktop notifications**: Notification replacement identifiers and actions dispatch through the ordinary command path on macOS, Windows, and Linux (#347).
- **Secondary-window lifecycle control**: Window descriptors can declare quit or hide-on-close behavior, preserve hidden-window identity when reopened, and expose the same model-driven window contract to TypeScript apps (#349, #351).
- **Mobile TypeScript cores and services**: TypeScript apps with services now compile into iOS and Android library archives, with mobile packaging and device-level runtime coverage (#346).
### Bug Fixes
- **Safe Linux alert dialogs**: GTK alert dialogs now initialize with a valid empty format string, avoiding a crash from a null constructor argument (#354).
- **Correct compiled-core tuple returns**: The SDK now pins the scriptc tuple-normalization fix and verifies bare-model and effect-tuple ABI returns with a compiled-core regression (#356).
### Improvements
- **Stronger TypeScript core guidance and diagnostics**: Subset rules now distinguish permanent guarantees from deliberately deferred capabilities and point authors to the appropriate service alternative (#345).
- **End-to-end services showcase**: The Feed Reader example now demonstrates the full TypeScript service workflow with typed feed parsing, shared data, fixtures, and replay coverage (#352).
- **Updated compiler integration**: scriptc advances through 0.0.31 with refreshed generated contracts, compatibility fixtures, and compiler-surface references (#344, #356).
### Contributors
- @ctate
- @ElSebas41
- @johnlindquist
## 0.9.0
### New Features
- **Ordinary TypeScript services behind a typed boundary**: Apps can place filesystem, process, JSON, regex, class, and exact-vendored npm work under `src/services/`; Native SDK generates the checked client and codecs, compiles a pinned static service executable, and carries keyed requests, typed streaming, cooperative cancellation, deadlines, supervision, and deterministic replay across the isolated boundary (#317, #321).
- **Optional in-process TypeScript services**: Services can use the same boundary through a linked, runtime-localized worker pool with per-key FIFO ordering, parallel independent keys, streaming, timeout and trap isolation, and replay that never starts the carrier; the explicit opt-in now follows the compiler's Windows, Linux, macOS, and cross-target matrix while the isolated child remains the automatic default (#334, #337).
- **Engine-owned model persistence**: TypeScript cores can persist committed models through capability-gated, atomically replaced snapshots with generated codecs, debounced writes, backup recovery, explicit restore and migration routes, rollback safety, and journal/replay support (#316).
- **SQLite record storage**: TypeScript and Zig apps can use a capability-gated record store for deterministic atomic CRUD effects backed by bundled SQLite across desktop and mobile hosts, with devhost parity and a complete Record Store example (#320).
- **Checked relational SQLite**: Append-only migrations, build-validated named SQL, transactions, generated typed commands and live-query subscriptions, replay, and the Relational Notes example make relational SQLite a first-class offline data layer across desktop and mobile (#326).
- **Model-driven menu-bar apps**: TypeScript apps can derive status-item labels, icons, tooltips, and rich menus from committed model state, while new macOS effects control hidden startup, fullscreen, Dock visibility, and launch-at-login behavior across both native hosts (#311, #314).
- **Platform services for TypeScript cores**: Typed effects now open external URLs, reveal filesystem paths, and format local time through validated macOS, Linux, and Windows backends (#315).
- **App-scoped credentials**: TypeScript and Zig cores can store, load, and delete credentials through capability- and permission-gated native providers, with redacted journals, deterministic replay placeholders, and hermetic devhost stores across desktop and mobile (#335).
- **Cross-compiled TypeScript cores**: The external core compiler now builds Linux and Windows GNU targets from macOS, Linux, or Windows and macOS targets from macOS, with target-independent contracts and cross-platform end-to-end batteries for Windows and Linux musl (#340).
### Bug Fixes
- **Working documentation root**: `/docs`, `/docs/`, and the matching Markdown route now resolve to the Introduction instead of ending at a 404 (#338).
### Improvements
- **Faster retained desktop frames**: Animation pumping and Windows wake scheduling now avoid stalled or redundant work, profiling uses monotonic frame-correlated telemetry, and physical macOS and Windows performance gates protect input latency and frame budgets (#313).
- **Measured, compiler-truth service tooling**: A dedicated TypeScript Services reference documents the two-tier model and failure semantics; production-carrier benchmarks measure cold start, latency, and throughput; generated compiler-surface references and manifest diffs keep capability claims mechanically honest; and scriptc advances through 0.0.28 with refreshed contracts and calibration (#325, #327, #328, #329, #333, #336).
### Contributors
- @ctate
- @carvalab
- @Railly
- @camilocbarrera
## 0.8.4
### New Features
- **Streaming fetch responses for TypeScript cores**: `Cmd.fetch` can now deliver line-framed HTTP responses through typed message arms with deterministic terminal errors, loud cancellation, duplicate-key rejection, and bounded line sizes; the rebuilt Chatbot example streams Vercel AI Gateway replies with live model selection and a Stop action (#300).
- **Desktop audio capture**: TypeScript cores can start bounded, timestamped microphone or system-output PCM streams on macOS and Windows with explicit lifecycle, permission, drop-count, and replay handling; the new Voice Memo example records, saves, and plays WAV files (#303).
- **Customizable macOS DMG packaging**: `native package` now creates polished drag-to-Applications disk images with generated or custom Retina backgrounds, configurable Finder geometry, positioned app and Applications entries, and staged files, directories, or links (#304).
- **Live TypeScript theme packs**: zero-config TypeScript apps can export `themePack(model)` to switch the built-in theme pack from app state without losing live system scheme, accessibility, accent, or scale inputs (#308).
### Bug Fixes
- **Smooth macOS dialog blur**: Host backdrop blur now uses an optimized three-pass Gaussian approximation and correct dirty-region invalidation, eliminating flat or stale dialog backgrounds while preserving the established scrim treatment (#299).
- **Byte-accurate PTY event keys**: TypeScript PTY event routes now expose echoed session keys as `Uint8Array`, matching the byte-text host, generated facade, and external-core contract (#307).
- **Reliable keyboard widget navigation**: Interactive canvas lists, trees, menus, and anchored controls now retain logical focus across clipped rows, scroll keyboard targets into view, and paint active and focus-visible states consistently (#308).
### Improvements
- **TypeScript component gallery**: The GPU component showcase is now a TypeScript core and Native markup app with isolated interactive specimens, model-driven Default and Geist switching, clearer navigation, and dedicated smoke coverage (#308).
### Contributors
- @ctate
- @marcusschiesser
- @NyxTools-M
## 0.8.3
### Bug Fixes
- **Packaged TypeScript app assets**: Runtime asset lookup now finds bundled macOS resources before the process working directory, so Native markup boot images such as the Kanban agent avatars render after launch (#297).
- **Unclipped drag landing motion**: Dropped cards now stay in the lifted drag layer through their landing animation while neighboring reflow remains clipped within its swimlane (#297).
### Improvements
- **Denser Kanban showcase**: The seeded board now includes twice as many Jira-style tickets and removes redundant issue glyphs from card metadata (#297).
### Contributors
- @ctate
## 0.8.2
### New Features
- **Native drag and drop for TypeScript apps**: Native markup's new `on-drag` channel delivers live, release, and cancellation geometry to compiled cores while the renderer lifts the source under the pointer, preserves one globally keyed insertion slot, animates neighboring items, and supports Escape cancellation; TypeScript cores can also map native multi-file drops into ordinary deterministic messages through `dropMsg` (#285).
- **Desktop notifications from model cores**: TypeScript apps can return fire-and-forget `Cmd.showNotification` effects and Zig apps can call `fx.showNotification`, with bounded validation and suppression during fake execution and session replay (#283).
### Bug Fixes
- **Explicit zero canvas padding**: Programmatic and compiled or interpreted Native markup views now preserve `padding="0"` instead of replacing it with the widget kind's default padding (#288).
### Improvements
- **TypeScript-first app authoring guidance**: Repository instructions, bundled skills, examples, package documentation, and the docs site now consistently lead with TypeScript cores and Native markup for new apps while keeping Zig as the explicit alternative and toolkit-extension tier (#284).
- **Agent ticket Kanban showcase**: The TypeScript Kanban example now presents numbered OpenAI- and Claude-assigned tickets, uses an icon-only add action, keeps columns scrollable, and extends its end-to-end coverage for the updated drag geometry (#295).
### Contributors
- @ctate
- @johnlindquist
- @Railly
## 0.8.1
### New Features
- **Safe presentational HTML in Markdown**: Markdown now lowers common GitHub-style inline and block HTML into native widgets, including links, details, aligned containers, and caller-resolved images, while scripts, styles, forms, embeds, event attributes, and unsupported or malformed markup remain inert literal text (#280).
### Bug Fixes
- **Reliable resolved Markdown images**: image discovery now follows renderable block starts, canonicalizes entity-encoded URLs consistently between loading and lookup, preserves aspect ratios within declared bounds, honors centered and end alignment, and ignores images inside comments, unsupported markup, code, and preformatted blocks (#281).
- **Payload-free HTTP write requests**: `Effects.fetch` now sends an explicit zero-length body for POST, PUT, and PATCH requests without a payload, preventing debug-build crashes and emitting the required `Content-Length: 0` header (#277).
### Improvements
- **History-driven release notes**: release preparation now builds the complete changelog entry and contributor list from the commits since the previous release, replacing the per-change fragment workflow (#278).
### Contributors
- @ctate
- @Railly
## 0.8.0
### New Features
- **Compiler-truth checks for TypeScript cores**: `native check` now ends with the pinned external core compiler's analyzer over the entry with the shipped SDK declarations mapped, so check and build share one compiler verdict. Type errors the frontend's own line would miss fail with the compiler's diagnostics verbatim; an analyzer that cannot reach a verdict defers to the build instead of wedging check.
- **TypeScript cores compile through the external core compiler**: the frontend checks `src/core.ts` and emits its contract sidecar, the exact-pinned compiler builds a native archive, and the app links a generated mirror over it — no JS runtime in the binary, nothing to configure.
- **The TS-to-Zig transpiled lane is removed** (a deliberate pre-1.0 break): `core_compiler = "transpiler"` in app.zon (and `-Dcore-compiler=transpiler`) is refused with a teaching, and `native check` runs the checker and contract only — no emitted Zig lands under `.native/check/`.
- **The compiler is a package dependency**: it ships exact-pinned with the SDK's `packages/core` (repo checkouts install it with `npm ci` there; an npm-installed CLI carries it automatically).
- **The core dev loop is restart-shaped**: markup hot reload and the instant `native dev --core` node loop are unchanged, and a core edit now pays a native compile measured in seconds on rebuild.
- **TypeScript cores are desktop-only for now**: a mobile target with `src/core.ts` is taught before lane selection (the external toolchain does not target mobile yet); Zig and markup cores stay fully supported on mobile.
- **Shipped type declarations**: `@native-sdk/core` now ships generated `sdk/*.d.ts` declaration files beside its TypeScript sources, so external tooling can resolve the SDK's types without compiling them.
### Improvements
- **Leaner TypeScript toolchain installs**: the unused `@typescript/typescript6` compatibility wrapper is no longer a dependency of `@native-sdk/cli` or `@native-sdk/core`. The frontend already imports its compiler directly through the exactly pinned `@typescript/old` alias, while consumer trees carrying their own wrapper remain unaffected.
### Contributors
- @ctate
## 0.7.2
### New Features
- **Geist-style code diffs**: `ui.code` and `<code>` can mark added and removed logical lines with theme-aware full-row washes, renderer-owned `+`/`-` markers, optional line numbers, and unchanged syntax-highlighted clipboard source.
### Improvements
- **Verified Zig setup**: repository and generated CI workflows now install Zig with `vercel-labs/setup-zig`, including signed archive and checksum verification.
### Bug Fixes
- **Theme-accurate disabled buttons**: disabled buttons now keep shadcn's knockout label treatment in the default theme and use Geist's gray-100/gray-700 swap, gray-400 edge, and distinct half-opacity tertiary register in the Geist theme.
- **Canonical documentation routes**: documentation now lives under `/docs/`, with permanent redirects from every previous URL, explicit canonical metadata, `.md` siblings, and a generated `llms.txt` that stays aligned with each page's canonical MDX source.
- **Geist primary tabs match the design system**: tab strips now use the reference 50px row, full-width bottom rail, content-hugging 14px triggers, 24px spacing, and 16px icon treatment without changing default-theme pill tabs; the GPU component gallery now pairs a compact theme picker with a scrollable component tree and focused specimen views.
- **Quiet Windows subprocesses**: `Effects.spawn` no longer opens or flashes a console window when a GUI or tray app launches a console-subsystem helper such as `node.exe`; interactive terminal children remain on the separate PTY API.
- **Responsive Windows GPU surfaces**: Windows now renders retained binary canvas packets with Direct2D and DirectWrite, applies dirty-region patches (including edge-safe GPU-resident backdrop blur), and limits RGBA-to-BGRA conversion and invalidation to dirty pixels when software fallback is required.
- **Exact Windows packet text and chrome**: Packet rendering now refuses when the bundled/custom font path cannot preserve engine-planned metrics, preserves explicitly positioned glyph runs, prevents system glyph substitution, and samples a covered, changed hidden-titlebar pixel for native caption contrast.
- **Truthful GPU backend types**: TypeScript creation options now expose only portable backend requests while view and frame state can report the concrete Direct2D renderer; explicit software requests bypass packet encoding and image uploads and stay on the reference renderer and pixel presenter.
- **Reliable registered-image replacement**: Unregistering and then re-registering identical pixels now recreates the removed GPU resource instead of retaining a stale cache key and silently omitting the image.
### Contributors
- @ctate
- @oshtz
## 0.7.1
### New Features
- **Declarative folder-to-code editor example**: `examples/code-editor` authors its complete view in hot-reloadable `.native` markup, unifies its titlebar, file pane, tab-strip canvas, and editor background, centers the opened folder name beside a trailing ghost Save icon in a custom titlebar, opens or replaces the focused window's folder with Cmd+O, creates independent editor windows with Cmd+N, builds a clean bounded folders-first disclosure tree with outline-free selection-only Up/Down navigation, leaf-to-parent Left movement, Left/Right expansion, in-place disk-backed Enter rename, Cmd+Enter permanent tabs, and folder focus independent from the active editor, and presents editable syntax-highlighted files (including `.mjs` and large practical sources) in a resizable second pane with flat VS Code-style tabs whose active tab has no top accent and breaks the baseline to meet the editor, replaceable italic previews that pin when double-clicked, dirty dots, active/hover close buttons, native Close/Close Others tab menus, wrapping Cmd+Shift+[/] tab cycling, Cmd+W tab-or-empty-window closing, and serialized disk-backed Save/Cmd+S.
- **Generated compiled-core facade**: `corewire --facade` now emits the complete compiler entry and matching profile from the contract sidecar, including explicit `--f64-slot` demotions, authored type provenance, and signed or unsigned integer proofs at every host ingress.
- **Facade contract hardening**: generated entries preserve subdirectory module paths, reconstruct private reachable types without invalid imports, preserve Model-first resolution for homonymous unbound bindings, decode optional and composite record fields with a running cursor, prove nullable integer helpers, handle signed and unsigned text-selection sentinels consistently, and refuse legacy sidecars that lack the authored facts a facade requires.
- **Effective sidecar projection**: `corewire --effective-sidecar` emits the contract after explicit slot demotions, and staged facade/profile/sidecar triples now describe one compiled layout.
- **Editable highlighted code**: `ui.code` and `<code>` keep their read-only default, while `editable` plus `on-input` opts into a syntax-colored multiline editor in both retained and direct rendering, with selection, caret-row highlighting, IME, clipboard, undo/redo, indentation-aware Tab input (tabs or inferred 28-space widths, defaulting to two spaces), and no textarea chrome.
- **Markdown source highlighting**: `markdown`/`md` joins the code lexer names with themed headings, lists, emphasis, links, inline and fenced code, and comments; the code-editor example selects it for Markdown files.
- **Stable line-number gutter**: numbered code reserves at least three marker columns, so short files keep a useful gutter while larger line counts still expand it.
- **Double-click messages in Native markup**: `on-double-press` exposes the canvas runtime's additive double-click channel to `.native` views, so the first click can select or preview and the second can perform or pin without a timer. Multi-click chains stay scoped to one control and physical pointer, and a third click returns to the ordinary press action instead of repeating the double action.
### Improvements
- **Composable code presentation**: `ui.code` and `<code>` now provide bare highlighted content without their own background, border, radius, shadow, or padding; wrap them in a panel or card when surface chrome is wanted. An enabled line-number gutter remains opaque while horizontally scrolling so source glyphs cannot clash with its pinned markers.
- **Flat tree keyboard hierarchy**: `treeitem` rows can declare a one-based `tree-level`, letting Left/Right find logical parents and children in loop-rendered flat trees, while `on-change` can keep arrow-key selection distinct from pointer activation.
### Bug Fixes
- **Live code docs preview**: The Code component page now loads its real WASM-backed engine scene instead of silently remaining on the static screenshot fallback.
- **Reliable large-code editing**: editable code now repaints only visible selected glyphs and caches longest-line width measurements, keeping large selections and steady-state no-wrap rendering inside bounded display-list and host-measurement budgets.
- **Complete wrapped long lines**: scrolling a single logical line beyond 128 wrapped rows now pages its visible glyphs instead of leaving the remainder blank.
- **Stable code-editor reads**: switching tabs no longer cancels a pinned file's load, and reopened secondary windows keep monotonic file-effect keys so late completions cannot populate a newer document.
- **Unsaved-edit protection**: opening another folder or closing a secondary editor window now refuses while that window still has dirty documents.
- **Steady editor tabs**: active and inactive tabs now share the same background and label alignment, so filenames no longer shift when selection changes.
- **Balance explorer rows**: file-tree hover and selection backgrounds now keep even visual gutters beside the sidebar edge and split handle while preserving compact label alignment.
- **Complete repository roots**: the explorer now indexes a folder when it expands instead of spending its bounded tree budget in an eager depth-first walk, so large subtrees cannot hide root files or unexplored sibling folders; `.next` and `.pnpm-store` remain visible but are not recursively indexed.
- **Familiar file opening**: Command+Down Arrow now opens the selected tree file as a persistent tab; Command+Enter remains available to the focused control.
- **Visible active tabs**: inactive tabs retain their bottom divider, and opening, clicking, or keyboard-cycling to a tab now minimally scrolls it into view horizontally without shifting an already visible tab.
- **Distinct new windows**: Command+N now opens each editor window slightly down and to the right of the active window so the new window is immediately apparent.
- **Clear empty-window title**: editor windows now show “Code Explorer” in the title bar until a folder is opened.
- **Stable editable-code repainting**: syntax-highlighted editors now keep unique retained command IDs while edited text and highlighted spans occupy different runtime storage, preventing a selected editor from crashing when the app deactivates.
- **Safe large widget text**: views keep their ordinary 64 KiB text pools inline and allocate practical source-file capacity only when a large layout or edit needs it, while edit, presentation, and context-menu workspaces stay off constrained native stacks and large single-line pastes continue stripping line breaks.
- **Folder-only macOS open dialogs**: `allow_directories = true` now matches Linux and Windows by selecting directories rather than allowing files alongside them in AppKit and CEF hosts.
- **Code-editor presentation polish**: JavaScript and TypeScript object keys and typed bindings now use the same syntax color as variables, while CSS declaration names retain their property color; numbered editors also use their full trailing width so fitting lines do not produce false horizontal scrolling.
- **Complete JSX and TSX syntax highlighting**: JSX-family code blocks now combine JavaScript or TypeScript token coloring with JSX tags and attributes instead of treating the whole file as plain HTML outside `{…}` expressions.
- **YAML syntax highlighting**: code surfaces, Markdown fences, and the code-editor example now recognize `yaml` and `yml`, coloring mapping keys, scalars, document markers, anchors, tags, and comments.
### Contributors
- @ctate
## 0.7.0
### New Features
- **Code component**: `ui.code` and markup `<code>` render highlighted source with the Geist Code Block palette in both built-in themes, wrapping by default, opt-in logical line numbers, unwrapped horizontal scrolling, and vertical scrolling for height-constrained surfaces; Markdown fences share the same component.
### Bug Fixes
- **Bounded transformed code rendering**: heavily scaled code surfaces now degrade within the shared command and text-byte budgets instead of rejecting the entire display-list refresh.
- **Polished Markdown lists and code blocks**: bullet and ordered-list markers now align with the first content line, while fenced code preserves source indentation and applies theme-aware highlighting with richer HTML/JSX tags and attributes.
### Contributors
- @ctate
## 0.6.3
### Bug Fixes
- **Native textarea editing shortcuts**: Up/Down now moves or extends the caret across visual lines, Command+Left/Right uses the current line boundary even through unbroken soft wraps, Command+Up/Down reaches the document boundary, and Command+Z / Command+Shift+Z provides bounded per-editor undo and redo from either the keyboard or macOS Edit menu while keeping controlled `TextBuffer` models synchronized.
- **Textarea indentation**: spaces typed at the start of an empty line now remain visible and advance the caret under word wrapping.
- **Textarea pointer selection**: Shift-click now extends the selection from the existing caret instead of replacing it.
- **Textarea line endings**: caret movement, deletion, and controlled selections now treat CRLF line endings as one indivisible boundary.
### Contributors
- @ctate
## 0.6.2
### New Features
- **Reliable desktop overlay windows**: window declarations and runtime creation now support transparent, always-on-top, click-through, and passive-show presentation applied before first visibility; canvas windows reveal after their first alpha-correct present without stealing focus, fall back to a late reveal if rendering wedges, and Windows composites multiple canvas layers while rejecting child surfaces its layered presenter cannot display.
- **Honest backend constraints**: Linux main WebViews inherit transparent-window alpha, macOS Chromium rejects transparent windows because windowed CEF content cannot supply alpha, and transparent Windows windows require chromeless chrome with no application menu because the layered compositor cannot capture Win32 non-client pixels.
- **Resizable transparent Windows windows**: the layered presenter keeps the nearly invisible system resize frame pointer-targetable without filling intentional alpha-zero regions in the client.
- **Hybrid overlay lifecycle**: canvas-only overlay windows stay free of implicit main WebViews in mixed WebView scenes and across hot reloads, while passive Linux windows restore from minimization without taking focus.
- **Reliable explicit focus on macOS**: focusing a system-WebView window now activates the app before asking AppKit to make the window key, so an inactive app can come forward as requested.
- **Imperative canvas overlays**: `runtime.createWindow` and `window.zero.windows.create` keep transparent windows without an explicit source canvas-only across hot reloads, and JavaScript can select the chromeless titlebar Windows requires.
- **Idle overlays stay idle**: transparent canvas windows retain their last presented image without entering a display-rate repaint loop, while software presenters still rebuild fully once when their shared pixel buffer changes surfaces.
### Bug Fixes
- **Container backgrounds render**: explicit backgrounds on `stack`, `row`, and `column` now paint across the laid-out frame with their configured radius.
### Contributors
- @ctate
- @jasonkneen
- @sepehr-safari
## 0.6.1
### Bug Fixes
- **Layered macOS cursors**: GPU surfaces now yield their cursor regions to higher-layer embedded webviews, so links, selectable text, and canvas widgets use the correct cursor in mixed canvas/webview windows such as Workbench.
- **Pointer-selected text edits**: editable fields now send pointer caret and selection changes through `on-input`, so model-owned text buffers delete or replace the highlighted span instead of editing at a stale caret.
### Contributors
- @ctate
## 0.6.0
### New Features
- **The external-source channel — `fx.openChannel`, TEA subscriptions done our way**: apps with long-lived external sources (sockets, file watchers, app-managed worker threads) get a first-class, journaled way to wake the UI loop and produce a Msg — no more timer-polling a shared queue. `fx.openChannel(.{ .key, .on_event, .max_pending? })` returns a THREAD-SAFE `ChannelHandle` whose `post(bytes)` stages into a per-channel non-lossy FIFO, wakes the host, and delivers one `.data` event Msg per accepted post on the next drain (bytes in drain scratch, bounded at `max_effect_channel_bytes`); `fx.closeChannel(key)` flushes the staged backlog and delivers exactly one `.closed` terminal with final drop totals. Channels share the keyed families' one key space — occupied from open until close delivers — and never fail from the caller's view: a duplicate occupied key or a full table answers with one `.rejected` event.
- **Back-pressure is part of the contract, and the post's answer names it**: `post` returns a `ChannelHandle.PostResult``.accepted`, `.dropped_full` (staging FIFO full: transient, skip and keep producing), `.dropped_oversized` (bytes over the post bound: a programming error no retry fixes), or `.closed` (the occupancy is over: exit the loop) — so a producer never has to guess "retry later" from "stop forever". Both drop answers count into `dropped_pending`/`dropped_total` on the NEXT delivered event — never silent drops, and never a blocked posting thread given a conforming host wake: the platform's `wake_fn` is contractually a bounded, non-blocking, enqueue-only nudge (documented at `PlatformServices.wake_fn`; every first-party host conforms — macOS `dispatch_async`, GTK `g_idle_add`, Win32 `PostMessageW`), and the runtime holds no channel lock across the call, so even a violating embedder wake hangs only its own posting thread, never a drain, close, or teardown. A violator still inside the hook at teardown is abandoned after a bounded wait, and the platform is then deliberately kept alive — destruction skipped, leaked process-lived, with one loud log — so the stale call can never execute into freed host state. Wakes are exactly as many as the loop needs: a refused post never wakes the host (a wake is issued only when a post makes new work drainable), and accepted posts COALESCE behind one latched wake per drain (a burst costs the host queue one entry, cleared at the drain boundary before it snapshots — so a post racing the drain always lands a fresh wake), meaning neither a refusal storm nor a fast producer can grow the loop's queue. Handle lifetime is safe by construction: the handle resolves through a generation-stamped process-lifetime header, so posts after close, after slot reuse, or after runtime teardown answer `.closed` instead of touching freed memory.
- **The journal fingerprint moves, a conscious break**: the `.channel` effect-record kind journals every delivered event as executor truth at the drain boundary, post bytes INLINE (channel posts are small-message-shaped — no blob store detour). Replay feeds the recorded events verbatim and never NEEDS the source — the channel open is an ordinary replayed dispatch that PARKS the occupancy (the key registers as live, duplicate opens reject symmetrically, admission rejections regenerate) and returns an inert handle whose every post answers `.closed`. Honesty about what re-runs: the opening update re-executes under replay, so a producer launched unconditionally really starts — socket connects and blocking setup before its first post included — and is stopped only AT that first post; `ChannelHandle.live()` is the producer-launch check (false for parked replay handles, refused opens, and closed occupancies — advisory, the post's own answer stays authoritative), so producers that consult it before launching keep replay fully offline, the `examples/channel-monitor` pattern. Impossible records (bytes over the post bound, byte-carrying terminals) refuse replay as damage. Journals from earlier builds are refused at the preamble with the standard re-record teaching.
- **Bridge refusal timing, a conscious break**: TS-tier refusals produced by the bridge itself — duplicate-spawn keys, image validation, channel admission — used to deliver their rejection Msg at the command cycle's own boundary, before anything else could run. They now stage into the engine's seq-stamped pending stream and deliver at the next host drain, so every rejection — engine-refused or bridge-refused — arrives in ONE seq-ordered stream in command order, which is what `Cmd.batch`'s performed-in-order contract requires across layers (a batch mixing the two authorities used to deliver its rejections out of order). The observable difference: a frame may render between the command cycle and the rejection Msg, so an app or test that asserted the rejection landed inside the same cycle now sees the intermediate model rendered once and the rejection one drain later.
- **TS tier first-class**: `Cmd.channelOpen(key, { event })` / `Cmd.channelClose(key)` (wire opcodes 0x15/0x16, additive within cmd_format_version 3) with a five-field event arm matched by name (`key`/`state`/`bytes`/`droppedPending`/`droppedTotal`; the three-member `ChannelState` union checked at build time). Posting is deliberately not a TS verb — transpiled cores are single-threaded: the TS tier opens, closes, and receives, and the native side feeds through `Effects.channelHandle(key)`.
- **`examples/channel-monitor`**: an app-owned worker thread samples its own process and posts each reading; the UI updates only when events arrive — no `fx.startTimer`, no polling, and Stop winds the detached worker down through the handle's own `.closed` answer, while a transient `.dropped_full` only skips a sample — the drop counters reach the status line.
- **Horizontal and two-axis canvas scrolling**: scroll views declare `axis="vertical|horizontal|both"` (builder `axis:`), horizontal offsets ride `value-x` with the same source-wins reconcile as `value`, the engine draws a bottom-edge scrollbar, keyboard scrolling gains Left/Right/Home/End on horizontal-capable regions, and macOS native scroll drivers carry both axes with OS momentum and rubber-band.
- **Independent per-axis wheel routing**: each axis of a wheel/trackpad gesture travels to the nearest ancestor scrollable on that axis, so a horizontal timeline holding a vertical list splits a diagonal gesture — `delta_y` scrolls the list, `delta_x` reaches the timeline.
- **BREAKING — `ScrollState` is two-axis now**: the one-axis `{offset, velocity, viewport_extent, content_extent}` record (TS: `offset`/`velocity`/`viewportExtent`/`contentExtent`) was replaced by per-axis fields `offset_x`/`offset_y`, `velocity_x`/`velocity_y`, `viewport_extent_x`/`viewport_extent_y`, `content_extent_x`/`content_extent_y` (TS: `offsetX``contentExtentY`); migrate a vertical region by reading the `_y` fields where it read the old ones — an `on-scroll` arm still declaring the old shape fails the build with a teaching that names the new fields.
- **Hover-driven Msgs — `on-hover-enter` / `on-hover-leave`**: widgets can now bind pointer hover as first-class TEA vocabulary (Elm's `onMouseEnter`/`onMouseLeave`): enter dispatches once when the pointer enters a bound element's hit region, leave once when it exits — discrete containment edges, never per-move — so hover previews, prefetch, and hover cards are ordinary Msgs. Legal on any element in markup and in Zig views (`ElementOptions.on_hover_enter` / `on_hover_leave`), and the TS tier gets the pair for free (payloadless events need no SDK types).
- Binding hover makes the element hover-hittable the way a bound press makes it pressable — but never pressable: clicks keep falling through, no accessibility action is announced, and no hover wash appears (a quiet content tile that binds hover stays visually quiet). Nested bound elements track containment independently; enters fire outermost-first, leaves innermost-first.
- Every enter is answered by exactly one eventual leave: the leave Msg is captured when the enter dispatches, so it still arrives when the exit is the element unmounting. Exits resolve exactly like the hover wash already does — moving off, the pointer leaving the window, dismissals, and content scrolling or reflowing out from under a stationary pointer all re-hit-test the last pointer position — and overlays occlude hover the way they occlude clicks.
- Opt-in and free when unbound: apps that bind no hover handlers keep an empty containment chain, no extra rebuilds, and no journal traffic. Where bound, hover Msgs derive deterministically from already-journaled pointer input, so recorded sessions replay them byte-identically with no journal format change.
- Touch honesty: hover comes from mouse and trackpad pointers only — touch input never synthesizes it, so anything reachable only by hover must stay reachable another way. Deliberate break: reserving pointer-id bit 63 as the touch-source stamp changes the meaning of a journaled field, so the session journal's semantic epoch moves and recordings from earlier builds refuse with the standard re-record teaching.
- `examples/notes`: hovering a note row now previews its title, age, and word count in the status bar (the browser status-line convention) without committing the selection.
- **Named keys grow `delete`, `home`, `end`, `pageup`, `pagedown`, `insert`, and `f1``f12`**: every desktop platform now reports them on GPU-surface key events (they previously surfaced on some platforms as private-use strings or not at all), and shortcuts and menu accelerators can bind them. Terminal-style consumers can encode the full navigation and function-key set; none of these require a modifier, matching platform convention (F5 alone is a valid accelerator).
- **Native context menus on Windows and Linux**: a right-click on a widget with a declared menu (or the zero-code editable-text and selected-text defaults) now presents the OS menu at the pointer on Windows (`TrackPopupMenu`) and Linux (`GtkPopoverMenu`), with the selection or dismissal riding the same journaled `context_menu_action` event macOS already emits — one authored menu, one replayable outcome, three desktop platforms.
- The `.context_menus` platform capability now reports true on both system-engine hosts, so feature-gated code takes the native path everywhere the system web engine runs.
- The engine fallback surface (hosts with no native presenter) now anchors the menu at the click point instead of the target widget's edge, matching where the pointer actually is on wide targets.
- Selections now resolve from a present-time snapshot of the shown items, so a menu left open across a rebuild (a timer reordering conditional items) dispatches the item the user saw, never the rebuilt tree's occupant of that slot.
- Deliberate automation-protocol break: recorded `context_menu_action` tokens are per-request generations instead of widget ids, so the protocol semantic epoch moves. Recordings from earlier builds are refused loudly at the preamble (their context-menu selections would otherwise be silently swallowed by the token gate); re-record with this build.
- **Windows pty transport — ConPTY, first-class**: `fx.ptySpawn` and the whole pty family now run on Windows through `CreatePseudoConsole` over an overlapped pipe pair, honoring the exact vocabulary contract the macOS/Linux backends implement — same spawn admission and environment policy (the bound host environment plus `TERM`; env names match case-insensitively, the Windows rule), same all-or-nothing `ptyWrite`, `ptyResize` via `ResizePseudoConsole`, `ptyKill` via `TerminateProcess` plus pseudoconsole teardown (which reaches every descendant still attached to the console), same coalesced output batches and lossless back-pressure, and the same exactly-one exit. The terminal example runs unchanged (its deterministic shell pick adds cmd.exe), and recorded sessions replay offline exactly as on POSIX.
- **Encoding honesty**: the pseudoconsole's pipe contract is UTF-8 with VT sequences in both directions, and the backend creates it with flags 0 — no `PSEUDOCONSOLE_INHERIT_CURSOR`, so conhost never opens with a cursor-position handshake the app would have to answer. There are no console-mode calls to make host-side: the VT modes live inside the pseudoconsole's conhost.
- **Differences stated plainly** (docs' platform matrix moved from "staged" to supported): Windows has exit codes only, so `signaled` never occurs there — a crash surfaces as `exited` with the NTSTATUS bit-cast to `i32` — and ConPTY output is conhost's VT rendering of the child's screen, not the child's raw byte stream.
- **TS tier: the pty command family**: `Cmd.ptySpawn(argv, { cols?, rows?, term?, event })`, `Cmd.ptyWrite(key, bytes)`, `Cmd.ptyResize(key, cols, rows)`, and `Cmd.ptyKill(key)` (wire opcodes 0x19-0x1C) expose the pty vocabulary to transpiled cores, with an event arm matched by field name (`key`/`state`/`bytes`/`code`/`reason`/`signal`/`droppedWrites`), where `key` is the app's own session key so two sessions routing one arm stay distinguishable. The native side owns the transport; the TS tier spawns, writes, resizes, kills, and receives.
- **`<terminal>` — the terminal as a markup built-in**: `ui.terminal(.{ .pty = key, .scrollback, .on_terminal })` (markup `<terminal pty={key} scrollback={offset} on-terminal="...">`) promotes the terminal from the example tier to a first-class element. It binds a model-owned pty effect key — the same id `fx.ptySpawn` named, the media-surface `surface` binding shape — and renders the framework-owned emulator session behind it: the grid painted as real text with geometric box drawing, a theme-derived ANSI palette, selection, cursor, and scrollback, all moved into the canvas (`canvas.TerminalGrid`, the `.terminal` widget kind) from the example. Focused, it routes keys, IME text, and wheel scrollback to the session; the live viewport text rides the widget's accessibility label so screen readers read the real screen and session fingerprints cover cell state.
- **The terminal state contract**: `on-terminal` delivers a `canvas.TerminalState` (`scrollback`, `history`, `cols`, `rows`) after every runtime-applied view-state change, and `scrollback` echoes it back under the scroll `value` source-wins reconcile rule. Only app-visible view state crosses the boundary — the emulator's cells, modes, and selection pins stay framework-owned and are never model state. Expressible in both authoring tiers, matched structurally for transpiled cores.
- **Teachings**: a `<terminal>` without `pty={binding}` is refused as dead markup (the media-surface-without-surface policy); a literal pty key, `pty`/`scrollback`/`on-terminal` on any other element, and children all teach exactly where they belong, in the validator and both markup engines alike.
- **Live `<terminal>` sessions, runtime-owned**: binding a pty key with `<terminal pty={key}>` now renders a REAL session — the runtime feeds the key's journaled pty output into a framework-owned emulator, routes the focused element's keys and IME text back out through `ptyWrite`, answers device queries, scrolls history on the wheel, and drives `ptyResize` from the element's laid-out extent through the shared cell-metrics seam. An app's terminal is `fx.ptySpawn` plus the element: no emulator wiring, no key encoding, no grid plumbing. Because the emulator is fed from the journaled byte stream and every outbound byte crosses the journaled write path, a recorded session replays to the same screen with no shell present.
- **Opt-in emulator, consumer-safe**: `AppOptions.terminal_sessions = true` (with a lazy `ghostty` pin in the app's own `build.zig.zon`) wires libghostty-vt behind the element; every other build — scaffolded apps, the docs preview, transpiled cores — gets a stub that renders the empty terminal surface and never traverses that dependency graph. `native_sdk.runtime.terminal_sessions_enabled` reports which half a build carries.
- **`examples/workbench`**: a live terminal beside a browser in one resizable split — the terminal is the element (no emulator code in the app), the browser is a webview pane snapped to a markup anchor with app-owned navigation history behind back/forward, reload, and an address bar.
- **Terminal — the pty effect vocabulary and a recordable terminal embed**: `fx.ptySpawn(.{ .key, .argv, .cols, .rows, .term?, .on_event })` opens a pseudo-terminal, forks the command onto it as its controlling terminal, and streams output back as coalesced `on_event` Msgs; `fx.ptyWrite(key, bytes)` sends stdin all-or-nothing and returns whether the payload was accepted (a caller that must not lose bytes retains a refusal and retries; verdicts are journaled so replay takes the identical path), `fx.ptyResize(key, cols, rows)` pushes a new grid (SIGWINCH), and `fx.ptyKill(key)` terminates the job. A pty is a spawn with a different transport — it rides the same `command` permission, the same environment policy, the same argv budgets, and the same one key space as spawns, fetches, and channels. macOS and Linux ship the real transport (openpty + a controlling terminal); Windows ships ConPTY (its own fragment); the null platform gets a scriptable fake pty so the whole vocabulary tests headless.
- **Output is coalesced per frame, never per read, and back-pressure is lossless**: bytes arriving between drains deliver as one batch bounded at 64 KiB, so `cat largefile` journals per-frame batches instead of a record per `read()`. The transport's staging ring never drops a byte — a full ring parks the reader and the kernel slows the child, a terminal's native flow control — and the exit event reports `dropped_writes` for any `ptyWrite` refused over the session's life.
- **One exit per spawn, honest classes**: exactly one `.exit` event ends every accepted (and every refused) spawn — `exited` with the child's code, `signaled` with the signal, `cancelled` after `ptyKill`, `rejected` for requests refused before a child existed (bad argv, zero grid, duplicate key, table full, unsupported platform), `spawn_failed` when the pty or exec could not start.
- **Recorded sessions replay byte-identical, offline — no shell present**: output bytes are the effect result, written at effect-result time into the content-addressed blob store beside the journal (`blobs/<sha256[..16]>`, identical batches deduplicated), with the journal record carrying the hash and length. Replay never spawns a process: the `ptySpawn` parks the pty (writes/resizes/kills go inert), the journaled batches and exit feed verbatim from the blob store, and the fingerprint checkpoints verify the replayed emulator grid frame by frame. Adding the pty record kind moved the journal format fingerprint — older recordings refuse at the preamble with the standard re-record teaching.
- **`examples/terminal`**: a keyboard-first terminal at the showcase bar — libghostty-vt (Ghostty's extracted VT core, pinned as the `ghostty-vt` Zig module) owns cell state, damage, scrollback, wrapping, reflow, and selection; the canvas paints the viewport as real text with theme-mapped ANSI-16, exact 256-color and truecolor, and wide CJK cells. Typing rides the IME-correct committed-text channel and the emulator's key encoder; cmd/ctrl+shift+space arms line/block cell selection, cmd/ctrl+arrows page the scrollback, and cmd/ctrl+C copies.
- **`UiApp.Options.on_text`**: the target-less committed-text seam — `on_key`'s typing twin — for apps that consume text without a focused text-entry widget (a terminal grid). Delivered for unclaimed `text_input` after the same widget-precedence routing `on_key` yields to, carrying the committed UTF-8 (IME results included) so consumers stay layout- and input-method-correct. Chrome may also declare a `variable_prefix` prefix whose command count is model-derived, for chrome whose shape changes per frame (a terminal grid, a data plot).
- **Video playback**: a new `<video>` element (registry code 68, attributes `controls`/`autoplay`/`loop`/`muted` at codes 82-85 with `src` riding the existing attribute) plays platform-decoded video through the media-surface texture channel — AVFoundation on macOS decodes straight into the compositor while the app core sees only commands and journaled events; Windows (Media Foundation) and Linux (GStreamer) stage the capability honestly: `video_playback` reports false and the load verbs answer with a teaching plus one explicit failed event until their decoders land.
- **The video command/event vocabulary**: `fx.loadVideo` mirrors the audio channel end to end — local-then-URL source cascade with the http(s) scheme check, transport verbs (`playVideo`/`pauseVideo`/`stopVideo`/`seekVideo`/`setVideoVolume`/`setVideoMuted`/`setVideoLoop`), key-stamped events (`loaded` with stream dimensions and duration, position ticks with the honest `buffering` flag, one `completed` at a non-looping natural end, explicit `failed`/`rejected`), and replace semantics that release the surface claim; TypeScript cores get `Cmd.videoLoad`/`videoCtl` at wire opcodes 0x17/0x18 with the by-name seven-field event-arm convention.
- **The session journal fingerprint moves** (a deliberate break — recordings from earlier builds are refused at the preamble; re-record with this build): the new `.video` effect-result kind (code 13) and platform-event tag journal every delivered event verbatim, so a recorded playback replays byte-identical on a host with no decoder and no texture producer attached, and texture contents stay out of session fingerprints exactly like every media-surface texture.
### Improvements
- **Build fingerprints replace version counters for the session journal and automation protocol**: the journal's `format_version` and the CLI/app `protocol` version are gone in favor of comptime layout fingerprints — a Wyhash over a canonical description reflected from the actual record, event, and command types — so any layout change moves the identity automatically, with no counter to remember to bump and no next integer for parallel branches to contend over. Since no journal or dropbox skew is ever migrated, identity beats ordering: "same or different" was the entire question the integers answered.
- Deliberate break: journals and automation sessions recorded by any earlier build are refused with the re-record teaching (the journal preamble now carries the u64 format fingerprint; the snapshot header stamps `protocol=0x...`), and skew refusals name fingerprints instead of version numbers.
- A small `semantic_epoch` remains for the rare meaning-only change with identical bytes (the context-menu token generations were one); layout changes need no action.
- `zig build print-pins` and `native version` print the fingerprints, so a build's wire identities can be quoted exactly.
### Bug Fixes
- **No more stale fringes when content reflows**: incremental canvas damage now covers the anti-aliasing bleed — the up-to-one-device-pixel ring rasterizers ink past a command's bounds — so a list-detail selection change that reflows conditional content (badge pills removed, shrunk, moved, or replaced under new keys) no longer leaves leftover edge pixels where the old content extended beyond the new. Every finalized incremental dirty rect (the refined union, each refined cluster on the retained-patch wire, and the summary fallback) inflates by one device pixel before surface clipping; full repaints are unchanged.
- **Terminal context menu**: right-clicking a `<terminal>` now presents the standard Copy and Paste actions, copying the emulator selection and sending pasted clipboard text to the bound PTY.
- **Natural terminal editing on macOS**: focused terminals now translate Option+Left/Right to word movement, Command+Left/Right to line boundaries, and Command+Delete to clearing back to the line start, instead of leaking unsupported modifier sequences into the shell prompt; Command+V now sends clipboard text through the terminal's bracketed-paste-aware input path.
- **Selectable terminal text**: `<terminal>` now supports pointer-drag cell selection, double-click word selection, triple-click line selection, and Cmd/Ctrl+C clipboard copy without forwarding the copy chord to the child.
- **Terminal Tab input**: focused live `<terminal>` components now send Tab and Shift+Tab to the PTY for completion, indentation, and TUI navigation, while focus-entry gestures and ended or unbound terminals retain ordinary traversal.
- **Video letterboxes instead of stretching**: the video surface now aspect-fits (contain) the decoded frame — centered at the stream's reported proportions, letterboxed or pillarboxed on black, never distorted. Contain is the video surface's one fit mode, stamped on the `<video>` element and on any app-claimed surface while its playback is live; unknown dimensions before the LOADED report keep the full-frame placeholder, and a source replacement re-fits from the new report. Camera and app-producer media surfaces are untouched.
- **Clear terminal focus**: terminal cursors now fill while their live session owns keyboard focus and switch to a hollow outline when focus leaves or the session ends.
- **Clean workbench terminal chrome**: the full-bleed terminal pane keeps keyboard focus without showing its clipped outer focus ring as a stray horizontal rule beneath the titlebar.
- **Workbench pane focus stays truthful**: clicking the embedded page now blurs the address bar and hollows the terminal caret; clicking either canvas pane restores its expected keyboard focus.
### Contributors
- @ctate
- @startewho
## 0.5.4
### New Features
- **`Cmd.imageLoad` — dynamic images, the first full media pipeline**: apps load images at runtime from disk or the network by a model-owned ImageId, the effect executor resolves the audio cascade's source order (local path first, then a verified content-addressed cache entry under `<caches>/images/`, then the network with an atomic cache install behind it), decodes through the platform codec into the existing registered-image storage, and exactly ONE result Msg comes back — `loaded` with the decoded width/height, or one honest failure class from the same vocabulary the direct registration API raises (`decode_failed`, `too_large`, `registry_full`, `unsupported`, `alloc_failed` — the host refused the memory the registration needed, resource exhaustion rather than corrupt bytes — the fetch taxonomy, `http_status` with the status carried through).
- **TS tier first-class**: `Cmd.imageLoad(id, { path?, url?, cachePath?, expectedBytes? }, { event })` with a five-field result arm matched by name (`id`/`state`/`width`/`height`/`status``id` echoes the requested ImageId so concurrent loads sharing one arm stay distinguishable; the fifteen-member `ImageState` union checked at build time), id expressions welcome (ids are model data), `Cmd.imageCancel(id)` ending a live load loudly (the event arm's "cancelled", freeing the id for a same-id retry; an id with no live load no-ops), `Cmd.imageUnregister(id)` releasing a loaded image's registry slot (the gallery eviction move past the 16-slot registry — synchronous registry surgery like registration itself, no result Msg, misses no-op; a load in flight still registers at its terminal, so cancel first to keep the slot free), opcodes 0x12/0x13/0x14 additive within cmd_format_version 3, and `TsUiApp`'s `image_cache_dir` deriving the content-addressed cache path from the URL so update never builds filesystem paths.
- **Markup `<image>` — the runtime-image leaf (element code 67)**: `image="{binding}"` binds the model-owned u64 ImageId in avatar's grammar (binding-only, required on the leaf, negative model values fail the build with a teaching, never a trap), wired through the validator, both engines, `native check`'s model contract, LSP hover docs, and the docs vocabulary; the `image` attribute's scope broadened from avatar-only to avatar+image.
- **Recorded sessions replay byte-identical, offline**: an image load's ENCODED source bytes are the effect result, journaled at effect-result time into a content-addressed blob store beside the journal (`blobs/<sha256[..16]>` in the session directory — identical bytes twice store one blob), with the journal record carrying hash + length and the dedup probe verifying an existing blob's bytes before trusting its name (a damaged blob repairs in place from the bytes in hand — recording self-heals the store instead of sealing a journal replay must refuse); replay reads the blob, verifies it against its address, re-runs decode + registration, and delivers the recorded result with no file, network, or cache touched, refusing loudly when the blob store is missing or damaged.
- **Journal format, stated plainly**: the image records bump the session-journal format to v7 (the `.image` effect-record kind plus the blob-address fields appended to every effect record); v6 and older journals are refused at the preamble with the standard re-record teaching — a v6 reader would have misparsed the longer records as corruption.
- **Zig tier**: `fx.loadImage(.{ .id, .path, .url, .cache_path, .expected_bytes, .on_result })` with `Effects.imageMsg(...)` routing, a fake-executor seam (`pendingImageLoad*`, `feedImageBytes` running the REAL decode+register path, `feedImageResult`), and `imageCachePath` deriving the cache convention; the encoded source is bounded at 1.25 MiB from every source alike and over-bound sources fail whole with `too_large` — a cut image never decodes, so there is no truncated delivery.
- **The menu-bar app lifecycle**: windows can declare `close_policy = "hide"` in app.zon (the default `"quit"` keeps today's behavior for every existing app) — the red close button hides the window instead of quitting, the app keeps running behind its status item, and the macOS Dock reopen re-shows it.
- New window verbs on the effects channel: `fx.showWindow(label)` un-hides and activates a window (the tray "Open" consequence; also restores a minimized window), and `fx.quitApp()` quits gracefully through the same shutdown path a last-window close takes — both mirrored in the TypeScript tier as `Cmd.showWindow(label)` and `Cmd.quitApp()`.
- Hidden state is honest, journaled window state: `WindowState.hidden` rides the frame channel, records into session journals, and replays.
- Implemented on macOS (windowShouldClose + orderOut, Dock reopen) and Windows (WM_CLOSE hides via SW_HIDE; the tray re-shows); Linux GTK has no status item to bring a hidden window back, so `"hide"` is refused loudly at build/create time with a teaching instead of stranding a window.
### Improvements
- **The tofu guard teaches font registration**: the font-coverage teachings — the `native markup check` error, the Debug view-build diagnostic, and the CLI usage text — now name registering a covering face (`UiApp.Options.fonts`) behind a model binding as the first remedy for text beyond bundled coverage, alongside vector icons and plain words.
- **A fonts page in the docs**: `/fonts` documents registering faces for scripts beyond bundled coverage — the `Options.fonts` scaffold shape, every registration-time validation error by name, ownership and lifecycle, how text resolves faces through the typography tokens, and per-platform truth including the unverified mobile seam.
- **The Chinese receipt runs natively on Windows in CI**: a new `zig build test-canvas-fonts` step runs the font-registry suite on the Windows runner, including the receipt test that registers a committed subsetted Noto Sans SC (OFL, license alongside) through the app-fonts seam and proves the rendered string is real ideograph outlines — compared against both the bundled face's rendering and the same registered face's own uncovered-string fallback, so tofu from any face fails the receipt.
- **Registered-image memory is on-demand**: each registered canvas image slot buffer is one lazy 1 MiB allocation from `Runtime.Options.allocator` at the slot's first registration (freed by `Runtime.deinit`; unregister/register churn reuses buffers, so the footprint stays bounded by the high-water slot count), so a runtime that never registers an image no longer carries the former 16 MiB embedded pixel pool.
- **New error on register**: `registerCanvasImage` / `registerCanvasImageBytes` (and the `fx.registerImage` / `fx.registerImageBytes` bindings) now surface `error.OutOfMemory` when a slot's pixel buffer cannot be allocated — the refusal happens before any registry mutation, so the registry is unchanged and the same registration can be retried once memory recovers.
### Bug Fixes
- **Large markup documents compile in the compiled engine**: `CompiledMarkupView` / `CompiledMarkupImports` no longer fail with "evaluation exceeded 1000 backwards branches" on `.native` documents past ~10KB — documents now carry their source size from parse/resolve time, so the comptime canonicalize pass sizes its branch quota in O(1) instead of re-measuring the tree inside the quota argument (which ran under the caller's default budget).
- **Linux `native dev` no longer crashes at startup**: Debug builds on x86_64 Linux segfaulted creating the first shell view (a general protection fault in the GTK host's `native_sdk_gtk_create_view`) because Zig 0.16.0's self-hosted x86_64 backend — the Debug default — mis-places stack-passed arguments in the host call's long mixed signature; the app executable now forces the LLVM backend on x86_64 like every other artifact in the build graph, and the ejected template's build does the same.
- **GTK host string caps**: the Linux host now refuses view create/update calls whose string lengths exceed the platform caps with a teaching warning instead of copying from a corrupted pointer, so a broken C-ABI boundary fails loudly at the seam.
- **Linux Debug scaffold smoke in CI**: a new `linux-dev-smoke` job scaffolds the default template, builds it `-Doptimize=Debug` (the `native dev` mode Release-shaped lanes never exercise), and drives it under Xvfb to the first presented frame.
- **Late-registered fonts re-measure open surfaces**: registering a face after views are installed (`runtime.registerCanvasFont` on a live runtime) now rebuilds every installed `UiApp` surface — the main canvas and declared windows — on the next presented frame, so text laid out before the face joined re-measures with the registered face instead of keeping its pre-registration widths under a repaint.
- **macOS host font state ends with its runtime**: `Runtime.deinit` now returns each registered id's host-side registration — the CoreText descriptor and its measurement caches, including the measured-width cache the host previously retained until memory pressure — so embedders that cycle runtimes no longer accumulate per-process font state; removal is ownership-token guarded, so an older runtime's teardown never removes a newer runtime's live face under a shared id.
- **Breaking**: `PlatformServices.registerGpuSurfaceFont` now returns the host's ownership token for the registration (`u64`; 0 from hosts that retain no per-id state), and the new optional `unregisterGpuSurfaceFont(id, token)` service returns that state at teardown — a deliberate break while the toolkit is pre-1.0, so host font lifetime has an owner. Embedders implementing a custom platform change `register_gpu_surface_font_fn` to return a `u64` token (0 is fine for a stateless accept) and may supply `unregister_gpu_surface_font_fn` to release per-id host state when the registering runtime deinits.
- **Ternaries with spread-literal arms compile from TypeScript cores**: `parsed === null ? q : { ...q, state: "ok", price: parsed }` — and the nested, `!==`, both-arms, argument, object-field, and `x === null ? { ...fallback } : x` spellings — no longer emit Zig that reads the null-narrowing capture before it binds (`use of undeclared identifier`) or evaluates both arms unconditionally; arms that build values statement-by-statement now lower into per-branch blocks feeding a typed temp, so exactly the taken arm runs, and pure-arm ternaries keep their tight `if`/`orelse` expression forms.
- **Optional switch payloads keep their optional through the capture**: reading a `number | null` payload (directly or via a `const` local) inside its `case` no longer mistypes the value as non-null, which routed `msg.parsed === null ? ... : ...` around the narrowing lowering and emitted `?f64` into `f64` slots.
- **Early-exit guards narrow like early returns**: `if (r === null) break;` in a parse loop (and the `continue`, labeled, multi-statement, `throw`-exit, and `if (x !== null) { ... } else { break/return }` spellings) now narrows the optional for the rest of the loop body the way tsc's flow analysis does, instead of emitting Zig field access on the still-optional value; `if (msg.kind !== "num") break;` narrows the union payload the same way.
- **Guard narrowing ends with its block**: a guard's captures no longer leak past the loop body or branch they narrow — reads after the construct see the unnarrowed value again (matching tsc, whose exit path may bypass the guard) instead of referencing an out-of-scope Zig capture.
- **Early switch-clause breaks stop the build**: an unlabeled `break` that exits a `switch` from inside a clause body now teaches at transpile time — Zig's `break` binds loops, so the old emission jumped past the enclosing loop instead of resuming after the switch.
- **A redundant kind guard no longer un-optionals a switch payload**: scoped kind-narrowing now restores the still-optional markers alongside the substitutions it snapshots, so `const marker = msg.kind === "got" ? 1 : 2;` inside `case "got":` no longer leaves a `number | null` payload typed non-null for the rest of the clause (which emitted `if (parsed != null) parsed + marker else 0` — invalid Zig operands on the `?f64`).
- **Inferred locals from narrowed ternaries value non-optional**: `const picked = q === null ? { ...fallback, price: 0 } : q;` (either polarity, no `: Quote` annotation) now types the local by the arm the condition narrows — `Quote`, exactly as tsc infers — instead of the raw optional, which declared a `?Quote` temporary that failed Zig compilation at its first non-optional use (`expected type 'Quote', found '?Quote'`).
- **A redundant nested `switch` on the same subject hands back the outer capture**: the arm cleanup now repopulates its narrowing maps from the snapshot instead of only deleting the arm's additions, so an inner arm's capture that OVERWROTE the outer arm's entry no longer leaks into the continuation after the inner switch (which emitted the inner capture name after its Zig block had closed — `use of undeclared identifier`).
- **Else-if chains keep post-if narrowing**: `if (x === null) return -1; else if (flag) { n = 2; } return x.v + n;` — and the else-if-else, chained else-if-else-if, and `!==`-polarity spellings — now narrow `x` after the statement like the plain-else form does; the else-if emission path returned before applying the post-if narrowing, so the fall-through read landed on the still-optional value (`optional type does not support field access`).
- **Reassigned `let` bindings never fuse into a `const`**: `let p = next(i); if (p === null) continue; p = { ...p, v: 10 };` no longer fuses the declaration and guard into `const p = next(i) orelse continue;` (Zig: `cannot assign to constant`); the binding stays a `var`, the guard keeps its plain null test, and later reads unwrap the live variable — which the assignment path keeps narrowed across provably non-null writes.
- **A branch that widens a narrowed optional stays widened past the merge**: `if (p === null) return -1; if (flag) { p = null; } if (p === null) return 0;` — the branch-exit restore that keeps narrowing CONTAINED (additions inside a branch die at its exit) no longer also resurrects a narrow the branch killed by assigning null (or a fresh optional-returning call); kills now re-apply after every branch, switch-arm, and kind-guard exit and propagate through nested blocks, so the post-merge re-check tests the live value instead of emitting `p.? == null` (Zig: `comparison of 'f64' with null`). The merge is conservative — a kill on any path that can reach the merge drops the narrow, and the re-check tsc demands anyway always compiles.
- **Compound-guard branches keep those kills dead too**: `if (r !== null && r > 0) { p = null; }` — where the branch emits under the chain's `.?` substitutions — no longer resurrects p's killed narrow when that substitution scope restores its snapshot (its restore ran after the branch re-applied the kill); the scope now rides the same kill-frame protocol as branch and switch-arm exits, and so does the chain-condition emitter, so every full-map narrowing restore in the emitter re-deletes killed entries on exit.
- **A kill on an always-exiting branch stays off the surviving flow**: `if (p.v < 0) { p = null; return -1; } return p.v;` inside a null guard — tsc keeps `p` narrowed at the second return because the killing branch left the function, and the emitter now agrees: a branch that always returns (or throws uncaught) drops its kills at the merge instead of deleting the narrow the surviving read depends on (which emitted field access straight onto the `?P`). Kills on paths that resume inside the function — fall-through arms, `break`/`continue` guards in loops, throws caught by an enclosing `try` — still merge outward and drive the post-merge re-check.
- **A guard in a lifted callback covers the trailing return it precedes**: `xs.map((p) => { if (p === null) throw bad; return p.v; })` lifts the callback as its statement prefix plus the trailing return's expression, and the prefix's narrowing scope closed before that expression emitted — the read landed on the raw `?P` (`optional type does not support field access`); prefix and trailing expression now share one flow scope, and the scope still closes before the callback's siblings in the emitted loop body.
- **A do-while body guard covers the trailing test it flows into**: `do { if (p === null) return -1; n += p.v; } while (p.v > 0);` — tsc evaluates the condition after the body, under the body's flow state, but the body's narrowing scope closed before the lowered `if (!(cond)) break;` emitted, so the test read the raw `?P` (`optional type does not support field access`); the body and the trailing test now share one narrowing scope, restored at the loop boundary. A guard read only by the test binds its capture too, `continue`-carried kills still widen the hoisted first-pass test onto the live optional, and `break`-carried kills still land only on the post-loop state.
- **Canvas-app hosts compile silently without a WebView SDK**: the informational `#pragma message` in the GTK host's WebKitGTK stub path (and its Windows WebView2 twin) is gone — zig renders every clang diagnostic of a failing C compile as `error:`, so on machines where a real, unrelated compile error occurred (for example a too-old GTK), the note itself surfaced as the first build-killing error and masked the actual cause; the stub is the expected state of every canvas app and now compiles with zero diagnostics, while a genuinely misconfigured web build still fails loudly via `#error`.
- **GTK host compiles against GLib 2.72**: the host now spells "no application flags" in a way that compiles on GLib older than 2.74, so distros that backport GTK 4.10 onto a GLib 2.72 base (Ubuntu 22.04-derived) build canvas apps out of the box.
### Contributors
- @ctate
- @codehz
- @nextpointer
- @perminder-klair
## 0.5.3
### New Features
- **`<media-surface>` — the dynamic texture channel**: a new element compositing textures produced outside the widget tree (video decoders, camera pipelines, external renderers like mpv) into the layout like any widget, with a stable Zig-tier producer API (`runtime.acquireMediaSurfaceProducer`) pushing RGBA8 frames from any thread — latest-wins, damage-tracked, paced by the compositor's presented-frame clock.
- **Pushes wake an idle compositor**: a push staging new bytes requests one coalesced frame through the platform's thread-safe cross-thread frame request (the automation watcher's wake path), so video keeps playing in an idle demand-driven app and a late-starting producer is adopted promptly; damage-skipped and stale-handle pushes wake nothing, and teardown disarms the binding under the same fence the wake call holds, so an orphaned producer can never wake a dead host.
- **Markup, both engines, and tooling**: `surface="{binding}"` binds the model-owned u64 surface id in the runtime-image-id grammar (binding-only, required, media-surface-scoped with teaching errors), wired through the validator, the compiled and interpreter engines, `native check`'s model contract, LSP hover docs, and a docs component page plus a "Media Producers" recipe.
- **Deterministic by policy**: texture contents are presentation chrome — goldens, reference screenshots, and session fingerprints see only the surface's id-derived placeholder, so a session recorded with a live producer replays fingerprint-identical with no producer attached; live GPU hosts composite the real texture through the existing image upload pipeline.
- **Reserved id namespace**: bit 63 of the ImageId space now belongs to media-surface textures; `registerCanvasImage` rejects ids with it set (`error.InvalidImageId`) so producer textures and registered images can never collide.
- **Cover fit stays inside the frame**: a `cover`-fit media surface carries the image widget's rectangular clip around its texture draw, so the fit-expanded texture can never paint over siblings on hosts that only mask corner radii.
- **Adopted-texture memory is on-demand**: each channel's texture buffer is one lazy frame-budget allocation from the new `Runtime.Options.allocator` at first adoption (freed by the new `Runtime.deinit`), so a runtime with no media producers carries zero media-texture bytes — an embedded pool would have put 32 MiB in every Runtime (measured on the docs wasm preview host: 169.5 MB → 137.5 MB per component tile). The allocator freezes into the runtime at init: mutating `options.allocator` on a live runtime never retargets ownership, so allocations and their frees always pair on one allocator.
- **Trackpad pinch reaches apps**: pinch-to-zoom now flows from the macOS host (`magnifyWithEvent:`) through phase-explicit `pinch_begin`/`pinch_change`/`pinch_end` input events into a view-global app channel — Zig cores declare `Options.on_pinch`, TypeScript cores export `pinchMsg(pinch)` with `PinchPhase`/`PinchEvent` in `@native-sdk/core/events`; each change carries a multiplicative delta (cumulative gesture scale is the product of `1 + scale`, applied memorylessly — `zoom *= 1 + scale`) and the pointer anchor rides view-local `x`/`y` (the pointer location during the gesture — zoom-at-cursor anchoring, not a between-the-fingers midpoint); every event names its source window and view (`window_id`/`label` in Zig, `windowId`/`label` in TypeScript), so multi-window apps tell pinches apart; a terminal Ended/Cancelled event that still measured a nonzero delta arrives as one last change before the end, so the product always matches what the OS reported. On macOS the delta is AppKit's raw per-event `NSEvent.magnification`, forwarded untransformed: raw magnification IS the multiplicative per-event delta — the convention every browser engine ships — so the product of `1 + scale` is the zoom users already experience for the same gesture in Safari and Chrome. The one guard is a per-event floor: a single event's magnification at or below -1 (a zoom inverting through zero scale — physically impossible, only a driver glitch could report it) clamps just above -1, so every emitted factor stays positive. Windows precision-touchpad and GTK gesture sources are staged follow-ups.
- **`widget-pinch` automation verb**: `native automate widget-pinch <view-label> <scale> [x y]` drives the real pinch event stream without a trackpad (`<scale>` is the gesture's FINAL multiplicative zoom — one change carrying `scale - 1`, anchor point defaulting to the view center), journaled like every synthesized input so recorded sessions replay the identical zoom. Automation protocol bumps to v7.
- **Session journal v5**: gpu-surface input records gain the pinch `scale` field and the pinch kinds; readers refuse v4 journals loudly in both directions, per the format's skew discipline — re-record sessions with this build.
### Bug Fixes
- **Registered CJK fonts render dense glyphs**: glyph outline budgets are now sized from real CJK faces (Noto Sans JP/SC/TC/KR measured; 1024 points / 128 contours per glyph), so everyday dense kanji like 鬱 ink as real outlines instead of notdef blocks; the per-font registration size bound rose to 24 MiB so full CJK faces register.
- **Glyph complexity validates at registration, not render**: a face whose `maxp` declares glyphs denser than the outline budgets — simple maxima and flattened-composite maxima (`maxCompositePoints`/`maxCompositeContours`) alike — is refused at registration with a teaching that names its numbers against the budgets (`error.FontExceedsGlyphBudgets`), instead of silently degrading individual glyphs to blocks at render time.
- **Font bytes are heap-allocated on demand**: the registry copies each registered file into an exact-size allocation from the runtime's new `Options.allocator` (freed by the new `Runtime.deinit`), so the 24 MiB bound is validation, not a storage reservation — a runtime with no registered fonts carries zero font bytes, where a reservation-shaped pool would have embedded 192 MiB in every Runtime (measured on the docs wasm preview host: 313.5 MB → 121.5 MB per component tile).
- **`EmbeddedApp.deinit` completes the embed lifecycle**: direct embedders end an embedded app with `defer embedded.deinit()` (idempotent), which returns the runtime's heap-owned registrations — without it, a host creating and destroying apps in one process leaked the registered font storage per cycle. The wrapper hosts and the C ABI's `native_sdk_app_destroy` route their teardown through the same deinit, one lifecycle owner.
- **Glyph raster budgets are derived from the registration gate**: the vector core's glyph-fill capacities (`GlyphRasterizer`: 18,560 edges/crossings, flattening clamped at 16 segments per curve) are computed from the outline budgets registration admits, so a truthfully-declared budget-maximal glyph — a 1024-point zigzag contour included — rasterizes at any size instead of hitting `VectorPathTooComplex` and degrading to the block fallback the gate promises cannot happen; the clamp binds only above ~128-px ems and keeps the polyline within 0.2% of the em, so existing renders are byte-identical.
- **Single-line fields never hold or paint line breaks**: pasting multi-line text into an input, text field, search field, or combobox now strips the line breaks at the edit seam (the HTML value-sanitization rule — lines join with nothing between them), covering clipboard paste from the shortcut and the context menu, typed and automation `text_input`, and IME composition, with the app's `on_input` hearing the same sanitized bytes the editor applied; a paste of only newlines inserts nothing.
- **Defensive render containment**: a single-line value that still holds a `\n` (a model-set value, an old journal) now paints as one line — breaks present as spaces — under a forced content-rect clip, so text can never escape the field's rounded border on any renderer.
### Contributors
- @ctate
- @IFTC-XLKJ
- @WhiteHades
- @jhodges10
## 0.5.2
### New Features
- **Anchored tooltips with hover intent**: `<tooltip anchor="above|below">` beside its trigger in a stack floats against the trigger and hands visibility to the runtime — it shows after the pointer has rested on the trigger for the show delay (default 600ms) and hides on leave, so sweeping a toolbar flashes nothing; after a pointer-hovered tooltip hides on leave, a shared 400ms warm window shows the next trigger's tooltip instantly (the other hide causes below — focus moving on, Escape, a press, view blur — deliberately open no warm window). Delay, warm window, and the behaviors below match shadcn/ui's defaults (Base UI). The model never hears hover, both engines lower it identically, and every transition steps on the recorded input/frame clock, so recorded hover-dwell sessions replay their show/hide frames byte-identically.
- **`tooltip-delay` attribute**: per-tooltip show delay in milliseconds (`0` shows the instant the trigger is hovered); absent follows the new `tooltip_show_delay_ms`/`tooltip_warm_window_ms` metric tokens. Registry attr code 80; static (non-anchored) tooltips keep their classic paint-when-rendered behavior.
- **Keyboard focus reveals immediately**: tabbing onto a tooltip-owning trigger shows its tooltip with no dwell (keyboard navigation is deliberate, and content revealed on hover or focus must not depend on pointer timing); focus moving on or Escape hides it just as immediately, without warming the pointer's skip window.
- **Hoverable content**: a shown tooltip's own bounds hold it open (WCAG 1.4.13; Base UI's `hoverable` default) — the pointer can cross the anchor gap into the tooltip along a bounded safe-polygon corridor, and it hides only after leaving both the trigger and the tooltip (with the usual warm window); the corridor resolves on the recorded frame clock, so replays stay byte-identical.
- **Scroll steps the machine**: every scroll path (wheel, kinetic steps, native drivers, keyboard scrolling) routes its hover change through the same intent transition a pointer move takes — a trigger scrolled out from under the pointer disarms/hides, and a trigger scrolled under it arms per normal.
- **Press dismisses**: ANY pointer-down — primary or secondary, including downs consumed by the context-menu gesture or a window-drag region — or Space/Enter on the focused trigger cancels a pending reveal, dismisses a shown tooltip, and closes the warm window, so an activated control never re-explains itself on the post-click hover. Keyboard activation and Escape also spend the standing focus reveal: a rebuild that replaces or rekeys the tooltip (the activation's own model update, typically) cannot resurrect it — it stays down while focus rests on the trigger, until the keyboard genuinely leaves and returns; the focus ring stays painted, and a later pointer hover re-earns the dwell normally.
- **Rebuild hygiene**: a rebuild that removes, rekeys, disables, or re-parents a tooltip's trigger resets that tooltip's armed/shown/warm state and re-stamps it hidden, even when the tooltip node itself survives.
- **View blur resets**: a canvas view that loses focus (to a sibling view or with the window) drops its entire tooltip conversation — armed delay, shown tooltip (keyboard- and pointer-owned), warm window — and re-stamps hidden, so no stale tooltip floats in a view the keyboard left. The window key-loss reset fires on the flag's own focused→unfocused edge, so it holds however the host announces the change: one gain event (macOS) or loss-before-gain (Windows, GTK), including a loss with no gain at all (every window inactive).
### Improvements
- **Search empty state names its scope**: the system monitor's no-match state now says search only sees the top 128 processes by CPU, so a miss on a quiet process reads as scope, not absence (both tiers).
### Bug Fixes
- **Dark-scheme accent focus rings settle down — and stay visible**: a `theme_accent` (or `canvas.accentOverrides`) now derives its dark-appearance focus ring at half the accent's saturation instead of the raw brand hue, contrast-floored at 3:1 (WCAG non-text) against the lightest dark tone controls commonly sit on (the house muted surface `#262626` — rings draw outside controls, so clearing the lightest adjacent container tone clears the page background and card surface too, in both shipped packs) — desaturation alone can cost a deep accent the bar it cleared (`#008000` fell from 3.9:1 to ~2.6:1 on the background; the floor lifts it back over 3:1 on background and card surface alike) — so the soundboard search field's ring no longer glares neon in dark mode; `canvas.accentFocusRing` exports the derivation so hand-authored token sets (the Zig soundboard's theme) state the identical ring.
- **Breaking**: `canvas.accentOverrides` now takes the resolved `ColorScheme` alongside the accent — a deliberate break while the toolkit is pre-1.0, so the one function under the natural name states the scheme it layers over; pass `.light` to reproduce the previous output exactly.
- **Escape in a search field now reaches your core**: Escape's clear (and its composition cancel) was a runtime-local editor operation — the field emptied on screen while the model kept the stale query and the list stayed filtered. Every keyboard-driven editor mutation now derives ONE edit that is applied to the retained editor AND stamped onto the dispatched event, so `on-input` hears Escape exactly like typing, paste, and the clear affordance — on both authoring tiers, and byte-identically under record→replay.
- **Automation and accessibility composition verbs ride the real input path**: `widget-action set_composition/commit_composition/cancel_composition` now dispatch the same ime input events a live IME session produces (journaled, mirrored to the core), and `set_selection` reaches the core's selection mirror through the stamped-edit channel.
- **Accessibility actions replay without double-dispatch**: a journaled assistive action (press, toggle, set_text, drag, ...) no longer also journals the key/text events its verb synthesizes — replaying the action re-derives them, so recorded AX-driven sessions replay each input exactly once instead of twice.
- **Direct verb calls journal outer-wins too**: an embed host's `widgetAction` and automation `widget_action` commands now record the same single `widget_accessibility_action` record the platform accessibility path does (the enum gained the composition kinds), so replay re-runs the verb — focus included — instead of delivering its untargeted key/ime children to whichever field the session happened to leave focused.
- **Grids keep their declared column slots**: children fewer than a grid's declared `columns` now keep the column-slot width and fill the leading slots, instead of stretching across the freed row — a search that narrows the soundboard album grid below its column count leaves image-forward tiles at their natural size.
- **"terminate request delivered" retires itself**: the system monitor's delivery notice now clears on the next applied sample instead of sitting in the footer forever; failure notes keep sticking (both tiers).
- **System monitor footer says UTC**: the sample-time stamp renders from the journaled clock in UTC, and the footer now labels it "UTC" instead of passing it off as local time — local rendering would need a journaled timezone channel to stay replay-byte-identical, so the label is the honest fix (both the Zig example and the TS port).
### Contributors
- @ctate
- @marcusschiesser
## 0.5.1
### Improvements
- **The TS scaffold's status bar earns its empty state**: a fresh scaffold said "stamped: -1ms" until the first Stamp press; the template's markup now branches on `{stampedMs < 0}` and says "press Stamp for a timestamp" instead — teaching the if/else markup shape in the starter while it's at it.
### Bug Fixes
- **Exact-fit text no longer elides under geometry pixel snapping on Windows**: edge snapping rounds a frame's two edges independently, so a hug-sized text box at a fractional position could come back up to a full device pixel narrower than the label it was measured for — past the elision slack, so the TS scaffold's centered counter painted "…" instead of its digit at 100% scale. The wrap/elision budget (`textWrapMaxWidth`) now hands back the full snap quantum (1/scale, was 0.5/scale), and the epsilon policy is documented at the seam: painted width may exceed the snapped frame by less than `1/scale + text_elision_slack`, always below the smallest real overflow (a glyph).
- **Spawn teardown crash window closed**: the effects channel now joins every spawn, fetch, and file worker thread that converges before its teardown returns (previously it gave up after ~5s and abandoned them), so cancelling or quitting while a real child is still streaming can no longer leave a stale worker writing into freed memory.
- **Spawn cancel reaches the whole process tree**: each spawned child runs in its own process group and cancel/teardown signals the group (POSIX), so shell-wrapped commands (`sh -c "a; b"`) no longer leave orphaned grandchildren holding the stream open past the cancel.
- **Every worker class tears down bounded**: spawn, fetch, and file workers now share one terminal guarantee — teardown returns within a budget and never frees memory a live thread can still touch. A file worker stuck in blocking I/O that nothing can converge (a write to a FIFO with no reader, a stalled network filesystem), or a spawn worker held hostage by a descendant that escaped the kill's process group (`setsid` daemonization, a shell's `set -m` background job) while holding the stdout pipe open, no longer hangs teardown: teardown interrupts the blocked syscall best-effort at half its budget and, past the full 15s, abandons the worker with one warning and a small deliberate leak. Everything an abandoned worker can still reach lives in process-lifetime storage, so the leak stays safe even when the app tears down the allocator behind the channel right after.
- **A fetch that cannot start cancellably is rejected, never run inline**: when the executor cannot start the exchange as a cancelable task, the fetch now delivers one honest `.rejected` terminal instead of silently running an exchange that would evade `cancel`, the timeout, and teardown's join.
- **The npm-installed CLI carries its TypeScript toolchain**: @typescript/typescript6 — and @typescript/old, the exactly-pinned alias of the real compiler its one-line entrypoint re-exports — are now regular dependencies of `@native-sdk/cli`, installed by npm in the same transaction as the CLI itself. The first `native check|dev|build|test` on a TypeScript app needs no network and no install step, and never runs npm: it works offline right after install, on read-only (system-owned) prefixes, and under `NODE_ENV=production` alike, because the bundled transpiler resolves the toolchain by node's own ancestor `node_modules` walk across every layout npm or pnpm produces. A repo checkout whose `packages/core` install hasn't happened yet is taught the one command (`cd <sdk>/packages/core && npm ci --include=dev`), and a direct `zig build` against an unresolvable toolchain fails with the same clean teaching and a resolved SDK path instead of a panic stack trace. TypeScript apps need Node.js 22.15+ (on the 23 line: 23.5+) on every layout — repo checkouts included, because every .ts module rides the same `module.registerHooks` type stripping: on a node without the hook the runner fails fast with a one-line upgrade teaching instead of node's raw `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`.
- **Drag headers lay out clear of the Windows caption buttons**: on hidden-titlebar windows, a `window-drag` header whose app never consumed the chrome channel's trailing inset rendered right-aligned content UNDER the DWM min/max/close cluster (system-monitor-ts's status text was truncated by the caption punch-out). The runtime now detects the collision after layout and re-lays the view once with the cluster reserved (`DesignTokens.window_controls`, stamped like `text_measure`), so drag-header content stops at the cluster's edge on every app — markup and builder, Zig and TS — while headers that already pad through the chrome insets (soundboard's trailing spacer) keep a byte-identical layout. The same mechanism covers the macOS mirror (a leading traffic-light cluster). Anchored floating children of the drag header resolve against the cleared rect too, so an end-aligned floater moves out from under the buttons just like flow content; anchored children of non-drag widgets keep byte-identical placement.
- **Standard titlebars follow dark mode on Windows**: windows with standard chrome now set `DWMWA_USE_IMMERSIVE_DARK_MODE` from the OS app color scheme — at creation (before first show, so a dark launch never flashes a light caption) and again on every appearance broadcast — so a dark-themed app no longer sits under a glaring white titlebar. Hidden-titlebar windows keep their higher-fidelity pixel-sampled caption color, and chromeless windows have no caption to tint.
- **Windows release exes are GUI-subsystem**: `native build` (and therefore everything `native package --target windows` wraps) no longer produces console-subsystem binaries, so launching a packaged app never flashes a terminal window behind it. Debug exes keep the console — the dev loop's logs live there — and redirected logging (`app.exe > log 2>&1`) still works on GUI exes. Packaging now reads the exe's PE subsystem, warns loudly when a console-subsystem binary is wrapped (stale zig-out or hand-supplied `--binary`), and carries the finding in the package stats, pinned by tests over synthetic PE headers. The web-frontend scaffold's standalone `build.zig` (Next/Vite/React/Svelte/Vue and `native init --full`) emits the same release-only assignment, so scaffolded apps get the posture without the SDK build graph.
### Contributors
- @ctate
## 0.5.0
### New Features
- **TypeScript authoring — write app cores in TypeScript**: `native init` now scaffolds a TypeScript app by default — `src/core.ts` (logic), `src/app.native` (view), `app.zon` (manifest), zero Zig to write — and the build transpiles the core to readable arena-backed Zig, so the shipped binary carries no JS engine and no GC and keeps the native dispatch path (~83ns per update). The `@native-sdk/core` package (the SDK cores import, plus the transpiler the CLI runs) publishes to npm with this release at the SDK's version. Zig cores remain first-class (`native init --template zig-core`), mixed TS-core + Zig-helper apps fall out of tree detection naturally, and a TypeScript app can eject to its emitted Zig at any time. The entries below are the pieces of this one feature (#119).
- **examples/ai-chat-ts — an AI chat client authored entirely in TypeScript + Native markup**: a conversation UI over an OpenAI-compatible chat-completions endpoint as two subset modules and zero Zig — `Cmd.fetch` with routed `{status, body}` results, the JSON wire format as pure byte math (`src/api.ts` encodes requests and parses `choices[0].message.content` / `error.message`, refusing anything malformed), conversation history in the Model, the composer on the SDK byte-splice text engine, endpoint/model/key through the env channel (no baked endpoint, no key anywhere in the tree — a teaching state until all three arrive), a model-level in-flight guard, and honest failed states that keep the history with a Retry. The e2e battery (`tests/ts-core/ai_chat_e2e_tests.zig`, in `zig build test-ts-core-e2e`) pins the exact request bytes turn by turn and replays a recorded conversation — transport failure and retry included — byte-identically with zero network; the README states the v1 boundaries plainly (buffered responses, compile-time fetch headers) (#119).
- **Docs: "Where Packages Go"** (`/typescript/packages`): the four first-class patterns behind "can I use npm?" — HTTP/AI APIs through `Cmd.fetch` (with a complete transpile-checked core sample), npm-heavy UIs as embedded web frontends, Node libraries as `Cmd.spawn` sidecars, and pure utilities vendored under `src/` or imported from the curated `@native-sdk/core/*` channel — with the core's no-npm boundary stated as the thing that buys replay, headless testing, and native dispatch speed (#119).
- **Eval wave 2 — six dual-track authoring cases, one language-blind spec each**: the eval harness gains `"frontend": "app-dual"` cases that run the SAME realistic ask on both authoring tracks — `<case>@ts` scaffolds a full TypeScript app (`native init --template ts-core`), `<case>@zig` the Zig app template — graded by shared checks plus per-track behavioral harnesses asserting one spec: fetch-JSON-into-a-sortable-table, debounced notes autosave (starter provided), a pomodoro timer with a completion sound, a seeded stale-cache delete bug to root-cause, a module split with byte-exact CSV export, and a shell-command system-info panel. The ts harnesses decode the Cmd/Sub wire format (`evals/harness-lib/cmdview.zig`); the zig harnesses ride the SDK's fake effects executor inside the workspace's own `native test` graph. `--track ts|zig` selects a lane; each track gets its current skills (`ts-core`+`native-ui` vs `native-ui`+`zig`), and `pnpm metrics` now reports per-track teaching-error encounters alongside first-pass compliance and the violation taxonomy (#119).
- **Transpiler: two real-app fixes the wave surfaced**: byte-element stores of computed values (`buf[i] = src[j]`) now emit with JS's exact ToUint8 wrap instead of stopping as invalid Zig, and records referenced from a declared text-input mirror union stay by value even when the core also stores its editor state in the Model — previously that pointer promotion silently broke `on-input` resolution (runtime view build and `native check`'s model contract alike) for any core keeping a `TextEditState` in its model (#119).
- **examples/soundboard-ts — the soundboard authored entirely in TypeScript + Native markup**: the launch-bar port of the Zig soundboard as three files of truth and zero Zig — `src/core.ts` (the committed catalog as const tables, REAL audio through the `Cmd.audioPlay` stream with the engine's local-then-URL cascade, the play-next queue, Copy Title on the clipboard, a motion-gated `Sub.timer` playback clock, and search through the full byte-splice text engine), `src/app.native` (the whole view: grid, album detail, songs library, context menus, the now-playing transport), and `app.zon`. An end-to-end suite (`zig build test-ts-core-e2e`) drives the shipping core and markup through playback, auto-advance, the stale-event window, volume, clipboard, search, record→replay, and a dispatch-latency budget; the README carries an honest ledger of where the port diverges from the Zig original (#119).
- **Generated TS wiring resolves the theme and the audio cache**: app.zon's `.theme` pack now reaches TypeScript apps (composed with the live system appearance), and the platform caches directory is resolved at launch so a core's URL audio playback caches under the conventional content-addressed path with no `cachePath` in the core (#119).
- **Transpiler emit-contract fixes**: the global `undefined` VALUE now emits the optional empty (`null`) — it previously emitted Zig `undefined`, uninitialized memory — and an early-exit null guard whose narrowed value goes unread emits a plain null test instead of an unused (uncompilable) capture (#119).
- **examples/system-monitor-ts — the system monitor authored entirely in TypeScript + Native markup**: the spawn-showcase port of the Zig system monitor as three files of truth and zero Zig — `src/core.ts` (the 2 s sampling cadence as a declarative `Sub.timer`, collect-mode `Cmd.spawn` for `ps`/`vm_stat`/`meminfo`, pure byte parsers over the collected stdout, the exact top-128-by-CPU selection, the confirmed SIGTERM action, and a runtime boot probe that discovers the host's sampler conventions where the Zig original switches at comptime — with the honest "no sampler for this OS" state when nothing answers), `src/app.native` (the whole view: chromeMsg-driven hidden-inset header, `<chart>` sparklines over the core's NaN-padded windows, the real table register with per-row context menus and controlled scroll, the modal confirmation), and `app.zon`. An end-to-end suite (`zig build test-ts-core-e2e`) drives the shipping core and markup through the probe cascade, the Zig example's committed real captures, the timer cadence with pause, search/sort, the kill round trip, and record→replay; the README carries an honest ledger of where the port diverges from the Zig original (#119).
- **Markup chart series bind f64 iterables**: `<series values="{binding}">` now accepts `[]const f64` model fields, decls, and fns alongside f32 — transpiled TS cores carry every number array as f64, so markup charts were unreachable from a TS model — narrowed per sample into the f32 chart pipeline in both engines, the validator, and `native check`'s model contract (#119).
- **The everyday string methods on core bytes — familiar spellings, byte-honest semantics**: TypeScript cores can now write `s.toUpperCase()`, `s.trim().toLowerCase().includes(q)`, `s.repeat(n)`, `s.padStart(w)`, `s.split(sep)`, `s.startsWith(p)`, `s.indexOf(t)`/`s.lastIndexOf(t)`, and `s.at(i)` directly on `Uint8Array` text. Every length/offset is a BYTE measure, search is byte-wise (`includes`/`indexOf`/`lastIndexOf` dispatch by argument type: bytes needle = substring search, number = the TypedArray element search), case mapping is Unicode 17 SIMPLE case mapping (locale-free, generated tables — 3.1 KiB in the binary — with invalid UTF-8 passing through unchanged), trim strips the exact JS whitespace set over UTF-8, and `split` returns a locally-owned `Uint8Array[]`. Native lowers each call onto rt kernel helpers; node runs the same methods from the same generated tables (devhost polyfill), so both runtimes produce identical bytes by construction — pinned by run-fidelity cases across Greek/Cyrillic casing, growth mappings, invalid-UTF-8 passthrough, repeat/pad edges, and split shapes, plus a machine-checked method matrix (#119).
- **The stays-out spellings teach their reason**: `charCodeAt`/`charAt`/`codePointAt`/`normalize`/`replace`/`replaceAll` on bytes teach the new NS1060 (byte text speaks the byte-honest method set), the locale family teaches NS1005, and the regex-taking methods teach NS1040 — never a bare "property does not exist". `trimAsciiSpaces` stays for LF-preserving line parsing; `.trim()` is the canonical whitespace trim (#119).
- **TS cores: generics, local function values, and the complete-language tail**: user-declared generic functions, interfaces, and type aliases now compile via per-call-site monomorphization from tsc's own resolved type arguments — one readable Zig fn per distinct instantiation (`pick__Task`, `pick__f64`), deduped, covering records/unions/arrays/optionals, recursion inside a generic, generics calling generics, and structural instantiation of generic types (`Box<Task>``Box__Task`); unresolvable call sites teach the new NS1053 (#119).
- **Const-bound local function values hoist**: `const scale = (x: number): number => x * 3;` (arrow or function expression) becomes an ordinary module-level fn when capture-free and fully annotated, usable by direct call (recursion included) or as an array-method callback (`xs.map(scale)`); captures, reassignment, storing/returning the value, and record-field calls teach the new NS1054 — and capturing a locally-owned array ends its ownership at the capture (NS1051 machinery) (#119).
- **The small-fry tail lands with node-byte-identical pins**: `for (const [i, x] of xs.entries())` (the destructured-pair loop form), `++`/`--`/assignments in provably order-exact value positions (`arr[i++]`, `const n = ++count`), `?.[i]` and `?.method()` optional-chain hops on supported receivers, plain `number`/`string` switch scrutinees (if/else chains with JS strict-equality and default-position semantics), and `typeof CONST` type-query aliases. Float-valued template holes stay deferred — there is no JS-exact f64 formatter in the runtime yet, and node/native divergence is never an option (#119).
- **TS cores: exceptions and data classes — the complete language, tier 2**: `throw`/`try`/`catch`/`finally` compile as deterministic control flow — a thrown subset value (one error shape per core, the new NS1057) unwinds through a native payload slot to the nearest catch, across helper calls and out of `.map`/`.filter` callbacks; `finally` lowers to a scoped defer running on every exit (control flow inside it teaches the new NS1058, JS's own no-unsafe-finally), the catch binding narrows once (`const err = e as ParseError;`), rethrow and nested try work, and an uncaught throw is a defined panic at the exported boundary — exactly where node's process would crash. Node-parity is pinned in the run-fidelity corpus, throw-mid-mutation of an owned array included (#119).
- **Data classes, no inheritance**: `class Task { fields; constructor; methods }` emits as a plain struct plus module-level functions; `new Task(...)` constructs a record-shaped value, `this` reaches fields and methods, and instance mutation (field writes, `this`-writing methods) follows the same local-ownership rule as arrays (NS1001/NS1051 at the boundaries). The class tail teaches by name: `extends`/`super`/`abstract` (new NS1055), accessors/statics/privates/`#`/class expressions/escaping `this` (new NS1056), generic classes (NS1053), `instanceof` (NS1041); class instances stay local values — Model storage teaches toward records (flagged follow-up) (#119).
- **Two reconcile potholes**: arrays OF byte buffers (`const parts: Uint8Array[] = []`) now route through array ownership — `parts.push(chunk)` on your own array works and runs node-identically (bytes VALUES keep their own discipline) — and a bare reference to a module-level function or const helper is a callback (`xs.map(encodeTurn)`, `xs.toSorted(byAscending)` — the same lowering as the arrow spelled inline), plus shorthand members in union literals (`{ kind: "range", v }`) (#119).
- **TypeScript apps are the `native init` default**: the scaffold is three files of truth and zero Zig — `src/core.ts` (logic), `src/app.native` (view), `app.zon` (manifest) — and the build detects the core from the tree (a `src/core.ts` transpiles at build time through generated wiring; `src/main.zig` stays the Zig core; both at once is a teaching error). The Zig template remains first-class via `native init --template zig-core` (#119).
- **`native dev --core`**: the TypeScript core-logic loop under node — dispatch Msgs as JSON lines, watch the model and effect transcript, run Sub timers and Cmd.delay on a virtual clock. Logic only, honestly not a renderer; `native dev` runs the real app (#119).
- **`native check` checks TypeScript cores**: a `src/core.ts` runs the @native-sdk/core subset checker first (NS diagnostics verbatim), then markup and app.zon as before — with a fresh model contract the markup pass type-checks bindings against the core's emitted model (#119).
- **Markup text input reaches TypeScript cores**: a core declares its own `TextInputEvent` mirror union and `on-input` matches it structurally, translating each runtime event into the core's union at dispatch — markup text field to TS `update` to re-render, end to end (#119).
- **Exported model helpers bind from markup**: an exported single-Model-parameter helper also emits as a Model declaration (`doneCount` binds as `{done_count}`), and `export const viewUnbound = [...]` emits the `view_unbound` lint opt-out — NS1031/NS1032 teach the collision and typo cases (#119).
- **TS cores: runtime fetch header values and journaled env deliveries** — the two gaps the ai-chat example surfaced, closed. `Cmd.fetch` header VALUES may now be runtime bytes (`{ authorization: bearerToken(model.apiKey) }` — header NAMES stay compile-time ASCII, NS1029/NS1030 bounds still gate what is knowable at build time and the engine err-arm rejects the rest); no wire change was needed — record 0x09 always carried values as length-prefixed byte fields built at dispatch time, only the emitter's literal-only rule and the node SDK's `FetchSpec` type moved. And the `envMsgs` channel now journals each launch delivery (an additive `.env` effect record: value in `payload`, arm name in `stderr_tail`, dispatch index in `key`), so replay feeds the RECORDED values with zero env reads — a session recorded with credentials set replays byte-identically on a machine where they are unset or different; journals without env records (older recordings) re-derive from the launch configuration exactly as before. `examples/ai-chat-ts` now sends a real `Authorization: Bearer <key>` header instead of the access_token query-parameter workaround, and its replay e2e drives both the unset-env and changed-env launches. Plus NS1051: an un-annotated spread local (`const turns = [...model.turns, next]`) now teaches its array-type annotation instead of the generic emit-time stop (#119).
- **TypeScript subset: grammar completeness**: the subset now means "TypeScript minus the ecosystem minus the purity violations" — never minus basic syntax. New in the mapping: `do...while` (body-first, `continue` re-tests, exactly node), labeled statements with labeled `break`/`continue` (loops and blocks, lowered to Zig labels), `default` arms on union-`kind` switches (the `else` prong over unnamed arms; dead-code defaults emit nothing), `**`/`**=` (JS pow corners pinned: NaN exponents, `±1 ** ±Infinity`, right-associativity), the shifts `<< >> >>>` and `~` (ToInt32 with the count masked & 31), the full compound-assignment family (`*= /= %= &= |= ^= <<= >>= >>>=` plus guarded `&&= ||= ??=`), unary `+`, const record destructuring (`const { total, done: doneCount } = stats;`), namespace imports over the core's own modules (`import * as util` — values, calls, and qualified types resolve to the flat emitted names), multi-counter for-inits and comma incrementors (`for (let lo = 0, hi = n; lo < hi; lo++, hi--)`), countdown incrementors (`i--`), hole-free template literals, `satisfies`, and the empty statement (#119).
- **Every exclusion now teaches**: twelve new rules close every generic-error hole — NS1039 (namespace aliases are dot-syntax, SDK intrinsics import by name), NS1040 (regexes), NS1041 (runtime type/shape tests: `typeof`/`in`/`instanceof`/`Object.*`), NS1042 (generators), NS1043 (comma/`void`/assignment-as-value), NS1044 (BigInt/Symbol), NS1045 (destructuring beyond const record fields), NS1046 (nested/stored function values and `?.()`), NS1047 (default exports, `export =`, value re-exports), NS1048 (loose `==`), NS1049 (`var`), NS1050 (generic declarations) — and NS1019 broadens to the full fixed-arity story (rest params, `arguments`, call spreads). `var` and generic helpers previously emitted broken Zig silently; both now stop with teachings, and same-file type/value homonyms are caught by NS1038 instead of colliding in the emitted module (#119).
- **The grammar matrix**: a new machine-checked suite (`packages/core/test/grammar_matrix.test.ts`) enumerates every statement, operator, expression, and declaration form of the language and pins each to its verdict (SUPPORTED emits and compiles under Zig; BANNED names its teaching rule; tsc-rejected stays tsc's), so the grammar can never grow a silent gap again. New run-fidelity corpora pin the node-vs-native behavior of every new mapping (pow corners, shift wrapping, `-0` preservation, do-while ordering, labeled-jump targets, default-arm matching, destructured aliases, `??=` on 0-vs-null) (#119).
- **Inference fixes surfaced by the matrix**: the float-demotion fixpoint no longer terminates one pass early (a two-hop chain — field → destructured alias → local — previously stranded a phantom NS1016 conflict), and shorthand `{ x }` properties now wire the VALUE symbol into inference (a shorthand from a host-boundary parameter previously kept a false integer proof and emitted mismatched Zig) (#119).
- **Stock-IDE support for TypeScript apps, working before the npm publish**: `native init` scaffolds `package.json` (the app's name plus an exact `@native-sdk/core` pin) and `tsconfig.json` (the checker's own compiler options, so editor errors match `native check` reality), and the CLI materializes `node_modules/@native-sdk/core` itself — a copy of exactly what the published package will contain — so VS Code et al. resolve `@native-sdk/core` and `@native-sdk/core/text` with full IntelliSense today. `native check|dev|build|test` keep the copy fresh (one info line on refresh) and `native doctor` reports version skew; once the package is on npm, a plain `npm install` writes identical content and the CLI recognizes the version and leaves it alone. None of it is build truth: builds transpile against the SDK checkout and work with node_modules deleted, tree detection still keys on `src/core.ts` alone, and the Zig template is untouched (#119).
- **@native-sdk/core is publish-shaped**: `files: ["sdk"]`, a typed exports map for `.` and `./text`, no bin and no runtime dependencies (the transpiler dependency moved to devDependencies), with a package test pinning the manifest shape. The soundboard-ts and system-monitor-ts example ports carry the same editor surface, and a ts-core e2e suite proves the whole contract with the real tsc: fresh scaffold and both ports typecheck with zero injected paths, and the transpiler still takes the core clean after `rm -rf node_modules` (#119).
- **TS cores: mutating array methods are legal on locally-owned arrays**: an array your function creates (a literal or a `.slice()`/`.map()`/`.filter()`/`.concat()`/`.toSorted()` copy) now takes the full mutating set — `push` (any seed, not just the empty builder), `pop`/`shift` (returning the find-miss optional), `unshift`, `splice` (JS index clamping, the removed array as its value), `reverse`, `fill`, in-place `sort` (the stable toSorted machinery applied in place), and indexed writes — with node-byte-identical semantics pinned by a new run-fidelity corpus (parser stacks, splice corners, shift/unshift order, sort stability, pop-on-empty) and a machine-checked mutation matrix beside the grammar matrix (#119).
- **Teaching errors now fire only at the true semantic boundaries**: shared data keeps NS1001/NS1022 (both rewritten around ownership, NS1022 naming the now-legal `const copy = xs.slice(); copy.sort(cmp);` idiom), and the new NS1051 teaches mutation after an escape — returned from a callback, passed to a call, stored, or aliased — with the escape kind and line named; an escape inside a loop gates the whole loop body, and early-exit returns stay legal (#119).
- **TypeScript cores reach the full app surface**: markup sliders and split dividers deliver their applied 0..1 fraction to a core's one-number float arm (`on-change="scrubbed"` — scrub-to-seek from markup), controlled scroll round-trips through a core-declared `ScrollState` mirror, and the generated wiring detects five host-event channels from plain exports — `frameMsg` (presented frames), `keyMsg` (the app-level key fallback), `appearanceMsg` and `chromeMsg` (system appearance and hidden-titlebar geometry as Msg arms), and `envMsgs` (launch environment variables as journaled boot Msgs) — plus `app.zon` `.assets.images` registered at launch as the `ImageId`s markup avatars bind (#119).
- **Export lists and value re-exports compile in TypeScript cores**: `export { a, b as c }` and `export { x } from "./m.ts"` now bind real names over existing declarations in the flat emitted namespace — un-renamed entries export the declaration itself, renames emit a `pub const c = a;` alias, and re-export chains resolve end to end (node ≡ native, pinned by run-fidelity). Renamed entry-module helpers join the markup binding surface under their exported names; NS1047 narrows to the genuinely unsound tail (`export default`, `export =`, `export * from`, renamed generics/classes, wiring config, SDK re-exports), and NS1014/NS1038 keep entry points and name uniqueness honest across the new forms (#119).
- **Heterogeneous throws with a narrowing catch**: a core may now throw several distinct kind-tagged shapes — the checker collects them into an implicit thrown union (or a declared union whose arms match), the payload slot is that union, and `catch (e)` narrows it with plain kind tests (`if (e.kind === "parse")`) with no `as` ceremony; rethrow re-raises the bound value, narrowing works across callback boundaries, and NS1057 narrows to genuinely unsound throws (untagged values in a mix, tag collisions, untyped escapes, `new Error`). All pinned node ≡ native by run-fidelity (#119).
- **Class statics and erased privacy**: `static` methods lower to receiver-less module functions under the class's mangled names (`Task.fromRow(...)` -> `Task__fromRow`), `static readonly` fields with initializers become module consts (`Task.LIMIT`), and `private`/`protected` keywords are accepted and erased (tsc enforces them at the type level — their whole meaning). Mutable statics teach NS1010 (module state), `this` inside a static member teaches toward the class name, and `#`-fields stay taught; NS1056 narrows accordingly. Pinned node ≡ native by run-fidelity (#119).
- **Three mutation loosenings**: `xs[xs.length] = v` on an owned array is a push (the one growth shape; compound forms stay taught), a reassigned `let` stays owned when every assignment installs a fresh owning construction (each reassignment resets the emitted builder), and passing an owned array into a `readonly T[]` parameter is a BORROW when the callee provably only reads it (coinductive analysis — recursion over borrowed slices stays legal; returns, stores, casts to mutable, and onward passes into mutable positions still end ownership). NS1001/NS1051 copy updated; mutation matrix rows moved and extended; all pinned node ≡ native (#119).
- **Accurate teaching for the Array statics (NS1059)**: `Array.from`/`Array.of`/`Array.fromAsync` now teach their own construction rewrites (the literal, the spread copy, the push-builder loop) instead of the generic runtime-shapes copy; `Array.isArray` keeps NS1041 — it really is a runtime type test. Comma expressions in value position stay taught (NS1043): the split-statement lowering is only JS-order-exact in the pinned positions, so they did not fall out of the machinery for free (#119).
### Improvements
- **The components reference is markup-first**: every component page leads with its Native markup sample (all fences validated against the live `native markup check`), interactive samples show the core side in both authoring languages behind the TS | Zig toggle (accordion, checkbox, radio, dialog, input, scroll, select, slider, split, chart), and the builder form moves to a consistent "Programmatic construction (Zig)" section at the end of each page — real API docs, framed as the Zig tier's programmatic alternative. The section index tells the one-language story, and four samples that shipped unnamed text controls now carry accessible names (#119).
- **The TypeScript authoring package is `@native-sdk/core`**: the transpiler package moved from `packages/ts-app-core` to `packages/core` under its real npm name — cores were already importing `@native-sdk/core`, and the dev-harness resolver now maps that one specifier straight onto the package's own SDK module (#119).
- **TS-first docs with a TS | Zig toggle**: code samples on the flagship pages (Quick Start, App Model, Native UI, State & Data Flow) show TypeScript first with the Zig form one tab away — the reader's language choice is remembered site-wide — and the new [TypeScript Cores](https://native-sdk.dev/typescript) page covers the app-core subset, Cmd/Sub effects, text-is-bytes, the node dev loop, capacity knobs, and the eject story. Toolkit-extension pages stay Zig on purpose: that tier is the machinery itself (#119).
- **Docs code presentation**: the TypeScript | Zig code toggle now renders as the same segmented pill control as the component previews' Default | Geist theme-pack toggle (one shared primitive, identical in dark mode), and code samples can carry a filename header — a file glyph plus the path (```` ```ts:src/core.ts ````), integrated into the toggle's header bar opposite the segments — applied across the quick-start, TypeScript, app-model, native-ui, and config pages' complete-file samples; copied markdown renders the path as a labeled line above a plain fence (#119).
- **Markup binds your model's field names exactly as you wrote them**: TypeScript cores now emit Zig with the TS spellings intact — fields, exported single-model helpers, Msg payload records, and locals alike — so `nextId` binds as `{nextId}` and `doneCount` as `{doneCount}`, ending the dual-naming rule (camelCase in core.ts, snake_case in app.native) that every author had to hold in their head. Zig cores are untouched: their fields were already the names markup binds. The whole pipeline follows from the one change — the model contract, `native check`'s typed pass, both markup engines, hot reload, and the eject story (the emitted module now mirrors your source, and markup keeps binding the same names after you adopt it) (#119).
- The TS-track host surfaces that matched emitted names structurally now speak the TS SDK spellings (`timestampMs`/`intervalMs`, `colorScheme`/`reduceMotion`/`highContrast`, `tabsProjected`, the audio arm's `positionMs`/`durationMs`), and the declared scroll-state mirror accepts the canvas spelling or the TS spelling — never a mix (#119).
- NS1031 collisions are now exact-name collisions (`doneCount` the helper vs `doneCount` the field); `viewUnbound` entries were already the TS names and stay so. Scaffold templates, both ports' views, the ai-chat view, docs, and the skill teach the one rule (#119).
- **zig-core starter parity**: `native init --template zig-core` now scaffolds the same app as the TypeScript template — counter, a ticking switch driving a repeating 1s `fx.startTimer`, a Stamp button reading the journaled clock (`fx.wallMs`), a bindable `total` helper, and the matching markup — with generated full-loop tests covering the timer and clock seams; the quick-start code toggle now shows both starters verbatim (#119).
### Bug Fixes
- **Packaging fails loudly when signing fails**: `native package --signing adhoc|identity` no longer exits 0 while shipping an unsigned bundle — output paths with spaces (`--output "My App.app"`) now sign correctly (the signing pipeline execs argv arrays instead of shell strings, which also unbreaks spaced paths and spaced identities in the notarization helpers), every signed bundle must pass `codesign --verify --deep --strict` before packaging succeeds, any codesign failure stops the package with codesign's own reason, and the package report proves the outcome with a `signing: adhoc (signed, verified)` line (#118).
- **Per-thread memory no longer scales with the canvas scratch**: the render planner's fixed scratch buffers lived in static thread-local storage, so on Windows every thread the process spawned (window host, COM, accessibility, workers) privately committed a full ~6.5 MB copy — most of a small app's working set. The scratch now allocates lazily on the one thread that actually plans frames: a scaffolded counter app's private working set drops ~4x, its `.tls` section shrinks from ~6.5 MB to under 200 bytes, and the executable itself is ~6.5 MB smaller. Linux and macOS binaries shed the same per-thread TLS block (#117).
- **Transpiler: a ternary initializer under null-guard fusion parenthesizes**: `const x = c ? f(a) : g(a); if (x === null) <exit>` lowers to Zig's `orelse` fusion, and the conditional's if/else expression now wraps in parens — bare, the `orelse` bound to the ELSE arm alone (a type error at best, the wrong value at worst). The same guard covers `??` over a ternary left side; pinned in the conformance corpus and the node/native run-fidelity corpus (#119).
### Contributors
- @ctate
- @SunkenInTime
- @sepehr-safari
## 0.4.4
### New Features
- **Native-only host builds (Windows)**: the build graph now infers web use from app.zon — a `.frontend` block, the `"webview"` capability, a `.shell` webview view, or the Chromium engine — and an app that declares none of them compiles its Windows host without the embedded WebView layer: no WebView2 header, no `WebView2Loader.dll` installed, staged, or referenced by the executable. A new `.webview_layer = "auto"|"include"|"exclude"` manifest field (and `-Dweb-layer`) overrides the inference, an exclude that contradicts a web declaration is rejected at validate, configure, and package time, and a native-only build that reaches webview creation at runtime fails fast with a teaching `WebViewLayerNotBuilt` error. `native check` and the package report print the web-layer verdict, and a CI cross-audit asserts the presence/absence of the loader reference in real cross-compiled executables (#107).
- **Native-only host builds (Linux)**: the WebKitGTK compile seam mirrors the Windows one — an app whose app.zon declares no web use compiles its GTK host with `NATIVE_SDK_ALLOW_WEBKITGTK_STUB`, so the executable neither links `webkitgtk-6.0` nor references any `webkit_`/`jsc_` symbol, building needs no WebKitGTK development package, and users need no `libwebkitgtk` at runtime. The web-layer auditor (`tools/audit_web_layer.zig`) grew a hand-rolled ELF reader (DT_NEEDED entries + dynamic symbols) that CI runs both ways: the native-only fixture must scan clean even with the dev package installed, and the Linux canvas smoke now builds on a runner without WebKitGTK at all. `native package` refuses to package a WebKitGTK-linking binary under a native-only decision, record→replay and automation-driven sessions are pinned on native-only apps, and the macOS GPU dashboard smoke asserts a native-only app spawns zero WebKit helper processes (#110).
### Improvements
- **Zig 0.16 guidance**: a new `zig` skill (`native skills get zig`) maps each pre-0.16 std idiom's compile error to the current one — `std.Io` file IO and writers, unmanaged `ArrayList`, `main(std.process.Init)`, spawning, clocks, `{t}`/`{f}` formatting, `build.zig` modules — with the same content for humans as the docs' Zig 0.16 Notes page; the native-ui skill carries the short table, and a failing `native build|test|dev` now points at the catalog when std members come up missing (#105).
- **Lazy Linux WebView startup**: GTK windows now create only GTK chrome at window creation and materialize the main `WebKitWebView` on first web use, so canvas-only apps do not start WebKit processes on Linux; child-WebView bridge responses no longer require a main WebView to exist (#106).
### Contributors
- @ctate
- @WhiteHades
## 0.4.3
### Improvements
- **Linear-light edge blending**: anti-aliased fringes on opaque rounded rectangles, path fills, and strokes now composite through a linear-light coverage path, removing the dark rims that sRGB blending produced on curved geometry while keeping opaque interiors, glyph coverage, and translucent overlays byte-identical (#89).
### Bug Fixes
- **Single-line fields handle overflowing values**: text, selection rects, composition underlines, and the caret now clip to the field's content rect, and a horizontal scroll offset keeps the caret visible — typing past the edge scrolls the value, Home scrolls back, and deleting never leaves trailing emptiness. Covers text fields, inputs, search fields, and comboboxes; values that fit render exactly as before (#90).
- **Cross-drive apps on Windows**: `native dev|build|test` no longer fails with `expected path relative to build root; found absolute path` when the app and the npm-installed SDK live on different drives — the generated build graph now bridges volumes with a `.native/sdk` directory junction (no admin rights needed) and keeps the zon dependency relative; the junction is retargeted automatically when the SDK moves or upgrades. Where the bridge cannot apply (`native eject`, full-shape `native init`, or a filesystem that refuses junctions), the CLI explains the cross-volume constraint and both ways out instead of writing a build Zig would reject (#92).
### Contributors
- @ctate
- @fleeting-zone
- @kvnwdev
## 0.4.2
### Improvements
- **Windows rendering is DPI-aware and sharper**: Windows apps now declare Per-Monitor V2 DPI awareness, each window carries its own device scale, and canvases, native child views, hidden-titlebar sizing, and explicit WebView frames re-render/re-round correctly when moved across mixed-DPI monitors (#81).
- **Smoother canvas geometry**: rounded-rect fills and strokes now render through continuous coverage while eligible hairline borders snap to crisp device-pixel columns, so arcs stay anti-aliased and 1px borders stay sharp under the default house and Geist packs (#81).
- **Canonical package and documentation metadata**: npm package metadata, release automation, docs, templates, and examples now point at the renamed `vercel-labs/native` repository and `native-sdk.dev`; `version:sync` stamps repository/homepage metadata into platform packages and `version:check` rejects drift before publish (#78, #80).
### Bug Fixes
- **Windows embedded WebView is real from a plain checkout**: the WebView2 SDK header and loader are vendored under `third_party/webview2/` (BSD-licensed), every build graph puts the header on the include path, and the host now refuses to compile with the WebView layer silently stubbed — previously every Windows build shipped the stub and WebView loads reported `WebViewNotFound` at runtime (#86).
- **WebView2 host conformance fixes**: a missing lambda capture in the bridge message handler, a mingw-compatible WRL event-handler factory, an `EventToken` shim, and STA COM initialization on the host thread let WebView2 environment creation and bridge messaging run on Windows (#86).
- **WebView2Loader.dll ships with the app**: `zig build` installs the architecture's loader next to the executable, `zig build run` resolves it during dev runs, generated frontend/package commands carry `NATIVE_SDK_PATH`, and `native package --target windows` includes the loader in the artifact (the Evergreen WebView2 runtime itself is preinstalled on current Windows) (#86).
- **Checkbox marks use the vector core**: checked boxes now draw one stroked polyline with round caps and joins instead of two aliased diagonal lines, and stroke caps ride the GPU packet path so the host and reference renderer agree (#87).
- **Path geometry lifetimes are owned by the builder**: chart, spinner, and checkbox path commands no longer borrow threadlocal frame scratch, so separately emitted trees cannot alias each other's path elements (#87).
### Contributors
- @ctate
## 0.4.1
### Bug Fixes
- **npm package assets**: Ship the SDK's root `assets/` directory in `@native-sdk/cli` so installed packages include `assets/native-sdk.manifest`, the default macOS icon, and entitlements needed by generated apps (#72, #75).
### Contributors
- @ctate
- @lzitser23
## 0.4.0
### New Features
- **zero-native is now the Native SDK**: The toolkit, CLI, and packages are renamed end to end — the CLI binary is `native`, the Zig module and build helper are `native_sdk` (`native_sdk.addApp`, `native_sdk.addMobileLib`), the embed C ABI prefix is `native_sdk_*`, and the npm CLI package is `@native-sdk/cli`.
- **Native-rendered apps by default**: `native init` scaffolds a native-rendered app — a declarative `.native` markup view plus Zig logic on the `UiApp` runtime (a `Model`, a `Msg` union, `update`, and a view) — with web frontends still available via `--frontend next|vite|react|svelte|vue`.
- Native markup: HTML-inspired views with flex layout, `{bindings}` to model fields and functions, typed `on-*` message dispatch, `for`/`if`/`else` structure tags (multi-child `for` bodies, `<else>` empty states), and keyed identity; a deliberately closed grammar keeps logic in Zig.
- Comptime compilation: views compile at build time into direct field access — release binaries carry no parser, and markup or binding mistakes are compile errors with line and column.
- Hot reload: dev builds watch every `.native` file — imported components and fragments embedded in Zig views included — and update the running window in place, preserving model state, selection, and widget identity.
- Expressions in bindings: arithmetic, comparisons, boolean logic, string concatenation, and a closed 17-function formatting library (`fixed`, `thousands`, `date`/`time`, `pad`, `plural`, ...), evaluated bit-identically by both markup engines; string-producing model functions bind directly through the build arena.
- Cross-file components: `<import>` splices template files (transitively, with cycle and duplicate diagnostics), template args take literal defaults, `<slot/>` marks where use-site children land, and `native eject component` transfers a library composite's canonical source into your app exactly once.
- `canvas.Ui`, the programmatic builder under the markup: structural widget identity, typed message handlers, flex-first layout, and per-element `opacity`/`transform` render channels for animated composition.
- **The modelview contract, checked in both directions**: `native check` verifies every binding path, iterable, key, message tag, payload type, and expression in every `.native` file against the app's reflected `Model`/`Msg` surface in milliseconds — with did-you-mean suggestions and a dead-state lint for model fields and messages no view uses.
- **Markup tooling**: `native markup check` (instant validation with positions), a language server (diagnostics, completion, hover), a TextMate grammar with editor setup, `native markup dump` over the canonical serialized document format, and the `native-ui` agent skill — the complete authoring reference, served through the skills CLI.
- **Two-way tooling**: `native automate provenance` reports where a live widget was authored (file, byte span, template instantiation chain), and `native automate edit` writes minimal-diff attribute and text edits back into the markup source — validated before anything touches the file, with hot reload closing the loop.
- **Full component catalog**: every built-in component is expressible in markup — tabs, tables, dialogs, drawers, sheets, selects, comboboxes, accordions, menus, badges, avatars, tooltips, inputs, and more — implemented in both engines with parity tests, alongside new composites in markup and Zig:
- Charts (`<chart>` / `ui.chart`): line, area, bar, and band series drawn through the vector path pipeline with design-token colors, deterministic downsampling past 256 points, axis labels on a nice-step lattice, and pointer hover details.
- Markdown (`<markdown>` / `native_sdk.markdown`): a GitHub-flavored subset — headings, inline styles, links, lists, task lists, fenced code, blockquotes, pipe tables, autolinks, and model-driven collapsibles — that degrades malformed input to text and never fails a build.
- Disclosure trees with the full ARIA tree keymap, steppers and timeline items, input groups with focus-within rings, chat bubbles with reaction pills and thread-width caps, and a `ui.nav` push/pop page container with stable per-page state.
- Resizable split panes with model-owned fractions, keyboard and assistive resize, and optional eased animation on model-driven moves.
- Windowed virtual lists: viewport-sized widget budgets at 100,000 items, variable row extents that converge to measured truth without visible jumps, tail anchoring for chat transcripts, and `on_reach_end`/`on_reach_start` for infinite fetch and history loading.
- Anchored floating surfaces (dropdowns, selects, popovers) that float above the tree with edge auto-flip; dismissal (Escape, click-outside, assistive dismiss) is a Msg the model owns, and focused selects get the full open/navigate/commit keymap.
- Vector icons: an SVG stroke-icon subset parser, 50 curated built-in icons, leading or trailing icon slots on buttons, toggle chips, list and menu rows, badges, and timeline items, app-registered icons comptime-parsed from your own SVGs, model-bound icon names, and a loud missing-icon fallback.
- **Text engine**:
- Inline styled spans — weight (resolved to real faces), italic, monospace, color tokens, underline, strikethrough, size scale, per-span backgrounds, and hit-testable links — wrap as one paragraph in Zig and markup alike.
- Honest single-line text: unwrapped text elides with a trailing ellipsis by default, an `overflow` policy knob keeps the deliberate hard cut available, and word wrap is an explicit opt-in — paint always agrees with measurement.
- `heading`/`display` typography rungs on the token ladder, first-class text alignment, and fixed grid column counts.
- **Selection and clipboard**: cmd/ctrl+C/X/V in editable fields through the platform clipboard, click-drag selection with copy on static text (surviving rebuilds, exposed to semantics and automation), and clipboard effects for app code.
- **Interaction model**:
- Presses fall through to the nearest pressable ancestor, so any element with a handler is a real hit target — nested pressables resolve to the deepest one, and text selection still works inside pressable rows.
- Press-and-hold, double-click, Enter as a list row's primary action, and an app-level key fallback (`Options.on_key`) with pinned precedence — quiet list rows stay transparent to app-owned selection models.
- Source-driven `autofocus`, observable typed scroll events (`on_scroll`), a built-in search-field clear affordance, and a quiet-hover style knob for content tiles.
- **Effect system**: the update loop's command half — `update` gains an effects channel of bounded, key-addressed effects that deliver exactly one terminal Msg each and are fully testable against a deterministic fake executor:
- `fx.spawn` runs subprocesses with streamed lines or whole-output collect mode (stderr tail included), raisable per-effect line bounds, and cancellation; `fx.fetch` runs HTTP(S) requests with an explicit failure taxonomy, timeouts, and a streaming response mode for line-oriented endpoints.
- `fx.readFile`/`fx.writeFile` persistence, `fx.startTimer`/`fx.cancelTimer`, `fx.writeClipboard`/`fx.readClipboard`, `fx.registerImageBytes` for runtime images, `fx.closeWindow`/`fx.minimizeWindow`, and the `init_fx` boot hook so loading states are in the very first paint.
- A facade time API (`nowMs`, `monotonicMs`) plus `Clock`/`TestClock` seams for deterministic time-dependent logic.
- **Audio, end to end on five platforms**: `fx.playAudio` with full transport (pause, resume, stop, seek, volume), real decoded durations, position ticks, and honest completion and failure reports — AVFoundation on macOS, Media Foundation on Windows, GStreamer on Linux, and the experimental mobile hosts on iOS (AVFoundation) and Android (MediaPlayer).
- Streaming with a verified track cache: URL sources resolve local file, then size-verified cache, then progressive stream (filling the cache in parallel for the next play), with honest `buffering` states and explicit failures — never a silent stall.
- Real spectrum analysis on macOS, Windows, and Linux: 32 log-spaced bands at ~25 Hz from the app's own playback, journaled at the effect boundary so record/replay repaints identical bars; hosts that cannot analyze report the capability honestly instead of fabricating bands.
- **Images**: a platform decode seam (CGImageSource, gdk-pixbuf, WIC) so the toolkit bundles no image decoders; runtime image registration renders through every path — GPU packets, software presentation, and screenshots — with pixels riding an out-of-band upload channel so image-bearing frames stay on the GPU path; avatars take a bound image with initials fallback.
- **Windowing and chrome**: model-declared secondary windows (presence is visibility; a user close dispatches a Msg), enforced window minimum sizes, and present-before-show so a canvas window never appears blank.
- Titlebar control on all three desktops: `hidden_inset`, a tall unified-toolbar variant, and fully `chromeless` styles; markup `window-drag` regions; and an `on_chrome` hook carrying the real overlay insets and control-cluster frames — with real system window controls preserved on Linux client-side decorations and Windows DWM caption buttons.
- Native context menus, declared per widget in Zig or markup (`<context-menu>`): the real OS menu where one exists, an anchored canvas surface elsewhere, editable-text cut/copy/paste defaults, and full automation support for enumerating and invoking items.
- A menu-bar status item with model-driven title and menu; canvas and WebView panes composed in one window; adoption of app-owned native views into the layout (`adoptViewSurface`); and native scroll drivers on macOS that give every scroll region OS momentum, rubber-band overscroll, and the system overlay scrollbar with zero app code.
- **Experimental iOS and Android host tiers — the toolkit owns the entire mobile app**: complete UIKit and Android hosts ship in the SDK over the embed C ABI, an app project carries zero host code, and embedding a hand-written host stays first-class.
- `native dev --target ios|android [--device name]` builds, installs, and launches on a simulator or emulator and streams the app log; `native package --target ios` emits an archive-ready Xcode project and `--target android` a complete generated host project plus a debug-signed APK — no build-system project, no plugin matrix.
- Touch, soft keyboard, and IME forwarding; safe-area and keyboard insets on the window-chrome channel plus host-reported form factor; platform text metrics; platform audio and image decoding; and damage-rect rendering so a keystroke repaints and uploads only the changed region instead of the whole screen.
- Declared platform chrome: apps project a tab set and primary action as a real system tab bar, and a model-owned page stack drives real push/pop transitions with the system edge-swipe back gesture — navigation state stays in the model and replays deterministically from the Msg journal.
- The soundboard ships the proof: one codebase, a desktop composition plus a compact phone shell selected by the host-reported form factor, running on the simulator via `native dev --target ios`.
- **Theme packs and design tokens**: named packs — the default register plus `geist`, the design register of the bundled Geist type family — compose with the live system appearance; interaction-state formulas, control metrics, and focus-ring geometry are all token-stated; new `success`/`warning`/`info` semantic color tokens; the stock theme follows the OS light/dark, high-contrast, and reduce-motion settings live; modal scrims blur the content behind them for real; app-registered TrueType fonts resolve everywhere a font id rides.
- **Deterministic rendering core**: a bounded, std-only TTF parser inks real anti-aliased glyphs (bundled Geist and Geist Mono) on every headless path — screenshots, mobile embeds, pixel goldens — while layout measures exactly what gets inked; an allocation-free vector rasterizer with bit-identical cross-platform coverage draws paths, icons, and charts.
- **Automation and testing**: `native automate` gains `assert` (regex polling against the accessibility snapshot), deterministic PNG screenshots, per-stage frame profiling (`profile on`), and widget verbs for hold, secondary click, context-menu invocation, drag, wheel, and tray actions.
- Deterministic session record and replay: journal every platform event and effect result, then re-run headlessly with checkpoint verification (`native automate record` / `replay --verify`).
- `native init` scaffolds a CI workflow: null-platform tests for every frontend plus a Linux automation smoke that drives the app's real binary under Xvfb.
- **Accessibility as machine checks**: unnamed interactive controls, icon-only controls without labels, and misused roles are validation errors (degradations report as warnings; `--strict` promotes); a deterministic tree-level audit catches labels that resolve empty at runtime, focus-unreachable widgets, and duplicate sibling labels; and assistive actions actuate through the same activation paths keyboard users take instead of reporting success on nothing.
- **Showcase examples**: calculator, notes (folders, trash, context menus), soundboard (a real music library with playback and search), deck (a radically re-skinned sibling proving theme packs and chrome passes), system-monitor (live effects-driven sampling), markdown-viewer (split-pane editor and preview), and feed (a 100,000-post virtual list) — each with a deterministic test suite, and a prepared real-music catalog that streams out of the box.
- **Docs site**: a full Components section (34 pages) where every preview is rendered offscreen by the engine itself and upgrades on hover to a live engine instance running in-page via a ~306 KB (gzip) wasm build; attribute tables generate from the validator's own vocabulary so docs cannot drift; the whole site restructured native-first with new State & Data Flow, App & Runtime, Theming, and Testing in CI pages.
- **Zero-config toolchain and distribution**: `native dev|build|test|check` work in a directory holding only `app.zon` and `src/` (`native eject` writes the build files exactly once when you want to own them); the pinned Zig toolchain downloads on consent with checksum verification; and `@native-sdk/cli` installs from npm with zero scripts — eight platform binaries plus the SDK source, so `native init && native dev` work offline right after install.
- **One-image app icons**: drop a single square PNG or SVG in `assets/`, and `native package` generates everything — a masked, grid-correct macOS `.icns`, a multi-size Windows `.ico`, Linux hicolor PNGs, and iOS/Android catalog icons — with exact linear-light downscales, teaching errors for bad sources, and no external tools.
### Improvements
- **Performance — frame cost scales with what changed, not view size**:
- GPU packets ride a compact binary encoding (~10x smaller than JSON, ~40x effective capacity — text-heavy frames no longer silently fall back to software rendering), steady-state frames ship incremental patches (~20x less wire per interaction), and repaints derive per-change dirty-rect lists so pixels between two far-apart changes stay retained.
- Per-command raster caches stop re-rasterizing unchanged content (host draw p50 dropped an order of magnitude on animated views); frame planning and widget reconciliation moved from quadratic scans to indexed lookups (end-to-end interaction p50 improved ~2.3-3.2x on large views); backdrop blur cost no longer scales with radius; a click emits one display list instead of three.
- Launch to glass: the first canvas frame presents before the event loop starts, first paint rasterizes across cores, the main WebView is created lazily, and warm launches measured 150→120 ms on the heaviest showcase app; `NATIVE_SDK_WINDOW_TIMING=1` prints a per-phase launch breakdown.
- Occluded windows throttle to a ~1 Hz heartbeat instead of spinning the frame clock (spectrum reports pause too); accessibility publishes only when the tree actually changed and defer off the input-to-glass path; frame pacing delivers exactly one event per display interval; input latency is measured to the responding present, honestly.
- `zig build bench-render` runs deterministic interaction scenarios against committed per-scenario budgets, and a percentile GPU perf check gates first-frame and input-to-present latency in CI.
- **Component fidelity**: the built-in components land a refined default look, verified pixel-for-pixel in CI under both theme packs.
- Measured control geometry and state washes, ring-offset focus rings, flat buttons with a quiet destructive treatment, segmented button groups rendered as one bar with collapsed seams, compact badges, and hairline tables.
- Reworked accordion, tabs, alert, and card treatments with sensible per-kind layout defaults; skeletons pulse and the caret blinks; select menus read like menus (row highlight, trailing checkmark for the committed option).
- Native cursor conventions (the pointing hand is reserved for true links), flat list rows, axis-aware separators, and edge-pinned scrolling with opt-in rubber-band overscroll.
- **Capacity and honesty**: per-view widget budgets quadrupled to 1024 nodes (command, glyph, and text budgets raised to match) with headroom telemetry in every snapshot; explicit `width`/`height` are definite bounds; layout overflow is diagnosed, dispatch errors degrade and record instead of exiting the app, and every effect-facing type and constant is exported from the `native_sdk` facade.
- **Teaching validation**: handlers on elements that can never receive them, `gap` on stacking containers, `wrap` on non-text elements, and literal glyphs outside the bundled font's coverage are all positioned teaching errors, enforced identically by the validator, both engines, and the language server.
- **Desktop parity**: the Linux and Windows hosts reach the macOS seam contract — app timers, appearance events, window options at create, interactive window moves, IME composition on Windows, and hidden-titlebar fidelity with real system controls; CI gains Windows canvas and effects smokes under Wine, a headless Linux canvas smoke, and a containerized Linux live-truth harness driving every showcase app on real GTK.
- **Observability**: automation snapshots report the live present path and mode, patch sizes, fallback reasons with byte counts, budget headroom, audio state, tray contents, and per-stage frame percentiles while profiling; `NATIVE_SDK_GPU_DRAW_TRACE=1` attributes every present.
- **Docs and skill accuracy**: the code-signing page documents the real ad-hoc Gatekeeper experience, form-control and picker docs match what the engine ships, the keyboard and interaction seams are documented where developers look, and stale commands and API shapes were fixed across the site.
- **Example polish**: showcase headers carry only working controls under hidden-inset titlebars, the soundboard adopts desktop list-selection conventions, notes gains Recently Deleted and dialog autofocus, the deck refined its hardware identity across feedback passes, system-monitor lands the standard settings flow, and every showcase app ships the zero-config scaffold shape with a real neutral default app icon.
- **Contributor workflow**: changelog fragments (`changelog.d/`) end merge conflicts on this file, and `scripts/gate.sh` runs a tiered local gate that scales with the diff.
### Bug Fixes
- **Input and focus**: clicked and tabbed-into fields always show a caret (drawn in the field's own ink, readable in every scheme); Escape dismisses surfaces opened from non-focusable triggers; Enter inserts a newline in textareas (the primary chord submits); programmatic focus is quiet on non-editables; composite rows hover, point, and press as one surface; cross-centered overflow distributes evenly.
- **Model-driven control state**: sliders, exclusive selections, and toggle-button chips follow the model when the source moves (a live drag is never yanked); disabled selection controls render disabled; idle disabled buttons no longer wear an accent outline.
- **Rendering correctness**: pixel snapping no longer wraps exact-fit text or elides exact-fit badges; packet text honors engine line breaks; text bounds cover glyph ink; mono runs read as monospace on every headless path; avatar initials center; the spinner actually spins and sizes to the icon register; offscreen screenshots clear with live tokens; render animations invalidate only the affected commands; one invalid UTF-8 byte can no longer hang the renderer; budget overflows apply atomically instead of tearing the retained tree.
- **macOS**: Debug builds no longer abort at launch on an SDK sanitizer trap; `resizable = false` is honored; frames keep pumping during live resize and menu tracking; occluded windows keep presenting and flush instantly on reveal; quitting mid-playback no longer crashes; the Chromium (CEF) host builds and runs again, verified live with child WebViews.
- **Windows and Linux**: Windows apps launch on real Windows (common-controls manifest, dynamic task-dialog resolution) and builds link again; embed input timestamps and network error classification fixed on Windows; Linux audio no longer sticks in a buffering state; a saturated frame loop no longer freezes GTK windows; runtimes heap-allocate in every runner, fixing startup crashes under default stack limits; GTK initial allocation and overlay z-order fixed.
- **Packaging**: signed bundles keep a valid code signature; packaged apps read their bundled assets and show their display name in the menu bar; archives are labeled with the real optimize mode; unbundled dev runs fall back to the embedded default Dock icon.
- **Automation and CLI reliability**: commands queue with delivery acknowledgments instead of overwriting a single slot; a landing command wakes an idle app (~4 ms consumption); CLI and app handshake on a protocol version, and stale publishers or binaries are refused loudly; parseable payloads land on stdout; clicks aim at the rendered control, not its stretched box; `native dev` runs Debug so hot reload is actually compiled in; no CLI verb exits silently, and `--help` exits 0 everywhere.
- **Hardening**: the markdown renderer survives hostile input (three quadratic blowups fixed, a fuzz corpus added); large models neither exhaust the comptime branch quota nor ride the stack (`UiApp.create` constructs in place); mobile embed libraries stage per target so cross-target builds cannot poison each other; oversized inline window sources fail loudly instead of leaving a blank window; docs live previews build, lay out with the selected pack's tokens, animate, and route keyboard shortcuts correctly.
- **Measured-label controls no longer elide under pixel snapping**: a control sized exactly to its measured label — toggle chips (the system monitor's "PID" sort chip painted "PI…"), buttons, segmented controls and tab triggers, menu and list rows, tooltips, checkbox/radio/switch labels, hug-sized status bars — could lose a fraction of a pixel to render-time geometry snapping and swap real glyphs for an ellipsis. Every measured-label intrinsic width now rounds UP to the snap grid (the badge rule from the previous round), the switch additionally reserves its snapped track extent, and themes without geometry snapping stay bit-identical.
### Contributors
- @ctate
## 0.3.0
### New Features
- **Keyboard shortcuts**: Add app-level keyboard shortcuts with manifest and runtime configuration, native delivery to Zig `Event.shortcut`, and typed JavaScript `window.zero` shortcut events (#62).
- **Manifest-driven runner shortcuts**: Load `app.zon` shortcuts automatically in generated runners, with a `RunOptions.shortcuts` override for apps that build shortcut lists in Zig (#62).
### Improvements
- **Shortcut documentation and validation**: Document the `app.zon` shortcut schema, portable key names, modifier behavior, backend support, and validation limits (#62).
- **Windows WebView2 child bridges**: Enable bridge-enabled trusted child WebViews on Windows WebView2, bringing that backend closer to the macOS and Linux system WebView behavior (#62).
### Bug Fixes
- **Shortcut matching and delivery**: Fix shortcut modifier handling, shifted punctuation matching, backend event routing, and edge cases across AppKit, GTK, WebView2, and macOS CEF (#62).
### Contributors
- @ctate
## 0.2.0
### New Features
- **Layered WebView runtime**: Model each native window as a stack of named WebViews, including the reserved startup `main` WebView and child WebViews with frame, layer, zoom, transparency, routing, resizing, reload, and close support across the native backends (#28).
- **JavaScript WebView API**: Add typed `window.zero.webviews.*` helpers and `zero-native.webview.*` built-in bridge commands for create, list, setFrame, navigate, setZoom, setLayer, and close operations (#28).
- **Isolated child WebViews**: Keep child WebViews bridge-isolated by default, allow trusted child chrome with `bridge: true`, enforce navigation policy on child URLs, and scope WebView commands to the calling native window (#28).
- **Browser example**: Add a browser-style example that demonstrates layered WebViews, browser controls, isolated page content, frontend asset handling, and the root `zig build run-browser` command (#28).
- **zero-native skills**: Ship CLI-served agent skills and reference material for building and automating zero-native apps (#38).
### Improvements
- **WebView and bridge documentation**: Document WebView APIs, built-in bridge commands, security boundaries, backend support, packaging, testing, and app model updates (#28, #38).
- **WebView smoke coverage**: Extend automation smoke tests to exercise child WebView create, resize, navigate, and close operations for system WebView and macOS CEF builds (#28).
- **CEF runtime builds**: Harden the CEF runtime workflows across macOS, Linux, and Windows, including Windows runtime build fixes (#25, #26).
- **macOS compatibility**: Set the native app baseline to macOS 11 (#22).
- **Contributor guidance**: Clarify signed commit requirements and contribution PR guidance (#10).
### Bug Fixes
- **Windows WebView builds**: Fix Windows WebView build failures before the layered WebView release.
- **React example dependencies**: Include the missing React example type dependencies (#11).
- **GitHub release notes**: Avoid duplicate contributor lists when creating GitHub releases (#24).
- **macOS package permissions**: Preserve executable permissions for packaged macOS app binaries (#39).
### Contributors
- @Anshuman71
- @PrathamGhaywat
- @ctate
## 0.1.9
### New Features
- **Linux and Windows desktop support**: Add platform-aware CEF tooling, Linux and Windows desktop build paths, Windows native host plumbing, and cross-platform CEF runtime packaging/release coverage.
### Contributors
- @ctate
<!-- release:end -->
## 0.1.8
+26 -17
View File
@@ -1,8 +1,8 @@
# Contributing
Thanks for helping improve zero-native. This guide is for maintainers and contributors working on the framework repository itself.
Thanks for helping improve the Native SDK. This guide is for maintainers and contributors working on the toolkit repository itself.
For app author documentation, start at [zero-native.dev](https://zero-native.dev).
For app author documentation, start at [native-sdk.dev](https://native-sdk.dev).
## Prerequisites
@@ -14,7 +14,7 @@ For app author documentation, start at [zero-native.dev](https://zero-native.dev
## Local Checks
Run the framework tests:
Run the toolkit tests:
```bash
zig build test
@@ -41,8 +41,8 @@ zig build run-webview
Check the npm CLI package:
```bash
npm --prefix packages/zero-native run version:check
npm --prefix packages/zero-native run scripts:check
npm --prefix packages/native-sdk run version:check
npm --prefix packages/native-sdk run scripts:check
```
Check the documentation site:
@@ -63,7 +63,7 @@ zig build run-webview -Dweb-engine=system
For Chromium on macOS, install CEF and run with the Chromium engine:
```bash
zero-native cef install
native cef install
zig build run-webview -Dweb-engine=chromium
```
@@ -85,11 +85,17 @@ zig build package
Package explicitly through the CLI:
```bash
zero-native package --target macos --manifest app.zon --assets assets --binary zig-out/lib/libzero-native.a
native package --target macos --manifest app.zon --assets assets --binary zig-out/lib/libnative-sdk.a
```
For Chromium packages, configure `.web_engine = "chromium"` and `.cef` in `app.zon`, or use temporary `--web-engine` and `--cef-dir` overrides while testing.
Verify an ad-hoc signed package's code signature survives packaging intact (macOS; skips loudly on hosts without `codesign`):
```bash
zig build test-package-signing
```
## Automation Development
Enable automation in a build:
@@ -101,19 +107,22 @@ zig build run-webview -Dautomation=true
Interact with the running app:
```bash
zero-native automate wait
zero-native automate list
zero-native automate bridge '{"id":"ping","command":"native.ping","payload":null}'
native automate wait
native automate list
native automate bridge '{"id":"ping","command":"native.ping","payload":null}'
```
Automation writes artifacts under `.zig-cache/zero-native-automation`.
Automation writes artifacts under `.zig-cache/native-sdk-automation`.
## Making a Pull Request
Thank you for your contribution! Please follow these steps to ensure a smooth review process:
1. Fork the repository and create a new branch for your feature or bug fix.
2. Make your changes and commit them with clear, descriptive messages.
3. Push your branch to your forked repository.
4. Open a pull request against the main repository's `main` branch.
Please cryptographically sign your commits so they show as **Verified** on GitHub. This requires a GPG or SSH signing key added to your GitHub account — see [GitHub's guide](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification). Note: the `Signed-off-by` trailer (`git commit -s`) is a DCO attestation and does **not** produce the Verified badge; you need `git commit -S` (uppercase) or `commit.gpgsign = true` in your git config.
Branch from `main` (fork first if you don't have push access), keep the change focused, and run the tiered local gate before opening the PR:
```bash
scripts/gate.sh fast # root suites + the example suites your diff touches
```
Do not edit `CHANGELOG.md` as part of a feature or fix PR; the release agent writes the complete entry from the release-range history. Open the PR against `main` describing what changed and why; for larger changes, open an issue first so the design can be discussed.
Commits must be cryptographically signed (`git commit -S`, or set `commit.gpgsign = true`) so they show as **Verified** — the `Signed-off-by` trailer from `git commit -s` is a DCO attestation, not a signature.
+105 -87
View File
@@ -1,120 +1,138 @@
# zero-native
# Native SDK
Build native desktop apps with web UI. Tiny binaries. Minimal memory. Instant rebuilds.
**Native SDK is the complete toolkit for building native desktop applications.**
zero-native is a Zig desktop app shell for modern web frontends. Use the platform WebView when you want the smallest possible app, or bundle Chromium through CEF when rendering consistency matters.
Native SDK exists because expressive UI and native performance should not be competing goals. Developers often choose web-based runtimes because they offer freedom, speed and control over the product experience. But that freedom often comes with a heavy runtime. Native SDK keeps the expressive authoring model and replaces the runtime with native rendering.
## Quick Start
Views are declarative markup in `.native` files, logic is plain TypeScript compiled to native code at build time — or Zig, first-class by choice — and Native SDK's own engine draws every pixel into real OS windows. No browser, no WebView, no JS runtime in the binary: Zig is how everything works, TypeScript and Native markup are how apps are authored.
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".github/assets/soundboard-dark.webp">
<img src=".github/assets/soundboard-light.webp" alt="The Soundboard example app rendered by the Native SDK engine: a music library with album cover art, search, and a playback bar" width="100%">
</picture>
<table>
<tr>
<td width="70%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".github/assets/notes-dark.webp">
<img src=".github/assets/notes-light.webp" alt="The Notes example app rendered by the Native SDK engine: a three-pane notes manager with folders, a note list, and an open note" width="640">
</picture>
</td>
<td width="30%">
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".github/assets/calculator-dark.webp">
<img src=".github/assets/calculator-light.webp" alt="The Calculator example app rendered by the Native SDK engine: a finished calculation above a full keypad" width="270">
</picture>
</td>
</tr>
</table>
<sub>Soundboard, Notes, and Calculator from <a href="./examples">examples/</a> — every pixel drawn by the Native SDK engine, captured through its deterministic reference renderer. The images follow your color scheme.</sub>
## Quick start
Install the CLI:
```bash
npm install -g zero-native
npm install -g @native-sdk/cli
```
Create and run an app:
```bash
zero-native init my_app --frontend next
native init my_app
cd my_app
zig build run
native dev
```
The first run installs frontend dependencies, builds the generated native shell, and opens a desktop window rendering your web UI.
A native window opens with a working counter. The whole app is three files of truth — view, logic, manifest — and no build config. The view is `src/app.native`, a markup file that binds values and dispatches messages (the counter row at its heart):
Read the full guide at [zero-native.dev/quick-start](https://zero-native.dev/quick-start).
```html
<row gap="8" main="center" cross="center" grow="1">
<button variant="secondary" on-press="decrement">-</button>
<text>{count}</text>
<button variant="primary" on-press="increment">+</button>
</row>
```
## Why zero-native
All logic lives in `src/core.ts`: a `Model` interface, a `Msg` union, and one pure `update` function — the only place state changes, plain TypeScript compiled to native code at build time:
### Tiny and fast
System WebView apps do not bundle a browser runtime, so the native shell stays small and starts quickly. Your app uses WKWebView on macOS and WebKitGTK on Linux.
### Choose your web engine
Pick the engine that fits the product. System WebView gives you a lightweight native footprint. Chromium through CEF gives you predictable rendering and a pinned web platform on supported targets.
### Fast native rebuilds
The native layer is Zig, so app logic, bridge commands, and platform integrations rebuild quickly. Your frontend can still use the web tooling you already know.
### Native power without heavy glue
Zig calls C directly, which keeps platform SDKs, native libraries, codecs, and local system integrations within reach when the WebView layer needs to do real native work.
### Explicit security model
The WebView is treated as untrusted by default. Native commands, permissions, navigation, external links, and window APIs are opt-in and policy controlled.
## Status
zero-native is pre-release. Desktop support now covers macOS 11+, Linux, and Windows build paths, with Chromium/CEF distributed as platform-specific runtimes.
## Core Concepts
`App` is the small Zig object that describes your application: name, WebView source, lifecycle hooks, and optional native services.
`Runtime` owns the event loop, windows, bridge dispatch, automation hooks, tracing, and platform services.
`WebViewSource` tells the runtime what to load: inline HTML, a URL, or packaged frontend assets served from a local app origin.
`app.zon` is the app manifest. It declares app metadata, icons, windows, frontend assets, web engine selection, security policy, bridge permissions, and packaging inputs.
`window.zero.invoke()` is the JavaScript-to-Zig bridge. Calls are size-limited, origin checked, permission checked, and routed only to registered handlers.
## Configuration
Most project-level behavior lives in `app.zon`:
```zig
.{
.id = "com.example.my-app",
.name = "my-app",
.display_name = "My App",
.version = "0.1.0",
.web_engine = "system",
.permissions = .{ "window" },
.capabilities = .{ "webview", "js_bridge" },
.security = .{
.navigation = .{
.allowed_origins = .{ "zero://app", "http://127.0.0.1:5173" },
},
},
.windows = .{
.{ .label = "main", .title = "My App", .width = 960, .height = 640 },
},
```ts
export function update(model: Model, msg: Msg): Model {
switch (msg.kind) {
case "increment":
return { ...model, count: model.count + 1 };
case "decrement":
return { ...model, count: model.count - 1 };
case "reset":
return { ...model, count: 0 };
}
}
```
Use `.web_engine = "system"` for the platform WebView. On supported macOS builds, use `.web_engine = "chromium"` with a `.cef` config when you want to bundle Chromium.
Prefer Zig for the core? `native init my_app --template zig-core` scaffolds the same app with `src/main.zig` — same loop, same runtime, first-class by choice.
## Documentation
Edit `src/app.native` while `native dev` runs and the window updates in place, keeping your state. `native dev --core` runs the TypeScript core under node for instant logic checks, `native check` validates the core and every view in milliseconds without building, and `native build` produces an optimized release binary.
The full documentation is at [zero-native.dev](https://zero-native.dev).
Read the full guide at [native-sdk.dev/quick-start](https://native-sdk.dev/quick-start).
- [Quick Start](https://zero-native.dev/quick-start)
- [Web Engines](https://zero-native.dev/web-engines)
- [App Model](https://zero-native.dev/app-model)
- [Bridge](https://zero-native.dev/bridge)
- [Security](https://zero-native.dev/security)
- [Packaging](https://zero-native.dev/packaging)
## What you get
**Beautiful by default** — Great software should not start from a blank slate. The built-in component catalog — buttons, tabs, text fields, dialogs, charts, virtual lists, and more — ships with considered typography, spacing, and color, so the app `native init` scaffolds already looks intentional the first time its window opens.
**Customizable by design** — Your app should have its own identity, not ours. Styling is design tokens end to end: color, radius, and typography resolve by name, re-resolve live when the theme changes, and can be replaced wholesale — `examples/soundboard` and `examples/deck` are the same music player separated only by tokens and a chrome pass.
**Native from the start** — Every interface is rendered without a browser or WebView. The engine draws into real OS windows while scroll physics, menus, dialogs, the tray, and text input stay with the operating system, and markup compiles into the executable at build time, so a release build carries no parser or interpreter — the scaffolded counter app builds to a single binary a few megabytes small.
**Predictable state** — State changes should be explicit, inspectable and easy to reason about. Events produce messages, messages update state, and state renders the interface; markup can bind and dispatch but never mutate. The loop is so deterministic that `native automate record` journals a session and `replay` reproduces it headlessly, verified frame by frame against state fingerprints.
**Simple authoring** — Interfaces should be easy to read, easy to write and easy to generate. Views are elements, flex layout, `{bindings}`, and expressions like `selected="{f == filter}"`, and `native check` validates every view against your app's actual `Model` and `Msg` — bindings, iterables, message tags — in milliseconds, with `file:line:column` errors that teach.
**AI is part of the workflow** — Native SDK is designed for a world where humans and AI agents build software together. Every app embeds an automation server, so any agent can read accessibility snapshots, drive widgets, assert on live state, and take deterministic screenshots of the running window; accessibility findings are machine-checked in `native check`; and the CLI ships the agent skills that teach all of it (`native skills list`).
## Examples
Framework-specific starter examples live in `examples/`:
The apps pictured above live in [examples/](./examples), most as zero-config projects — `app.zon` plus `src/`, no build files — run straight from their directory with `native dev`. Start with the TypeScript examples when learning the primary authoring path. The `-ts` suffix on `soundboard-ts` and `system-monitor-ts` is historical because those apps are ports kept beside older Zig originals. Chatbot is TypeScript-only and follows the unsuffixed naming used by new apps created with `native init`.
- `examples/next`
- `examples/react`
- `examples/svelte`
- `examples/vue`
| Example | What it shows |
| --- | --- |
| [`chatbot`](./examples/chatbot) | TypeScript + Native markup end to end: modules, a text editor, streaming fetch effects, and replay-safe configuration. |
| [`soundboard-ts`](./examples/soundboard-ts) | The full music-player showcase in TypeScript + Native markup: audio, search, assets, timers, and context menus. |
| [`system-monitor-ts`](./examples/system-monitor-ts) | A live process monitor in TypeScript + Native markup: subprocess effects, tables, charts, and timers. |
| [`calculator`](./examples/calculator) | A complete small app: markup keypad, keyboard input, chrome shortcuts, theming. |
| [`notes`](./examples/notes) | Persistence through the effects channel: debounced writes, restore on boot, dialogs, search. |
| [`soundboard`](./examples/soundboard) | Album grid with decoded cover art, context menus, timers, and a custom theme. |
| [`deck`](./examples/deck) | The soundboard player rebuilt as a dense hardware chassis: two windows, same widgets, different tokens. |
| [`feed`](./examples/feed) | A 100,000-row list, virtualized with runtime-owned scrolling. |
Each example is a complete zero-native app with `app.zon`, a Zig shell, and a minimal frontend project. Run one with `zig build run` from its directory.
The unsuffixed showcase apps above predate the TypeScript default and retain their Zig cores as first-class alternative implementations. The full catalog in [examples/README.md](./examples/README.md) also covers guarded OS capabilities, GPU surfaces, WebView composition, web-frontend shells, and the iOS/Android embed hosts.
Mobile embedding examples are available too:
## Platforms
- `examples/ios`
- `examples/android`
macOS is the primary development platform and carries the deepest support: Metal presentation, OS scroll physics, native context menus, app menus, tray, and dialogs. Linux runs the full showcase through the deterministic software renderer in real windows, with pointer, keyboard, scroll, native context menus, IME composition, and HiDPI; Windows runs on a Win32 host with native context menus and IME composition and is exercised in CI, including real input injection. Mobile support is experimental: iOS is simulator-proven through the embed library and Android cross-compiles with the full embed ABI, but APIs and tooling on both are still evolving — desktop is the mature surface. WebView surfaces coexist on every desktop platform. The [platform support matrix](https://native-sdk.dev/platform-support) documents exactly what each host supports today.
These show how an iOS or Android host app links the zero-native C ABI from `libzero-native.a`.
## Documentation
For local framework development, see [CONTRIBUTING.md](./CONTRIBUTING.md).
The full documentation is at [native-sdk.dev](https://native-sdk.dev).
- [Quick Start](https://native-sdk.dev/quick-start) — install to a running, tested app
- [Philosophy](https://native-sdk.dev/philosophy) — the six principles behind the toolkit
- [App Model](https://native-sdk.dev/app-model) — the model/message/update loop, wiring, and hot reload
- [TypeScript Cores](https://native-sdk.dev/typescript) — the app-core subset, effects, subscriptions, and the node dev loop
- [Native UI](https://native-sdk.dev/native-ui) — every element, attribute, and pattern in the markup
- [Components](https://native-sdk.dev/components) — the component catalog
- [State & Data Flow](https://native-sdk.dev/state) — derive-don't-store, bindings, and text editing
- [Testing](https://native-sdk.dev/testing) — full-loop UI tests, headless on any machine
- [Automation](https://native-sdk.dev/automation) — snapshots, widget driving, record/replay, screenshots
- [Capabilities](https://native-sdk.dev/capabilities) — guarded OS services: notifications, clipboard, dialogs, credentials
- [Packaging](https://native-sdk.dev/packaging) — from binary to distributable app
- [Platform Support](https://native-sdk.dev/platform-support) — what each host supports today
## Contributing
Native SDK is pre-1.0: APIs still move, and the toolkit is evolving quickly. Bug reports and focused pull requests are welcome — for larger changes, open an issue first so the design can be discussed. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the development setup and local checks.
## License
[Apache-2.0](./LICENSE)
+23
View File
@@ -0,0 +1,23 @@
# Releasing
Releases are manual, single-PR affairs. The maintainer controls the changelog voice and format.
To prepare a release:
1. Create a branch (e.g. `prepare-v1.2.0`)
2. Bump the version in `packages/native-sdk/package.json`
3. Run `npm --prefix packages/native-sdk run version:sync` to update all version references
4. Review the git history since the previous release and write the complete changelog entry at the top of `CHANGELOG.md`, under a new `## <version>` heading wrapped in `<!-- release:start -->` and `<!-- release:end -->` markers
5. Populate the entry's `### Contributors` from commit authors and `Co-authored-by` trailers in the release range, using GitHub handles when available; this marked block is also the GitHub release body
6. Remove the `<!-- release:start -->` and `<!-- release:end -->` markers from the previous release entry; only the latest release should have markers
7. Open a PR and merge to `main`
## Writing the changelog
Follow the existing format and voice. Group changes under descriptive headings such as `### New Features`, `### Bug Fixes`, and `### Improvements`. Give each bullet a bold lead-in followed by a concise description, and include PR numbers when available. Do not prefix entries with commit hashes.
The release entry should cover the complete git range since the previous release, including changes whose individual PRs did not touch `CHANGELOG.md`.
CI compares the version in `packages/native-sdk/package.json` to what's on npm. If it differs, it cross-builds the CLI for every platform, creates the GitHub release with the binaries, publishes the per-platform binary packages (`packages/native-sdk/npm/*`), and publishes `@native-sdk/cli` last — so the main package only lands once every binary package it pins is live. If npm already has the version but the GitHub release is missing assets, CI recreates the GitHub release from the marked changelog entry.
Publishing uses npm trusted publishing (OIDC) — there is no npm token secret. One-time setup: on npmjs.com, each of the nine packages (`@native-sdk/cli` plus the eight `@native-sdk/cli-*` platform packages under `packages/native-sdk/npm/*`) must have a GitHub Actions trusted publisher configured with repository `vercel-labs/native`, workflow `release.yml`, and environment `Release`. Every publish runs with `--provenance`. If a package is missing its trusted-publisher configuration, `npm publish` fails loudly with an OIDC authentication error for that package.
+8 -8
View File
@@ -1,7 +1,7 @@
.{
.id = "dev.zero_native",
.name = "zero-native",
.display_name = "zero-native",
.id = "dev.native_sdk",
.name = "native-sdk",
.display_name = "Native SDK",
.version = "0.1.0",
.icons = .{ "assets/icon.icns", "assets/icon.ico" },
.platforms = .{ "macos" },
@@ -10,10 +10,10 @@
.bridge = .{
.commands = .{
.{ .name = "native.ping", .origins = .{ "zero://inline", "zero://app" } },
.{ .name = "zero-native.window.list", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
.{ .name = "zero-native.window.create", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
.{ .name = "zero-native.window.focus", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
.{ .name = "zero-native.window.close", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
.{ .name = "native-sdk.window.list", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
.{ .name = "native-sdk.window.create", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
.{ .name = "native-sdk.window.focus", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
.{ .name = "native-sdk.window.close", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
},
},
.security = .{
@@ -25,6 +25,6 @@
.web_engine = "system",
.cef = .{ .dir = "third_party/cef/macos", .auto_install = false },
.windows = .{
.{ .label = "main", .title = "zero-native", .width = 720, .height = 480, .restore_state = true },
.{ .label = "main", .title = "native-sdk", .width = 720, .height = 480, .restore_state = true },
},
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 35 KiB

+15
View File
@@ -0,0 +1,15 @@
<!-- Generated by `zig build generate-icon` (tools/generate_app_icon.zig). Edit the tool, not this file. -->
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
<defs>
<linearGradient id="plate" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#262626"/>
<stop offset="1" stop-color="#171717"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="12" stdDeviation="16" flood-color="#000000" flood-opacity="0.3"/>
</filter>
</defs>
<rect x="100" y="100" width="824" height="824" rx="185.4" fill="url(#plate)" filter="url(#shadow)"/>
<rect x="372" y="272" width="380" height="380" rx="84" fill="#ffffff" fill-opacity="0.52"/>
<rect x="272" y="372" width="380" height="380" rx="84" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 853 B

+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!-- Application manifest embedded into Windows app executables. Declaring
the common-controls v6 side-by-side dependency activates the modern
control styling and the v6-only exports (TaskDialogIndirect); without
it the loader binds the system-default v5 assembly. Declaring
per-monitor-v2 DPI awareness makes GetDpiForWindow report the real
monitor DPI (instead of a virtualized 96) so the canvas rasterizes
at device scale and Windows never bitmap-stretches the window.
dpiAwareness carries an ordered fallback list: readers take the first
value they recognize, so PerMonitorV2 degrades to PerMonitor where v2
is unknown. The dpiAwareness element itself is ignored before Windows
10 1607; there the legacy dpiAware element beside it picks up, and its
true/pm value requests per-monitor awareness where supported with
system-DPI awareness as the floor. Readers that understand dpiAwareness
prefer it over dpiAware, so the two elements coexist safely. -->
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>
+3922 -122
View File
File diff suppressed because it is too large Load Diff
+12 -2
View File
@@ -1,8 +1,15 @@
.{
.name = .zero_native,
.fingerprint = 0x338d08a1e3dd81aa,
.name = .native_sdk,
.fingerprint = 0xc309966142f33087,
.version = "0.1.0",
.minimum_zig_version = "0.16.0",
// No dependencies. The terminal emulator (libghostty-vt) is
// deliberately NOT pinned here: a pin in this file is materialized
// into every consumer's package directory even when lazy and
// unused. Apps that want live `<terminal>` sessions pin it in their
// own build.zig.zon and ask for it with
// `addAppArtifacts(.{ .terminal_sessions = true })` — see
// examples/workbench, which also owns that path's test coverage.
.dependencies = .{},
.paths = .{
"README.md",
@@ -11,13 +18,16 @@
"SECURITY.md",
"app.zon",
"assets",
"build",
"build.zig",
"build.zig.zon",
"docs",
"examples",
"packages",
"src",
"templates",
"tests",
"third_party/sqlite",
"tools",
},
}
+2641
View File
File diff suppressed because it is too large Load Diff
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env node
// Run a TypeScript module of the @native-sdk/core transpiler tier under
// node from ANY SDK layout:
//
// node ts_run.mjs <module.ts> [args...]
//
// ONE code path for every layout: a load hook strips EVERY .ts module with
// the transpiler's own pinned TypeScript compiler (shipped as a dependency
// of @native-sdk/cli, or a repo checkout's `npm ci` install inside
// packages/core — either way resolved from the target module's location by
// node's ancestor node_modules walk). Node's own type stripping is never
// relied on: it refuses node_modules-resident .ts by design
// (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING), and outside node_modules
// it only became DEFAULT in node 22.18 — so a checkout target on
// 22.15-22.17 would die with ERR_UNKNOWN_FILE_EXTENSION if the hook let it
// "fall through". Hooking everything makes the 22.15 floor true for both
// layouts. Then the runner imports the requested module with argv
// respliced so the target sees its usual shape (its own path at argv[1],
// its arguments from argv[2]).
//
// On node builds without module.registerHooks (pre-22.15, or 23.0-23.4 —
// the hook landed in 22.15 and 23.5, so ">=22.15" alone is not the
// capability line) NO .ts target can run — node_modules-resident
// stripping is refused by design and default stripping outside
// node_modules only landed in 22.18, which is above this tier anyway —
// so the runner fails fast with one teaching line (upgrade to Node.js
// 22.15+, on the 23 line 23.5+) before importing it, instead of
// surfacing node's raw extension/stripping error.
import module, { createRequire } from 'node:module';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { pathToFileURL, fileURLToPath } from 'node:url';
const target = process.argv[2];
if (!target) {
console.error('usage: node ts_run.mjs <module.ts> [args...]');
process.exit(2);
}
const targetPath = resolve(target);
// Drop the runner from argv so the target module parses its own argv
// exactly as when node runs it directly.
process.argv.splice(1, 1);
if (typeof module.registerHooks !== 'function') {
// No load hooks on this node (pre-22.15, or a 23.0-23.4 build). Every
// .ts target needs the hook — node_modules-resident stripping is
// refused by design, and native default stripping outside node_modules
// is 22.18+ — so any .ts target would fail deep inside node with a raw
// extension/stripping error. Teach the fix instead.
if (targetPath.endsWith('.ts')) {
console.error(
`TypeScript apps need Node.js 22.15+ (on the 23 line: 23.5+); you're running ${process.version} - upgrade node and re-run.`,
);
process.exit(1);
}
} else {
let ts = null;
module.registerHooks({
load(url, context, nextLoad) {
if (url.startsWith('file:') && url.endsWith('.ts')) {
const filePath = fileURLToPath(url);
// The frontend's own pinned compiler, resolved from the target
// module's location (packages/core/node_modules after the taught
// `npm ci`, or the dependency npm installed beside the CLI):
// resolving from the target finds our own nested/hoisted exact
// pin first, so a consumer tree's conflicting hoisted typescript
// never wins nearest-wins over it (same reasoning as
// typed_ast.ts).
if (ts === null) {
try {
ts = createRequire(targetPath)('@typescript/old');
} catch {
// A missing toolchain reaches here only on a direct
// `node ts_run.mjs` run: every CLI verb gates resolution with
// a fuller per-layout teaching before this runner is spawned.
// Keep the direct-run error sane and name the checkout fix.
console.error(
`ts_run.mjs: the transpiler's TypeScript toolchain (@typescript/old) does not resolve from ${targetPath} - on a repo checkout, run \`npm ci --include=dev\` in packages/core.`,
);
process.exit(1);
}
}
const { outputText } = ts.transpileModule(readFileSync(filePath, 'utf8'), {
fileName: filePath,
compilerOptions: {
target: ts.ScriptTarget.ESNext,
module: ts.ModuleKind.ESNext,
verbatimModuleSyntax: true,
},
});
return { format: 'module', source: outputText, shortCircuit: true };
}
return nextLoad(url, context);
},
});
}
await import(pathToFileURL(targetPath).href);
+4
View File
@@ -1,3 +1,7 @@
node_modules/
.next/
next-env.d.ts
.next-gate/
.next-agent/
.next-check/
.next-final/
+64 -1
View File
@@ -1,10 +1,73 @@
import createMDX from "@next/mdx";
import { createRequire } from "node:module";
import { readdirSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const withMDX = createMDX();
// Resolve the plugin to an absolute path (still a string, so the config
// stays serializable for Turbopack). A bare "remark-gfm" is require()d
// from the MDX loader's own package context, which under pnpm's strict
// module isolation cannot see this app's dependencies — production
// builds resolved it, the Turbopack dev server did not.
const require = createRequire(import.meta.url);
const docsContentDir = fileURLToPath(new URL("./src/app/docs", import.meta.url));
function docsSlugs(dir = docsContentDir, segments = []) {
const slugs = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
slugs.push(...docsSlugs(path.join(dir, entry.name), [...segments, entry.name]));
} else if (entry.name === "page.mdx" && segments.length > 0) {
slugs.push(segments.join("/"));
}
}
return slugs;
}
const withMDX = createMDX({
options: {
// GFM is what gives .mdx pages pipe tables (plus autolinks and
// strikethrough) — without it, table markdown renders as a plain
// paragraph of pipes.
remarkPlugins: [[require.resolve("remark-gfm")]],
},
});
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ["ts", "tsx", "md", "mdx"],
// CI-style builds set NEXT_DIST_DIR so `pnpm check` never shares .next
// with a running dev server (a shared dist dir corrupts the dev cache).
distDir: process.env.NEXT_DIST_DIR || ".next",
// The gate builds into .next-gate INSIDE this dir; without an ignore,
// the dev watcher sees every one of those build files land and
// recompiles continuously whenever a gate runs.
watchOptions: {
ignored: ["**/.next-gate/**", "**/.next-check/**"],
},
async redirects() {
// Config redirects preserve the request query string. Keeping these out
// of the prerendered catch-all route avoids baking a query-less Location
// header into every legacy URL's static response.
const legacyDocsRedirects = docsSlugs().flatMap((slug) => [
{ source: `/${slug}`, destination: `/docs/${slug}`, permanent: true },
{ source: `/${slug}.md`, destination: `/docs/${slug}.md`, permanent: true },
{ source: `/md/${slug}`, destination: `/docs/${slug}.md`, permanent: true },
]);
return [
// The Philosophy page became the Introduction, the opening page of the docs.
{ source: "/philosophy", destination: "/docs/introduction", permanent: true },
// docsSlugs() only yields nested slugs, so the /docs segment itself has
// neither a route nor a generated redirect and 404s. It is the parent of
// every documentation link on the site and the likeliest hand-typed entry
// point, so open it on the Introduction instead. The .md sibling keeps the
// Markdown surface whole for agents that reach for it.
{ source: "/docs", destination: "/docs/introduction", permanent: true },
{ source: "/docs.md", destination: "/docs/introduction.md", permanent: true },
...legacyDocsRedirects,
];
},
};
export default withMDX(nextConfig);
+9 -3
View File
@@ -1,5 +1,5 @@
{
"name": "@zero-native/docs",
"name": "@native-sdk/docs",
"version": "0.0.0",
"private": true,
"type": "module",
@@ -9,7 +9,7 @@
"build": "next build",
"start": "next start",
"typecheck": "tsc --noEmit",
"check": "pnpm typecheck && pnpm build"
"check": "pnpm typecheck && pnpm build && node scripts/check-doc-routes.mjs && node scripts/check-code-toggle.mjs && node scripts/check-wasm-preview.mjs"
},
"dependencies": {
"@mdx-js/loader": "^3",
@@ -17,11 +17,12 @@
"@next/mdx": "^16.1.6",
"clsx": "^2.1.1",
"geist": "^1.7.0",
"next": "^16.1.6",
"next": "^16.2.9",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "^19",
"react-dom": "^19",
"remark-gfm": "^4.0.1",
"shiki": "^4.0.2",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7"
@@ -34,5 +35,10 @@
"@types/react-dom": "^19",
"tailwindcss": "^4",
"typescript": "^5"
},
"pnpm": {
"overrides": {
"postcss@<8.5.10": ">=8.5.10"
}
}
}
+291 -52
View File
@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
postcss@<8.5.10: '>=8.5.10'
importers:
.:
@@ -22,10 +25,10 @@ importers:
version: 2.1.1
geist:
specifier: ^1.7.0
version: 1.7.0(next@16.2.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6))
version: 1.7.0(next@16.2.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6))
next:
specifier: ^16.1.6
version: 16.2.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
specifier: ^16.2.9
version: 16.2.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
@@ -38,6 +41,9 @@ importers:
react-dom:
specifier: ^19
version: 19.2.6(react@19.2.6)
remark-gfm:
specifier: ^4.0.1
version: 4.0.1
shiki:
specifier: ^4.0.2
version: 4.0.2
@@ -124,89 +130,105 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -264,8 +286,8 @@ packages:
'@types/react': '>=16'
react: '>=16'
'@next/env@16.2.5':
resolution: {integrity: sha512-Lb9ElHD2klcyeVD25vW+siPFqz9QMzDUSgvFZNO+dZEKoMHex4viJhVuzBhrXKqb+UKnih7mVYbt50/7KLsSCA==}
'@next/env@16.2.9':
resolution: {integrity: sha512-ki5VxxXfzD/9TDe13wyeTKIjQTAwBVpnr8KhRDUr8ltMUq1/NBpWNT5tiPoxiGl+PHM4X2ahSOiPk6iAimIzPg==}
'@next/mdx@16.2.5':
resolution: {integrity: sha512-U1r0I3Ga5/PYKH+loar1OfWCjkZXwG6qFovDzyAFPI2Nxi9gLOWZQ3dLNC5znSGLPToJauRbqgi3kfkKEFqNig==}
@@ -278,50 +300,54 @@ packages:
'@mdx-js/react':
optional: true
'@next/swc-darwin-arm64@16.2.5':
resolution: {integrity: sha512-BW+8PGVmsruomXHsitD8JG6gny9lEdobctjBwvtPF8AKtxGDR7nR35FOl/oK9UAPXBOBm+vx0k8qtpeHOXQMGQ==}
'@next/swc-darwin-arm64@16.2.9':
resolution: {integrity: sha512-HkfxNYUCmcct0Xsqib5KxqMSHV4AHJq857BNRchyBDs4YS19aHzVfn1kDuBYKqLLQBjXgnkIsjV2Kd4d2wzYhw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@16.2.5':
resolution: {integrity: sha512-ZoCGnCl9LlQJWmqXrZAUlNxvuNmclvE+7zUif+nDydkkehl9FKxHJ+wxSQMj+C37BYFerKiEdX9s9o02ir975Q==}
'@next/swc-darwin-x64@16.2.9':
resolution: {integrity: sha512-7IAtK4MeybpqRV9GRABWEhJ62mOS+rzWOzOTFie4cSEtm12xsoOMJRcECoZx3FHPzFAqN/IJtHqWAFOLfl152w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@16.2.5':
resolution: {integrity: sha512-AwcZzMChaWkOTZt3vu+2ZMIj8g4dYQY+B8VUVhlFSQ2JtvyZpefyYHTe00D6b6L7BysYw7vl3zsvs9jix8tl5Q==}
'@next/swc-linux-arm64-gnu@16.2.9':
resolution: {integrity: sha512-hBD75iWpUtkL9SmQmcRhmLomn9jgkPzCEkbOcLgHymPEKzv+6ONy13RRiIEz/iEObjkS2Jlb5gYS2XGoS3X4rw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@16.2.5':
resolution: {integrity: sha512-QqMgqWbCBFsfiQ7BF3dUlW8HJy1LWhpcqbTpoHMWA9IV+TnWwDKozQJA5NdIAHjQ00yX2Q7AUkLr/XK4n77q8A==}
'@next/swc-linux-arm64-musl@16.2.9':
resolution: {integrity: sha512-qZTI3pf9SGc/obr8NkQAekBxmp1QK+kVm+VAf3BALLfFAj+1kUhkTxmrWpVos9R/UYIA8AWX2p6cGI5WdwzVUA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@16.2.5':
resolution: {integrity: sha512-3hzeiFGZtyATVx9pCeuzTshXmh50vHZitqaeZiyJZaUmjQyrfjsVUgS8apOj1vEJCIpKJM/55F45yPAV2kpjsA==}
'@next/swc-linux-x64-gnu@16.2.9':
resolution: {integrity: sha512-xm0HfRNX+UkH4R3c18ynswjj5o5uEj/7iI9p9omdtTSIsRCzQqkGMA+10nzJ4EHnYC3as65IMhbbl5fWRUWHYg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@16.2.5':
resolution: {integrity: sha512-0mzZV/mAt7Qj2tYNdTB6AqrS8dwng/AQLSYC5Z1YLpZdi2wxqKDPK7RY2RvjB1fXyJfOfdA3l/yTF5yLi+WfuQ==}
'@next/swc-linux-x64-musl@16.2.9':
resolution: {integrity: sha512-QumimHkGEG6vM3PfEDWKyKen03NcqLOkeKB1EfcPe7VxzmEiCa4jNnMyBn/US5zcd/VE1CI+O8Ovb3lfjVHfGw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@16.2.5':
resolution: {integrity: sha512-f/H4nZ2zJBvA8/+HpsB9mNonF9zfQoAU6D0WxJrfzhJDvJLfngVN85oqxUyrDVK99DIFfFYhLpGa5K+c5uotSw==}
'@next/swc-win32-arm64-msvc@16.2.9':
resolution: {integrity: sha512-hzQpKZvw8rAwI6A2uQh6SacCSvNAXaIkPNsWwzqqfRiIMiXMfH936skDhz1OO6KpvdKkJrgHHtqQOq5PIXOvdQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@16.2.5':
resolution: {integrity: sha512-nuP7DHs4koAojsIxVPkihNgKiRUKtCU65j5X6DAbSy8VBrfT/o90bCLLHPf51JEdOZwZMFzM6e0NiGWfIWjVAg==}
'@next/swc-win32-x64-msvc@16.2.9':
resolution: {integrity: sha512-qr2VL3Ce5QrwgO2yh1ujSBawrimjVKX8FGF/cOynmdYKJY0BdHpGVNIRK1tqONB10Vkm25Ub1BD2bkjWs4+96w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -1088,24 +1114,28 @@ packages:
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.2.4':
resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.2.4':
resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.2.4':
resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.2.4':
resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==}
@@ -1275,6 +1305,10 @@ packages:
esast-util-from-js@2.0.1:
resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==}
escape-string-regexp@5.0.0:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
estree-util-attach-comments@3.0.0:
resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==}
@@ -1384,24 +1418,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
@@ -1429,9 +1467,33 @@ packages:
resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==}
engines: {node: '>=16'}
markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
mdast-util-find-and-replace@3.0.2:
resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
mdast-util-from-markdown@2.0.3:
resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==}
mdast-util-gfm-autolink-literal@2.0.1:
resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==}
mdast-util-gfm-footnote@2.1.0:
resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==}
mdast-util-gfm-strikethrough@2.0.0:
resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==}
mdast-util-gfm-table@2.0.0:
resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==}
mdast-util-gfm-task-list-item@2.0.0:
resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==}
mdast-util-gfm@3.1.0:
resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
mdast-util-mdx-expression@2.0.1:
resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
@@ -1459,6 +1521,27 @@ packages:
micromark-core-commonmark@2.0.3:
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
micromark-extension-gfm-autolink-literal@2.1.0:
resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==}
micromark-extension-gfm-footnote@2.1.0:
resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==}
micromark-extension-gfm-strikethrough@2.1.0:
resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==}
micromark-extension-gfm-table@2.1.1:
resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==}
micromark-extension-gfm-tagfilter@2.0.0:
resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==}
micromark-extension-gfm-task-list-item@2.1.0:
resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==}
micromark-extension-gfm@3.0.0:
resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==}
micromark-extension-mdx-expression@3.0.1:
resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==}
@@ -1548,14 +1631,19 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@3.3.15:
resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
next-themes@0.4.6:
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
peerDependencies:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
next@16.2.5:
resolution: {integrity: sha512-TkVTm9F2WEulkgGljm4wPwNgvCCWCVw6StUHsZb8WZpHFRjepoUWg3d7L4IMg7IyjcJ4Co9eVhpro8e8O+KarQ==}
next@16.2.9:
resolution: {integrity: sha512-MEOJiq/UvuezAdqVSceHbqDgZt1kDw2tpGVOlsdIoJsQdbN2JY2hpVG4xnXGkbdJUOEWhnRfiu/O4Hpc9Juwww==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -1587,14 +1675,14 @@ packages:
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
postcss@8.4.31:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.14:
resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.16:
resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==}
engines: {node: ^10 || ^12 || >=14}
property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
@@ -1676,6 +1764,9 @@ packages:
rehype-recma@1.0.0:
resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==}
remark-gfm@4.0.1:
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
remark-mdx@3.1.1:
resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==}
@@ -1685,6 +1776,9 @@ packages:
remark-rehype@11.1.2:
resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
remark-stringify@11.0.0:
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
@@ -2006,7 +2100,7 @@ snapshots:
'@types/react': 19.2.14
react: 19.2.6
'@next/env@16.2.5': {}
'@next/env@16.2.9': {}
'@next/mdx@16.2.5(@mdx-js/loader@3.1.1)(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.6))':
dependencies:
@@ -2015,28 +2109,28 @@ snapshots:
'@mdx-js/loader': 3.1.1
'@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.6)
'@next/swc-darwin-arm64@16.2.5':
'@next/swc-darwin-arm64@16.2.9':
optional: true
'@next/swc-darwin-x64@16.2.5':
'@next/swc-darwin-x64@16.2.9':
optional: true
'@next/swc-linux-arm64-gnu@16.2.5':
'@next/swc-linux-arm64-gnu@16.2.9':
optional: true
'@next/swc-linux-arm64-musl@16.2.5':
'@next/swc-linux-arm64-musl@16.2.9':
optional: true
'@next/swc-linux-x64-gnu@16.2.5':
'@next/swc-linux-x64-gnu@16.2.9':
optional: true
'@next/swc-linux-x64-musl@16.2.5':
'@next/swc-linux-x64-musl@16.2.9':
optional: true
'@next/swc-win32-arm64-msvc@16.2.5':
'@next/swc-win32-arm64-msvc@16.2.9':
optional: true
'@next/swc-win32-x64-msvc@16.2.5':
'@next/swc-win32-x64-msvc@16.2.9':
optional: true
'@radix-ui/number@1.1.1': {}
@@ -3014,6 +3108,8 @@ snapshots:
esast-util-from-estree: 2.0.0
vfile-message: 4.0.3
escape-string-regexp@5.0.0: {}
estree-util-attach-comments@3.0.0:
dependencies:
'@types/estree': 1.0.9
@@ -3049,9 +3145,9 @@ snapshots:
extend@3.0.2: {}
geist@1.7.0(next@16.2.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6)):
geist@1.7.0(next@16.2.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6)):
dependencies:
next: 16.2.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
next: 16.2.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
get-nonce@1.0.1: {}
@@ -3192,6 +3288,15 @@ snapshots:
markdown-extensions@2.0.0: {}
markdown-table@3.0.4: {}
mdast-util-find-and-replace@3.0.2:
dependencies:
'@types/mdast': 4.0.4
escape-string-regexp: 5.0.0
unist-util-is: 6.0.1
unist-util-visit-parents: 6.0.2
mdast-util-from-markdown@2.0.3:
dependencies:
'@types/mdast': 4.0.4
@@ -3209,6 +3314,63 @@ snapshots:
transitivePeerDependencies:
- supports-color
mdast-util-gfm-autolink-literal@2.0.1:
dependencies:
'@types/mdast': 4.0.4
ccount: 2.0.1
devlop: 1.1.0
mdast-util-find-and-replace: 3.0.2
micromark-util-character: 2.1.1
mdast-util-gfm-footnote@2.1.0:
dependencies:
'@types/mdast': 4.0.4
devlop: 1.1.0
mdast-util-from-markdown: 2.0.3
mdast-util-to-markdown: 2.1.2
micromark-util-normalize-identifier: 2.0.1
transitivePeerDependencies:
- supports-color
mdast-util-gfm-strikethrough@2.0.0:
dependencies:
'@types/mdast': 4.0.4
mdast-util-from-markdown: 2.0.3
mdast-util-to-markdown: 2.1.2
transitivePeerDependencies:
- supports-color
mdast-util-gfm-table@2.0.0:
dependencies:
'@types/mdast': 4.0.4
devlop: 1.1.0
markdown-table: 3.0.4
mdast-util-from-markdown: 2.0.3
mdast-util-to-markdown: 2.1.2
transitivePeerDependencies:
- supports-color
mdast-util-gfm-task-list-item@2.0.0:
dependencies:
'@types/mdast': 4.0.4
devlop: 1.1.0
mdast-util-from-markdown: 2.0.3
mdast-util-to-markdown: 2.1.2
transitivePeerDependencies:
- supports-color
mdast-util-gfm@3.1.0:
dependencies:
mdast-util-from-markdown: 2.0.3
mdast-util-gfm-autolink-literal: 2.0.1
mdast-util-gfm-footnote: 2.1.0
mdast-util-gfm-strikethrough: 2.0.0
mdast-util-gfm-table: 2.0.0
mdast-util-gfm-task-list-item: 2.0.0
mdast-util-to-markdown: 2.1.2
transitivePeerDependencies:
- supports-color
mdast-util-mdx-expression@2.0.1:
dependencies:
'@types/estree-jsx': 1.0.5
@@ -3310,6 +3472,64 @@ snapshots:
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
micromark-extension-gfm-autolink-literal@2.1.0:
dependencies:
micromark-util-character: 2.1.1
micromark-util-sanitize-uri: 2.0.1
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
micromark-extension-gfm-footnote@2.1.0:
dependencies:
devlop: 1.1.0
micromark-core-commonmark: 2.0.3
micromark-factory-space: 2.0.1
micromark-util-character: 2.1.1
micromark-util-normalize-identifier: 2.0.1
micromark-util-sanitize-uri: 2.0.1
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
micromark-extension-gfm-strikethrough@2.1.0:
dependencies:
devlop: 1.1.0
micromark-util-chunked: 2.0.1
micromark-util-classify-character: 2.0.1
micromark-util-resolve-all: 2.0.1
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
micromark-extension-gfm-table@2.1.1:
dependencies:
devlop: 1.1.0
micromark-factory-space: 2.0.1
micromark-util-character: 2.1.1
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
micromark-extension-gfm-tagfilter@2.0.0:
dependencies:
micromark-util-types: 2.0.2
micromark-extension-gfm-task-list-item@2.1.0:
dependencies:
devlop: 1.1.0
micromark-factory-space: 2.0.1
micromark-util-character: 2.1.1
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
micromark-extension-gfm@3.0.0:
dependencies:
micromark-extension-gfm-autolink-literal: 2.1.0
micromark-extension-gfm-footnote: 2.1.0
micromark-extension-gfm-strikethrough: 2.1.0
micromark-extension-gfm-table: 2.1.1
micromark-extension-gfm-tagfilter: 2.0.0
micromark-extension-gfm-task-list-item: 2.1.0
micromark-util-combine-extensions: 2.0.1
micromark-util-types: 2.0.2
micromark-extension-mdx-expression@3.0.1:
dependencies:
'@types/estree': 1.0.9
@@ -3501,30 +3721,32 @@ snapshots:
nanoid@3.3.12: {}
nanoid@3.3.15: {}
next-themes@0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
dependencies:
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
next@16.2.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
next@16.2.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
dependencies:
'@next/env': 16.2.5
'@next/env': 16.2.9
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.10.27
caniuse-lite: 1.0.30001792
postcss: 8.4.31
postcss: 8.5.16
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
styled-jsx: 5.1.6(react@19.2.6)
optionalDependencies:
'@next/swc-darwin-arm64': 16.2.5
'@next/swc-darwin-x64': 16.2.5
'@next/swc-linux-arm64-gnu': 16.2.5
'@next/swc-linux-arm64-musl': 16.2.5
'@next/swc-linux-x64-gnu': 16.2.5
'@next/swc-linux-x64-musl': 16.2.5
'@next/swc-win32-arm64-msvc': 16.2.5
'@next/swc-win32-x64-msvc': 16.2.5
'@next/swc-darwin-arm64': 16.2.9
'@next/swc-darwin-x64': 16.2.9
'@next/swc-linux-arm64-gnu': 16.2.9
'@next/swc-linux-arm64-musl': 16.2.9
'@next/swc-linux-x64-gnu': 16.2.9
'@next/swc-linux-x64-musl': 16.2.9
'@next/swc-win32-arm64-msvc': 16.2.9
'@next/swc-win32-x64-msvc': 16.2.9
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
@@ -3550,15 +3772,15 @@ snapshots:
picocolors@1.1.1: {}
postcss@8.4.31:
postcss@8.5.14:
dependencies:
nanoid: 3.3.12
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.14:
postcss@8.5.16:
dependencies:
nanoid: 3.3.12
nanoid: 3.3.15
picocolors: 1.1.1
source-map-js: 1.2.1
@@ -3708,6 +3930,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
remark-gfm@4.0.1:
dependencies:
'@types/mdast': 4.0.4
mdast-util-gfm: 3.1.0
micromark-extension-gfm: 3.0.0
remark-parse: 11.0.0
remark-stringify: 11.0.0
unified: 11.0.5
transitivePeerDependencies:
- supports-color
remark-mdx@3.1.1:
dependencies:
mdast-util-mdx: 3.0.0
@@ -3732,6 +3965,12 @@ snapshots:
unified: 11.0.5
vfile: 6.0.3
remark-stringify@11.0.0:
dependencies:
'@types/mdast': 4.0.4
mdast-util-to-markdown: 2.1.2
unified: 11.0.5
scheduler@0.27.0: {}
semver@7.7.4:
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Some files were not shown because too many files have changed in this diff Show More