ctate/code-diff
62 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9f832c765b | Fix code diff metadata and selection layering | ||
|
|
0d0cf1e65c |
Add diff annotations to code component
- Add Geist-style added and removed line annotations across the Zig and markup APIs. - Preserve diff rendering through layout, editing, scrolling, and retained invalidation with focused tests. - Ship dedicated light/dark docs previews, rebuilt live WASM, documentation, and a changelog fragment. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
cce5359d5b |
corewire --profile: the library-mode compiler profile beside the facade (#196)
* Facade channel entries take the wire shape; the packer becomes nsc_core_pack_msg
- Each wired channel now exports a wire-shaped entry mirroring the ABI
header's C declaration (bytes as buffers, u8 modifier booleans, the
pinch phase as its declaration-order member index) that builds the
event record, runs the channel-function gate, and packs the result.
- The produced-message-or-null packer moves to one export,
nsc_core_pack_msg, freeing the nsc_core_<channel>_msg names for the
wire shapes; conformance drives both surfaces.
- Wired channels fence their new facade declarations (event records,
channel-function names, PinchPhase) against type-table collisions.
* corewire --profile emits the library-mode compiler profile
- emit_profile.zig projects the sidecar into the profile JSON: the abi
block's mode symbols, an export map binding every attested facade
export to its marshalled signature (wire-shaped channel entries
included), and the contract-sidecar section with echoed generations
and identity-getter symbols.
- The determinism block carries the SDK policy as release-pinned data:
deny-fences over the ambient surfaces with teachings naming the
sanctioned Cmd/Sub routes, the shared async teaching, and trap
remediations; emission is deterministic byte-for-byte.
- main.zig grows --profile beside --out/--facade with the same staging,
alias nets, and check-runs-everything discipline.
* Facade slices spell plain arrays; flattened records stay undeclared
- Sequence types spell T[] (the declaration site's readonly already pins
immutability; the readonly-array operator has no contract projection).
- Single-use synthesized records flatten into their one arm literal and
no longer take an interface or a named encoder, keeping the
synthesized names free for consumers that re-derive them.
- The pinch wire entry's export-map signature drops a stray f64: six
parameters, six marshalling classes.
* Name the string constraints by their enforcer in the profile test comment
- The teachings/remediations comment points at the loader's rules
instead of restating provenance.
* Profile and facade carry the whole contract a library-mode consumer reads
- Export-map names keep the facade's fixed nsc_core_ spellings while
symbols take the contract's prefix; the profile's entry is the facade
path made relative to the profile's own directory, refusing pairs
with no relative spelling; --profile alone runs the facade emitter's
refusal checks.
- Facade entry stubs carry the contract's declared shapes (cmd-returning
init/update return the [state, effect] tuple, subscribing contracts
declare the subscriptions stub), the unbound facts also ride the split
modelUnbound/msgUnbound consts, and the appearance/chrome/env channel
conventions ride exported consts; the transpiler passes the split
unbound pair through unemitted like viewUnbound.
- The determinism fence table pins to the 0.0.11 surface manifest: the
wall clock (stdlib.date., perf_hooks.) and the process family join
with teachings naming the journaled clock and host-delivered inputs.
* Harden the split-const, entry-path, and profile-string edges
- The transpiler refuses a modelUnbound/msgUnbound constant that is not
a string-literal list: the names are reserved contract vocabulary, and
a data const under them would emit references without a declaration.
- The facade fences the split unbound names only when their consts
actually declare, so a contract type under either spelling projects
whenever no collision exists.
- The profile's relative-entry resolution carries the environment map
(drive-relative spellings resolve against the drive's own working
directory), and a non-UTF-8 entry spelling refuses with a teaching
before it can corrupt the JSON, with an emitter-side backstop.
* Declaration forms spell record storage; path and slot edges pinned
- The facade declares node-stored records as interfaces and value-stored
records as object-literal type aliases, and the transpiler's type
table learns the alias form (a struct exactly like an interface, with
storage still decided by the promotion walk) — a contract emitter
re-deriving storage from declaration form now lands on the contract's
own classes.
- Profile-relative entry spellings convert separators only on Windows
(a POSIX backslash is a filename byte), and the emitter's
release-pinned note records why integer-slot declarations stay
absent: the pinned prover demands inline wholeness proofs at every
construction of a slot-declared shape and does not trust classed-slot
reads, refusing spread updates, structural twins, and the facade's
own constructors alike.
* Unbound vocabulary, singleton unions, and storage edges hold both ways
- The checker refuses references to viewUnbound/modelUnbound/msgUnbound
as module data (the build reads them without emitting, so a reference
would name a missing declaration), symbol-resolved to the module
consts.
- A bare kind-tagged object literal classifies as a one-arm tagged
union, never a struct holding a textual kind.
- The facade fences viewUnbound only when the const declares, refuses a
record referenced by node and value at once (one declaration cannot
state both storages), and refuses a host-constructed channel arm
whose named record cannot flatten into the arm the host fills.
* Reserved-list references resolve by symbol; alias records stay whole
- The reserved-const reference check resolves through import aliases and
shorthand value symbols, catching a renamed binding or `{ modelUnbound }`
by what it declares rather than its spelling; the split pair joins the
entry-only export set and the rename guard.
- Object-literal aliases classify as structs only when every member is a
plain record property (identifier-named, annotated, non-optional);
quoted or optional properties refuse as unsupported aliases instead of
registering a struct with silently missing or wrongly required fields.
- A kind-tagged shape with an optional discriminator is no union: the
source permits the untagged value, so the shape refuses instead of
projecting a mandatory tag.
* Alias form pins value storage in promotion; false attestations refuse a profile
- markPromotions reads declaration form: an object-literal alias keeps
its by-value layout however the Model reaches it (the alias spelling
IS the value-storage form), while interfaces stay with the
reachability walk; pinned with the value/pointer pair test and the
emitted Zig compiles with a by-value model record.
- The profile is enforcement data, so a contract attesting
deterministic: false or async_free: false refuses emission with a
teaching naming the attestation — a compile under the fences attests
true by construction, and emitting would move the contradiction
downstream.
- The transpiler test suite gains pins for the alias/singleton/split-
const behaviors landed this branch.
* Value-record aliases refuse the shapes value storage cannot carry
- NS1061 teaches the whole family at check time: the model root stays
an interface, a model-kept alias holds scalar fields only (heap-backed
fields would dangle across frame resets under the shallow commit),
model arrays carry reference-stored records, identity comparison over
a value record refuses (no reference to compare), and an alias that
reaches itself by value refuses (no finite layout).
- Scalar aliases the model keeps directly and heap-carrying aliases
outside the model tree stay clean; pinned across all six shapes in
the checker suite.
* Entry roots keep their contract shapes; by-value recursion and wrapped identity refuse
- NS1062 teaches at check time when Model does not declare a record or
Msg does not declare a kind-tagged union, whatever declaration form
produced the wrong shape (a plain object alias, an interface Msg, or
a tagged singleton Model), instead of failing inside the emitted
module's dispatch and commit machinery.
- The by-value recursion walk covers every by-value node — value
records and tagged unions alike — so a singleton union reaching
itself refuses; arrays still break cycles by indirection.
- The identity-comparison guard unwraps union operand types and follows
plain type aliases, so a nullable or alias-wrapped value record
refuses the same as a bare one.
* Presence checks pass the identity guard; optional arms and class roots refuse
- The value-record identity guard fires only when BOTH comparison sides
can carry a record at once: a nullable presence check compares the
option and stays exact, so generated optional encoders and idiomatic
null gates compile.
- An optional payload property keeps a shape out of the kind-tagged
union classification (the source permits the absent member while the
emitted arm would demand it; absence spells | null), so the Msg root
teaches instead of requiring an unrepresentable payload.
- The entry-root shape rule covers class declarations: a class named
Msg is a struct and refuses with the union teaching.
* Flattening stays a value move; the root seeds node storage; assertions cannot shed identity
- Synthesized single-use records inline only when referenced by VALUE:
a node-stored payload keeps its named declaration and pointer in both
projections, however its name is spelled.
- The model root seeds the node-stored set (reference storage by
contract), so a value reference to it refuses as mixed storage
instead of re-deriving as node downstream.
- The identity guard's other-side test is structural: an
assertion-erased operand still counts as a record, so shedding the
alias name cannot slip a value-record comparison past the teaching.
* Integer-fixture envelopes pack through nsc_core_pack_msg
- The mixed-class envelope test drives the packer under its own name
and adds the wire-shaped key entry's nothing-produced route, so the
integer fixture exercises both channel surfaces.
* Identity peels assertions; record writes, inert lists, and skewed splits refuse
- The value-record identity guard types the peeled operand expressions
(assertions erase at emission), so a structural respelling on both
sides cannot shed the record's name.
- Record fields have no in-place write in the emitted layout: mutation
through a mutable interface or alias property refuses with the
reconstruction teaching instead of emitting an unassignable store.
- The facade refuses model-kept value records with non-scalar fields
and model sequences of value records, so corewire --check agrees
with what the facade's compilers accept.
- The split unbound consts must restate viewUnbound's resolved facts
exactly (unresolvable and missing entries both teach), and a
reserved-list const declared in an imported module refuses whether
exported or not.
* Identity stops at emission too; singleton unions claim syntactic literal tags only
- Equality emission gains the NS1061 re-derivation: a by-value struct
operand stops the build with the identity teaching, so comparisons
reaching emission through generic instantiation or erased assertions
stop the same way the checker teaches directly (pointer records keep
their identity ==).
- The checker guard reads each operand through both the peeled and the
spelled view, so an assertion that NAMES the record refuses at check
time as well.
- The singleton-union reading claims only a syntactic literal tag: a
record whose kind field resolves through a named literal union stays
the attested struct.
|
||
|
|
dbe15a7472 |
The <terminal> markup built-in: snapshot painter, widget kind, and element surface (#195)
* Canvas terminal grid: the resolved cell model and painter - TerminalGrid snapshots carry resolved cells (cluster bytes, final colors, wide/spacer occupancy) so the painter needs no emulator: real text runs, geometric box drawing, selection wash, cursor register, keyboard caret, and the scrollback thumb, with row-atomic command/text/glyph budgets - terminal_box.zig promotes the example's exact-cell-bounds box geometry (U+2500-259F) into the canvas - TerminalState is the app-visible view-state record (scrollback, history, cols, rows) the terminal element's event delivers * The .terminal widget kind: grid rendering, focus, and the editable-text register - Stable code 62; renders the bound TerminalBinding's grid snapshot through the terminal grid painter (full-bleed background, inset text region, shared per-view budget reserves) and the honest empty surface unbound; the house focus ring when focused - Joins the editable-text register everywhere but the TextBuffer pipeline: hit target, press claimer, focusable, I-beam cursor, textbox role — the emulator, not an editor state, owns the input model - Markup-inexpressible for now (documented exclusion); the <terminal> element entry lifts it * The <terminal> element: registry, both engines, contract, and teachings - Element 69 (widget kind terminal, a11y control) with pty (88, binding-only, required — dead markup otherwise) and scrollback (89, the scroll value source-wins echo); event terminal (13) delivers the new terminal_state payload (canvas.TerminalState) on the element only - Validator, interpreter, and compiled engine stay in lockstep: pty scoping/grammar, scrollback and on-terminal scoping, leaf rule, and the declared-record mirror (declaredTerminalStateRecord) for transpiled cores; contract classifies terminal_state arms and checks on-terminal payloads - Ui.terminal sugar, terminalMsg/translatedTerminalMsg constructors, msgForTerminal, and the TerminalGridLookup seam the app loop installs to resolve bound pty keys into published grid snapshots; re-pinned registry fingerprints via print-pins * Wire libghostty-vt into native framework builds - Adds the ghostty pin to build.zig.zon and imports the ghostty-vt module into every native instantiation of the framework module (desktop, print-pins, and the user-app graph in build/app.zig); the wasm docs preview and pty-unsupported targets leave the comptime-gated import unresolved - Two terminal_grid.zig comments now state the concrete degrade mechanism they describe * Document the <terminal> element and its state contract - Adds the declarative <terminal> section to the terminal docs page (both authoring tiers), covering the pty-key binding, the TerminalState echo, and layout-derived sizing - Changelog fragment for the element, the state contract, and the teachings * Painter lifetime, id/budget correctness, a11y, and saturation fixes - Rounded box corners allocate path elements from the builder's own store (strokePath retains the slice by reference; a stack local dangled for the retained renderer) - Box-cell command ids use an 8-command stride and the per-row reserve counts nine per column (eight box segments plus a background run): a 4-stride collided adjacent double-cross ids and the reserve under-counted, risking a frame-failing overflow instead of row-atomic degrade - Text preflight compares against the builder's live cumulative counter, so a grid sharing the store with earlier widgets degrades against the remaining space - Terminal widget floors its command budget to a positive value (0 means unbounded to the painter); a positive budget below the fixed prologue overhead paints nothing, checked before the prologue emits - A torn row (builder store exhausted mid-row) does not count as painted and stops further rows, so the cursor never draws over dropped content - Screen text rides the widget's text VALUE (semanticTextValue), not the accessible name, so an author label stays the stable control name - translatedTerminalMsg saturates instead of trapping on narrow declared fields - screen_text doc states its true scope (plain-text coverage, not colors/cursor/selection) - Regression tests for path lifetime, adjacent double-cross id uniqueness, and the shared-store text preflight * Terminal painter budget accuracy, id disjointness, and contract completeness - Command budget is checked per row against that row's actual upper-bound cost (rowCommandCost) instead of a flat worst-case-per-column reserve, so a wide cheap terminal paints its rows instead of reserving them all away - Per-row path-element preflight for rounded corners, plus a row-atomic rollback that drops a torn row's partial commands whole so nothing half a row reaches the glass - Box/block cells keep SGR underline (geometry replaces the glyph, not its decoration) - The focus ring on a bound terminal takes its id from the painter's reserved offset, disjoint from every grid command id - Contract validates the pty binding (undefined field caught, valid one marked used) and requires a bare on-terminal tag (an authored payload is dead data); contract format bumped to 3 so stale artifacts regenerate - TerminalBinding.grid documents its borrowed-pointer lifetime contract - Regression tests: wide-terminal painting, focus-ring id disjointness, on-terminal bare-tag * Terminal id-arithmetic wrapping, focus reachability, and audit registration - The prologue budget check accounts for commands already in the builder, so a full builder degrades to nothing instead of overrunning its absolute ceiling - All command-id arithmetic wraps (paintIdBase spans the full u64 space, so a base near maxInt no longer traps on a checked add) in both the grid painter and the box geometry - Horizontal box/block merging breaks on an underline mismatch, so a run keeps each cell's decoration instead of spreading the first cell's - Pointer, autofocus, and automation focus reveal the terminal's ring (a new canvasWidgetShowsPointerFocusRing predicate covers editable text plus terminal), so the ring is reachable by click, not only Tab - The focus-paint dirty-bounds register and the minimum hit-target audit both include the terminal, so a focus enter/leave damages the ring region and an undersized terminal is flagged - Regression tests for the builder-aware prologue guard and near-maxInt id wrapping * Terminal budgets count view-global text and path totals; merge-aware cost - rowCommandCost mirrors horizontal box merging: a long mergeable run (a border of identical box glyphs) costs one geometry command plus an underline, not nine per column, so a wide box row paints instead of being skipped on a bogus estimate - Text and path-element preflights degrade against the whole display list (every draw_text and fill/stroke_path, referenced sibling and static content included), matching the runtime's per-view CanvasResourceCounts limits, so a terminal sharing a view with text or icon widgets cannot push the frame over CanvasTextTooLarge/CanvasPathElementLimitReached - A builder smaller than the widget command reserve gets the whole capacity as its budget, so a small buffer paints what fits instead of flooring to a below-prologue budget that painted nothing - Regression tests: merged wide box row, referenced-sibling text preflight * LSP doc tables cover the terminal vocabulary - Hover/completion docs for the terminal element, its pty and scrollback attributes, and the on-terminal event, satisfying the doc-table conformance test that holds the tables equal to the registry * Terminal id namespaces by stride, nested double corners, and name honesty - Command ids follow the widget part-id convention scaled to a 24-bit slot space (id * 2^24 + offset, every painter offset below the stride), so two terminals' commands are disjoint by construction — the multiplicative spread let one widget's offsets land in another's namespace; an anonymous grid (id 0) now emits every command with id 0, the unkeyed convention, through the box geometry included - Pure-double corners (2554/2557/255A/255D) draw as two nested L joins — the double frame a terminal face draws — instead of both bars riding through the joint as hash crossings with inward stubs - The merged-run cost charges a double piece's two bars plus its underline, so a row of unmergeable underlined doubles stays under the hard ceiling - text= on terminal is a teaching error in the validator and both engines: the text channel is runtime-owned (the live screen rides it), so an authored value would rename the control every frame and falsely satisfy the accessible-name lint — the name comes from label - elementHoldsChildren classifies terminal as a leaf, so container roles that promise child structure are refused on it - Regression tests: budget ceiling under unmergeable underlined doubles, the anonymous-id convention, cross-grid id disjointness with the adversarial id pair, nested-corner geometry, and the text/role teachings * Glyph preflight charges atlas entries, not code points - The runtime atlas keys on (font, glyph, size, subpixel_x, subpixel_y) with four x buckets, so one distinct scalar can occupy four entries across a row's columns under a fractional cell width; the preflight now charges every new code point at that variant multiplier, so a grid within its budget can never undercount its own atlas usage and fail the frame with GlyphAtlasListFull - The glyph budget option and the widget mirror are documented in atlas-entry units; the budget regression test asserts the 4x charge * Box cost per glyph and zero-add rows pass the shared ceilings - rowCommandCost charges each non-merged box glyph its own worst case (box.maxCommands, kept lockstep beside the paint switch): a wide row of one-bar pieces costs what it paints instead of a flat joint worst case that rejected it whole - A row adding no text or path elements paints even when earlier widgets already spent past the grid's reserved share — the ceilings bound what the grid adds, never what siblings consumed - Regression tests: a 198-column row of one-bar pieces paints under the widget budget; an all-box row paints beside a text widget that exhausted the share * Terminal build-time leaf enforcement, a path reserve, and a scoped id claim - Both markup engines refuse a pty-less terminal and terminal children at BUILD time (the image-leaf policy), so hot reload — which skips the validation pass — fails the build instead of silently rendering an unbound surface or dropping the child; interpreter regression test added - The painter holds back a path-element reserve (widget_path_reserve, 512) for the widgets emitted after the grid, so rounded-corner rows degrade instead of starving a following chart into ChartPathElementListFull - paintIdBase's doc states its guarantee precisely: the widget part-id convention scaled to a 24-bit slot space, with the identical accepted residual (ids agreeing modulo the shifted range share a namespace, as they do for widgetPartId) * Cluster-isolated runs, engine-scoped scrollback, terminal-aware widget diff - Multi-codepoint clusters (combining marks) paint as their own single-cell run at their exact cell origin, so a text layout that advances a mark by a full glyph can never shift later cells off the grid; plain single-scalar runs keep the merged fast path - Both markup engines refuse scrollback off a terminal at build (validator parity for unvalidated paths), instead of storing an inert generic option - The widget-tree diff reports paint damage for terminal bindings: pty/scrollback changes compare, and a BOUND grid is never reported clean (its snapshot may be republished or refreshed in place), so partial-invalidation consumers never leave stale terminal pixels - Regression tests: cluster neighbor keeps its cell origin, misplaced scrollback fails the engine build, changed bound grids diff paint-dirty * Scope the libghostty-vt dependency to the terminal example - Drops the framework-wide ghostty wiring (root dependency, module imports in the repo and user-app build graphs): no framework code imports the module yet, and resolving ghostty at configure time runs its full build script — whose bench tooling pulls SharedDeps regardless of the lib-vt emit option, walking lazy dependencies (wuffs, translate_c) whose build scripts fail in consumer package stores that carry the whole transitive closure - Scaffolded and consumer builds no longer traverse ghostty at all; the terminal example keeps its own pin (the merged slice-A wiring), and the runtime session slice will bring the dependency back behind a consumer-safe seam when framework code actually imports it * Derive the terminal path reserve from the chart series ceiling - widget_path_reserve now derives from max_chart_points_per_series (stroke polyline plus area fill with closure slack): a flat 512 fell three elements short of one maximal filled-line series, so rounded-corner rows could still starve a following chart into ChartPathElementListFull - A lockstep test pins the reserve to the derivation |
||
|
|
19f2933e08 |
Validate integer-class attestations and decode boundary slots by class (#194)
* Validate integer_slots structurally with the u64 class in the checker
- Resolve every leftover integer_slots entry against the sidecar's own tables, teaching unresolvable paths, wrong-spelling slots, and the grammar's slice-element gap distinctly (V10)
- Accept the closed integer-class set {i64, u64}; f64 and unknown classes refuse with their own teachings
- Message-side slot paths spell the union's authored name in both the reader's bijection and the extractor's emission
* Carry the u64 wire class through the canonical codec
- Encode/decode accept u64 beside i64: 8-byte unsigned LE, total over the full wire range in both classes
- exactF64Unsigned holds the one-sided 2^53 exactness line for u64-attested dispatch payloads
- Golden vectors pin the +-(2^53 - 1) extremes, sign handling, and full-range round trips byte-for-byte
* Decode integer slots per their attested class in the generated mirror
- The spelling authority consults integer_slots by slot path: a u64 attestation refines the i64 spelling to the unsigned twin (8-byte unsigned LE decode), every non-attested slot stays f64
- Dispatch narrows u64-attested payloads through exactF64Unsigned; i64-attested slots keep the signed guard
- Tests pin every attestable path form with mixed classes and the empty list generating no integer slot anywhere
* Exercise attested integer classes through generated mirrors in the harness
- New hand-written integer fixture with mixed i64/u64 attestations; its generated mirror decodes envelope, dispatch, and snapshot bytes over the +-(2^53 - 1) extremes and the full 8-byte wire range, byte-exact against hand-computed vectors
- The external-core parity suite drives the i64-classed arms through real dispatch at the provable extremes and reads the exact integers back from the decoded mirror
- The parity build runs corewire --check over the supplied external sidecar before the tests; everything still skips cleanly when no archive is supplied
* Host adapters accept the u64 number class beside i64
- Event-record shape checks, the number/number_bytes routing entries, and the channel-record validation take u64 fields exactly where i64 stands; widening assignments were already class-agnostic
- The mini core grows u64-classed now and fetch arms proving both routing paths deliver the exact unsigned values
* Facade routes u64-attested slots through an unsigned encoder
- nscfU64 (emitted only when a slot attests the u64 class) refuses negative, fractional, and past-2^53 values instead of encoding two's-complement bytes a mirror would read as huge unsigned values
- Slot paths thread through the facade's encoders and value builders; u64-attested sample values stay non-negative so the deterministic sample model encodes
- The integer fixture's compiled facade joins the conformance parity axis: snapshot and envelope bytes match the canonical encoder for mixed classes
* Parity boundary script follows each slot's attested class
- The integer-extremes script includes negative crossings only for signed-attested arms, so a supplied sidecar attesting u64 on those slots still compiles and runs
- The decoded-mirror comparison widens both classes into one comparison type
* Refuse u64 attestations on host-supplied signed slots
- The checker refuses the u64 class on scroll-state axes and text-selection bounds: the host supplies negative values there, which the unsigned class cannot carry
- The pty event shape keeps code and signal signed (non-exited terminals deliver -1 sentinels); droppedWrites still takes either integer class
- The parity boundary script and its comparison handle f64-classed slots, so a sidecar predating integer attestation still compiles and runs
* Narrow the signed-slot refusals to values the host actually signs
- Two distinct slots spelling one path refuse as unaddressable (a message union named helpers can collide with a helper's return slot), so one attestation can never silently govern two slots
- Scroll refusal covers only offsets and velocities; extents, selection bounds, and pty signal take the unsigned class (the host supplies them non-negative), while pty code and chrome geometry stay signed
- Chrome geometry refuses u64 at check time and in the channel-record validation (embedders report signed content coordinates)
* Pin the signed-slot rules to the exact host domains
- The scroll refusal applies only to records a message arm routes (a model-only scroll-shaped record keeps its attestation), and the chrome refusal narrows to the control cluster's x/y position — insets and sizes are non-negative extents and take the unsigned class
- Channel-record validation takes a per-field signed list instead of a blanket switch
- A negative host number reaching a u64-classed arm or field (a pre-epoch clock, a -1 exit sentinel) teaches at the crossing instead of faulting in the cast
* Clamp audio scalars into the exact-integer delivery window
- Audio positions and durations clamp below 2^53 at every delivery entry (live platform events, fed fake/replay events, spectrum reports) — the video scalar clamp's twin, so integer-classed Msg fields and the mirrors never see an out-of-window host value
- The parity script's selection sample follows the supplied sidecar's classes: signed keeps the negative-value coverage, unsigned exercises a backward selection instead
* Refuse out-of-window audio records at replay instead of reshaping them
- The audio twin of the video scalar damage gate: a journaled position or duration at or past 2^53 can only be a damaged or hand-edited journal now that the delivery boundary clamps, so replay refuses it before the feed
- Predicate pinned at the window edges in the existing damage-gate test style
* Bump the journal semantic epoch for the audio scalar window
- Epoch 5: audio scalars now clamp below 2^53 at delivery and replay refuses wider journaled values as damage, so an older recording carrying one refuses as a different generation instead of being misreported as damaged
|
||
|
|
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 |
||
|
|
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 |
||
|
|
dd9307656a |
Hover Msg bindings: on-hover-enter and on-hover-leave (#188)
* Add the hover-enter/hover-leave event pair to the markup vocabulary - Registry events 11/12 (on-hover-enter / on-hover-leave): payloadless Msg bindings, legal on any element like the press family. - Binding stamps Widget.hover_msgs — hover-hittable via the chart hover-details rule (no press claim, no wash, no a11y action) — and widgetHoverMsgChainFromNode collects nested listeners outermost-first for containment tracking. - Both markup engines, the builder handler table (UiHandlerEvent.hover_enter/hover_leave), and parity/chain tests; the markdown arena canary's linear factor absorbs the larger Ui.Node. * Track hover-Msg containment in the runtime and dispatch enter/leave - Each view keeps a standing containment chain recomputed at exactly the wash-resolution seams (pointer phases, scroll re-hit-tests, layout adoption, rebuild and dismissal prunes), so hover Msgs and the wash never disagree; cancel is the window-leave edge and unbound apps keep an empty chain. - UiApp diffs the chain against its delivered mirror at the tail of every runtime event — leaves innermost-first, enters outermost-first, leave Msgs captured at enter time so unmounted elements still deliver the pair; a pass cap bounds flapping apps. - Tests pin nested containment, cancel, enter-that-unmounts, scroll-under-stationary-pointer, and record/replay determinism through the reference session's raw pointer moves. * Prove the hover pair end to end over a transpiled TS core - The markup fixture's task rows bind on-hover-enter/on-hover-leave with for-each payloads into a hoveredId mirror; payloadless events need no TS SDK type surface. - The e2e test drives raw pointer moves through the null platform: enter with the row payload, row-to-row handoff, and the paired leave clearing the mirror. * Document the hover pair and give notes a status-bar hover preview - Markup reference, LSP/vocab doc tables, and the native-ui skill cover on-hover-enter/on-hover-leave: containment semantics, the pairing guarantee, wash separation, and the touch-honesty note. - examples/notes: hovering a note row previews its title, age, and word count in the status bar without committing the selection, with a real-pointer test. - Changelog fragment for the feature. * Harden hover-Msg capture, delivery, and the touch-honesty gate - Captured leave Msgs deep-copy their payload slices into slot-owned bytes (a standing hover outlives the build-arena pair), with live-tree fallback for payloads the budget cannot own; enters resolve from the live tree per edge. - Containment advances only while a hover-capable pointer is live (a hover-phase move, which touch contact cannot produce), so taps, scrubs, and post-fling re-hit-tests never synthesize hover while mice keep full click/drag fidelity. - Delivery degrades per edge (one failed dispatch no longer swallows sibling edges), the drain also runs on a handler's error path, and a transiently missing handler tree defers the transition instead of consuming enters as silence. * Close the hover delivery seams: direct dispatches, pointer identity, and unbounded captures - Public dispatch and drainEffects settle hover edges at their own tails (re-entrancy guarded), so direct dispatches deliver an unmounted element's leave without waiting for a platform event; drain passes degrade per pass and the flap cap is sized past any honest cascade. - The hover-capable proof is scoped to the pointer identity that earned it, and a consumed secondary-stream cancel retires containment like the tooltip machine's pointer-left-view reading — a touch contact can never ride a mouse's proof on hosts that distinguish pointers. - Leave captures are arena-backed (any payload size) and the standing view label is copied out of runtime storage before dispatches that can compact the view array. * Anchor hover containment to the proven pointer and reserve a touch id bit - A proven pointer's wheel refreshes the chain's re-hit anchor before the scroll reconcile, so a wheel arriving ahead of its coalesced motion event derives containment from where the pointer really is. - platform.touch_pointer_id_bit rides the existing pointer_id field: hosts stamp touch-sourced events and the runtime refuses hover proof to stamped ids, so an OS-synthesized mouse-shaped float for a tap can never make touch hoverable; host-side stamping call sites are documented at the bit. - Dismiss events drain hover edges at their tail (the automation dismiss verb dispatches one standalone), and the leave-capture copier handles error sets, error unions, and vectors. * Make hover containment per-listener and its captures slot-owned - The mirror diff is an id-set diff with capture slots decoupled from chain position: an outer listener unbinding (or binding) while an inner one stands dispatches edges for the changed id only — the retained entry never flickers and keeps its captured leave. - Point-blind scroll re-hit-tests use the proven pointer's own anchor, so containment hands off correctly even after another device cleared the shared pointer position. - Allocation failure while capturing a leave defers the enter (and the entering tail) to the next drain instead of dispatching an enter whose paired leave is already lost; the copier's non-transient refusal narrows to single-item pointers. * Close the enter/leave pairing seams: platform window-leave and tree currency - GTK connects the motion controller's leave signal and Windows arms TrackMouseEvent per hover session, both emitting the pointer cancel macOS already sends from mouseExited — window-leave retires hover state on all three desktop hosts (press-in-flight streams settle through their own release/capture change). - Rebuild and slot-rebuild wrap in one currency invariant: a failed build or publication marks the handler trees stale, entering edges defer until a rebuild lands (never resolving through a tree the runtime refused, never consumed as absent), and captured leaves dispatch regardless — a broken destination cannot withhold them. - Standing captures refresh whenever the build generation moves — a leave handler added mid-hover is captured before an unmount needs it, payloads deliver their latest binding, and unbinding retains the last capture; pinned by late-bind, failed-publication, and recovery tests. * Refine hover currency to per-tree families and harden capture copies - Tree currency splits per family: a main-only rebuild can never restore currency for a secondary-window tree whose publication failed — only a clean pass over the slots does — and every drain gate resolves the flag for its own destination. - Capture refresh is copy-then-swap into a spare slot, so a failed allocation keeps the still-valid capture it was replacing. - The slice copy preserves the payload type's own alignment and sentinel, pinned end to end by a builder-only app binding a 64-byte-aligned leave payload through capture, unmount, and delivery. * Carry slot-rebuild bookkeeping everywhere and honor host pointer hand-offs - Every slot rebuild path — the full pass and the direct resize/install sites — stamps the slot family stale on failure and ticks the build generation on success; only the clean full pass restores currency. - The GTK click gesture wires cancel: a transferred or broken grab rolls a pipeline-visible press back with a pointer cancel (a drag-claimed press rolls back silently), so a stale pressed flag can never suppress later leaves. - Windows WM_MOUSELEAVE skips the cancel while the cursor is still inside the client rect — the HTTRANSPARENT window-drag hand-off, not a real departure — so entering a hidden-titlebar drag header freezes hover instead of dispatching false leaves. * Stamp hover currency at the install window itself, per tree - Staleness marks exactly the window between publication and handler-tree adoption: a build or layout failure keeps the old, still-matching pair current (edges flow even when an idle app performs no further rebuild), and post-install follow-up failures never defer. - Each window slot carries its own currency, so one window's failed publication never defers hover into its siblings, and that window's own next successful rebuild restores it wherever it was driven from. - Capture-refresh allocation failures surface through the dispatch-error machinery instead of being swallowed; tests pin the enter-only deferral, leaves-never-wait, and build-failure-keeps-pair-live paths. * Open the hover staleness window at adoption and bound capture indirection - The currency stamps sit immediately after each setCanvasWidgetLayout: publication rejection is validated-then-atomic, so a refused chrome build, oversized-text rebuild, or slot budget overflow keeps the old still-matching pair current instead of deferring hover enters indefinitely. - The leave-Msg copier bounds slice indirection (64 hops): a cyclic value graph refuses as unsupported instead of recursing toward allocator exhaustion or stack overflow. * Track adoption exactly, drain standalone edits, and refresh captures per transition - The currency stamp keys on a per-view adoption counter incremented the moment copyWidgetLayoutTree replaces the retained tree, so a failure in the publication pipeline's post-adoption steps still marks the pair stale while a validated-then-atomic rejection never does. - Standalone keyboard events (accessibility selection edits, context-menu cut/paste/select-all) carry a flag the hover drain honors at their own tail — an edit that unmounts the hovered listener no longer waits for an input cycle that never comes. - Leave captures refresh at every rebuild commit, not just at drain time, so two dispatches in one cycle (payload moved, then unmounted) deliver the latest binding; a proven pointer's consumed secondary release outside the view retires containment — the leave the frozen right-drag stream suppressed. * Keep capture slots leak-free and refuse unownable hover pairs whole - The enter loop and the unwind release any slot a mid-batch capture refresh installed before assigning or dropping a position, so repeated re-entry with rebuilding edge handlers can never exhaust the slot budget; pinned by a 40-cycle churn test asserting zero slots held after exit. - An unownable leave payload (a single-item pointer) now refuses the PAIR: the enter never dispatches, the exit owes nothing, and the refusal settles with one warning instead of retrying every drain — no enter without a deliverable leave. - Capture-refresh failures at rebuild commits land in the dispatch-error ring, so a stale payload delivered after a swallowed allocation failure can no longer hide behind rebuilds that reported success. * Size the capture pool for both transition populations and stamp adoption at the tear - The slot pool covers the departing chain's held captures plus a fully refreshed standing mirror plus one swap slot, and claiming degrades like allocation pressure instead of trapping if the accounting is ever wrong. - The adoption witness moves inside copyWidgetLayoutTree at its destructive boundary, so a per-node failure mid-copy (an invalid command name escapes the pre-validation) counts as adopted-and-torn while up-front rejections leave it unmoved. - A leave rebind no copy can own on a STANDING element warns once and degrades to live-tree resolution (a later ownable rebind upgrades), and sentinel-terminated array payloads capture with their sentinel stamped; both pinned by probe-app tests. * Give hover containment its own hit-test policy and a half-open outside test - Hover-Msg listeners are invisible to the interactive hit test (wash, cursor, press routing, text selection) and resolvable only through the new hover-containment policy, so binding hover provably never paints a wash or steals a click — even from an overlapping sibling; pinned by a probe asserting the interactive hit test finds nothing where the containment chain stands. - The consumed-release outside test uses the engine's own half-open rectangle containment, so a release at exactly the right or bottom edge retires the chain like every hit test already treats that point. * Resolve hover state by view identity and prune with the hover predicate - Hover capture refresh and drain lookups key on window id plus canvas label, so a replacement window reusing a closed window's label (even with identical structural ids) can never answer for its predecessor's captures. - Chain pruning uses a hover-specific survival predicate — a hover-only listener is never evicted into a false leave, and a widget whose hover bindings a rebuild removed stops standing — and a torn mid-copy failure prunes containment against the partial tree so owed leaves dispatch at that failure's own drain. - Windows WM_MOUSELEAVE suppression requires the point's owner to be one of the child's own ancestors (the HTTRANSPARENT hand-off signature): an overlapping sibling pane taking the cursor is a genuine departure and cancels. * Own the dispatch-error event name and bump the journal semantic epoch - DispatchError keeps its event name in inline storage with an accessor (the detail pattern, 64-byte cap): records are copied by value into a ring that outlives every caller's buffer, so a name recorded through reusable storage can never dangle; pinned by a clobbered-buffer test and every consumer swept to the accessor. - Reserving pointer-id bit 63 as the touch-source stamp changes a journaled field's meaning: the session journal's semantic epoch bumps 3 to 4, so older recordings refuse with the standard re-record teaching; the changelog states the conscious break. * State the secondary-stream scope honestly in the input comments - Only the secondary down/up/cancel stream is consumed: hosts report drag motion without a button, so containment follows a right-drag on the primary path (the mouseenter/mouseleave convention), exactly as the wash does — the comments now say so instead of claiming a freeze. - The GTK leave handler's suppression comment scopes itself to click-gesture presses and names the settling paths (release, gesture cancel, the runtime's outside-release check). * Teach the markup fixture sidecar the hover pair - The hand-written contract sidecar (the schema's independent ground truth) gains the hoveredId model field and the hover_row/hover_off f64 arms in declaration order, so the corewire mirror stays fingerprint- and contract-byte-identical to the transpiled lane. - The facade wire-tag pins move with the union: zoomed sits at tag 11 behind the hover pair. * Retire containment when a proven primary release lands outside the view - A captured drag's release beyond the surface clears the chain and the hover proof instead of re-hit-testing: hosts held a grab through the drag (no motion-leave fired) and send no later cancel, so an overflowing listener could stay entered off-view and a parked off-view anchor could re-enter one on a later rebuild. - The primary rule mirrors the consumed secondary release; pinned by a down/drag-out/release-out test asserting the delivered leaves, the empty chain, and the retired proof. |
||
|
|
b57224d0d1 |
Pair compiled cores with their own sidecars and pin the export attestation order (#187)
* Let the parity suite pair a compiled core with its own contract sidecar - NATIVE_SDK_EXTERNAL_CORE_SIDECAR points the generated mirror at the caller-supplied archive's own emitted sidecar, so the boot-time build_id fence checks the pairing the ABI ratified instead of always reading the committed fixture sidecar - the committed fixture sidecar stays the default for stub-shaped callers that restate its identity * Pin the normative canonical order for the sidecar's export attestation - The reader's expected-order array, the extractor's emitted list, and the fixture sidecar all state one order: version/identity entries, init, the mode-provided entries, the remaining unconditional suffixes, then wired channel entries - V11's teaching names the expected suffix at the diverging position |
||
|
|
64ab5f205c |
corewire: generate core bindings from a machine-readable contract sidecar (#185)
* Add corewire: contract-sidecar reader, validator, and shim emitter - Parse core.contract.json (schema format 1) via std.json with teaching refusals naming exact field paths; unknown fields warn, unknown format/enum values refuse whole-file - Enforce the reader-checkable validation rules (V1-V11): hash encodings, one type namespace, reference resolution and reachability, acyclicity, tag bounds, descriptor consistency, unbound lists, channel/export biconditionals, and the integer-slot bijection - Emit core_shim.zig: mirror Model/Msg/channel types with the sidecar's names, order, and number classes, declaration-order tag table, descriptor-driven dispatch stubs, snapshot decoder, and helper forwarders; extern bindings isolated in core_abi.zig so an ABI revision is a one-file edit * Wire the sidecar-shim conformance harness into zig build - Pair every ts-core fixture (host, markup, soundboard, system-monitor, ai-chat) with a corewire-generated mirror: the markup fixture from a committed hand-written sidecar, the rest extracted from the transpiled modules at build time so corpus sidecars cannot go stale - Hold both lanes byte-identical by layout_fingerprint.describe of Model/Msg and by the serialized model-contract artifact (anonymous payload records mask only Zig's per-module instance counter) - Force full analysis of every generated shim against a stub core exporting the draft ABI's symbol set, so dispatch stubs, snapshot decoders, and channel forwarders compile and link; new sidecar-conformance step, wired into zig build test beside the ts-core e2e suites * Record sidecar schema gaps and observations from the conformance corpus - Four underdeterminations: number_bytes field order, integer-slot grammar coverage for slice elements and union arm payloads, and the synthesized-name container for anonymous helper returns - Observations on worked-example spellings, anonymous-type name counters, and the channel-entry payload encoding pin * Format corewire sources with zig fmt * Harden corewire emission edges and buffer lifetimes - Double-buffer the shim's model arenas so a previously returned root survives the decode that replaces it (the transpiler lane's one-generation grace at compaction), pinned by a test - Refuse sidecars whose type, message, or helper names collide with declarations the generated shim itself must make; keep authored types that merely spell like synthesized names as top-level declarations unless referenced exactly once - Escape every sidecar string landing in generated Zig literals and every reflected string the extractor writes into JSON; bound tabled unions and enums to the one-byte wire index; restate the deterministic/async_free attestations as shim constants for host policy gates * Keep corewire's checker as strict as generation and its errors honest - --check now runs the full pipeline and discards the text, so a sidecar can never pass the checker and refuse at generate time - JSON-parse memory pressure propagates as OutOfMemory instead of masquerading as malformed input; a diagnostic that cannot be recorded terminates loudly instead of vanishing - Record the synthesized-vs-authored table-entry ambiguity as a schema gap and document the fidelity-driven inlining bias at its site * Close corewire's remaining name and mode edges - model_unbound accepts exported helper names (the dead-state opt-out vocabulary spans fields and bindable model methods); schema wording gap recorded - --check and --out are mutually exclusive modes; combining them is a usage error instead of a checker that writes files - Channel forwarders and the wire-message decoder speak the sidecar's message union name instead of a hard-coded spelling; the void TypeRef is refused outside bare union arms and decodes as no value; optional glue names reserve only when their glue is emitted * Prove the attested export set at link time and tighten shape checks - boot() references every abi.exports symbol via doNotOptimizeAway, so an object missing an attested entry point fails at link instead of lurking behind lazy extern binding - Text-input routing applies the markup engines' full structural predicate (payload shapes, not just the eleven tag names); near-miss unions ride the record entry - Boolean and presence discriminants past 1 refuse as layout skew instead of decoding silently; UpdateResult reserves only when emitted; the view_unbound spelling is fenced against helpers, fields, and arms that would shadow the opt-out tuple * Alias the wiring's root spellings and reserve the attestation glue - A sidecar naming its roots something other than Model/Msg gets transparent aliases under the wiring's fixed spellings, so the staged main's re-exports and the reflecting seams keep compiling; the alias names reserve against colliding table entries - referenceAttestedExports joins the reserved emission names - State at the attestation site why the no-extra-exports direction is the producer's conformance obligation (a linker ignores unreferenced extras; the generator never opens the object) * Refuse un-buildable channel arms and the discard identifier at tool time - The appearance and chrome arms must carry the exact structural records the host builds by field name; a named-type payload of any other shape refuses with the shape teaching instead of a compile error inside generated wiring - A name spelled _ is the discard token, not a declarable identifier; it now takes the quoted form like keywords and primitives * Hold channel record checks to by-value storage and bare arms to empty payloads - The structural vocabularies the host constructs by field name (chrome insets/buttons, text-input payload records) accept by-value records only; a node reference mirrors as a pointer neither consumer's shape takes - A channel message on a bare arm must carry zero payload bytes; stale bytes refuse as layout skew instead of dispatching silently * Sharpen the number_bytes and helper-return gap records - State the wire consequence of the missing field-order fact and why the generated shim itself cannot skew (one mirror type feeds both encode and decode) - Record that anonymous helper returns lack a pinned mirror spelling as well as a container rule; the generator deliberately keeps them named until a producer ratifies an inline convention * Reserve channel detection names unconditionally - The host wires channels by probing declaration names, so subscriptions/commandMsg/frameMsg/keyMsg/pinchMsg/appearanceMsg/ chromeMsg/envMsgs are off limits even when the channel is absent — a type under one of those names would falsely activate the channel and fail as a non-function - Event-record names (FrameEvent/KeyEvent/PinchEvent/PinchPhase) stay conditional; they are plain declaration collisions, never probes * Fold the ratified core-ABI deltas into the binding layer - helper_call traps on an unknown index through the panic sink instead of returning a status; the shim glue drops its check - Lifetimes follow arena truth: boot_cmd bytes live until the next frame_reset, dispatch, or init; the snapshot buffer is transient and does not survive a frame reset; collect folds the transient-arena reset and joins the runtime-owned mode symbols - Narrow signature integers are documented as C plumbing for host-produced values, never payload crossings; the sink covers detected traps only (escaped exceptions arrive with an Uncaught prefix; hardware faults stay with the host's process-wide handler) * Emit core_facade.ts: the TypeScript projection of the contract sidecar - One sidecar, two projections: corewire --facade emits a subset-TS module carrying the mirror types (interfaces, literal-union enums, kind-tagged message union), per-arm wire-tag constants and typed constructors, the generator-carried viewUnbound list, identity constants, and deterministic zero/sample model builders - The canonical value encoding rides subset arithmetic: byte values stay integer-derived end to end (owned Uint8Array runs concatenated by copy), and the f64 bit pattern extracts exactly by halving scans and comparisons, covering denormals, signed zero, the infinities, and the canonical quiet NaN; nsc_core_probe_i64/f64 expose the scalar encoders for parity proofs - Emission is deterministic, TS-identifier and nsc-name-space fences refuse collisions with teachings, and the module compiles through the shipped checker as a validation proxy (boundary-fed scans stay out of shared helpers and sparse writers zero their buffers explicitly, per the emitted lane's allocation and number-model rules) * Prove facade byte parity in the conformance harness - Compile every fixture's generated core_facade.ts through the shipped transpiler in-graph (the compile is the subset-acceptance proof) and execute its encoders: scalar probes across f64 edge cases (signed zero, denormals, the infinities, the canonical quiet NaN, the 2^53 bounds) and i64 two's complement must match native bit patterns - Facade-encoded zero and sample model snapshots must byte-match the canonical encoder over the shim's sidecar-classed mirror, via a by-name value conversion that normalizes inference-decided classes and reference storage; constructor tags ride declaration order - Record the FACADE-GAPS section: each surface the projection cannot carry, pinned to the subset rule that excludes it * State the facade generator's input model in its module doc - The generator consumes contract facts (author-module declarations plus profile constants), which precede compilation in production; the conformance harness feeds those facts from corpus sidecars as its adapter * Reword the helper-return gap note to name the missing emitter convention * Close facade projection edges: paths, parens, fences, fallbacks - corewire refuses aliased generation paths (--out/--facade naming one file, or any output naming the sidecar itself) - Composite slice element spellings parenthesize (a bare number | null[] would type the null as the array) - The generated NscfContractError and NSCF_POW declarations join the facade's name fence, and field names may not take the reserved nsc space (constructor parameter fallbacks live there, collision-proof) * Normalize output-path checks and refuse valueless contract shapes - Generation-path aliasing compares lexically normalized paths, so ./x and x cannot slip past the overwrite fences - Empty message unions, tabled unions, and enums refuse at read time: a valueless union or enum has no declarable mirror form in either projection - number_bytes descriptor fields join the reserved-nsc-space fence (they become constructor parameters and record members without ever entering the type table) * Canonicalize NaN in both encoders and fence the remaining shadows - The canonical value encoding maps every NaN — negative or payload-bearing — to the one quiet pattern: payload bits are not values in either source language and engines may rewrite them at any store, so a bit-preserving encoder could never be deterministic across producers; both encoders and the parity suite pin it - A type named Uint8Array refuses (it would shadow the ambient byte type out from under every facade encoder) - Comment sanitizers also blank U+2028/U+2029, the line terminators a TypeScript scanner honors * Keep both projections declarable under renamed and exotic contracts - The Zig emitter fences every parameter and local name its generated bodies bind (a module-level type under any of them would shadow) - The facade declares its roots under the profile's designated Model and Msg exports whatever the contract calls them, aliasing the contract names — the root commit machinery and dispatch wiring key on the exact spellings (renamed-root facade transpiles end to end) - viewUnbound carries only names the facade declares (message arms and model fields); nested optionals refuse with a teaching (one null, one absence level); strict-mode reserved words join the identifier fence * Key synthesized flattening on the contract's message name and fence unbound homonyms - Constructor-side synthesized-record detection patterns on the contract's own message name, so renamed unions flatten consistently with their arm types (pinned end to end for an Event_loaded payload) - A name that is an unbound arm and a bound field (or the reverse) refuses: the projection's single name-resolved viewUnbound list would mark both declarations; names unbound on both sides ride once * Flatten synthesized union payloads and fold case in path aliasing - Synthesized inline records inside tabled unions flatten beside kind (the authored multi-field arm shape), in the arm type, the union encoder, and the value builders alike; single named payloads keep the erased-member value spelling - Output-path aliasing folds ASCII case: the default volumes on two of the three desktop platforms are case-insensitive, so differently cased spellings of one file count as aliases everywhere * Ask the filesystem about output aliases the spelling checks cannot see - Beyond lexical and ASCII-case checks, an output path that already resolves to the sidecar's own file (Unicode case folding, links) refuses via inode identity, and the facade write re-checks against the shim file the same invocation just wrote * Compare output aliases by canonical path, before any write - sameExistingFile resolves canonical paths instead of comparing bare inode numbers, which collide across volumes; canonical paths are unique per volume, so distinct files can never compare equal (hard links keep distinct canonical paths and stay noted as out of scope) - The out/facade pair check runs in the preflight loop too, so two existing links to one file refuse before the shim write, not after * Land outputs by rename so aliased entries can never truncate shared content - writeOutput writes a sibling temp file and renames it over the target: a rename replaces the destination's directory entry and never writes through it, so a hard link to the sidecar (invisible to canonical-path preflight) keeps the input intact under its other names — verified with a hard-linked output - The identity helper's note states why hard links passing it are harmless by construction * Claim staging spellings in preflight and refuse inexact integer crossings - The .corewire-tmp staging names join the alias checks, so a sidecar spelled as an output's staging path refuses before anything writes - Scalar dispatch entries route integer payloads through a checked narrowing: values at or past 2^53 alias on the f64 wire, so they trap with a teaching instead of corrupting silently (number, number_bytes, and scroll-state paths alike), matching the bridge's shipped bound * Stage outputs exclusively under unique names and widen synthesized identities - The staging file is created exclusively with a per-invocation nonce: exclusive creation can never truncate an existing entry whatever it links to, and concurrent invocations stay off each other's bytes (a hard-linked staging-prefix entry now survives generation intact) - The extractor's synthesized source_hash and build_id cover the complete reflected surface — unbound lists, channel wiring, entry-shape flags, and the export set included — so any contract-visible fixture edit moves both identities * Pin the two schema-underdetermination dispositions as tests - number_bytes mirrors number-first (the one order every producer of the shape declares; the descriptor carries no order fact), and a bytes-first record rides the record family with its order carried explicitly — both directions now executable - A single-use pattern-named record inlines to mirror the emitted module's anonymous records — the direction every corpus fixture requires while the schema carries no synthesized marker * Stage both projections before committing either - A combined --out/--facade run writes both staging files completely before the first rename, so a write failure can never leave a fresh shim beside a stale facade; a failure between the two renames names both files as a possibly skewed pair and exits nonzero so the caller regenerates - Concurrent invocations aimed at one output path remain the caller's serialization to provide, stated at the commit site * Canonicalize existing ancestors for future outputs and clean sibling staging - Alias comparison canonicalizes the deepest existing ancestor directory and carries the not-yet-existing tail verbatim, so two spellings of one future file through symlinked or case-folded parents refuse before generation (verified with a symlinked output directory) - A staging failure deletes the sibling projection's already-staged file instead of leaving it beside the exit * Canonicalize per component, recheck after the shim commit, keep samples linear - Path canonicalization walks components against the filesystem, so .. applies to the real parent instead of lexically across a symlink (verified: link/../core.contract.json refuses as the sidecar itself) - Once the shim rename lands, the facade target re-resolves against the now-existing file, catching aliases no spelling check can see — filesystem Unicode normalization included (verified with NFC/NFD spellings on the default macOS volume) - Sample sequences emit two elements at the outermost level and one per nested level, keeping generation linear in TypeRef nesting depth * Fence helper names against the generated glue they would shadow - Model methods shadow file-scope declarations inside the struct, so a helper named after generated glue (callHelper above all) would capture the forwarders' own call sites; such names refuse at tool time with a teaching * Walk paths with the native component parser and record the arm-storage gap - canonicalSpelling parses roots and separators with the platform's component iterator, so drive and UNC spellings normalize to one prefix instead of splitting on a single separator - Any facade-staging failure shape deletes the already-staged shim file; helpers may not shadow the mirror's own type names inside the model struct (field types resolve against methods first) - A record reused as a bare message arm rides by reference in the emitted lane, which the named-type payload family cannot say: the extractor refuses the construct with a teaching and SCHEMA-GAPS records the missing storage-kind fact and its closure * Keep drive-relative spellings lexical and refuse dishonest integer bytes - A drive-relative Windows path (C:foo) names a file under that drive's own working directory, which cannot resolve portably; canonicalization keeps it lexical instead of folding it under the drive root into false aliases - The facade's i64 encoder throws the kind-tagged teaching value for non-integer or out-of-range input instead of truncating into wrong canonical bytes (probes and misclassified slots alike) * Resume canonicalization after a dot-dot pop and reserve the field capture - A .. that unwinds a nonexistent tail re-probes the filesystem, so a symlink after the pop resolves instead of riding unresolved past the alias guard (verified with a nonexistent scratch segment) - The tag-table consistency block's field capture joins the reserved emission names * Refuse contract shapes the projected subset cannot author - Quoted (non-identifier) property names and one-member enums have no accepted spelling in the shipped subset (probed: both refuse at the emitter), so the facade refuses such contracts with teachings instead of emitting members the compile would drop or misread; the Zig mirror keeps expressing both - FACADE-GAPS records the pair * Fence flattened kind fields and escape TS line terminators in literals - A number_bytes field or synthesized inline record field spelled "kind" would declare the flattened arm's discriminator twice (message arms and tabled-union arms alike); such contracts refuse with a teaching — named record payloads ride a value member and stay unaffected - tsString escapes LS and PS, the scanner's remaining line terminators beside the already-escaped LF and CR (NEL is ordinary text), so exotic member names cannot end an emitted string literal early; both pins ride the unit suite * Refuse model-reachable records as message arm payloads in the mirror - The compiled core stores a model-graph record by reference when it doubles as an arm payload, a storage fact the record family cannot carry; mirroring by value would silently change the reflected layout, so the generator refuses the shape with a teaching until the schema says storage — message-only records stay by value, matching the emission and the conformance corpus * Reserve view_unbound as a type name and the intrinsic TS type keywords - The unbound-list tuples nest inside Model and Msg, where a same-named top-level type would be shadowed at every field-type resolution; such a type now refuses at tool time - any/unknown/never/bigint/symbol join the facade's reserved words: a declaration under an intrinsic type keyword would bind every reference to the built-in and erase the contract silently * Key the arm-storage refusal on node storage, not model reachability - The sidecar itself says which records ride by reference (node TypeRefs); only those keep reference storage when doubling as arm payloads, so the refusal now targets node-stored records exactly — value-promoted model records (a text-input union's caret and selection payloads) pass through by value as the emission keeps them, pinned in both directions * Note the facade-constructor consequence of the missing synthesized marker - Gap 4 now records that the inline bias is visible in constructor parameter shapes as well as in type-name artifacts, and why the compiled layouts agree either way (a collapsed named record and a flattened anonymous one carry the same fields in the same order) * Pin anonymous-name detection to digits and make codecs strictly positional - Anonymous container names carry the compiler's instance counter to the end of the name; an authored type merely containing the marker keeps its declared identity in extraction - The canonical codec's enum and union discriminants ride declaration-order positions on BOTH sides, never numeric values (explicit-valued types round-trip, pinned by test) - tsString and the comment sanitizers propagate allocation failure instead of returning unescaped or empty text * Hold facade names legal through the whole proxy pipeline - One authority validates every name category the facade declares (types, arms, fields, enum members) against BOTH languages: the compiled module takes identifiers verbatim, so its charset, keyword, primitive, and discard rules apply on top of TypeScript's — with the TypeScript reserved words scoped to declarations only (reserved words are legal members, as the corpus's own number field proves) - Anonymous-type detection keys on the FINAL compiler suffix, so an authored name containing the marker keeps its identity while an anonymous member nested under it is still recognized (pinned both ways); the string escaper is pinned directly as defense in depth * Anchor anonymity to the parent name and widen file-identity checks - Synthesized classification requires two fields (smaller inline arms collapse at emission and never table), so the two projections can never diverge on a one-field pattern-named record - Extraction recognizes an anonymous member only when its name prefix IS the enclosing parent's own name beside the final counter suffix, so authored marker-bearing names keep their identity at any nesting - Existing-file identity falls back to full metadata agreement (inode, kind, size, links, both timestamps) when canonical paths differ, the honest net for mounts that expose one directory twice * Reflect aliased root names, disambiguate slot paths, consume attestations - Extraction takes the root type names from the reflected types, so an aliased root export tables and names its real type consistently - Names containing dots refuse wherever they would join an i64 slot path (two different slots could spell one path), and the V10 bijection now consumes entries one-to-one so a single attestation can never cover two slots * Harden identity hashing, alias checks, and graph-walk bounds - Frame the synthesized identity surface with labeled NUL-separated sections covering both root type names, so renaming the message union or moving an unbound name between lists moves source_hash and build_id - Treat same-directory-entry aliases as conflicts while letting hard links pass: rename lands on the entry, so only the entry's identity matters - Bound TypeRef nesting at 256 levels in the reader and validate the named-type graph iteratively with a chain-depth ceiling, refusing pathological documents with a teaching instead of exhausting the stack * Bound expanded value depth, vet abi.prefix, gate all corewire suites - Depth validation now sums optional/slice wrapping with record chaining, so compound nesting refuses at 256 expanded levels instead of overflowing downstream walks - abi.prefix must be a linker-symbol-safe spelling (ASCII letters, digits, underscores, non-digit start); embedded NULs and spaces refuse with the offending byte and offset - Register sidecar.zig and emit_facade.zig test roots in zig build test - imported files' tests never ran under an importer's root |
||
|
|
87fa3f92d0 |
Video playback: AVFoundation through the media surface, replayable end to end (#184)
* Add the video playback tier: platform seam, effects channel, journal v9
- One video channel mirroring audio end to end: loadVideo with the local-then-URL cascade and surface claim, transport verbs (play/pause/stop/seek/volume/mute/loop), key-stamped events, honest failed/rejected degrades, and automation-snapshot mirrors
- Pixels never enter the core: the platform decoder pushes RGBA8 through a copyable VideoFrameSink into the media-surface texture channel the load claimed
- Journal format v9: the .video effect-result kind (code 13) and platform-event tag (code 25) append the video fields after the v8 channel fields and journal every event verbatim for byte-identical replay with no producer attached
* Cover the video tier: lifecycle, frames, teardown, replay identity
- Fake and real executor batteries mirror the audio suite: request capture, event round trips, transport mirrors, rejection classes, cascade order, staged-host degrade, straggler swallowing, and the quit-while-playing stop hook
- Sink coverage proves decoded frames reach the claimed surface, replace/stop release the claim, and a stale sink push lands inert
- A recorded playback replays byte-identical into a decoder-less host with no producer attached, fingerprint and model equal; journal codecs round-trip the video event and effect shapes
* Export the video effect types and decline video on the embed host
- native_sdk.EffectVideo/EffectVideoEventKind/EffectVideoSource ride the SDK roots like their audio twins
- The mobile embed host declines video_playback until a shim registers a real decoder, the audio honesty rule
* Decode video on macOS with AVFoundation; teach on Windows and Linux
- One AVPlayer in the AppKit host: AVPlayerItemVideoOutput frames fitted to the sink's pixel budget, BGRA-to-RGBA vImage swizzle, a 1/60s run-loop frame pump plus the audio tier's 0.5s position clock, loop wraps without a completion, and paused seeks still paint their frame
- The frame sink crosses the C ABI as a callconv(.c) trampoline over the runtime's VideoFrameSink; a released claim answers 1 and the host stops its pump
- Windows and Linux stage the capability honestly: video_playback reports false and the load verbs answer a named teaching plus error.UnsupportedService; the CEF host stubs the video C ABI like audio
* Add Cmd.videoLoad and videoCtl to the TypeScript tier at opcodes 0x17/0x18
- videoLoad claims the named media-surface, resolves the local-then-URL cascade, and bakes autoplay/loop/muted into one flags byte; videoCtl drives play/pause/stop/seek/volume/muted/loop by verb ordinal with an f64 value
- The event arm is the seven-field record matched by NAME with the exact five-member state union both directions, the audio arm convention; refusals teach with the NS1027/NS1029/NS1030 vocabulary
- The host routes events on the TSVI key namespace, parks loads under the fake executor like audio, and covers the wire byte-for-byte in the package, host, and e2e suites
* Declare video from markup: the <video> element, house chrome, reconciler
- Element code 68 with flag attrs controls/autoplay/loop/muted at codes 82-85 (src rides the existing attr name; registry law keeps names unique); a leaf on all three surfaces — validator, interpreter, compiled engine — rejecting children like image
- ui.video composes the playback surface on the framework-owned surface id plus runtime-consumed transport chrome (ghost play/pause, proportional scrub slider, clipped time readouts in the built-in bar's register); presence IS playback: the ui-app reconciler loads on src change, applies loop/muted deltas, and stops when the element leaves the view
- Handler-less playback stays honest under replay: platform video events steer the channel mirrors while journaled effect records remain the only Msg source, so house-chrome sessions replay with live readouts and identical fingerprints
* Scope declarative video ownership to the playback it started
- The reconciler stops or retunes the channel only while the active key is the declaration's own: an update handler that loaded its own playback owns the single player, and a departing <video> element must not kill it
* Add the video-player example: house chrome and custom controls
- Player screen is one declarative ui.video with the house transport chrome; Custom screen composes its own bar (transport, +/-10s, proportional scrub, volume, mute, loop) from the command vocabulary over a bare media surface
- No bundled media: the launch argument or the source field names a local clip or http(s) URL, so a live macOS check is one command away
- Headless tests drive the whole transport with the fake executor's synthetic events, the automation widget path, and the null decoder behind the declarative screen
* Document the video element across the docs surfaces
- Components page, catalog entry, native-ui element table row, and the LSP/vocab doc strings for src/controls/autoplay/loop/muted
- Deterministic preview scenes render the placeholder with the house chrome (regenerated video webps only; the untouched catalog keeps its committed renders)
- Changelog fragment states the new element, the command vocabulary, the staged platforms, and the journal v9 break
* Resolve video attribute docs in the markup LSP hover path
- attributeDoc consults the video scoped table so controls/autoplay/loop/muted hover like every registry attribute; the coverage pin now names the video composite
* Make the video-player example's docs and status line honest
- README run instructions use the real CLI verbs: native dev for build-and-run, or native build plus the zig-out binary with the clip argument (the argument passthrough shape)
- The status line now matches each screen's ownership model: the declarative Player screen shows a static teaching (its transport state lives in the runtime-owned chrome, so an event-fed status there could only lie or stall), while the Custom screen keeps the event-fed loading/dimensions/finished/failed line it owns; tests pin both across load, playback, pause, and completion
- Direct-SDK examples handle the runtime's video event in their exhaustive event switches
* Stage video events non-lossily and deliver fed terminals under replay
- Video events leave the lossy pending ring for their own non-lossy stage (the image/channel discipline): a loop-side .rejected or .failed is its load call's only terminal and a fed event is one recorded delivery, so a burst past the ring's capacity must never evict one — pinned by a 40-rejection burst test
- Fed events capture the handler at feed time and deliver their journaled values verbatim when the channel no longer resolves them: under replay the platform .failed event that follows the record applies the channel reset first, and delivery-time resolution silently dropped the Msg the recording dispatched — pinned by a recorded mid-playback failure replaying fingerprint-identical
- loadVideo's deterministic refusal classes extract to the pure videoLoadRejected, one source of truth a caller-side validator can consult
* Refuse an invalid video_load before it re-routes the bridge entry
- The engine keeps the current playback when it rejects a load, but the bridge re-keyed its single routing entry optimistically first: a refused replacement left the surviving stream's events and transport verbs answering to the refused key and arm
- The bridge now consults the engine's own videoLoadRejected gate before committing the entry and stages the rejection Msg to the refused arm directly (stageLoopMsg, the channel-admission precedent), leaving the entry and the engine untouched
- Pinned: a rejected replacement delivers its rejection while the live stream keeps its events and its wire-key gate
* Drive the house video chrome from keys and rebuild it in every window
- Keyboard activation (Enter/Space) of the transport's play/pause control now drives the video channel exactly like the pointer release: the control advertises Play/Pause to focus and accessibility, so the intent must act instead of being consumed silently — pinned beside the pointer-toggle test
- Runtime-consumed control paths and handler-less video events rebuild through one helper that follows dispatch's discipline: the main canvas against ITS window (a control event from a secondary window used to target that window with the main canvas label and error) and the window slots after it, so a <video controls> declared in a secondary window's tree repaints from the moved mirrors
* Fail a macOS video load whose conversion buffer cannot allocate
- The frame tap's reusable BGRA-to-RGBA conversion buffer failing to allocate used to degrade to a silent zero-frame pump while the load still acknowledged LOADED: playback reported positions forever and could never deliver a pixel
- videoAttachOutputForItem now reports the failure and the status hop answers with the FAILED terminal instead of the acknowledgment - the honest degrade; an audio-only item (no video geometry) keeps its documented honest-absence path
* Journal handler-less video terminals and quarantine stale fed events
- Loop-side video terminals now stage and journal with no Msg handler bound (the image arm's rule): a declarative playback binds no handler, but its synchronous .failed is executor truth — the record is what replays the channel reset, and the staged delivery's wake re-renders the chrome
- A fed event whose staged key no longer names the live playback (replaced before its drain, or a replayed platform .failed already applied the terminal) delivers its staged values verbatim and leaves the live channel alone — applying a replaced stream's terminal would reset the replacement
- A journal-fed video record claiming a millisecond or dimension scalar at or past 2^53 refuses replay as a damaged record at the gate (no recorder writes one, and the TS tier's exact-integer widening would trap on it) — pinned by a hand-patched journal
* Route every video event by the key of the load that produced it
- The bridge's engine key now carries the issuing load's event-arm tag in its low byte (videoKeyForTag): a staged synchronous .failed that delivers AFTER a replacing load re-keyed the single entry still routes the arm of the load it answers, instead of handing the old stream's failure to the replacement's arm
- videoEventMsg routes by the event's own key tag, never the mutable entry's; the wire-key gate on transport verbs is unchanged
- Pinned: a replaced load's straggling terminal routes its own arm while the replacement's stream keeps its own; the request-key pins move to the tagged shape
* Reconcile <video src> from every window's tree and repaint Msg-less failures
- Secondary-window builds now feed the video reconciler: Ui.video promises that declaring the element IS the playback in every window's tree, but slot builds discarded the declaration — the main canvas wins when both declare (one player, one owner), the first declaring slot keeps ownership until it stops declaring or its window closes (reconcile-close included)
- The main build stamps the video mirrors it rendered; drainEffects compares that stamp after a Msg-less drain and re-renders the chrome when they moved — a handler-less declarative playback's synchronous failure no longer leaves controls advertising a playback that is gone
* Paint the poster frame for paused macOS video loads
- A load acknowledged while paused (autoplay = false, the documented poster-frame shape) ran no frame timer and never pumped its first decoded frame: the surface held the placeholder until the user played, paused, or seeked
- LOADED now arms a bounded first-frame hunt: the frame timer polls until the first frame pushes (a paused load then stops the timer; a playing one keeps it), surrendering honestly after ~3s if the output never yields one
* Restart the macOS frame timer when buffered playback actually begins
- The poster hunt could stop the pixel clock while AVPlayer was still waiting to play (a remote autoplay stream yielding its poster mid-buffer, or a hunt surrendering past its bound), and nothing restarted it when the waiting phase ended: audio played and positions ticked with no frames flowing
- The timeControlStatus hop now arms the frame timer whenever playback reports rolling (idempotent beside videoPlay's own arm), for local and remote sources alike
* Deliver a staged video terminal past stop instead of panicking
- A Cmd.batch([videoLoad, videoStop]) on a host whose load fails at once stages the .failed before stop retires the bridge entry; the drain then hit the entry gate's panic even though the terminal is the load call's only answer
- videoEventMsg drops the entry gate: every event reaching it was produced by a bridge-issued load and carries its arm tag in its own key (the engine swallows its post-stop stragglers itself), so routing needs no entry at all - pinned by the load-then-stop batch shape
* Retain every window's video declaration and promote on the owner's close
- The reconciler kept only the owning window's <video src>: closing that window (or its declaration vanishing) stopped playback and left another window's mounted video inactive until an unrelated rebuild
- Slot declarations now retain one entry per window (the table matches the window budget); when the owner closes or stops declaring, the next retained declaration promotes inside the same dispatch - pinned by a two-window close-promotes test with no on_close Msg and no rebuild
* Retry the frame wake for identical pushes still awaiting adoption
- A refused frame-wake request leaves pending clear so a retry can land, but the push-boundary fingerprint short-circuit returned before the wake: a static frame pushed again after a transient request_frame_fn refusal short-circuited forever and the staged bytes were never adopted
- An identical push now falls through to the wake while bytes are still staged; only an adopted frame's repeat stays a pure no-op
* Expose videoPlayback through the bridge alias and the TS feature type
- platformFeatureFromString gains the videoPlayback camel-case alias every other feature carries, so window.zero.platform.supports("videoPlayback") answers the platform truth instead of InvalidPlatformFeature
- NativeSdkPlatformFeature adds video_playback/videoPlayback so TS clients can compile the support query
* Stamp every platform video event with the load that produced it
- The engine mints a token per loadVideo, passes it through the load seam, and every host echoes it in each event: takeVideoMsg swallows an event whose token is not the current load's, so a replaced playback's queued terminal can neither reset the replacement, release its claim, nor route through its handler - pinned by a stale-failed-after-replace regression
- macOS hardening on the same theme: the item end/failure notification blocks re-check note.object against the current item (a removeObserver cannot recall a block already enqueued on the main queue), and the host carries the token across its terminal emits' teardown
- The journaled video platform event carries the token, so replay's mirror steering swallows stale recorded events exactly as live (the re-run loads mint the same deterministic sequence); the null platform echoes tokens like the real hosts and its fake position advance saturates instead of trapping on hostile deltas
* Pair replayed video deliveries with their events; token-gate fed entries; stop abandoned players
- A recorded video Msg dispatched synchronously inside its platform event's dispatch, but replay delivered the fed record at the NEXT drain: an update loading the next clip from its completion handler ran too late and the new clip's .loaded event was swallowed against the old load's token - takeVideoMsg under replay now pops the fed head (the journal's contiguity puts each event's record immediately before it) and dispatches it during the same event, pinned by a chained-load playlist session replaying byte-identical
- Pending video entries carry the load token that staged them, and delivery resolves against the live channel only while it still IS that load: the public key alone cannot tell two loads under one app key apart, and a stale fed terminal resolving against a same-key replacement would have reset it
- failVideoChannel silences the platform player it abandons (best effort): a load that succeeded before a later step refused kept its player decoding while the reset channel forgot it, and the inactive channel skipped it at teardown too
* Keep refused and uninstalled declarations out of the video reconciler
- A declared src the engine's own gates refuse (a malformed URL, say) no longer commits the reconciler's tracked ownership: the running playback keeps its identity - so removing the element later stops IT, instead of hashing the refused source and stranding the playback forever; the refused src is remembered separately and taught once, never re-attempted every rebuild - pinned by a refused-declaration ownership test
- A secondary window's declaration is captured only after its build pass and layout succeed and the tree installs: a build whose layout errors never displays, so its declaration never steers the playback either
* Link CoreVideo wherever appkit_host.m builds standalone
- The frame pump's CVPixelBuffer calls are real CoreVideo symbols: the central build gained the framework, but the generated-app template and the standalone example builds still linked only AVFoundation - a generated macOS project failed to link
- The template and every example build.zig that compiles the AppKit host now link CoreVideo beside AVFoundation
* Gate the poster hunt on a real push and clamp millisecond CMTimes
- videoPumpFrame initialized its push result to the success code, so a NULL base address or failed vImage permutation ended the poster hunt and stopped the frame timer with nothing on the surface; a sentinel now keeps the hunt alive until a push actually returns success
- NativeSdkCMTimeFromMs narrowed the u64 millisecond position straight into the signed CMTimeValue, turning absurd seeks negative; the value now clamps at INT64_MAX so AVFoundation clamps to the duration as documented
* Retire the null video player when a non-looping playback completes
- advanceVideo left the fake player loaded after its completion, so post-completion transport calls succeeded where a live host's torn-down player refuses them (and a replayed play could emit a second completion)
- the completion now unloads before the event returns, matching the live hosts' retire-before-emit order; tests pin the unload and the failed-play degrade path
* Stop the custom playback when an empty source commits
- loadCustom returned early on an empty committed source, leaving the previous video rolling under a status line that said "no source"
- an empty commit now stops the playback and resets the transport mirrors, with a test pinning the stopped channel and the honest status line
* Route replayed video records by the journaled load identity
- Every .video effect record now carries the producing load's token; replay feeds through feedVideoRecord, which resolves the token against the live channel or a retired-load park instead of binding the record to whatever the channel holds at feed time.
- loadVideo and stopVideo park the outgoing load's identity under replay, so a synchronous terminal staged inside the very dispatch that then replaced or stopped its playback still delivers the recorded Msg with the recorded identity - never EffectNotFound, never a reset of a same-key replacement the recording kept playing.
- The replay pairing in takeVideoMsg now token-gates fed entries against the platform event, and two session tests pin the batch shapes: load-then-stop and load-then-replace under one app key.
* Check the Core Video lock result before touching the pixel buffer
- A failed CVPixelBufferLockBaseAddress never maps the buffer, so reading the base address was undefined and the unconditional unlock unbalanced the lock count; the frame now drops with the buffer released.
- The poster hunt stays latched across the dropped frame - only an actual push verdict may end it, the converted-but-unpushed rule.
* Document why rate > 0 is transport intent at the playing derivations
- Per the AVPlayer.h contract, a playback waiting in AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate keeps rate at the requested value ("not currently effective but instead indicates the rate at which playback will start or resume"), so a stalled-but-unpaused stream already reports playing=1 + buffering=1 and the transport control offers Pause.
- AVPlayer resets rate to 0.0 on its own only with waits-to-minimize-stalling disabled - the local-file configuration, where playback would not self-resume and offering Play is the honest affordance.
* Keep the controls-bearing video element zero-intrinsic
- The <video controls> wrap column now adopts the media sizing contract (WidgetLayoutStyle.zero_intrinsic): the transport bar's intrinsic size never leaks into the element, so an unsized element in a hug container measures zero like the bare surface instead of rendering a controls-only strip.
- The wrap clips its content, so chrome cannot paint past a box the layout granted nothing; declared width/height stay definite through the frame and min/max bounds.
- A ui test pins both shapes: zero in a hug container, surface-above-bar inside a declared 320x180 box.
* Bound video scalars at delivery, sweep replay parks, republish flag deltas
- Platform video events clamp position, duration, and dimensions into the exact-integer delivery window (max_effect_video_scalar_exclusive, 2^53) at takeVideoMsg, whatever a host or embedder reports - the engine-side guarantee behind replay's damage gate, so an honest recording can never be refused as damage; a past-window readout now records and replays clamped, pinned by test.
- Replay-side retired video identities release at the first drain-pass boundary after parking (any journaled record for them feeds before that pass's event dispatches), so a long replayed playlist parks and releases one entry per clip instead of accumulating them; pinned at the channel level.
- Same-src declarative loop/muted deltas republish the runtime mirror in the same reconcile, so an automation snapshot taken after the flip reports the new value instead of the one published before the rebuild.
* Journal the video cascade's resolved source for replay
- loadVideo now journals a Msg-less .video_load record (the .clock/.env discipline) carrying which source the recording host's cascade resolved - a missing local file that fell through to the url is filesystem truth the replayed fake load cannot re-probe.
- Replay queues each record and the replayed load consumes it by its deterministic token, so videoSnapshot() and the automation mirror report .stream with the optimistic buffering flag exactly as the recording did, handler-less declarative playbacks included.
- Pinned by a record/replay test on an assets-absent host; the handler-less house-chrome pin now expects exactly the one Msg-less record.
* Keep buffering honest across paused streams and widen the fake's loop wrap
- Buffering means an un-paused stream waiting for bytes: a fresh URL load now starts the flag from the autoplay intent (engine mirror and replayed cascade resolution alike), the macOS host derives it purely from timeControlStatus instead of presetting it, and pauseVideo clears it - a conforming host emits no pause acknowledgment that could.
- The macOS time-control observer stays silent when the transition lands on paused: pause emits nothing by contract and position reports are for playback in motion; waiting and playing transitions still emit.
- The null platform's advanceVideo runs its loop wrap in widened arithmetic so a past-u64 delta lands on the exact residue instead of a saturated one; pinned along with the paused-stream and pause-clears-buffering shapes.
* Spill the replayed cascade-resolution queue past its inline capacity
- Loads per dispatch are unbounded by contract and every .video_load record lands before the dispatch's event, so the replay-side queue now grows geometrically past its inline 64 (freed when it empties and at deinit) instead of refusing the 65th record as a false divergence - the pending stages' non-lossy discipline.
- Pinned by a record/replay burst one past the inline capacity on an assets-absent host.
* Cross-check the journaled key on every replayed video pairing
- Reminted tokens pair records with replayed loads by position; the journaled key now proves the load at that position is the one the recording issued - feedVideoRecord refuses a mismatch as divergence, and a cascade resolution consumed under a different key latches one for the finish check.
- Replay now ends with a consistency check (the .finish replay control): a structurally valid journal whose queued cascade resolutions the replayed timeline never claimed fails as ReplayEffectDivergence instead of reporting success.
- Pinned at the channel level: wrong-key feeds refuse, an unclaimed or misclaimed resolution fails finishReplay, and the honest pairing stays silent.
* Copy the captured video declaration and verify the load bijection under replay
- The main-canvas <video src> capture now copies the source out of the build arena into app-owned storage (the slot captures' rule): a later failed rebuild resets the arena the old capture borrowed, and a window-close reconcile could hash or load overwritten bytes.
- Every non-rejected real load now journals exactly one .video_load record - refused cascades included (failVideoLoad) - so the replayed loads and the journaled records form a bijection: a replayed load with no record at its position, and a record whose position was never issued, both latch divergence for the finish check.
- The journal semantic epoch bumps to 2: identical bytes, changed replay meaning - an earlier recording's failed loads would replay as false divergence, so old journals refuse with the re-record teaching. Pinned: the extra-load shape fails finishReplay and the captured src aliases the app-owned buffer.
* Run the replay finish check even when no records were fed
- A latched video-load divergence can exist with zero fed effect records (the recording journaled nothing; the replayed timeline loaded anyway), so the end-of-journal consistency check now runs unconditionally; hookless apps answer ReplayUnsupported, which is honestly nothing-to-check.
- Pinned: a zero-record recording replayed into a build that declares a video fails as ReplayEffectDivergence instead of reporting success.
* Refuse non-http(s) schemes at the videoLoadUrl seam
- The seam documents streaming-only and promises to reject bad arguments before the platform is asked, but forwarded any scheme - a direct caller could hand file:///... to a host whose media stack opens it (AVPlayer does); the scheme gate now holds at the seam for every caller, matching the engine's own load validation.
- Pinned: file:, ftp:, and unparseable URLs refuse as InvalidVideoOptions without reaching the platform; https passes.
* Retire the null platform's video player before the load probes refuse
- The macOS host stops the previous player before the file-existence probe, so a refused load leaves no playback behind; the null platform now models the same ordering in videoLoad and videoLoadUrl instead of returning early with the replaced player still emitting frames and events under its old token.
- Pinned: a VideoSourceNotFound load leaves nothing loaded, no pending acknowledgment, and no position ticks.
* Fail frame-less video loads and keep the seek mirror on platform truth
- An asset with no video geometry (an audio-only file loaded as video) now fails the load instead of acknowledging a playback that can never paint - sound over a permanently blank surface is the same broken promise as the conversion-buffer failure the attach path already refuses.
- seekVideo applies the platform seek before moving the mirror: a player the host already retired (a completed non-looping playback) refuses the call, and the snapshot and house slider must stay on the frame actually on the glass; pinned in the post-completion transport test.
* Refuse post-completion seeks on the fake video channel too
- A non-looping natural end retires the platform player (retire-before-emit), so a later seek refuses live and the mirror keeps the terminal position; the fake executor now latches the completion and refuses identically, so replayed chrome and snapshots land exactly where the recording left them instead of scrubbing a player that no longer exists.
- Pinned by a record/replay parity test: a scrub after the natural end keeps position at the duration on both sides, model and fingerprints identical.
* Pair replayed video deliveries from any stage position; stop cancels the TS stream
- The replay pairing in takeVideoMsg now scans the pending stage for the event's fed entry instead of checking only the head: a regenerated loop-side rejection staged earlier in the same dispatch keeps its own drain-time order and no longer reverses the recorded Msg order; pinned by a record/replay ordering test.
- VideoMsgFn may return null - the adapter tier's cancel gate - and the TS bridge uses it to honor Cmd.videoStop's wire contract (stop CLOSES the stream, no events for the key after this): a staged synchronous terminal from the very batch that stopped the stream drains through the engine but never reaches the app, and reopening the tag lifts the latch.
- The house videoMsg constructor never returns null, so Zig-native apps keep the one-terminal-per-load delivery whole; the bridge pin now asserts the swallow and the reopen.
* Cancel the key's staged video answers inside the engine at TS stop
- Cmd.videoStop's cancel now runs where the answers live: stopVideoCancel removes every staged-but-undrained entry for the key before the channel goes idle, so nothing for a stopped stream can reach the app - even the synchronous terminal of a load-fail-stop batch, and even when a later load reuses the same event tag before the drain (a bridge-side tag latch could not tell those generations apart).
- A cancelled answer never journals, so replay regenerates and cancels the same entries and the timelines stay identical with no extra machinery; the VideoMsgFn seam returns to plain Msg and the Zig-native stopVideo keeps its one-terminal-per-load delivery.
- The bridge pin now covers the same-tag reopen-before-drain shape.
* Mirror synchronous load refusals under replay; token-scope ownership and cancel
- The .video_load record's video_kind now carries the load's outcome: a refusal reset the live channel before loadVideo returned, so the replayed fake load resets at the same instant (parking its identity for the journaled terminal) and a snapshot an update reads inside the very dispatch matches the recording's; semantic epoch bumps to 3 (same bytes, the field gained meaning). Pinned by a record/replay probe test.
- Declarative video ownership now rides the load token the reconciler captured at its own load, never the derived key alone - the key is a pure function of the source string, and a manual load carrying it must survive declaration removal and flag deltas untouched; pinned.
- stopVideoCancel is token-scoped: the stopped stream is the latest accepted load, so a replaced predecessor sharing the arm's public key keeps its owed terminal - only stop cancels; pinned at the channel level.
* Refuse post-completion play on the fake video channel too
- A non-looping natural end retires the platform player, so play meets an absent player live: one .failed terminal and the channel resets before playVideo returns; the fake executor now models the same refusal (failVideoChannel on plain fake; under replay just the identity park and reset, because the journaled terminal delivers itself), so a snapshot an update reads right after its own play answers the same on every executor.
- Pinned by a record/replay probe test: the mid-dispatch snapshot reads inactive on both sides, the terminal delivers at its recorded wake, and models and fingerprints match.
* Break the video timer retain cycles and gate video-load outcomes at replay
- Both video timers (the 1/60 pixel clock and the 500ms readout) are now weak-host block timers that self-invalidate when the host is gone: a target-selector repeating timer retains its target through the run loop, so a host destroyed mid-playback without an orderly stop could never dealloc - leaving AVPlayer, the timers, and the conversion buffer permanently retained.
- Replay refuses a .video_load record whose video_kind is neither .loaded nor .failed as a damaged journal: the recorder writes exactly those two outcomes, and anything else would steer the replayed fake load into a state the recording never had; pinned by a hand-patched-journal test.
* Commit the video declaration at install; provenance-gate video rejections
- The main-canvas <video src> capture now stages during the build and commits only when the rebuild installs: a build that fails downstream never mounted, and the retained tree on the glass still shows the old declaration - a later reconcile acting on the unmounted capture would stop or replace a playback the presented tree still declares.
- Replay refuses a .video record whose kind and token are recorder-impossible: rejections stamp token 0 (a refused load never minted one) and every delivery carries its minted token, so a .rejected re-stamped onto a delivered record can no longer slip through the regeneration skip and silently omit the recorded Msg; pinned by a hand-patched-journal test.
* Fail the replay finish check on undelivered fed video results
- A fed record is one recorded delivery, and the event that consumed it live follows it in every honest journal - so a fed entry still staged when the journal ends means truncation or hand-editing, and finishReplay now refuses instead of reporting success; regenerated loop-side answers may honestly outlive the last drain on both timelines and stay exempt.
- Pinned: a fed-but-undelivered result fails the finish check and the same result delivered makes it silent again.
* Restart a finished playback from Play instead of failing it
- A non-looping natural end retires the player, so Play answered with one failed event and the seek was guaranteed to spring back - a broken response to a valid finished state. Effects.restartVideo is the resume path: a fresh load of the channel's own remembered source with autoplay, keeping key, surface, handler, and the loop and mute flags, journaling like any load.
- The house transport's toggle restarts when the completion latch is set and its scrub disables (Play is the live affordance); the snapshot and chrome state expose completed so custom players can do the same - the video-player example's Play now restarts its finished clip.
- Pinned: clicking the house toggle after the natural end issues a fresh platform load and the playback runs again.
* Validate replayed video payloads and source shapes; retire the example's dead seeks
- The replay pairing now requires the fed record's payload to equal what the platform event resolves to: the recorder journals every delivery verbatim from that event, so an altered kind or scalar around an intact identity refuses as divergence instead of handing the app a Msg the mirrors contradict; pinned by a hand-patched-journal test.
- A journaled cascade resolution must be one the load's request shape could select (.local needs a path, .stream needs a url); an impossible pairing latches divergence for the finish check, pinned at the channel level.
- The video-player example's seek-family controls (slider, back, forward) disable after a natural completion - a retired player refuses seeks and the thumb would spring back; Play stays live and restarts.
* Document the video output's vended-size contract at the frame tap
- The pixel-buffer width/height attributes are client requirements AVPlayerItemVideoOutput satisfies by scaling every vended frame, not hints: a 3840x2160 H.264 source tapped with fitted 1866x1050 attributes vends 1866x1050 buffers, so a 4K playback never hits the oversized-frame drop - that guard is defense in depth against a hypothetical non-conforming host, and the comment at the attributes site now says so.
* Keep declared ownership across restart; saturate past-window seeks
- The house transport's restart mints a fresh load identity, so the declarative reconciler re-captures its ownership token when the finished playback was its own - removal and same-source flag deltas keep working after a replay-from-end; pinned in the restart test.
- The TS bridge no longer rewinds a finite seek past the exact-integer window to zero: it saturates just below the window, where the engine's duration clamp lands it at the end - an oversized forward seek means the end, and only NaN and negatives (not millisecond offsets at all) seek to 0; pinned at the bridge level.
* Class non-finite seeks as invalid offsets, never oversized forward seeks
- Infinity reached the past-window saturation branch and sought an active video to its end, though the literal validation rejects non-finite offsets: the saturation now requires a FINITE value, so Infinity seeks to 0 like NaN and negatives - not millisecond offsets at all.
- Pinned at the bridge level beside the finite past-window seek.
* Keep the reserved video surface id inside the f64 exact window
- Surface ids ride the TS wire as f64, and a source-less <video> with custom controls is fed by the app's own Cmd.videoLoad naming exactly the reserved id - the old value sat near 2^63, so the bridge's exact-integer validation rejected it and the element could only ever show its placeholder from TypeScript.
- The id moves below 2^53 (mnemonic preserved, bit 63 clear, still a valid producer id reserved by convention); pinned: in the window, outside the derived-texture namespace, and round-trips the f64 wire unchanged.
* Deterministic seek mirror, journaled handler presence, and baked rotation
- The seek mirror now gates on the deterministic completion latch on every executor: a completed playback keeps its terminal position live and replayed alike (the platform is not even asked - the retired player could only refuse), and residual platform verdicts are fire-and-forget so an exotic host's answer can never diverge replay's mirrors from the recording's.
- Every .video record journals whether its delivery dispatched a Msg, and feedVideoRecord requires the replayed handler presence to agree - a record whose Msg silently vanished, or a Msg live never dispatched, refuses as divergence instead of a silent consume.
- Rotated media renders upright: a track with a non-identity preferredTransform gets a properties-of-asset video composition baked into the vended frames (verified against a portrait-flagged H.264 asset: the raw tap vends encoded orientation, the composition vends display orientation); identity-transform assets skip the render pass and track-less streams have no transform to bake.
* Repaint the chrome when the reconcile moves playback; seek on slider key steps
- A src change loading an autoplaying replacement reconciles after the build installs, so the just-installed chrome advertised the OLD transport state while its control acted on the new one - Play on the label, pause in effect. The rebuild now runs one guarded repass when the reconcile moved the mirrors the build rendered; the repass reconciles an unchanged declaration, so the mirrors are a fixed point. Pinned: the replacement's pause glyph shows in the same build.
- Keyboard and assistive steps on the house seek slider arrive as set_value intents with no widget change event behind them; they now map their fraction onto the duration and drive the channel like the pointer scrub instead of being consumed silently for the next tick to snap back. Pinned with a focused arrowright step.
* Repaint secondary-window chrome when its own reconcile moves playback
- The slot rebuild reconciles its captured declaration after the slot tree installs, so a first mount of an autoplaying <video controls> in a secondary window rendered its chrome from the still-inactive snapshot - disabled Play over a playing video - until some later platform event, seconds away on a slow stream. The slot path now runs the main rebuild's guarded repass when the reconcile moved the mirrors the build rendered; the repass reconciles an unchanged declaration, so the mirrors are a fixed point.
- Pinned: the slot's first installed build shows an enabled pause toggle.
* Latch missing video records; scope bridge verbs to their own stream
- A recorded video event arriving under replay with a handler bound and no fed record before it now latches divergence for the finish check: the recorder journals every handled delivery immediately before its event, so absence is truncation or hand-editing - never a Msg to drop silently; pinned at the channel level.
- Bridge video verbs now prove ownership with the load identity their own accepted load minted (Effects.videoMintedToken): a wire key whose stream was since replaced by a load the bridge never issued - a declarative element's - no-ops its transport verbs, and its stop cancels only its own stream's staged answers while the playback on the channel survives untouched; pinned at the bridge level.
* State the replace-is-not-stop rule at the videoLoad contract
- A replaced load still delivers the terminal it owes, routed to its own event arm - the never-silent promise applies to the replaced stream's failure exactly as to any other, and only Cmd.videoStop cancels undelivered answers. The wire contract now says so explicitly where the open-or-replace semantics are defined; the bridge's routing and the arm-separation pin already enforce it.
* Remember volume across a failed load; gate impossible terminal payloads
- Volume is a remembered preference the next load re-applies, so the bridge's ownership gate now lets videoSetVolume through when the channel is IDLE - a failed load's handler routinely sets it before retrying, and with nobody's playback on the channel there is nothing to protect; a foreign live playback still gates it. Pinned both ways.
- Replay refuses a .video record whose payload shape the recorder never writes for its kind: terminals deliver with playing and buffering false and no dimensions, and a completion pins position to the duration - a synchronously failed load's record has no platform event behind it to cross-check, so the gate is where a hand-set width refuses; pinned by a hand-patched-journal test.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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
|
||
|
|
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 |
||
|
|
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
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
f8aee3a96a | Fix CI content checks and GTK prototype | ||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
5b86c81088 | Prepare 0.2.0 release |